背景 在我們日常工作中,代碼寫著寫著就出現下列的一些臭味。但是還好我們有SOLID這把‘尺子’, 可以拿著它不斷去衡量我們寫的代碼,除去代碼臭味。這就是我們要學習SOLID原則的原因所在。 設計的臭味 僵化性 具有聯動性,動一處,會牽連到其他地方 脆弱性 不敢改動,動一處,全局癱瘓 頑固性 不易改動 ...
背景
在我們日常工作中,代碼寫著寫著就出現下列的一些臭味。但是還好我們有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, 我們需要修改draw_all_shape
函數來適配新的類型。
依賴註入實現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, 子類Square
和Circle
繼承Shape
. 並且在子類中重寫了父類的方法。函數draw_all_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
- 這樣的設計的好處是,如果需要再繪製一個三角形,那麼我們只需要增加一個新類並繼承
Shape
.無需修改shape
類和draw_all_shape
就可以實現三角形類的繪製。 - 當我們在類中或函數中需要使用大量的if-else邏輯判斷時,很有可能代碼就違反了OCP原則。
LSP:Liskov 替換原則
- 派生類應該可以替換父類中的方法使用,而不會改變程式原本的功能。
- 派生類重寫方法的參數應該和父類的保持一致或多於父類,不能少於父類。
- 派生類重寫方法的返回值必須和父類返回值類型一致。
- 違反LSP原則,通常也會違反OCP原則。
首先我們來看一段違法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()
當執行delete_user
時,就會拋出TypeError
錯誤,Admin
類中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
- 上例中通過添加
allow_disable
的方法,解決了Admin類不能disable
的問題。 - 當派生類不正確的重寫父類方法的時候,就會違反LSP原則,我們在繼承類的時候重寫方法的時候,尤其- 要註意是否違反了LSP原則。
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()
當我們執行fly_animal
時,就會拋出TypeError
的錯誤。此時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原則。
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類。Button
和Lamp
具有強耦合關係。所以,當Lamp變動時,會影響到Button類。違法了DIP原則的高層模塊依賴於底層模塊。
Optimize
定義一個抽象類ElectricAppliance
Button 和 Lamp 都依賴這個抽象類。 解決了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原則。
本文來自博客園,作者:煙熏柿子學編程,轉載請註明原文鏈接:https://www.cnblogs.com/aaron-948/p/16235001.html