(2022.09.28 Wed)
SOLID是計算科學(xué)家Robert C. Martin, a.k.a., Uncle Bob提出的面向?qū)ο笤O(shè)計和模式設(shè)計的原則粘捎,可保證代碼的可懂榴芳、可讀不傅、可測試性("To create understandable, readable, and testable code that many developers can collaboratively work on.")捺檬,即代碼的穩(wěn)定性和擴展性。SOLID原則探討了如何在類中安排函數(shù)和數(shù)據(jù)結(jié)構(gòu),類之間如何聯(lián)系赃泡,或者說面向?qū)ο蟮拇a如何細(xì)分(split up),代碼的哪些部分設(shè)計為面向內(nèi)部或外部鞋真,代碼如何復(fù)用其他代碼等問題崇堰。這里雖用到了"類(class)",但并不代表該原則只適用于面向?qū)ο缶幊躺Аn愂呛瘮?shù)和數(shù)據(jù)的耦合組(coupled grouping)海诲。每個軟件系統(tǒng)都有這樣的組合,可能叫類或其他名字檩互,SOLID原則正是適用于這些組合特幔。
該原則的目標(biāo)是創(chuàng)建中間層軟件架構(gòu)使其
- 可被改變
- 易懂
- 成為其他軟件系統(tǒng)的基本組件(the basis of components)
SOLID
- The Single Responsibility Principle
- The Open-Closed Principle
- The Liskov Substitution Principle
- The Interface Segregation Principle
- The Dependency Inversion Principle
The Single Responsibility Principle, SRP獨立責(zé)任/功能原則
A module should be responsible to one, and only one, actor.
你可能會顧名思義地將其理解為一個函數(shù)能且只能處理一個功能。這里更準(zhǔn)確的理解是
A module should have one, and only one, reason to change.
但考慮到開發(fā)的需求提出者是人闸昨,這個說法的更準(zhǔn)確表述是
A module should be responsible to one, and only one, user or stakeholder.
又考慮到提需求的可能是一組或一類人蚯斯,或某角色的人,故更準(zhǔn)確地說法是
A module should be responsible to one, and only one, actor.
該表述中出現(xiàn)了module(模塊)一詞饵较,其最簡單的定義是一個源文件(source file)拍嵌。然而有的編程語言和開發(fā)環(huán)境不用源文件包含代碼,所以module的描述可以是函數(shù)和數(shù)據(jù)結(jié)構(gòu)的一個集合(cohesive set)循诉。
Case
Uncle Bob給出的案例如下:
有一個類Employee
横辆,其中有三個方法
+
calculatePay
- 財務(wù)部制定,匯報給CFO
+
reportHours
- 人事部制定茄猫,匯報給COO
+
save
- DBA制定狈蚤,匯報給CTO
三個方法放在同一個類中困肩,開發(fā)者將不同部分耦合在一起,會導(dǎo)致潛在的問題脆侮。
比如锌畸,calculatePay
和reportHours
都需要計算非加班工時(non-overtime hours)。開發(fā)者為避免代碼重合他嚷,設(shè)計了一個方法regularHours
用于計算非加班工時蹋绽。
現(xiàn)在CFO提出對非加班工時的計算方法提出修改,而COO不需要這樣的修改筋蓖。此時卸耘,開發(fā)者修改regularHours
函數(shù),結(jié)果可滿足CFO要求粘咖,但能會導(dǎo)致COO想要的結(jié)果無法滿足要求蚣抗。
這種問題的發(fā)生源自開發(fā)者將不同角色的代碼放在了一起,而SRP原則建議將不同角色需要/依賴的代碼分開瓮下。
解決方案 -
最簡單的解決方案是將數(shù)據(jù)從Employee
分離翰铡,同時不同的方法單獨創(chuàng)建類。
數(shù)據(jù)保存在類EmployeeData
中讽坏,該類僅僅是包含數(shù)據(jù)的類锭魔,無其他方法。三個操作的類互相獨立且隔離路呜,分別為
PayCalculator
+
calculatePay
HourReporter
+
reportHours
EmployeeSaver
+
save
這種設(shè)計的缺點在于需要實例化三個類迷捧。解決該問題的模式為facade pattern(外觀模式)。該模式簡單來說就是當(dāng)系統(tǒng)中含有多個子系統(tǒng)胀葱,且多個用戶需要分別調(diào)用不同的子系統(tǒng)時漠秋,設(shè)置一個外觀類/接口,該接口同時調(diào)用所有子系統(tǒng)抵屿,用戶可通過該接口按需調(diào)用不同的子系統(tǒng)庆锦。類似于不同的電器各自有一個開關(guān),設(shè)置了一個總開關(guān)控制所有電器轧葛,總開關(guān)即facade搂抒。比如用戶對電腦開機,CPU執(zhí)行freeze朝群、jump和運行三個操作燕耿,內(nèi)存執(zhí)行l(wèi)oad操作,硬盤執(zhí)行read操作姜胖,而用戶無需分別去操作這三個部分誉帅,只需要一個外觀接口,即開關(guān),即可實現(xiàn)對不同硬件部分的操作蚜锨。
EmployeeFacade
+
PayCalculator.calculatePay
+
HourReporter.reportHours
+
EmployeeSaver.save
(2022.09.29 Thu)
The Open-Closed Principle, OCP 開閉原則
A module should be open for extension but closed for modification.
OCP原則被認(rèn)為是在OOP編程中最重要的原則档插。簡單來說,在設(shè)計模塊時需要保證模塊可被擴展亚再,而不對模塊做修改郭膛。
比如,在一個支付系統(tǒng)中氛悬,支付方式可以是信用卡则剃,也可以是借記卡,因此可寫成如下形式的類/模塊
class PaymentProcessor:
def pay_debit(self, order, security_code):
print("Processing debit payment type")
print(f"Verifying security code: {security_code}")
order.status = "paid"
def pay_credit(self, order, security_code):
print("Processing credit payment type")
print(f"Verifying security code: {security_code}")
order.status = "paid"
此時如捅,需要加入新的支付模式棍现,比如在線支付方式,如AliPay镜遣、PayPal等己肮。按現(xiàn)有模式和代碼,需要修改類PaymentProcessor
悲关,違背OCP原則谎僻。
修改方案是用一個基類(PaymentProcessor)來定義底層支付邏輯,之后通過子類的創(chuàng)建寓辱,比如DebitPaymentProcessor來實現(xiàn)具體支付方法艘绍。這樣每當(dāng)添加一種新的支付方式,直接實現(xiàn)一個新的子類即可秫筏。
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def pay(self, order, security_code):
pass
class DebitPaymentProcessor(PaymentProcessor):
def pay(self, order, security_code):
print("Processing debit payment type")
print(f"Verifying security code: {security_code}")
order.status = "paid"
class CreditPaymentProcessor(PaymentProcessor):
def pay(self, order, security_code):
print("Processing credit payment type")
print(f"Verifying security code: {security_code}")
order.status = "paid"
(2022.12.15 Thur)
另一個案例
首先看bad example鞍盗。員工類做為基類,其中包括attributes為name
和salary
跳昼。不同類型的員工繼承該基類,并定義各自的工作方法肋乍,比如tester的工作方法是test
鹅颊,developer的工作方法是develop
。有一個公司類company
墓造,其中對員工的工作方法根據(jù)員工的工種來判斷員工的工作行為堪伍。
class Employee:
def __init__(self, name: str, salary: str):
self.name = name
self.salary = salary
class Tester(Employee):
def __init__(self, name: str, salary: str):
super().__init__(name, salary)
def test(self):
print("{} is testing".format(self.name))
class Developer(Employee):
def __init__(self, name: str, salary: str):
super().__init__(name, salary)
def develop(self):
print("{} is developing".format(self.name))
class Company:
def __init__(self, name: str):
self.name = name
def work(self, employee):
if isinstance(employee, Developer):
employee.develop()
elif isinstance(employee, Tester):
employee.test()
else:
raise Exception("Unknown employee")
之所以認(rèn)為這是一個糟糕案例,想想我們向company中加入新的工種觅闽,比如analyst帝雇。繼承employee
類,定義其工作方法蛉拙,在company
類中又要加入新的判斷尸闸。每次有新的工種加入,company
類就要被修改。
一個改進方案如下
from abc import ABC, abstractmethod
class Employee(ABC):
def __init__(self, name: str, salary: str):
self.name = name
self.salary = salary
@abstractmethod
def work(self):
pass
class Tester(Employee):
def __init__(self, name: str, salary: str):
super().__init__(name, salary)
def test(self):
print("{} is testing".format(self.name))
def work(self):
self.test()
class Developer(Employee):
def __init__(self, name: str, salary: str):
super().__init__(name, salary)
def develop(self):
print("{} is developing".format(self.name))
def work(self):
self.develop()
class Company:
def __init__(self, name: str):
self.name = name
def work(self, employee: Employee):
employee.work()
carbon = Company("Carbon")
developer = Developer("Nusret", "1000000")
tester = Tester("Someone", "1000000")
carbon.work(developer) # Will print Nusret is developing
carbon.work(tester) # Will print Someone is testing
employee
抽象類有個抽象方法work
吮廉,其子類需要實現(xiàn)work
方法苞尝。Developer中的work
方法調(diào)用develop
方法,而tester的work
方法調(diào)用test
方法宦芦。在company
類中宙址,調(diào)用員工的方法僅需要調(diào)用employee
類的work
方法。如果employee
類中新加了analyst
類调卑,進需要在analyst
子類中實現(xiàn)work
方法即可抡砂,而無需對company
方法做任何修改。
The Liskov Substitution Principle, LSP 里式替換
Subclasses should be substitutable for their base classes
派生類(derived class)可替代基類(base class)恬涧,即在派生類替代基類之后注益,本該基類(卻改成調(diào)用派生類)的用戶感受不到異常。
Rectangular-Square問題
從案例入手解釋LSP气破,在本例中類square
不是類rectangle
的適合子類聊浅,因為rectangle
中的高度和長度可以獨立改變,而square
中的兩條邊需要同時改變现使。而調(diào)用rectangle
的用戶如果改為調(diào)用square
則會產(chǎn)生混亂低匙。
有如下測試過程,rectangle
可通過碳锈,但square
無法通過顽冶。
rectangle r = ...
r.setW(5)
r.setH(10)
assert r.area == 50
如果想同時正確調(diào)用rectangle
和square
,需要用戶端對形狀做判斷售碳,并做出相應(yīng)操作强重。考慮到這樣的修改取決于調(diào)用的對象類型贸人,因此對象就是不可替代的(not substitutable)间景。
LSP適用于類、JAVA接口艺智、REST接口等倘要。從架構(gòu)層面理解LSP的最佳方式是看看違背LSP將會帶來什么。
開發(fā)者開發(fā)了出租車分配系統(tǒng)十拣,通過URI調(diào)用不同公司的出租車用于傳輸信息和訂車封拧。不同的公司使用了相同的URI規(guī)則。比如預(yù)約司機John的車可通過如下的URI
url = 'purpletaxi.com/'
/driver/bob
/destination/ord
/pickupTime/1530
/pickupAddr/24maplest
不同公司的API都遵從如上所示的規(guī)則夭问。假設(shè)A公司有個新來的開發(fā)者泽西,不曾閱讀API規(guī)范文檔,將/destination
改成了/dest
缰趋,使得接口不可替代捧杉。這將導(dǎo)致分配系統(tǒng)在遇到A公司的訂單時失敗陕见。(如運行正常將在訂單系統(tǒng)中加入對出租車公司的判斷。)
(2022.12.15 Thur)
另一個案例糠溜。學(xué)校成員有老師淳玩、學(xué)生和管理者。不同成員從成員基類member
繼承生成非竿。
from abc import ABC, abstractmethod
class Member(ABC):
def __init__(self, name: str, age: int):
self.name = name
self.age = age
@abstractmethod
def save_database(self):
pass
class Teacher(Member):
def __init__(self, name: str, age: int, teacher_id: str):
super().__init__(name, age)
self.teacher_id = teacher_id
def save_database(self):
print("Saving teacher data to database")
class Student(Member):
def __init__(self, name: str, age: int , student_id: str):
super().__init__(name, age)
self.student_id = student_id
def save_database(self):
print("Saving student data to database")
下面給成員加入新pay
方法蜕着,支付工資。學(xué)生無法支付红柱,因為他們沒有工資承匣。
from abc import ABC, abstractmethod
class Member(ABC):
def __init__(self, name, age):
self.name = name
self.age = age
@abstractmethod
def save_database(self):
pass
@abstractmethod
def pay(self):
pass
class Teacher(Member):
def __init__(self, name, age, teacher_id):
super().__init__(name, age)
self.teacher_id = teacher_id
def save_database(self):
print("Saving teacher data to database")
def pay(self):
print("Paying")
class Manager(Member):
def __init__(self, name, age, manager_id):
super().__init__(name, age)
self.manager_id = manager_id
def save_database(self):
print("Saving manager data to database")
def pay(self):
print("Paying")
class Student(Member):
def __init__(self, name, age, student_id):
super().__init__(name, age)
self.student_id = student_id
def save_database(self):
print("Saving student data to database")
def pay(self):
raise NotImplementedError("It is free for students!")
運行時將拋出異常,這與Liskov substitution原則不符锤悄。解決這個問題韧骗,將pay
方法從member
類中移除,并創(chuàng)建新類payer
零聚。對teacher
和manager
使用多繼承袍暴,從member
和payer
兩個類。而student
則只繼承member
一個類隶症。
from abc import ABC, abstractmethod
class Payer(ABC):
@abstractmethod
def pay(self):
pass
class Member(ABC):
def __init__(self, name, age):
self.name = name
self.age = age
@abstractmethod
def save_database(self):
pass
class Teacher(Member, Payer):
def __init__(self, name, age, teacher_id):
super().__init__(name, age)
self.teacher_id = teacher_id
def save_database(self):
print("Saving teacher data to database")
def pay(self):
print("Paying")
class Manager(Member, Payer):
def __init__(self, name, age, manager_id):
super().__init__(name, age)
self.manager_id = manager_id
def save_database(self):
print("Saving manager data to database")
def pay(self):
print("Paying")
class Student(Member):
def __init__(self, name, age, student_id):
super().__init__(name, age)
self.student_id = student_id
def save_database(self):
print("Saving student data to database")
payers: List[Payer] = [Teacher("John", 30, "123"), Manager("Mary", 25, "456")]
for payer in payers:
payer.pay()
至此政模,不符合Liskov substitution原則的問題得到解決。
The Interface Segregation Principle, ISP 接口隔離原則
Many client specific interfaces are better than one general purpose interface
ISP是支持COM一類組件底層的技術(shù)(enabling tech supporting component substrates such as COM)蚂会,可使得組件和類更有用和便攜(portable)淋样。
ISP的核心比較簡單。類如果有多個用戶胁住,則針對每個用戶創(chuàng)建專用接口趁猴,僅調(diào)用該用戶使用的方法,而不是在一個類中調(diào)用所有用戶需要的所有方法彪见。
比如儡司,三個用戶都需要調(diào)用OPS
類的操作,設(shè)user1只用OPS
類中的op1
方法余指,user2只用op2
方法枫慷,user3只用op3
方法。
假設(shè)OPS
類是用Java實現(xiàn)的類浪规。user1的源碼因為疏忽,依賴了op2
和op3
探孝。一旦op2
的源碼變更笋婿,則user1需要重新編譯和重新部署,即便變更的部分和user1的業(yè)務(wù)無關(guān)顿颅。
這個問題可通過隔離的方法解決缸濒。OPS
類不變,派生子類U1OPS
,派生類中只有op1
方法庇配,U1OPS
專供user1只用斩跌,user1不再直接以來OPS
類。此時OPS
類的變更將不會導(dǎo)致user1的重編譯和重部署(redeployed)捞慌。
簡言之耀鸦,用戶需要什么就提供什么,不需要的一概不提供啸澡。
The Dependency Inversion Principle, DIP 依賴倒置原則
Depend upon Abstractions. Do not depend upon concretions.
按照DIP原則袖订,最靈活的系統(tǒng)是其中的源代碼僅依賴抽象(abstractions)而非具體類或具體實現(xiàn)(concretions)。
在Java一類的靜態(tài)語言(statically typed)中嗅虏,use
洛姑、import
和include
等命令只能指向包含接口抽象類和其他抽象聲明的源代碼模塊,不依賴任何具體類(concrete)皮服。在動態(tài)類型語言中楞艾,如Ruby和Python,這個原則同樣適用龄广。源碼依賴不該引用具體模塊(concrete modules)硫眯,但在動態(tài)語言中定義具體模塊相對復(fù)雜,特別地蜀细,可定義為其中函數(shù)被調(diào)用的模塊舟铜。
當(dāng)然,將這個想法作為標(biāo)準(zhǔn)并不現(xiàn)實奠衔,因為軟件系統(tǒng)一定會依賴很多具體實現(xiàn)谆刨。比如Java的String
類是具體而且很難迫使它改為抽象類。對具體類java.lang.string
的調(diào)用也不可且不該避免归斤。String
類非常穩(wěn)定痊夭,修改該類的行為很罕見且被嚴(yán)格控制。開發(fā)者無需焦慮對String
的頻繁修改脏里∷遥基于此,我們傾向于在實踐DIP時忽略操作系統(tǒng)和平臺的穩(wěn)定性迫横。容忍具體依賴因為信任其不變和穩(wěn)定番舆。不穩(wěn)定的具體類(volatile concrete)才是應(yīng)該避免依賴的,也就是那些處在活躍開發(fā)和頻繁更新的類矾踱。
案例如前面提到的支付類
class PaymentProcessor:
def pay_debit(self, order, security_code):
print("Processing debit payment type")
print(f"Verifying security code: {security_code}")
order.status = "paid"
def pay_credit(self, order, security_code):
print("Processing credit payment type")
print(f"Verifying security code: {security_code}")
order.status = "paid"
類初始化時恨狈,security_code
不當(dāng)設(shè)置成短信驗證、郵箱驗證等特定的方式呛讲,而應(yīng)設(shè)置為一個抽象類禾怠,在類中判斷驗證類型返奉。
class Authorizer(ABC):
@abstractmethod
def is_authorized(self) -> bool:
pass
class SMSAuth(Authorizer):
authorized = False
def verify_code(self, code):
print(f"Verifying code {code}")
self.authorized = True
def is_authorized(self) -> bool:
return self.authorized
class PaymentProcessor(ABC):
@abstractmethod
def pay(self, order):
pass
class DebitPaymentProcessor(PaymentProcessor):
def __init__(self, security_code, authorizer: Authorizer):
self.security_code = security_code
self.authorizer = authorizer
self.verified = False
def pay(self, order):
if not self.authorizer.is_authorized():
raise Exception("Not authorized")
print("Processing debit payment type")
print(f"Verifying security code: {self.security_code}")
order.status = "paid"
DIP同樣可以應(yīng)用于微服務(wù)開發(fā)。比如服務(wù)間的直接通信可由message bus或消息隊列代替吗氏。采用消息隊列時芽偏,服務(wù)將消息放到一個單一的普通位置,而無須介意是哪個服務(wù)會使用該消息弦讽。
Reference
1 Robert C. Martin, Design Principles and Design Patterns
2 Robert C. Martin, Clean Architecture, Prentice Hall
3 RollingStarky, Uncle Bob 的 SOLID 軟件設(shè)計原則——Python 實例講解污尉,簡書
4 SOLID principle with Python, Muhammet Nusret ?zate?, medium