Search code examples
pythonoopsolid-principlesobject-oriented-analysisdependency-inversion

Is Dependency Inversion Necessary to Ensure Decoupling between Caller and Callee?


I am trying to understand the dependency inversion principle (DIP) via some simple, but concrete codes and classes (implemented in python) from this tutorial. I am summarising it (with my own comments and understanding) to save you the pain of going through the whole thing.

Basically we are building a currency-converter application where we are separating the main application logic from the currency converter itself. The codes (some comments and docstrings mine) are as follows.

Snippet 1
#!/usr/bin/env python3
# encoding: utf-8

"""Currency converter application using some exchange API."""
class FXConverter:
    """The converter class."""
    def convert(self, from_currency, to_currency, amount):
        """
        Core method of the class. Assume the magic number 1.2 is from some API like 
        Oanda
        """
        print(f'{amount} {from_currency} = {amount * 1.2} {to_currency}')
        return amount * 1.2


class App:
    """The Application"""
    def start(self):
        """The main method to create and invoke the converter object."""
        converter = FXConverter()
        converter.convert('EUR', 'USD', 100)


if __name__ == '__main__':
    app = App()
    app.start()

Now, the tutorial claims that (direct quotation)

In the future, if the FX’s API changes, it’ll break the code. Also, if you want to use a different API, you’ll need to change the App class.

So they proposed this.

Snippet 2
#!/usr/bin/env python3
# encoding: utf-8

"""Currency converter application using dependency inversion."""
from abc import ABC


class CurrencyConverter(ABC):
    def convert(self, from_currency, to_currency, amount) -> float:
        pass

class FXConverter(CurrencyConverter):
    def convert(self, from_currency, to_currency, amount) -> float:
        print('Converting currency using FX API')
        print(f'{amount} {from_currency} = {amount * 1.2} {to_currency}')
        return amount * 1.2 # The tutorial seems to have a typo here. 

class App:
    def __init__(self, converter: CurrencyConverter):
        self.converter = converter

    def start(self):
        self.converter.convert('EUR', 'USD', 100)

if __name__ == '__main__':
    converter = FXConverter()
    app = App(converter)
    app.start()

Question

To me, the quotation does not seem to ring true, and that goes to the heart of why I cannot wrap my head around the DIP. Even if FXConverter uses a different exchange API (suppose bloomberg instead of Oanda) would not the change stay localised to the convert method? So long as the convert method maintains the signature

convert(str, str, float)->float # The strings must be valid currency names 

the App.start should be happy. The necessity of maintaining this valid method signature is

  • not done away even in the DIP version.
  • automatically enforced in a more type-safe language like Rust or C++. In a stricter language, probably I would enum the range of possible currencies to make sure the string variable is not free form like US$ or £ etc.

That is why I am failing to see how the DIP contributes to better decoupling at all, when what we actually need is adherence to the function/method signature as in a statically typed language?

In general, when A invokes B (object methods, or functions etc.), can we assume that

  • A is unaware of the internal workings of B
  • B is unaware of what A does with the result

so that the desired decoupling is automatically enforced?


Solution

  • Ok, so first of all note that your two code snippets are not exactly equivalent. The second one attaches converter to self. While it is minor difference, you should note this only distracts from the main point. They could both be implemented in any way (either attaching to self or not) and the concept of decoupling and DI will stay the same. That being said attaching dependencies to self in a constructor is a typical approach in DI.

    Second difference is that the first code builds FXConverter inside itself. This leads to problems that now FXConverter is an internal detail, and so it is impossible to gracefully replace it with anything else. Unless you know everything about both App and FXConverter. That's why these are "coupled" now. Dependencies should be passed, instead of build internally.

    But even this is not the real point. Consider second snippet but with slightly modified constructor:

    def __init__(self, converter: FXConverter):
        self.converter = converter
    

    so I declare converter to be FXConverter now. This leads to the same issues. The constructor's signature does not allow to pass different implementation, you are still coupled with FxConverter, which is a concrete implementation.

    In all those snippets App actually depends on CurrencyConverter interface/abstract class. And that is fine. It is normal to write code that depends on interfaces and/or concrete signatures. It is bad to write code that depends on concrete implementation.

    So it is all about converter variable, not really about .convert('EUR', 'USD', 100) call.


    Example.

    One concrete advantage is: say you want to write automatic tests for App. But the converter does some heavy thing, for example it makes network calls to retrieve current exchange rate. You don't want to make such calls in tests, it will slow down everything significantly, especially when you want to test like 10000 cases. Plus with external resource such tests are not necessarily deterministic.

    But more importantly: you don't want to know what FxConverter really does. I mean you do care ultimately, but for the purpose of testing you don't. You don't want to think whether it does HTTP call internally or not. It doesn't matter to App. App only cares about input/output, not about internals of FxConverter. Besides, performance can be tweaked later if necessary.

    So what can you do? With snippet 1 (or even with my modified constructor) you can't do anything, unless you know internals of App and know how to hardcore mock (actually to duck type) internals so that it works. But then you have a leakage of implementation - every small change in the internals may affect tests. For example, say App adds caching. You now have to modify tests. Because of mocks that for example check the number of .convert() calls, which is unfortunately a very popular way of testing things. Even though this might only be an optimization that does not affect output.

    You won't believe how often I had to rewrite tens of tests only because I made a small change in implementation that didn't affect input/output at all. Quoting colonel Kurtz: "the horror... the horror..."

    Now with the second snippet, you write your own implementation of CurrencyConverter. One that operates in memory, in deterministic way. And then you instantiate App with that implementation. The constructor is declared to accept any CurrencyConverter implementation, so it declares to work just fine with your test implementation. Now you can run tests and everything works as expected. Heck, you can even pass mock as CurrencyConverter if you don't want to bother with test implementation. The point is that you are no longer limited to patching.

    Of course ultimately you will still need integration tests, to ensure that concrete implementations are not only doing what they claim they do but that they also work well together. However you will need such tests anyway. And integration tests typically take lots of time. But structuring code in decoupled way increases coding performance. For example you can run fast initial tests like 500 times a day, and integration tests only once per week (during final build for example) and have high confidence that they will pass.

    Btw, this way of testing is also necessary with other languages that have poor (or no at all) support for mocking. Like most strongly typed languages.

    That is why I am failing to see how the DIP contributes to better decoupling at all, when what we actually need is adherence to the function/method signature as in a statically typed language?

    Yes, this is exactly what DIP is about: adherence to signatures. Not to concrete implementations:

    Coupled variant: A calls B. So A depends on B.

    Decoupled variant: A depends and calls interface C, B implements C and is passed to A as dependency. Both A and B depend on abstract interface C, but not on each other.

    The point is that interfaces don't do anything, they are just declarations, rich signatures. Concrete implementations do things. It is better to not have that as dependency.

    Finally, whether a language is statically typed or not is not really relevant. The very same thing can be said about C++ (pass abstract class instead of concrete one) or Rust (pass trait instead of struct). You won't see DI often being used with low level languages like C++ or Rust, because (runtime) DI is not actually free. It requires virtual calls (which often kills optimizations like inling), some bookkeeping as well. The performance hit is very little, but non-zero. With high level app (say webserver) that won't be noticable. But when you use C++ or Rust, then probably you write something that requires performance and you do care about it a lot and want to squeeze everything you can from that lemon. That's what those languages were designed for. That being said it is still ok and preferable to use DI in non performance critical sections.