Good Engineering Practices for OOD
Solid Principles
S: Single Responsibility Principle (SRP):
- A class should have only one reason to change.
- The following handles both read/write of files and encryption, which violates SRP
class FileManager:
def __init__(self, file_path):
self.file_path = file_path
def read_file(self):
pass
def write_file(self, data):
pass
def encrypt_data(self, data):
pass
def decrypt_data(self, data):
pass
- In this refactored version, the `FileManager` class now focuses solely on file management operations.
class FileManager
def __init__(self, file_path):
self.file_path = file_path
def read_file(self):
pass
def write_file(self, data):
pass
class DataEncryptor:
def encrypt_data(self, data):
pass
def decrypt_data(self, data):
pass
Open/Closed Principle (OCP):
The function animal_sound does not conform to the open-closed principle because it cannot be closed against new kinds of animals. If we add a new animal, Snake, We have to modify the animal_sound function. You see, for every new animal, a new logic is added to the animal_sound function. This is quite a simple example. When your application grows and becomes complex, you will see that the if statement would be repeated over and over again in the animal_sound function each time a new animal is added, all over the application. Goal is to decrease amount of if else statements
class Animal:
def __init__(self, name: str):
self.name = name
def get_name(self) -> str:
pass
animals = [
Animal('lion'),
Animal('mouse')
]
def animal_sound(animals: list):
for animal in animals:
if animal.name == 'lion':
print('roar')
elif animal.name == 'mouse':
print('squeak')
animal_sound(animals):
The Animal class has been enhanced with the addition of the make_sound method. Each animal class extends the Animal class and provides its own implementation of the make_sound method, defining how it produces its unique sound.
In the animal_sound function, we iterate through the array of animals and simply invoke their respective make_sound methods. By following this design, the animal_sound function remains unchanged even when new animals are introduced. We only need to include the new animal in the animal array. This adherence to the Open-Closed Principle ensures that the code is extensible without requiring modifications to existing code.
class Animal:
def __init__(self, name: str):
self.name = name
def get_name(self) -> str:
pass
def make_sound(self):
pass
class Lion(Animal):
def make_sound(self):
return 'roar'
class Mouse(Animal):
def make_sound(self):
return 'squeak'
class Snake(Animal):
def make_sound(self):
return 'hiss'
def animal_sound(animals: list):
for animal in animals:
print(animal.make_sound())
animal_sound(animals):
Liskov Substitution Principle:
This implementation violates the Liskov Substitution Principle because you can't seamlessly replace instances of Rectangle with their Square counterparts.
Imagine someone expects a rectangle object in their code. Naturally, they would assume that it exhibits the behavior of a rectangle, including separate width and height attributes. Unfortunately, the Square class in your codebase violates this assumption by altering the expected behavior defined by the object's interface.
To address this issue and apply the Liskov Substitution Principle, let's introduce a base Shape class and make both Rectangle and Square inherit from it:
class Rectangle
def __init__(self, width, height):
self.width = width
self.height = height
def calculate_area(self):
return self.width * self.height:
class Square(Rectangle)
def __init__(self, side):
super().__init__(side, side)
def __setattr__(self, key, value):
super().__setattr__(key, value)
if key in ("width", "height"):
self.__dict__["width"] = value
self.__dict__["height"] = value:
By adhering to the Liskov Substitution Principle and introducing a common base class (Shape), you ensure that objects of different subclasses can be seamlessly interchanged wherever the superclass is expected. Both Rectangle and Square are now siblings, each with their own set of attributes, initializer methods, and potentially more separate behaviors. The only shared aspect between them is the ability to calculate their respective areas
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def calculate_area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def calculate_area(self):
return self.width * self.height
class Square(Shape):
def __init__(self, side):
self.side = side
def calculate_area(self):
return self.side ** 2d
-
SOLID Principles:
- Single Responsibility Principle (SRP): A class should have only one reason to change.
- Open/Closed Principle (OCP): Software entities should be open for extension but closed for modification.
- Liskov Substitution Principle (LSP): Subtypes should be substitutable for their base types without altering the correctness of the program.
- Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they do not use.
- Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules. Both should depend on abstractions, and abstractions should not depend on details.
-
DRY (Don't Repeat Yourself): Avoid duplication of code by abstracting common functionalities into reusable components or functions. This principle emphasizes the importance of modularity and maintainability.
-
KISS (Keep It Simple, Stupid): Strive for simplicity in design and implementation. Complex solutions should be avoided in favor of simpler, more straightforward ones whenever possible.
-
YAGNI (You Aren't Gonna Need It): Avoid adding unnecessary features or functionality to the software based on speculation about future needs. Only implement features that are necessary based on current requirements.
-
Separation of Concerns (SoC): Divide the software into distinct sections, with each section addressing a separate concern or responsibility. This principle promotes modularity, maintainability, and reusability.
-
Composition Over Inheritance: Prefer composition (building objects by assembling smaller, reusable components) over inheritance (creating new classes by extending existing ones). This approach leads to more flexible and maintainable code.
-
Fail-Fast Principle: Identify and report errors as soon as they occur, rather than allowing them to propagate and potentially cause more significant issues later on. This principle helps in diagnosing and fixing problems quickly.
-
Law of Demeter (LoD): Also known as the principle of least knowledge, this principle states that a module should have limited knowledge about other modules. It promotes loose coupling between components, which enhances maintainability and flexibility.
-
Single Source of Truth (SSOT): Store each piece of information in the system in a single, authoritative location. This ensures consistency and reduces the risk of data inconsistencies.
-
Testing Principles:
- Test-Driven Development (TDD): Write tests before writing code to ensure that the code meets the requirements.
- Unit Testing: Test individual units (e.g., functions, methods, or classes) of code in isolation to ensure they behave as expected.
- Integration Testing: Test the interactions between different units or modules to ensure they work together correctly.
- Continuous Integration (CI) and Continuous Deployment (CD): Automate the process of building, testing, and deploying software to ensure that changes are integrated and deployed smoothly.