背景
在我们日常工作中,代码写着写着就出现下列的一些臭味。但是还好我们有SOLID这把‘尺子’, 可以拿着它不断去衡量我们写的代码,除去代码臭味。这就是我们要学习SOLID原则的原因所在。
设计的臭味
僵化性
具有联动性,动一处,会牵连到其他地方
脆弱性
不敢改动,动一处,全局瘫痪
顽固性
不易改动
粘滞性
耦合性太高
不必要的复杂性
代码设计过于复杂
不必要的重复
提高复用性,减少重复
晦涩性
代码设计不易理解
SRP-单一职责原则
一个类只做一件事情。当然一件事情,不是说类中只有一个方法。而是类中的方法都是属于同一种职责。
不能因为第二职责的原因去改动这个类。
一个很好的例子:在我们封装request库时,我们需要实现以下4个方法。
class MyRequestClient:
def post(self:
pass
def get(self:
pass
def update(self:
pass
def delete(self:
pass
#上面的方法就是属于同一职责。 如何还有其他的方法,那么这个类就不符合单一职责原则。
#例增加以下方法:
def get_db_data(self:
pass
def to_object(self:
pass
OCP-开放封闭原则
对扩展开放,对修改封闭。
无需改动自身代码,就可以扩展它的行为。
对类的改动往往是新增代码就可以了,而不是去修改原有的代码。
使用子类继承、依赖注入、数据驱动的方法可以实现OCP原则。
首先我们来看一个违反OCP原则的例子。
#bad code
def circle_draw(:
print(f"this is circle draw"
def square_draw(:
print(f"this is square draw"
def draw_all_shape(shapes:
for shape in shapes:
if shape == "circle":
circle_draw(
if shape == "square":
square_draw(
这段代码的问题是如果再有新的类型需要draw, 我们需要修改函数来适配新的类型。
依赖注入实现OCP原则
我们定义了一个抽象类Shape,子类Square和Circle继承Shape. 并且在子类中重写了父类的方法。函数draw_all_shape是绘制所有图形。
from typing import List
from abc import ABCMeta, abstractmethod
class Shape(metaclass=ABCMeta:
@abstractmethod
def draw(self:
pass
class Square(Shape:
def draw(self:
print(f"this is square draw"
class Circle(Shape:
def draw(self:
print(f"this is circle draw"
def draw_all_shape(shapes: List[Shape]:
for shape in shapes:
shape.draw(
我们定义了一个抽象类Shape,子类和继承. 并且在子类中重写了父类的方法。函数是绘制所有图形。
参数注入实现OCP原则
def circle_draw(:
print(f"this is circle draw"
def square_draw(:
print(f"this is square draw"
def draw_all_shape_by_function(data: Dict[str,Callable]:
for key,value in data.items(:
value(
data = {
"circle": circle_draw,
"square": square_draw
}
draw_all_shape_by_function(data=data
Conclusion
这样的设计的好处是,如果需要再绘制一个三角形,那么我们只需要增加一个新类并继承.无需修改类和就可以实现三角形类的绘制。
当我们在类中或函数中需要使用大量的if-else逻辑判断时,很有可能代码就违反了OSP原则。
LSP:Liskov 替换原则
派生类应该可以替换父类中的方法使用,而不会改变程序原本的功能。
派生类重写方法的参数应该和父类的保持一致或多于父类,不能少于父类。
派生类重写方法的返回值必须和父类返回值类型一致。
违反LSP原则,通常也会违反OSP原则。
首先我们来看一段违法LSP的例子
from typing import Iterable
class User(:
def __init__(self, user: str -> None:
self.user = user
def disable(self -> None:
print(f"{self.user} disable!"
class Admin(User:
def __init__(self, user: str = "Admin" -> None:
self.user = user
def disable(self:
raise "Admin do not disable!"
def delete_user(users: Iterable[User]:
for user in users:
user.disable(
当执行时,就会抛出 错误,类中方法违法了LSP替换原则。
Optimize
#Good
from typing import Iterable
class User(:
def __init__(self, user: str -> None:
self.user = user
def allow_disable(self:
return True
def disable(self -> None:
print(f"{self.user} disable!"
class Admin(User:
def __init__(self, user: str = "Admin" -> None:
self.user = user
def allow_disable(self:
return False
def delete_user(users: Iterable[User] -> None:
for user in users:
if user.allow_disable:
user.disable(
Conclusion
上例中通过添加 的方法,解决了Admin类不能的问题。
当派生类不正确的重写父类方法的时候,就会违反LSP原则,我们在继承类的时候重写方法的时候,尤其- 要注意是否违反了LSP原则。
DIP 依赖倒置原则
程序中所有的依赖都应该终止于抽象类或接口。
任何类都不应该从具体类派生。
任何方法都不易应该重写它的任何基类已经实现了的方法。
高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
首先看一个违反DIP原则的例子:
class Lamp:
def turn_on(self:
print("turn on the lamp"
def turn_off(self:
print("turn off the lamp"
class Button(:
def __init__(self -> None:
self.lamp = Lamp(
def turn_on(self:
return self.lamp.turn_on(
def turn_off(self:
return self.lamp.turn_off(
当有一天,button需要控制televsion时,就需要修改Button类。和 具有强耦合关系。所以,当Lamp变动时,会影响到Button类。违法了DIP原则的高层模块依赖于底层模块。
Optimize
定义一个抽象类 Button 和 Lamp 都依赖这个抽象类。 解决了和 具有强耦合的问题。
class ElectricAppliance(metaclass=ABCMeta:
@abstractmethod
def turn_on(self:
pass
@abstractmethod
def turn_off(self:
pass
class Lamp(ElectricAppliance:
def turn_on(self:
print("turn on the lamp"
def turn_off(self:
print("turn off the lamp"
class Television(ElectricAppliance:
def turn_on(self:
print("turn on the televison"
def turn_off(self:
print("turn off the televison"
class Button:
def __init__(self, electric_appliance: ElectricAppliance -> None:
self.electric_appliance = electric_appliance
def turn_on(self:
self.electric_appliance.turn_on(
def turn_off(self:
self.electric_appliance.turn_off(
conclusion
要确定代码是否违反了DIP原则,需要观察一个类中是否嵌入了调用其他类或函数。如果是,那么很可能是违反了DIP原则。
ISP 接口隔离原则
客户应该不依赖它不使用的方法。
一个类只做一件事。
首先来看一个违反ISP原则的例子:
class Animal(metaclass=ABCMeta:
@abstractclassmethod
def run(self:
pass
@abstractclassmethod
def speak(self:
pass
@abstractclassmethod
def fly(self:
pass
class Dog(Animal:
def run(self:
return "Dog Running"
def speak(self:
return "Dog Speaking"
def fly(self:
raise TypeError("Dog can not fly"
class Bird(Animal:
def run(self:
raise TypeError("Bird can not run"
def speak(self:
return "Bird Speaking"
def fly(self:
return "Bird fly"
def fly_animal(animals: Iterable[Animal]:
for animal in animals:
animal.fly(
当我们执行时,就会抛出的错误。此时Animal抽象类是一个胖类,违法了ISP原则。
Optimize
将Animal抽象类分解为三个新抽象类,FlyingAnimal, TalkingAnimal, RunningAnimal, 底层代码按需继承。
#good
class FlyingAnimal(metaclass=ABCMeta:
@abstractclassmethod
def fly(self:
pass
class RunningAnimal(metaclass=ABCMeta:
@abstractclassmethod
def run(self:
pass
class TalkingAnimal(metaclass=ABCMeta:
@abstractclassmethod
def talk(self:
pass
class Dog(RunningAnimal,TalkingAnimal:
def run(self:
return "Dog Running"
def talk(self:
return "Dog Speaking"
class Bird(FlyingAnimal, TalkingAnimal:
def talk(self:
return "Bird Speaking"
def fly(self:
return "Bird fly"
def fly_animal(animals: Iterable[FlyingAnimal]:
for animal in animals:
print(animal.fly(
conclusion
接口隔离原则看似和单一职责原则相似,单一职责原则是针对模块,类,方法的设计。接口隔离原则更注重在调用者的角度,按需提供接口。
写更小的类,大多数情况下是个好主意。
违反ISP原则也可能会违反LSP原则和SRP原则。
当子类重写了一个不需要的方法时,很可能违反了ISP原则。