Last active
April 12, 2016 01:57
-
-
Save metal3d/edce016017444a3be533a782a68a0c5f to your computer and use it in GitHub Desktop.
DI python
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
# Why DI Framework are useful | |
Dependency injection is back in fashion. That's probably the result AngularJS, Symfony2, and many Java frameworks success that offer a "DI" implementation. But what is "DI", and why are we able to *not use* "DI" framework with Python ? | |
## What is dependency injection | |
Dependency injection is a pattern, or it is rather a way to implement dependencies between classes. The approach is basically to avoid classes to directly instanciate objects, and make use of interfaces and/or factories. That's all, nothing more complicated. | |
To illustrate the definition, it's simpler to look at an example. | |
We have a website selling some products. After the user filled up cart, he should pay the bill. But we want to propose several payment methods | |
- Paypal | |
- Stripe | |
- Credit card | |
Each payment method have its own API, scenario and implementation. So you can imagine the difficulty to prepare views and how to check payment to save information in database. | |
Anyway, you are not a bad developer and you created classes that have the same implementation - you don't have "interfaces" in python, but it's not very important. So for this example, we can use classes that contains a "charge" method: | |
```python | |
class PayPalPayment(object): | |
def __init__(self): | |
# call api to authenticate payment | |
pass | |
def charge(self, amount): | |
# charge payement, paypal will redirect user | |
# in a payment page... | |
``` | |
Other payment class should have some identical method signature. | |
And you often see something like the above code: | |
```python | |
class Cart(object): | |
def __init__(self): | |
self.amount = 0.0 | |
pass | |
def payment(self, choosed_method): | |
# very bad implementation | |
if choosed_method == "paypal": | |
p = PayPalPayment() | |
p.charge(self.amount) | |
# and so on | |
elif choosed_method == "stripe": | |
#... | |
``` | |
You now realize that something is wrong. If you want to add another payment method, it will be complicated. You will need to create a case for each payment method you've added. So, it's definitely a bad idea. | |
> Dependecy Injection will help. | |
Erase everything we've made, and let's start at Cart class that uses "DI": | |
```python | |
class Cart(object): | |
def __init__(self): | |
self.amount = 0.0 | |
pass | |
def payment(self, choosed_method): | |
# better, the payment method is injected | |
p = choosed_method() | |
p.charge() | |
``` | |
So, what is "choosed_method" ? | |
It's a **factory**, nothing more. | |
But, before going further, let's speaking about Python constructors | |
## Python constructors are... factories | |
Yes ! whatever you may think about constructors in Python, keep in mind that there are factories and nothing more. | |
Just to be sure that everything is ok with that strange idea, have a look on this line: | |
```python | |
a = b() | |
``` | |
Now, ask yourself: | |
> is "a" the result of "b" function, or "a" an instance of "b" class ? | |
And the answer is... **the result of the function**. Whatever the type of "a", we called a "b" function. | |
If "b" is a class, calling "b()" returns a "b" instance. And this is the exact definition of a factory. So, in this case, the answer of the question is **"both the return value of "b" function and an instance of "b" class".** | |
And this is strongly interesting for the next section. | |
Now, get back to our example. | |
## Back to payment | |
We will later see that we can still use "string" as parameter, but for now we have to understand some concepts. | |
So, for now, instead of having to send the "payment method" as string, we can pass the "type" of payment to use in our Cart class. | |
```python | |
c = Cart() | |
c.amount = 15.00 | |
c.payment(PaypalPayment) | |
# or | |
c.payment(StripePayment) | |
``` | |
Inside Cart::payment() method, "choosed_method" will be "PaypalPayment" or "StripePayment", and it will be **"called" as factory**. Because the call of those classes will create a new instance. | |
But what is very nice is that we can give a "function" instead, that is an implemented factory. Example: | |
```python | |
def test_payment(): | |
if stripe_is_mandatory: | |
return Stripe() | |
elif paypal_is_mandatory: | |
return PaypalPayment() | |
return CreditCart() | |
``` | |
We can inject this: | |
```python | |
c = Cart() | |
c.amount = 15.00 | |
# give the factory | |
c.payment(other_payment) | |
# or, with classes definitions | |
c.payment(PaypalPayment) | |
# ... | |
c.payment(StripePayment) | |
``` | |
And we have nothing to change in our Cart::payment method ! | |
## Factories can return factories | |
A lot of dependency injection frameworks propose that kind of call: | |
```python | |
payment = get_payment("paypal") | |
``` | |
That very useful because you may use a form value to know wich object to use. | |
This isn't complicated to implement in python, we only have to create a "get_payment" factory that returns factory **but not an instance**. | |
```python | |
def get_payment(method): | |
if method == None: | |
# we can have a default_method somewhere | |
if default_method is not None: | |
return default_method | |
return CreditCard | |
if method=="paypal": | |
return PaypalPayment | |
if method == "stripe": | |
return StripePayment: | |
``` | |
Now we can use a string to charge consumer: | |
```python | |
c = Cart() | |
c.amount = 15.00 | |
c.payment("paypal") | |
``` | |
And inside Cart class, you may implement "payment" method: | |
```python | |
class Cart(object): | |
def __init__(self): | |
self.amount = 0.0 | |
pass | |
def payment(self, choosed_method): | |
p = get_payment(choosed_method) | |
p.charge() | |
``` | |
But that's not very useful. We only moved the same code as the beginning of the article in a separated function. The good way to implement dependency injection is to call "get_payment" when we need it. | |
The second possible implementation is to send the factory instead of the name. So you should call | |
```python | |
c = Cart() | |
c.amount = 15.00 | |
c.payment(get_payment("paypal")) | |
# or | |
c.payment(get_payment()) | |
``` | |
Now, we are closer to common "DI". In Cart class, "payment" method get the type of "Payment" and will instanciate it at time. | |
So, we're now back to the "better" implementation we've seen earlier: | |
```python | |
class Cart(object): | |
def __init__(self): | |
self.amount = 0.0 | |
pass | |
def payment(self, choosed_method): | |
# better, the payment method is injected | |
p = choosed_method() | |
p.charge() | |
``` | |
At last, we can be able to accept both string and factories. That way, we allow several usages of the "payment" method. | |
```python | |
class Cart(object): | |
def __init__(self): | |
self.amount = 0.0 | |
pass | |
def payment(self, choosed_method): | |
p = None | |
if type(choosed_method) in (str, basestring): | |
p = get_payment(choosed_method) | |
else: | |
p = choosed_method() | |
p.charge() | |
``` | |
## So, the DI definition is ? | |
To be precise, DI means that your working class (Cart in our example) shouldn't directly create dependent objects. It must be able to call factories. | |
There is no problem to use DI framework, but it's often not so useful and you probably only need to create you class with a bit more attention to accept injection. | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment