Best Python program architecture ?
Unfortunately, the most common metaphor for software development is building and construction. […] Rather than construction, software is more like gardening — it is more organic than concrete.[…] You constantly monitor the health of the garden, and make adjustments (to the soil, the plants, the layout) as needed.
(c) The Pragmatic Programmer, Andy Hunt and Dave Thomas
This story shows how does it important to know not only programming languages syntax and rules, but has a knowledge of patterns and paradigms in designing code. Good programmer is not only a mathematician or a person who has logical thinking, it’s also a great story teller !
Before you begin
No matter what type of tasks do you perform. It might be a web backend or a data science. But rules of a good quality architecture have the same implementation in each work branch.
I recommend you to read other my articles to make your code better:
Python is a great language to show the whole beauty of coding. With right implementation it becomes a very powerful tool in your arsenal. Good luck !
Prerequisites
- Installed Python 3.x
- No external packages needed
- Basic knowledge of OOP & programming patterns + knowledge of Python zen (execute import this)
Good or bad code ?
Good programmer is not only a mathematician or a person who has logical thinking, it’s also a great story teller.
If you have worked long enough in programming industry, you must admit that every time you write a code you are passing three interesting stages from “Unbelievable, i’m a genius!” to “Well, the code is fine” to “What a moron did it ?!”.
If you never have had such an experience — you are not growing in your skills.
Also, a good practice is to work with someone’s code. You see that your type of thinking is not only one possible in this world and you are trying to understand it and in this way you grow. So it’s a really good practice.
But how to differ a good code from a bad one ?
In my opinion, a good code must have next peculiarities:
- It must be written like a story: soft start, next you see the main parts and read every part like a short story that consist of other smaller sentences. Each block of a code has simple, single and understandable meaning (SRP).
- Documentation and comments: only important and necessary comments are allowed. Do not need to write “bread” on a bread, agree ? In my opinion every programmer must tend to write a code that doesn’t need any comments, all the naming and code syntax can tell it itself. But simple docstring is needed for each function, class and file. In case of a good code it might be redundant, but it will reduce a lot of time for the person who will work with this code after you.
- Easy to support, modify and scale: every code must be written such a way when you receive a task like: “add feature X, but remove feature Y and make feature Z works for billion of users instead of few thousands”. You spend a minimum of possible time to implement it. It includes easy way of adding new features, removing old features or scaling your application.
You can follow these ten simple signs to define your code quality. Your code is good if it is …
- Debuggable — while you debugging, it’s must be easy and fast.
- Loggable — debug is good, but how do you want to debug a production server on a run ? Code must have logs and these logs must be understandable
- Testable — tests must be present, but to write them you need minimum of efforts and not to change your code for tests specially !
- Fast-failable — If some part of the code can fail, you need to discover this case in a very short term.
- Idempotent — “if you do this again it won’t matter.” — tells us Andrew.
- Intelligible — means your code must be easy to read not only for you, but for your colleagues too.
- Modifiable — also means “easy to make fixes and changes on demand”
- Documentable — good code must document itself, but use proper naming always.
- Modular — your code must consist of particular modules with high cohesion inside them. Means module “sql” should do only SQL queries and nothing more like to send mail and load images.
- Buildable — how many times you have cloned others repos from GitLab or GitHub, but spent days to at least set it up ? This means code is not buildable. If you do a code, it must be able to run “from the box”.
Best architecture
There are a lot of ways to build a good architecture. Even cool to use such a words combination object-oriented design.
General Responsibility Assignment Software Patterns (or Principles), abbreviated GRASP is a good start to design a perfect application.
But there is one good rule you can use to design best architectures.
Best software design has high cohesion and loose coupling.
What doe these terms mean ?
Cohesion — is a degree to which the elements inside a module belong together. May be high or low.
Coupling — is a degree to which the module components have knowledge of other definitions of other separate components. Subareas include the coupling of classes, interfaces, data, and services. May be loose or tight.
On the image below you can see two examples. On the top line of the image (a)is an example of high cohesion and loose coupling what is good. The bottom line (b) is an example of low cohesion and tight coupling what is bad.
Let’s go through it.
Cohesion
High cohesion means all parts of your module or class move around only one main idea. For example you have to design a class (or classes) to describe a user and its behaviour.
To describe cohesion think about it like about a football team with a high fighting spirit where each player knows a role and what to do and happy to be in this team.
While designing your classes/modules think of every module like “do all members of this class enjoy being here ?”.
High cohesion in Python class will be like:
class User:
def __init__(self, name, age):
self.__name = name
self.__age = age
@property
def age(self):
return self.__age
@age.setter
def age(self, value):
self.__age = value
@property
def name(self):
return self.__name
@name.setter
def name(self, value):
self.__name = value
class UserActions:
def __init__(self):
self._phrase = "My name is {name} and I am {age} years old"
def tell_phrase(self, user):
print(self._phrase.format(name=user.name, age=user.age))
user = User(name="John", age=25)
actions_provider = UserActions()
actions_provider.tell_phrase(user)
Class “User” has only data describing attributes and methods to set or get them. Class “UserActions” has only one function to interact with “User” class instance. If we want to add a new behaviour to our user we need to add a new function to “UserActions” class, but not to change “User” class.
What about low cohesion ? Low cohesion you can easily compare with opposite to a good team a bad one, but there is another one example. Low cohesion is like an organisation with a toxic boss and toxic atmosphere inside the collective. No one knows what to do. All the tasks being performed by a long and complex procedure and what the most important, in case of emergency like fire all the organisation will burn. The same in coding.
Low cohesion Python class example will look like :
class User:
def __init__(self):
self.name = None
self.age = None
self._phrase = "My name is {name} and I am {age} years old"
def define_name_and_age(self, name, age):
self.name = name
self.age = age
def send_mail_of_the_phrase(self, other_user):
mail_client = MailClient()
phrase = self.get_phrase()
mail_client.send(phrase, to=other_user)
def get_phrase(self):
return self._phrase.format(name=self.name, age=self.age)
def tell_phrase(self):
print(self.get_phrase())
user = User()
user.tell_phrase() # oops 'My name is None and I am None years old'
user.define_name_and_age(name="Alex", age=37)
user.tell_phrase() # My name is Alex and I am 37 years old
Probably the code with low cohesion will be shorter in lines, but it will be messy, unsupportive, hard to modify and hard to understand.
Even you see, I used proper naming and all the Python best practices, but low cohesion ruin my perfect code.
So use high cohesion in your designs !
Coupling
Coupling means almost the same the cohesion does, but for interaction between modules, not inside them.
We discovered it’s better to have a high cohesion inside your module, class or function rather than low one. But in case with interaction between modules it’s better to have the number of connections as minimum as possible. It means loose coupling.
Loose coupling is like a well done organisation. Every department is a module and know the duties. In case of performing a task, there is one and only one possible way to do it.
For example you have 3 classes: buyer, seller and deals manager.
Buyer wants to buy products and seller can sell them. But their relations is under control of a deals manager.
This simple task requires good knowledge in designing patterns. But you can do it only in case you minimise the coupling connections and make it loose.
Loose coupling between Python classes will look like :
class Person:
def __init__(self, goods, money=0):
"""
:param goods: list
:param money: int
"""
self.money = money
self.goods = goods
class Buyer(Person):
def buy(self, product_name, price):
"""
:param product_name: str
:param price: int
:return: None
"""
self.goods.append(product_name)
self.money -= price
class Seller(Person):
def sell(self, product_name, price):
"""
:param product_name: str
:param price: int
:return: None
"""
product_position = self.goods.index(product_name)
del self.goods[product_position]
self.money += price
class DealsManager:
def __init__(self):
self._prices = {"apples": 10, "bread": 5}
def sell_a_product(self, product_name, buyer, seller):
"""
:param product_name: str
:param buyer: Buyer class instance
:param seller: Seller class instance
:return: buyer instance, seller instance, str(response)
"""
if product_name not in seller.goods:
return buyer, seller, "Seller has no such product named {}".format(product_name)
product_price = self._prices.get(product_name)
if product_price is None:
return buyer, seller, "Product {} is not managed by the DealsManager, sorry".\
format(product_name)
if product_price > buyer.money:
return buyer, seller, "Buyer has not enough money"
self._perform_a_deal(buyer, product_name, product_price, seller)
return buyer, seller, "Success"
@staticmethod
def _perform_a_deal(buyer, product_name, product_price, seller):
buyer.buy(product_name, product_price)
seller.sell(product_name, product_price)
deals_manager = DealsManager()
buyer_john = Buyer(goods=[], money=100)
seller_mike = Seller(goods=["apples", "bread"], money=100)
buyer_john, seller_mike, response = deals_manager.sell_a_product(
"apples", buyer_john, seller_mike
)
print("Deal status is {}".format(response))
print("Seller has {} dollars and {} in goods".format(seller_mike.money, seller_mike.goods))
print("Buyer has {} dollars and {} in goods".format(buyer_john.money, buyer_john.goods))
This is a one of many possible solutions for this task. DealsManager calls buy() or sell() functions from Buyer and Seller classes correspondently and nothing more. It means one possible connection for each class. And interaction with DealsManager is simple too. Just pass buyer and seller and a product name. Here is only one moment — DealsManager is a monopolist that dictates market prices, but it’s ok :)
What about tight coupling ? Oh, here we go.
You have to download a text, next to remove all letters “a” from it and save it. We will have three classes: Loader, Processor and Saver.
Tight coupling between Python classes will look like :
class Saver:
def __init__(self, file):
self.filename = file
def save(self, text):
with open(self.filename, 'w') as file:
file.write(text)
class DataFlow:
def __init__(self, file):
self.saver = Saver()
self.filename = file
self.text = ""
def load(self):
with open(self.filename, 'r') as file:
self.text = file.read()
class Processor:
def __init__(self, letter, filename):
self.letter_to_remove = letter
self.loader = DataFlow(filename)
def process_text(self):
self.loader.load()
text = self.loader.text
filtered_text = ""
for char in text:
if char == self.letter_to_remove:
continue
filtered_text += char
self.loader.saver.save(filtered_text)
If you think it is a good code, re-read it twice. It is a crazy mess that will harm everyone who will try to modify it or scale.
Don’t do like this, use only loose coupling !
Conclusion
What we can say now ?
We know how to differ good and bad code and have 10 rules to define it.
We also know how to make good program architecture by following a simple rule: high cohesion and loose coupling.
Saw few examples of a code how to do and how not to.
Remember to always grow not only basic syntax skills and PEP8 rules, but things like GRASP, SRP, KISS, SOLID, YAGNI etc. With this knowledge you will be much more cooler coder than you was before.
So good luck and all the best wishes !