Source From Here
IntroductionSOLID is an acronym for the first five object-oriented design (OOD) principles by Robert C. Martin (also known as Uncle Bob).
These principles establish practices that lend to developing software with considerations for maintaining and extending as the project grows. Adopting these practices can also contribute to avoiding code smells, refactoring code, and Agile or Adaptive software development.
SOLID stands for:
In this article, you will be introduced to each principle individually to understand how SOLID can help make you a better developer.
Single-Responsibility Principle
Single-responsibility Principle (SRP) states:
For example, consider an application that takes a collection of shapes—circles, and squares—and calculates the sum of the area of all the shapes in the collection. First, create the shape classes and have the constructors set up the required parameters.
For squares, you will need to know the length of a side:
- class Square:
- def __init__(self, length):
- self.length = length
- class Circle:
- def __init__(self, radius):
- self.radius = radius
- import math
- class AreaCalculator:
- def __init__(self, shapes):
- self.shapes = shapes
- def area_sum(self):
- sum_of_area = 0
- for s in self.shapes:
- if isinstance(s, Square):
- sum_of_area += pow(s.length, 2)
- elif isinstance(s, Circle):
- sum_of_area += pow(s.radius, 2) * math.pi
- return sum_of_area
- def __repr__(self):
- return self.str()
- def __str__(self):
- return f"Area sum of {len(self.shapes)} shape(s) is {self.area_sum()}"
- shapes = [Circle(2), Square(5), Square(6)]
- ac = AreaCalculator(shapes)
- print(ac)
Now let's consider a scenario where the output should be converted to another format like JSON.
All of the logic would be handled by the AreaCalculator class in this design. This would violate the single-responsibility principle. The AreaCalculator class should only be concerned with the sum of the areas of provided shapes. It should not care whether the user wants JSON or HTML.
To address this, you can create a separate SumCalculatorOutputter class and use that new class to handle the logic you need to output the data to the user:
- import json
- class SumCalculatorOutputter:
- def __init__(self, calculator:AreaCalculator):
- self.calculator = calculator
- def json(self):
- return json.dumps({'sum': round(self.calculator.area_sum(), 1)}, indent=4)
- def text(self):
- return f"Area sum is {self.calculator.area_sum():.01f}"
- sc_output = SumCalculatorOutputter(ac)
- print(f"JSON output: {sc_output.json()}")
- print(f"TEXT output: {sc_output.text()}")
- JSON output: {
- "sum": 73.6
- }
- TEXT output: Area sum is 73.6
Open-Closed Principle
Open-closed Principle (OCP) states:
This means that a class should be extendable without modifying the class itself.
Let’s revisit the AreaCalculator class and focus on the area_sum method:
- def area_sum(self):
- sum_of_area = 0
- for s in self.shapes:
- if isinstance(s, Square):
- sum_of_area += pow(s.length, 2)
- elif isinstance(s, Circle):
- sum_of_area += pow(s.radius, 2) * math.pi
- return sum_of_area
A way you can make this area_sum method better is to remove the logic to calculate the area of each shape out of the AreaCalculator class method and attach it to each shape’s class. Here is the area method defined in Square:
- class Square:
- def __init__(self, length):
- self.length = length
- def area(self):
- return pow(self.length, 2)
- class Circle:
- def __init__(self, radius):
- self.radius = radius
- def area(self):
- return math.pi * pow(self.radius, 2)
- class AreaCalculator:
- def __init__(self, shapes):
- self.shapes = shapes
- def area_sum(self):
- sum_of_area = 0
- for s in self.shapes:
- sum_of_area += s.area()
- return sum_of_area
However, another problem arises. How do you know that the object passed into the AreaCalculator is actually a shape or if the shape has a method named area?
Coding to an interface is an integral part of SOLID. Create a ShapeInterface that supports area:
- from abc import ABC, abstractmethod
- class ShapeInterface(ABC):
- @abstractmethod
- def area(self):
- raise NotImplementedError()
- class Square(ShapeInterface):
- def __init__(self, length):
- self.length = length
- def area(self):
- return pow(self.length, 2)
- class Circle(ShapeInterface):
- def __init__(self, radius):
- self.radius = radius
- def area(self):
- return math.pi * pow(self.radius, 2)
- from typing import List
- import math
- class AreaCalculator:
- def __init__(self, shapes:List[ShapeInterface]):
- self.shapes = shapes
- def area_sum(self):
- sum_of_area = 0
- for s in self.shapes:
- if isinstance(s, ShapeInterface):
- sum_of_area += s.area()
- else:
- raise Exception(f"Unexpected class={s.__class__}")
- return sum_of_area
Liskov Substitution Principle
Liskov Substitution Principle states:
This means that every subclass or derived class should be substitutable for their base or parent class.
Building off the example AreaCalculator class, consider a new VolumeCalculator class that extends the AreaCalculator class:
- class Cuboid(ShapeInterface):
- def __init__(self, width, height, depth):
- self.width, self.height, self.depth = width, height, depth
- def area(self):
- return self.width * self.height
- class VolumeCalculator(AreaCalculator):
- def __init__(self, shapes):
- self.shapes = shapes
- def area_sum(self):
- sum_of_area = 0.0
- for s in self.shapes:
- sum_of_area = s.area()
- return sum_of_area
- class SumCalculatorOutputter:
- def __init__(self, calculator:AreaCalculator):
- self.calculator = calculator
- def json(self):
- return json.dumps({'sum': round(self.calculator.area_sum(), 1)}, indent=4)
- def text(self):
- return f"Area sum is {self.calculator.area_sum():.01f}"
- shapes = [Circle(2), Square(5), Square(6)]
- solid_shapes = [Cuboid(1, 2, 3)]
- ac = AreaCalculator(shapes)
- vc = VolumeCalculator(solid_shapes )
- sc_out1 = SumCalculatorOutputter(ac)
- sc_out2 = SumCalculatorOutputter(vc)
- # Ok
- print(sc_out1.json())
- # Error: TypeError: type list doesn't define __round__ method
- print(sc_out2.json())
- class VolumeCalculator(AreaCalculator):
- def __init__(self, shapes):
- self.shapes = shapes
- def area_sum(self):
- sum_of_area = 0.0
- # logic to calculate the volumes and then return an array of output
- for s in self.shapes:
- sum_of_area = s.volume()
- return sum_of_area
Interface Segregation Principle
Interface segregation principle states:
Still building from the previous ShapeInterface example, you will need to support the new three-dimensional shapes of Cuboid and Spheroid, and these shapes will need to also calculate volume. Let’s consider what would happen if you were to modify the ShapeInterface to add another contract:
- from abc import ABC, abstractmethod
- class ShapeInterface(ABC):
- @abstractmethod
- def area(self):
- raise NotImplementedError()
- @abstractmethod
- def volume(self):
- raise NotImplementedError()
This would violate the interface segregation principle. Instead, you could create another interface called ThreeDimensionalShapeInterface that has the volume contract and three-dimensional shapes can implement this interface:
- class ShapeInterface(ABC):
- @abstractmethod
- def area(self):
- raise NotImplementedError()
- class ThreeDimensionalShapeInterface(ABC):
- @abstractmethod
- def volume(self):
- raise NotImplementedError()
- class Cuboid(ShapeInterface, ThreeDimensionalShapeInterface):
- def __init__(self, width, height, depth):
- self.width, self.height, self.depth = width, height, depth
- def area(self):
- return self.width * self.height
- def volume(self):
- return self.width * self.height * self.depth
Dependency Inversion Principle
Dependency inversion principle states:
This principle allows for decoupling.
Here is an example of a PasswordReminder that connects to a MySQL database:
- class MySQLConnection:
- def connect(self):
- return 'Database connection'
- class PasswordReminder:
- def __init__(self, db_connection):
- self.db_connection = db_connection
Later, if you were to change the database engine, you would also have to edit the PasswordReminder class, and this would violate the open-close principle.
The PasswordReminder class should not care what database your application uses. To address these issues, you can code to an interface since high-level and low-level modules should depend on abstraction:
- class DBConnectionInterface(ABC):
- @abstractmethod
- def connect(self):
- raise NotImplementedError()
- class MySQLConnection(DBConnectionInterface):
- def connect(self):
- return 'Database connection'
- class PasswordReminder:
- def __init__(self, db_connection:DBConnectionInterface):
- self.db_connection = db_connection
Supplement
* Software Design - Introduction to SOLID Principles in 8 Minutes
* Design Patterns in Plain English | Mosh Hamedani
沒有留言:
張貼留言