If an engineer is asked to build a software project from scratch, or improvise the existing project, how he/she designs the plan, i.e. whether she thinks of entities or behavior? Technically both approaches can solve the given problem. How to pick one, let us see! We at our office have many design debates every day, and this article is a side effect of one of our recent meetings. In this article, I discuss the separation of concerns and all possible ways to do that. We finally see what the benefits/drawbacks of using modules vs. classes are.
Python encourages modules for namespacing:
Modules + Functions ≥ Classes
moreover, Java encourages interfaces for code level agreements and classes for namespacing
Classes + Interfaces > Modules + Functions
In this article, I am targeting Python developers, who are developing software and want to explore various design concepts.
You can find the code samples here:
Let us say we have a software project which deals with many subsystems (technical requirements) like:
We have to implement the project in Python. If our project needs to use all of these to achieve some useful purpose, there are two ways of modeling the code.
Thinking entities is thinking a system as an entity in software and treating it as a single point of contact for performing operations related to that entity.
Behavioral thinking is first trying to figure out all possible actions of software and grouping them under the type of behavior.
After inspecting the above requirements, let us have a few files called get, put, and, delete which has the business logic for our projects. Since Python holds namespacing in modules, we can create modules for all of these functions.
pythonicBehavioral/
get.py
put.py
delete.py
main.py
tests.py
__init__.py
delete.py
# imports ...
def helper_for_delete(*args, **kwargs):
pass
def delete_from_s3(*args, **kwargs):
print("deleting key from s3")
def delete_from_db(*args, **kwargs):
print("deleting a row from db")
get.py
# imports...
def read_from_s3(*args, **kwargs):
print("reading the file from S3")
def read_from_db(*args, **kwargs):
print("query the rows from db")
def decode_file(*args, **kwargs):
print("working on decoding the artifact")
put.py
# imports ...
def write_to_s3(*args, **kwargs):
print("writing file into s3")
def write_to_db(*args, **kwargs):
print("inserting rows into db")
def encode_audio_file(*args, **kwargs):
print("encoding the audio file")
def encode_text_file(*args, **kwargs):
print("encoding the text file")
We defined functions in modules(namespaces) according to the behavior (outward/inward/some other). Now we can use them in our main program by importing them.
main.py
# some imports
import os
from get import read_from_s3
from delete import delete_from_s3
from put import encode_audio_file
def main(key):
read_from_s3()
# some logic
delete_from_s3()
# some logic
encode_audio_file()
if __name__ == '__main__':
main(os.environ.get("key"))
Here, we imported functions from the modules and used them to build our logic in the main program. This design is pretty good in terms of unit testing because everything is a function. To write tests, we can import all functions into our tests.py and write a TestCase and mocks**.** A simple and straight forward unit testing(skipping tests file in the above code samples for brevity).
Note: You can run the project with
$ python3 pythonicBehavioral/main.py
Using Python ≥ 3.7.2 here
Now let us convert our behavioral design into entity based. A random developer from our meeting saw above design and shouted, “if I need to add a new subsystem, I have to modify three files to add new behaviors.” Yes, that is the drawback of the above system. To add a new subsystem with a set of behaviors you should modify many modules because modules are the namespaces.
The behavior-based design is “start small.”
For all who worked on Java/Spring Boot knows how designing a spring boot app enforces you to have three separate responsibilities.
This design has the obvious benefit of separation of duties. Controller developers do not know how service is going to do magic behind. It is a perfect example of abstraction, one of the import OOP principles. If we visualize how we can break thoughts into entities instead of behaviors, it looks like below
In this picture, Handler 1 may not know what Artifact service is doing behind the scenes. Above design achieves the separation of concerns based on entities. It is precisely how spring boot MVC encourages. Sadly Python doesn’t have types and interfaces. So we need to use Abstract classes & abstract class methods for writing entity based programs in Python. Let us implement for S3, DB, and Artifact.
entityBased/
services.py
enums.py
main.py
tests.py
__init__.py
Let us how the refactored project looks like:
rawenums.py
from enum import Enum # Standard library
class StorageType(Enum):
S3 = "s3"
DB = "db"
class ArtifactType(Enum):
AUDIO = "audio"
TEXT = "text"
main.py
# some other imports
import os
from services import StorageService, ArtifactService
def main(key):
s3_service = StorageService.get_storage("s3") # or "db"
audio_service = ArtifactService.get_artifact("audio")
# Use methods on those objects as per program logic
s3_service.read()
audio_service.encode()
if __name__ == '__main__':
main(os.environ.get("key"))
services.py
# some other imports
from abc import ABC, abstractmethod # Standard library
from enums import StorageType, ArtifactType
class Service(object):
pass
class StorageService(Service, ABC):
# Methods every child service should implement
@abstractmethod
def read(self):
pass
@abstractmethod
def write(self):
pass
@staticmethod
def __get(type):
return {
StorageType.S3.value: S3Service,
StorageType.DB.value: DBService
}.get(type)
# Factory for services
@classmethod
def get_storage(cls, type):
storage_class = cls.__get(type) # Get storage type
storage_instance = storage_class()
if not issubclass(storage_class, cls):
raise Exception(cls, " ", "interface is not satisfied")
return storage_instance
class S3Service(StorageService):
def read(self):
print("reading the file from S3")
def write(self):
print("writing the file into S3")
class DBService(StorageService):
def read(self):
print("query the rows from DB")
def write(self):
print("inserting rows into DB")
class ArtifactService(Service, ABC):
# Methods every child service should implement
@abstractmethod
def decode(self):
pass
@abstractmethod
def encode(self):
pass
@staticmethod
def __get(type):
return {
ArtifactType.AUDIO.value: AudioArtifactService,
ArtifactType.TEXT.value: TextArtifactService
}.get(type)
# Factory for services
@classmethod
def get_artifact(cls, type):
artifact_class = cls.__get(type) # Get storage type
artifact_instance = artifact_class()
if not issubclass(artifact_class, cls):
raise Exception(cls, " ", "interface is not satisfied")
return artifact_instance
class AudioArtifactService(ArtifactService):
def decode(self):
print("decoding the audio file")
def encode(self):
print("encoding the audio file")
class TextArtifactService(ArtifactService):
def decode(self):
print("decoding the text file")
def encode(self):
print("encoding the text file")
import os
from services import StorageService, ArtifactService
def main(key):
s3_service = StorageService.get_storage("s3") # or "db"
audio_service = ArtifactService.get_artifact("audio")
# Use methods on those objects as per program logic
s3_service.read()
audio_service.encode()
if __name__ == '__main__':
main(os.environ.get("key"))
In the above program:
$ python3 entityBased/main.py
Using Python ≥ 3.7.2 here
The obvious main benefits of this design are:
I personally like the above design as it is neat and can even be broken into a single module per service class.
The entity-based design is “start big”
Now let us try to find a middle ground solution. After seeing previous designs, why can’t we have modules based on entities?
It means let us have a file for each entity(instead of class)
entityBasedBehavior/
s3.py
db.py
audio.py
text.py
main.py
tests.py
__init__.py
This design is exactly a behavioral pythonic approach with some minor fixes. It only uses modules and functions and brings the benefits of behavioral approach(unit testing friendly) and eliminates some of its drawbacks like the testing subsystems.
Achtung! Exercise: The code for this approach is an exercise to the reader.
The hybrid design is “start small”
It all boils down to personal preferences and an extra thought while choosing an entity-based or behavior-based design for implementing Python software. Both the approaches are decent and do the job and, it is the developer’s option to go for either to “start small” or “start big.”
I hope you enjoyed my article. This article is an output of many of my recent learnings. Use the above design principles even though you are a Node.js developer or a Java Developer.
30s ad
☞ The Python Bible™ | Everything You Need to Program in Python
☞ The Ultimate Python Programming Tutorial
☞ Python for Data Analysis and Visualization - 32 HD Hours !
☞ Hello Python - Python Programming for Beginners
☞ Python Tutorials for Beginners - Learn Python Online
☞ Learn Python in 12 Hours | Python Tutorial For Beginners
☞ Complete Python Tutorial for Beginners (2019)
☞ Python Tutorial for Beginners [Full Course] 2019
☞ Python Programming Tutorial | Full Python Course for Beginners 2019