2021年6月3日 星期四

[OO 文章收集] SOLID: The First 5 Principles of Object Oriented Design

 Source From Here

Introduction
SOLID 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:
* S - Single-responsiblity Principle
* O - Open-closed Principle
* L - Liskov Substitution Principle
* I - Interface Segregation Principle
* D - Dependency Inversion Principle

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:
A class should have one and only one reason to change, meaning that a class should have only one job.

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:
  1. class Square:  
  2.     def __init__(self, length):  
  3.         self.length = length  
For circles, you will need to know the radius:
  1. class Circle:  
  2.     def __init__(self, radius):  
  3.         self.radius = radius  
Next, create the AreaCalculator class and then write up the logic to sum up the areas of all provided shapes. The area of a square is calculated by length squared. The area of a circle is calculated by pi times radius squared:
  1. import math  
  2.   
  3. class AreaCalculator:  
  4.     def __init__(self, shapes):  
  5.         self.shapes = shapes  
  6.           
  7.     def area_sum(self):  
  8.         sum_of_area = 0  
  9.         for s in self.shapes:  
  10.             if isinstance(s, Square):  
  11.                 sum_of_area += pow(s.length, 2)  
  12.             elif isinstance(s, Circle):  
  13.                 sum_of_area += pow(s.radius, 2) * math.pi  
  14.                   
  15.         return sum_of_area  
  16.       
  17.     def __repr__(self):  
  18.         return self.str()  
  19.       
  20.     def __str__(self):  
  21.         return f"Area sum of {len(self.shapes)} shape(s) is {self.area_sum()}"  
To use the AreaCalculator class, you will need to instantiate the class and pass in an array of shapes and display the output at the bottom of the page. Here is an example with a collection of three shapes:
  1. shapes = [Circle(2), Square(5), Square(6)]  
  2. ac = AreaCalculator(shapes)  
  3. print(ac)  
Output:
Area sum of 3 shape(s) is 73.56637061435917

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.
The problem is with the output method that makes AreaCalculator handles the logic to output the data.

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:
  1. import json  
  2.   
  3. class SumCalculatorOutputter:  
  4.     def __init__(self, calculator:AreaCalculator):  
  5.         self.calculator = calculator  
  6.           
  7.     def json(self):  
  8.         return json.dumps({'sum': round(self.calculator.area_sum(), 1)}, indent=4)  
  9.       
  10.     def text(self):  
  11.         return f"Area sum is {self.calculator.area_sum():.01f}"  
The SumCalculatorOutputter class would work like this:
  1. sc_output = SumCalculatorOutputter(ac)  
  2. print(f"JSON output: {sc_output.json()}")  
  3. print(f"TEXT output: {sc_output.text()}")  
Output:
  1. JSON output: {  
  2.     "sum"73.6  
  3. }  
  4. TEXT output: Area sum is 73.6  
Now, the logic you need to output the data to the user is handled by the SumCalculatorOutputter class. That satisfies the single-responsibility principle.

Open-Closed Principle
Open-closed Principle (OCP) states:
Objects or entities should be open for extension but closed for modification.

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:
  1. def area_sum(self):  
  2.     sum_of_area = 0  
  3.     for s in self.shapes:  
  4.         if isinstance(s, Square):  
  5.             sum_of_area += pow(s.length, 2)  
  6.         elif isinstance(s, Circle):  
  7.             sum_of_area += pow(s.radius, 2) * math.pi  
  8.               
  9.     return sum_of_area  
Consider a scenario where the user would like the area_sum of additional shapes like triangles, pentagons, hexagons, etc. You would have to constantly edit this file and add more if/else blocks. That would violate the open-closed principle.

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:
  1. class Square:  
  2.     def __init__(self, length):  
  3.         self.length = length  
  4.           
  5.     def area(self):  
  6.         return pow(self.length, 2)  
And here is the area method defined in Circle:
  1. class Circle:  
  2.     def __init__(self, radius):  
  3.         self.radius = radius  
  4.           
  5.     def area(self):  
  6.         return math.pi * pow(self.radius, 2)  
The refined AreaCalculator:
  1. class AreaCalculator:  
  2.     def __init__(self, shapes):  
  3.         self.shapes = shapes  
  4.           
  5.     def area_sum(self):  
  6.         sum_of_area = 0  
  7.         for s in self.shapes:  
  8.             sum_of_area += s.area()  
  9.                   
  10.         return sum_of_area  
Now, you can create another shape class and pass it in when calculating the sum of area without breaking the code.

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:
  1. from abc import ABC, abstractmethod  
  2.   
  3. class ShapeInterface(ABC):  
  4.     @abstractmethod  
  5.     def area(self):  
  6.         raise NotImplementedError()  
Next is to modify your shape classes to implement the ShapeInterface:
  1. class Square(ShapeInterface):  
  2.     def __init__(self, length):  
  3.         self.length = length  
  4.           
  5.     def area(self):  
  6.         return pow(self.length, 2)  
  7.   
  8. class Circle(ShapeInterface):  
  9.     def __init__(self, radius):  
  10.         self.radius = radius  
  11.           
  12.     def area(self):  
  13.         return math.pi * pow(self.radius, 2)  
From AreaCalculator, you can check if the shapes provided are actually instances of the ShapeInterface; otherwise, throw an exception:
  1. from typing import List  
  2. import math  
  3.   
  4. class AreaCalculator:  
  5.     def __init__(self, shapes:List[ShapeInterface]):  
  6.         self.shapes = shapes  
  7.           
  8.     def area_sum(self):  
  9.         sum_of_area = 0  
  10.         for s in self.shapes:  
  11.             if isinstance(s, ShapeInterface):  
  12.                 sum_of_area += s.area()  
  13.             else:  
  14.                 raise Exception(f"Unexpected class={s.__class__}")  
  15.                   
  16.         return sum_of_area  
That satisfies the open-closed principle.

Liskov Substitution Principle
Liskov Substitution Principle states:
Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.

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:
  1. class Cuboid(ShapeInterface):  
  2.     def __init__(self, width, height, depth):  
  3.         self.width, self.height, self.depth = width, height, depth  
  4.           
  5.     def area(self):  
  6.         return self.width * self.height  
  7.       
  8. class VolumeCalculator(AreaCalculator):  
  9.     def __init__(self, shapes):  
  10.         self.shapes = shapes  
  11.           
  12.     def area_sum(self):  
  13.         sum_of_area = 0.0  
  14.         for s in self.shapes:  
  15.             sum_of_area = s.area()  
  16.               
  17.         return sum_of_area  
Recall that the SumCalculatorOutputter class resembles this:
  1. class SumCalculatorOutputter:  
  2.     def __init__(self, calculator:AreaCalculator):  
  3.         self.calculator = calculator  
  4.           
  5.     def json(self):  
  6.         return json.dumps({'sum': round(self.calculator.area_sum(), 1)}, indent=4)  
  7.       
  8.     def text(self):  
  9.         return f"Area sum is {self.calculator.area_sum():.01f}"  
If you tried to run an example like this:
  1. shapes = [Circle(2), Square(5), Square(6)]  
  2. solid_shapes = [Cuboid(123)]  
  3. ac = AreaCalculator(shapes)  
  4. vc = VolumeCalculator(solid_shapes )  
  5.   
  6. sc_out1 = SumCalculatorOutputter(ac)  
  7. sc_out2 = SumCalculatorOutputter(vc)  
  8.   
  9. # Ok  
  10. print(sc_out1.json())  
  11. # Error: TypeError: type list doesn't define __round__ method  
  12. print(sc_out2.json())  
You will get an TypeError error informing you of an array to round operation. To fix this, instead of returning a list from the VolumeCalculator class area_sum method, return float instead:
  1. class VolumeCalculator(AreaCalculator):  
  2.     def __init__(self, shapes):  
  3.         self.shapes = shapes  
  4.           
  5.     def area_sum(self):  
  6.         sum_of_area = 0.0  
  7.         # logic to calculate the volumes and then return an array of output  
  8.         for s in self.shapes:  
  9.             sum_of_area = s.volume()  
  10.               
  11.         return sum_of_area  
That satisfies the Liskov substitution principle.

Interface Segregation Principle
Interface segregation principle states:
A client should never be forced to implement an interface that it doesn’t use, or clients shouldn’t be forced to depend on methods they do not use.

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:
  1. from abc import ABC, abstractmethod  
  2.   
  3. class ShapeInterface(ABC):  
  4.     @abstractmethod  
  5.     def area(self):  
  6.         raise NotImplementedError()  
  7.       
  8.     @abstractmethod  
  9.     def volume(self):  
  10.         raise NotImplementedError()  
Now, any shape you create must implement the volume method, but you know that squares are flat shapes and that they do not have volumes, so this interface would force the Square class to implement a method that it has no use of.

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:
  1. class ShapeInterface(ABC):  
  2.     @abstractmethod  
  3.     def area(self):  
  4.         raise NotImplementedError()  
  5.   
  6. class ThreeDimensionalShapeInterface(ABC):          
  7.     @abstractmethod  
  8.     def volume(self):  
  9.         raise NotImplementedError()  
  10.   
  11. class Cuboid(ShapeInterface, ThreeDimensionalShapeInterface):  
  12.     def __init__(self, width, height, depth):  
  13.         self.width, self.height, self.depth = width, height, depth  
  14.           
  15.     def area(self):  
  16.         return self.width * self.height  
  17.       
  18.     def volume(self):  
  19.         return self.width * self.height * self.depth  
That satisfies the interface segregation principle.

Dependency Inversion Principle
Dependency inversion principle states:
Entities must depend on abstractions, not on concretions. It states that the high-level module must not depend on the low-level module, but they should depend on abstractions.

This principle allows for decoupling.

Here is an example of a PasswordReminder that connects to a MySQL database:
  1. class MySQLConnection:  
  2.     def connect(self):  
  3.         return 'Database connection'  
  4.       
  5.       
  6. class PasswordReminder:  
  7.     def __init__(self, db_connection):  
  8.         self.db_connection = db_connection  
First, the MySQLConnection is the low-level module while the PasswordReminder is high level, but according to the definition of D in SOLIDwhich states to Depend on abstraction, not on concretions. This snippet above violates this principle as the PasswordReminder class is being forced to depend on the MySQLConnection class.

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:
  1. class DBConnectionInterface(ABC):  
  2.     @abstractmethod  
  3.     def connect(self):  
  4.         raise NotImplementedError()  
  5.       
  6. class MySQLConnection(DBConnectionInterface):  
  7.     def connect(self):  
  8.         return 'Database connection'  
  9.       
  10.       
  11. class PasswordReminder:  
  12.     def __init__(self, db_connection:DBConnectionInterface):  
  13.         self.db_connection = db_connection  
This code establishes that both the high-level and low-level modules depend on abstraction.

Supplement
Software Design - Introduction to SOLID Principles in 8 Minutes
Design Patterns in Plain English | Mosh Hamedani

沒有留言:

張貼留言

[Git 常見問題] error: The following untracked working tree files would be overwritten by merge

  Source From  Here 方案1: // x -----删除忽略文件已经对 git 来说不识别的文件 // d -----删除未被添加到 git 的路径中的文件 // f -----强制运行 #   git clean -d -fx 方案2: 今天在服务器上  gi...