This content originally appeared on Level Up Coding - Medium and was authored by Pavel Fokin
Clean Architecture in Python
How to implement polymorphic interfaces within applications.
Polymorphism is often explained as part of the OOP paradigm. But Python has other powerful features to implement abstractions than object-oriented design. There are a few main ways to create polymorphic interfaces within an application. And choosing the proper way becomes important when we want to implement clean and powerful architecture.
Theory
Polymorphism and Type System are the main features of a language that allows us to communicate ideas into a code. Actually, polymorphism helps us to make code reusable, testable, and maintainable.
Let’s start with some formal definition from Wikipedia.
Polymorphism is the provision of a single interface to entities of different types or the use of a single symbol to represent different types.
The main kinds of polymorphism are:
- Ad hoc polymorphism
- Parametric
- Subtyping
- Row polymorphism
- Duck typing
Ad hoc variant goes to the languages with a possibility to overload functions, C for example. Parametric is for the languages that have generics and type inference (C++, Haskell). We can use row polymorphism in Typescript. Python has the two of these types. They are Subtyping and Duck typing.
Showcase
For example, let come up with a simple showcase of some storage. This storage can add an item and get it by its primary key. This case is a business case, and we do not know what storage we have. It can be a database or even another service. We do not worry about these details.
Subtyping/subclassing
Classes and OOP design often appear when we start talking about patterns and architecture of code. Inheritance allows new objects to be defined from existing ones. And by defining the hierarchy of classes, we can model complex relationships between objects in our domain.
Lets define abstract class Storage that has an interface to add and to get items.
When we define class, along with that, we introduce a new type in our code. We can add type hinting to the function use_storage(storage: Storage). And use type checkers, for instance, mypy or pyre.
Then we define two specific classes Memory and Persistent which are subclassed from our abstract Storage.
These two classes inherit methods and properties of their parent class Storage. We can use them anywhere the Storage is expected and pass the instances of these classes to our use_storage function.
What are the pros and cons of defining abstract Storage and introduce hierarchy of classes?
Pros:
- define interface to show methods we have to implement for Storage;
- we can use type checkers.
Cons:
- we don’t really need it.
Abstractions have two sides, on one hand, they help to hide complexity, from another, they add complexity. And it is important to keep balance.
In this case, I would say that it is “You Aren’t Gonna Need It” (YAGNI) than about real benefits. What if we remove the abstract class and will go only with the specific implementations? That leads us to the duck typing.
Duck typing with classes
As being a highly dynamic language, some of Python’s principles exist in the form of agreement. Encapsulation of the private attributes in classes is a classic example. There is an agreement that methods or attributes started with an underscore(_) are private. Also, we can remember import this that defines the philosophy of the whole language. We may say that duck typing is also kind of agreement.
If it acts like a duck, then it must be a duck.
Duck typing in Python is the row polymorphism not related to a type. In this case, the object’s behavior is determined by its structure. This means that we can provide any entity that contains the methods or the attributes we expect.
Practically, when we define a class we define an interface. In this sense, that we define how we can interact with an instance of that class.
In our example, we get rid of abstract class and inheritance and leave only our specific classes.
These implementations still follow the same interface and can be used in the same use_storage function.
Pros:
- make things simpler.
Cons:
- no class with explicit interface.
Classes are a great tool to model real-world behavior in our code. But sometimes, we don’t have an internal state that should be encapsulated and/or changed, or some entity will exist as a single instance. There are cases when classes become over-engineering.
We can go further and remove classes.
Duck typing with modules
An entity is not required to be a class to be polymorphic. Modules can also follow the rule of duck typing. We can define interfaces on the higher level by using modules, their hierarchy, and imports.
Let’s move forward by defining a directory structure for our example.
storage/
__init__.py
memory.py
persistent.py
Then move out implementations respectively.
And we can use it in the same function use_storage.
import storage.memory
import storage.persistent
use_storage(storage.memory)
# or
use_storage(storage.persistent)
These objects have all the advantages.
Pros:
- they are testable, with a help of the unittest.mock library;
- they can follow encapsulation, with the usage of the imports you can hide details inside a module;
- you have Singleton pattern by default.
Cons:
- no inheritance for a modules, but it can be replaced with a composition in some cases.
One more advantage is that you can have an expressive and well-named structure in application. Defining high-level code flow by the modules and use them explicitly in a code. I think this is what Robert Martin called Screaming Architecture.
So what does the architecture of your application scream? When you look at the top-level directory structure, and the source files in the highest-level package, do they scream “Health Care System,” or “Accounting System,” or “Inventory Management System”? Or do they scream “Rails,” or “Spring/ Hibernate,” or “ASP”?
Robert. C. Martin. Clean Architecture. p. 196
Conclusion
There are plenty of programming concepts in Python, and a developer can choose a functional, object-oriented, or procedural paradigm depending on current tasks. There is no universal advice.
The same is with polymorphic interfaces, we can choose any of the ways to implement entities and their relationships in the code. Clean, expressive, and extensible are the features we want to see as the result. Abstractions can hide complexity, KISS and YAGNI can help stay clear, and remember that
Simple is better than complex.
Code examples can be found in my GitHub repo py-polymorphism-study.
Hidden Power of Polymorphism in Python was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Pavel Fokin
Pavel Fokin | Sciencx (2021-03-02T01:24:23+00:00) Hidden Power of Polymorphism in Python. Retrieved from https://www.scien.cx/2021/03/02/hidden-power-of-polymorphism-in-python/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.