-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
295 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
# Factory functions | ||
|
||
Factory functions allow the container to wire dependencies that require additional logic to create | ||
or be able to inject objects it doesn't own. | ||
|
||
Typically getting the necessary dependencies is enough to construct an object. However, there are scenarios | ||
where you need to delegate the creation of an object to a special function called a | ||
[factory](https://en.wikipedia.org/wiki/Factory_(object-oriented_programming)). | ||
|
||
## Use cases | ||
|
||
Some of the use cases for factories are as follows: | ||
|
||
* Object construction needs additional logic or configuration. | ||
* Depending on the runtime environment or configuration, you may need to create different objects | ||
inheriting from the same base (See [Strategy Pattern](https://en.wikipedia.org/wiki/Strategy_pattern)) or configure them differently. | ||
* Gradually introduce DI into an existing project where the container should be able to inject dependencies created elsewhere. | ||
Such as injecting the same databaser connection as the rest of the application. | ||
* Eliminate services which have only one method that returns the same object and instead inject the object directly. | ||
* A service that returns a db connection | ||
* A service which returns the current authenticated user | ||
* Register the result of a service's method as its own service. Instead of calling `auth_service.get_current_user()` every time, inject the authenticated user directly. | ||
|
||
## Usage | ||
|
||
In order for the container to be able to inject these dependencies you must register the factory function. | ||
You can do this by using the `@container.register` decorator or by calling `container.register(fn)` directly. | ||
|
||
When the container needs to inject a dependency it checks known factories to see if any of them can create it. | ||
|
||
|
||
!!! info | ||
The return type of the function tells the container what type of dependency it can create. | ||
|
||
!!! warning | ||
Factories can only depend on objects known by the container! | ||
|
||
## Examples | ||
|
||
Assume in the context of a web application a class `User` exists and represents a user of the system. | ||
|
||
```python | ||
# Instead of doing the following over and over again | ||
def get_user_logs(auth_service: AuthService): | ||
current_user = auth_service.get_current_user() | ||
... | ||
|
||
|
||
|
||
# You can create a factory and inject the authenticated user directly. | ||
# You may want to create a new type to make a disctinction on the type of user this is. | ||
AuthenticatedUser = User | ||
|
||
container.register | ||
def get_current_user(auth_service: AuthService) -> AuthenticatedUser: | ||
return auth_service.get_current_user() | ||
|
||
# Now it is possible to inject the authenticated user directly wherever it is necessary. | ||
def get_user_logs(user: AuthenticatedUser): | ||
... | ||
``` | ||
|
||
## Links | ||
|
||
* [Introduce to an existing project](introduce_to_an_existing_project.md) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
# Introduce to an existing project | ||
|
||
It can be challenging to add DI to an existing project which doesn't yet use it. One of the issues you will run into | ||
sooner or later is being able to share resources between code which uses DI and the rest of the application | ||
which does not. | ||
|
||
This is especially useful to allow the container to inject dependencies created elsewhere. | ||
|
||
Think of a database connection. Your application probably already has one. Instead of opening a second connection | ||
using a new database service, you can instruct the container how to get the connection either via a service or | ||
by using factory functions. | ||
|
||
Another case might be an existing service that is already constructed, and you wish to be able to inject. | ||
|
||
## Using a Service | ||
|
||
A typical way to solve this would involve create a service with a single method | ||
that uses existing functionality to get the desired object and simply returns it. | ||
|
||
```python | ||
# Example of a service acting as a factory | ||
@container.register | ||
@dataclass | ||
class DbConnectionService: | ||
self.conn = get_db_connection_from_somewhere() | ||
``` | ||
|
||
Here, it is possible inject `DbConnectionService` and call `.conn` to get the connection. While this works, it's not the best way to go. | ||
|
||
## Using Factory functions | ||
|
||
To handle this more elegantly, WireUp lets you register functions as factories. | ||
You can do this by using the `@container.register` decorator or by calling `container.register(fn)` directly. | ||
|
||
|
||
```python | ||
@container.register | ||
def get_db_connection_from_somewhere() -> Connection: | ||
return ... | ||
|
||
# Alternatively | ||
|
||
container.register(get_db_connection_from_somewhere) | ||
``` | ||
|
||
Now it is possible to inject `Connection` just like any other dependency. | ||
|
||
|
||
## Links | ||
|
||
* [Factory functions](factory_functions.md) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
from dataclasses import dataclass | ||
from unittest import TestCase | ||
|
||
from test.fixtures import Counter, FooBar | ||
from test.services.random_service import RandomService | ||
from wireup import DependencyContainer, ParameterBag, wire | ||
|
||
|
||
class ThingToBeCreated: | ||
def __init__(self, val: str): | ||
self.val = val | ||
|
||
|
||
class TestContainerStaticFactory(TestCase): | ||
def setUp(self) -> None: | ||
self.container = DependencyContainer(ParameterBag()) | ||
|
||
def test_injects_using_factory_with_dependencies(self): | ||
self.container.register(RandomService) | ||
self.container.params.put("dummy_val", "foo") | ||
|
||
@self.container.register | ||
def create_thing(val=wire(param="dummy_val")) -> ThingToBeCreated: | ||
return ThingToBeCreated(val=val) | ||
|
||
@self.container.autowire | ||
def inner(thing: ThingToBeCreated): | ||
self.assertEqual(thing.val, "foo") | ||
|
||
inner() | ||
|
||
self.assertEqual(create_thing("new").val, "new", msg="Assert fn is not modified") | ||
|
||
def test_injects_using_factory_returns_singletons(self): | ||
self.container.params.put("start", 5) | ||
|
||
@self.container.register | ||
def create_thing(start=wire(param="start")) -> Counter: | ||
return Counter(count=start) | ||
|
||
@self.container.autowire | ||
def inner(c1: Counter, c2: Counter): | ||
c1.inc() | ||
|
||
self.assertEqual(c1.count, 6) | ||
self.assertEqual(c1.count, c2.count) | ||
|
||
inner() | ||
|
||
def test_injects_on_instance_methods(self): | ||
this = self | ||
|
||
class Dummy: | ||
@self.container.autowire | ||
def inner(self, c1: Counter): | ||
c1.inc() | ||
this.assertEqual(c1.count, 1) | ||
|
||
@self.container.register | ||
def create_thing() -> Counter: | ||
return Counter() | ||
|
||
Dummy().inner() | ||
|
||
def test_register_known_container_type(self): | ||
self.container.register(RandomService) | ||
|
||
with self.assertRaises(ValueError) as context: | ||
|
||
@self.container.register | ||
def create_random_service() -> RandomService: | ||
return RandomService() | ||
|
||
self.assertEqual( | ||
f"Cannot register factory function as type {RandomService} is already known by the container.", | ||
str(context.exception), | ||
) | ||
|
||
def test_register_factory_known_type_from_other_factory(self): | ||
with self.assertRaises(ValueError) as context: | ||
|
||
@self.container.register | ||
def create_random_service() -> RandomService: | ||
return RandomService() | ||
|
||
@self.container.register | ||
def create_random_service_too() -> RandomService: | ||
return RandomService() | ||
|
||
self.assertEqual( | ||
f"A function is already registered as a factory for dependency type {RandomService}.", | ||
str(context.exception), | ||
) | ||
|
||
def test_register_factory_no_return_type(self): | ||
with self.assertRaises(ValueError) as context: | ||
|
||
@self.container.register | ||
def create_random_service(): | ||
return RandomService() | ||
|
||
self.assertEqual( | ||
"Factory functions must specify a return type denoting the type of dependency it can create.", | ||
str(context.exception), | ||
) | ||
|
||
def test_factory_as_property_accessor(self): | ||
@self.container.register | ||
class FooGenerator: | ||
def get_foo(self) -> FooBar: | ||
return FooBar() | ||
|
||
@self.container.autowire | ||
def inner(foobar: FooBar): | ||
self.assertEqual(foobar.foo, "bar") | ||
|
||
@self.container.register | ||
def foo_factory(foo_gen: FooGenerator) -> FooBar: | ||
return foo_gen.get_foo() | ||
|
||
inner() |
Oops, something went wrong.