I am currently reading Clean Architecture by Robert C. Martin (Uncle Bob). In the chapter on dependency inversion, he writes:
Don’t refer to volatile concrete classes. Refer to abstract interfaces instead. This rule applies in all languages, wether statically or dynamically typed.
Of course, this is not always possible. For example, the
str class in Python is a concrete implementation. It would also be ridiculous trying to avoid using it directly. But, depending on
str is not an issue that the dependency inversion principle is really concerned with. This is because the
str class is very stable.
Not all classes and dependencies are as stable as the
str class of the standard library, though. We should especially avoid depending on our own classes that are under current development and constantly changing.
How Can We Avoid Depending on Concrete Implementations?
To use an implementation, you need to know about it. Unless you use a pattern like dependency injection (let somebody else tell you the implementation to use — but that is something for another post) you gonna need to import the concrete class.
To limit the dependency on concrete implementations, we need to create abstract interfaces (or abstract base classes in Python, see the
abc package of the standard library). The concrete implementation inherit from the abstract base class and fill it with functionality (the concrete implementation).
Now, the code that is using the concrete implementation (the consumer) should only rely on the functionality that is defined by the abstract base class.
How Can We Make Sure to Only Use Features of the Abstraction?
We could look only at the abstract class while working with the concrete one and only use the methods and attributes defined in the abstract base class. Yea… that sounds fun. Or we can use a type checker like
In our consumer we can define the instance of the concrete class as being of the abstract base class’ type. If we do so,
mypy will show a warning that we are using an attribute that is not defined by the type we said we were using.
Let’s Implement This
First, we define some abstract class (in
import abc class AbstractBase(abc.ABC): """Abstract class.""" @abc.abstractclassmethod def multiplystring(self, single_string: str, muliplier: int) -> str: """Create long string with multiple versions of given string."""
Next, we create an implementation of the abstract class (in
import abstract class ConcreteClass(abstract.AbstractBase): """Concrete implementation.""" def multiplystring(self, single_string: str, multiplier: int) -> str: return single_string * multiplier def doublestring(self, single_string: str) -> str: return single_string * 2
ConcreteClass actually implements the functionality defined by the abstract base class, but also adds an another method. The consumer of the concrete class should never use the additional methods. If it does, then it depends on the concrete implementation. And that should be avoided as much as possible.
Now, let’s create a consumer (in
consumer.py) that uses the concrete implementation, but should only rely on what is defined in the abstract base class.
import concrete concrete_obj1 = concrete.ConcreteClass() print(concrete_obj1.multiplystring("something", 3)) print(concrete_obj1.doublestring("something"))
No warnings so far. We are using an interface that we shouldn't and
mypy is not warning us.
This is because we have not "told"
mypy yet that we only want to use the "interface" (attributes and methods) defined by the abstract base class.
mypy that we only want to use the functionality of the abstract base class. To do so, we add a type annotation to declare our variable to be of the abstract base class’ type.
import abstract # add this import import concrete ... # define type as abstract base class concrete_obj2: abstract.AbstractBase = concrete.ConcreteClass() print(concrete_obj2.multiplystring("something", 3)) print(concrete_obj2.doublestring("something"))
mypy on this file.
$ mypy consumer.py consumer.py:28: error: "AbstractBase" has no attribute "doublestring" Found 1 error in 1 file (checked 1 source file)
mypy is now warning us, that we are trying to use functionality that is not defined by the type we said we wanted to use.
How Is This Helpful?
While somebody is developing the
ConcreteClass, they might consistently add and remove methods and attributes.
In the meantime, we are working on the consumer and might want to make use of some features that is already part of the concrete class.
If we don’t have an abstract interface in-between the consumer and the concrete implementation, we might start using functionality that is later removed from the concrete implementation.
On the other hand, if we do have an abstract base class, then we on the consumer side know which functions we can rely on using. Also, the abstract base class tells the developers of the concrete implementation which attributes and methods should be included in the concrete implementation. They can extend this functionality, but they can not remove any features (otherwise their class remains abstract and can not be instantiated).
One requirement here is, of course, that the interface is stable. If somebody keeps adding and removing methods and attributes in the abstract base class, then nothing is won. But, it is much easier to keep something abstract stable than it is to keep a concrete implementation stable.
Can We Completely Remove the Reference to the Concrete Implementation?
No, not really. If we want to use something, we need to define its use at some point. Meaning somewhere we need to import it and create the source dependency.
What you can do is remove the dependency from the consumer and let a different layer tell the consumer which concrete class to use. This would be the dependency injection pattern. I will not go into this too deep here, because that is its own huge topic.
Just quick, the dependency injection pattern involves four roles:
- Client — wants to use a service
- Interface — defines how the client wants to use a service
- Service — the object being used by the service
- Injector — creates the service and tells the client to use it
In our example: the client is our consumer, the interface is the abstract base class and the service is the concrete class.
What we don’t have is the injector.
To enable the injection, we would need to change our consumer from a script into a function.
import abstract def consumer_func(obj: abstract.AbstractBaseClass) -> None: obj.multiplystring("something", 3)
Our consumer is now a function and declares that it expects an object of type
abstract.AbstractBaseClass to be passed in. Inside the function we make use of the methods of the abstract base class. If we were to reference methods not defined in it,
mypy would warn us again.
Now where is that object coming from? At this point we would need an additional module that defines the injector. Something like this (in
import consumer import concrete concrete_obj = concrete.ConcreteClass() consumer.consumer_func(concrete_obj)
Instead of running the consumer directly, we are running it through the injector. The injector also tells the consumer which concrete implementation to use.
Why would we want to do this?
The advantages you get by using the dependency injection have mainly to do with flexibility and ease of testing. You could now switch out the concrete implementation without having to change the consumer. This is particularly handy during testing. You could even test the consumer without having any concrete implementation, yet. The Wikipedia article on dependency injection goes into more detail.
I only wanted to mention it here, because it seemed like a natural next step.
Hope you found this article helpful. If I missed something or got this all wrong, or all right, let me know.