五分鐘了解軟件開(kāi)發(fā)的 20 項(xiàng)基本原則
設(shè)計(jì)原則是軟件開(kāi)發(fā)的基礎(chǔ),作為軟件工程師,可以在工具、語(yǔ)言、框架、范式和模式中找到這些設(shè)計(jì)原則,它們是"優(yōu)秀"、"可讀"代碼的核心支柱,一旦理解了這些原則,就可以在任何地方看到。
洞察和應(yīng)用這些基本原則的技能是優(yōu)秀工程師和差勁工程師的區(qū)別所在。如果不了解這些基本原則,任何框架或工具都無(wú)法幫助你提高編寫優(yōu)秀代碼的質(zhì)量。此外,如果不了解這些基本原則,你就會(huì)成為工具的人質(zhì)。
本文并不是參考指南,而是一份將需要不斷刷新的核心原則系統(tǒng)化的清單。
抽象(Abstraction)
[抽象](https://en.wikipedia.org/wiki/Abstraction_(computer_science "抽象")是最重要的一般性原則之一。抽象意味著只關(guān)注重要部分,而忽略其他細(xì)節(jié)。抽象與封裝是相輔相成的,封裝是一種隱藏被抽象部分的實(shí)現(xiàn)的方法。
在軟件開(kāi)發(fā)中,可以將其視為定義一種類型、接口或函數(shù)簽名,作為工作合約。主要好處是不需要知道實(shí)現(xiàn)細(xì)節(jié)就可以使用某些東西,因此可以更好的專注于對(duì)開(kāi)發(fā)者來(lái)說(shuō)至關(guān)重要的東西。
這一原則并不局限于應(yīng)用開(kāi)發(fā)。作為開(kāi)發(fā)者,通過(guò)語(yǔ)言語(yǔ)法從操作系統(tǒng)的底層操作中抽象出來(lái)。反過(guò)來(lái),操作系統(tǒng)通過(guò) CPU、內(nèi)存、網(wǎng)卡等將語(yǔ)言從底層操作中抽象出來(lái)。越深入研究,你就越會(huì)明白這只是一個(gè)抽象問(wèn)題。
封裝變化(Encapsulate what varies)
如你所見(jiàn),抽象可以表現(xiàn)為不同形式--從數(shù)據(jù)(實(shí)現(xiàn))抽象到分層抽象。使用抽象的一般原則是"封裝變化",即確定可能發(fā)生變化的部分,并為其聲明具體接口。這樣,即使內(nèi)部邏輯發(fā)生變化,客戶端仍可進(jìn)行相同的交互。
假設(shè)我們需要計(jì)算貨幣兌換,目前只有兩種貨幣,可以這樣計(jì)算:
if (baseCurrency == "USD" and targetCurrency == "EUR") return amount * 0.90;
if (baseCurrency == "EUR" and targetCurrency == "USD") return amount * 1.90;
但將來(lái)可能會(huì)添加另一種貨幣,這就需要修改客戶端代碼。與其這樣,還不如將所有邏輯抽象并封裝在一個(gè)單獨(dú)的方法中,并在需要時(shí)從客戶端調(diào)用該方法。
function convertCurrency(amount, baseCurrency, targetCurrency) {
if (baseCurrency == "USD" and targetCurrency == "EUR") return amount * 0.90;
if (baseCurrency == "EUR" and targetCurrency == "USD") return amount * 1.90;
if (baseCurrency == "USD" and targetCurrency == "UAH") return amount * 38.24;
…
}
DRY
DRY[2](don't repeat yourself,不要重復(fù)),也被稱為 DIE(duplication is evil,重復(fù)是邪惡的),指的是不應(yīng)該在代碼庫(kù)中重復(fù)信息或知識(shí)。
"每項(xiàng)知識(shí)在系統(tǒng)中都必須有單一、明確、權(quán)威的表示"
--Andy Hunt,Dave Thomas,The Pragmatic Programmer。
減少重復(fù)代碼的好處在于更改和維護(hù)的簡(jiǎn)便性。如果你在多個(gè)地方重復(fù)相同的邏輯,發(fā)現(xiàn)錯(cuò)誤后,很可能會(huì)忘記修改其中的某個(gè)地方,這將導(dǎo)致看似相同的功能出現(xiàn)不同的行為。反之,找到重復(fù)功能,將其抽象為過(guò)程、類等形式,賦予有意義的名字,并在需要的地方使用。這樣做可以實(shí)現(xiàn)單點(diǎn)更改,最大限度減少對(duì)功能的破壞。
KISS
KISS[3](keep it simple、stupid,保持簡(jiǎn)單、愚蠢)一詞是由飛機(jī)工程師Kelly Johnson提出的,他向自己的工程團(tuán)隊(duì)提出挑戰(zhàn),要求他們?cè)O(shè)計(jì)的噴氣式飛機(jī)必須能夠在實(shí)戰(zhàn)條件下由普通機(jī)械師僅使用特定工具進(jìn)行維修。
其主要理念是注重系統(tǒng)的簡(jiǎn)潔性,在只使用真正需要的工具的同時(shí),增加對(duì)系統(tǒng)的理解,減少過(guò)度設(shè)計(jì)。
YAGNI
在設(shè)計(jì)解決方案時(shí),需要考慮兩件事:如何使其更好的適應(yīng)當(dāng)前系統(tǒng),以及如何使其具有可擴(kuò)展性以滿足未來(lái)可能的需求。在第二種情況下,為了更好的可擴(kuò)展性而過(guò)早構(gòu)建功能的愿望通常是錯(cuò)誤的:即使你現(xiàn)在認(rèn)為這樣做可以降低集成成本,但這種代碼的維護(hù)和調(diào)試可能并不容易,而且會(huì)帶來(lái)不必要的復(fù)雜性。這就違反了前面的原則,增加了解決當(dāng)前問(wèn)題的冗余復(fù)雜性。此外別忘了,你所假定的功能很有可能在將來(lái)并不需要,而你只是在浪費(fèi)資源。
這就是 YAGNI (You aren’t gonna need it,你不會(huì)需要它)的意義所在。不要誤解,你應(yīng)該考慮解決方案將來(lái)會(huì)有什么用途,但只有在真正需要的時(shí)候才添加代碼。
LoD
得墨忒耳定律[4](LoD,Law of Demeter),也稱為最少知識(shí)原則,或者"不要與陌生人說(shuō)話"。由于 LoD 通常與 OOP 有關(guān),因此在這種情況下,"陌生人"指的是與當(dāng)前對(duì)象沒(méi)有直接關(guān)聯(lián)的任何對(duì)象。
使用 LoD 的好處在于可維護(hù)性,具體表現(xiàn)為避免無(wú)關(guān)對(duì)象之間的直接接觸。
因此,當(dāng)你與某個(gè)對(duì)象交互時(shí),如果不符合以下情況之一,就違反了這一原則:
- 當(dāng)對(duì)象是某個(gè)類的當(dāng)前實(shí)例時(shí)(通過(guò) this 訪問(wèn))
- 當(dāng)對(duì)象是類的一部分時(shí)
- 當(dāng)對(duì)象通過(guò)參數(shù)傳遞給方法時(shí)
- 當(dāng)對(duì)象在方法中實(shí)例化時(shí)
- 當(dāng)對(duì)象在全局范圍可用時(shí)
舉個(gè)例子,考慮客戶要向銀行賬戶存款的情況。我們最終可能會(huì)有三個(gè)類--Wallet(錢包)、Customer(客戶)和Bank(銀行)。
class Wallet {
private decimal balance;
public decimal getBalance() {
return balance;
}
public void addMoney(decimal amount) {
balance += amount
}
public void withdrawMoney(decimal amount) {
balance -= amount
}
}
class Customer {
public Wallet wallet;
Customer() {
wallet = new Wallet();
}
}
class Bank {
public void makeDeposit(Customer customer, decimal amount) {
Wallet customerWallet = customer.wallet;
if (customerWallet.getBalance() >= amound) {
customerWallet.withdrawMoney(amount);
//...
} else {
//...
}
}
}
可以在 makeDeposit 方法中看到違反 LoD 的行為。從 LoD 的角度訪問(wèn)客戶錢包是正確的(盡管從邏輯角度看這是個(gè)奇怪的行為)。但在這里,銀行對(duì)象調(diào)用了客戶錢包對(duì)象的 getBalance 和 withdrawMoney,因此是在與陌生人(錢包)而不是朋友(客戶)對(duì)話。
下面是修復(fù)方法:
class Wallet {
private decimal balance;
public decimal getBalance() {
return balance;
}
public boolean canWithdraw(decimal amount) {
return balance >= amount;
}
public boolean addMoney(decimal amount) {
balance += amount
}
public boolean withdrawMoney(decimal amount) {
if (canWithdraw(amount)) {
balance -= amount;
}
}
}
class Customer {
private Wallet wallet;
Customer() {
wallet = new Wallet();
}
public boolean makePayment(decimal amount) {
return wallet.withdrawMoney(amount);
}
}
class Bank {
public void makeDeposit(Customer customer, decimal amount) {
boolean paymentSuccessful = customer.makePayment(amount);
if (paymentSuccessful) {
//...
} else {
//...
}
}
}
現(xiàn)在,與客戶錢包的所有交互都通過(guò)客戶對(duì)象進(jìn)行。這種抽象有利于松散耦合,能更輕松的修改Wallet和Customer類內(nèi)部的邏輯(Bank對(duì)象不應(yīng)關(guān)心Customer的內(nèi)部實(shí)現(xiàn))以及測(cè)試。
一般來(lái)說(shuō),當(dāng)一個(gè)對(duì)象有兩個(gè)以上的點(diǎn)時(shí),LoD 就會(huì)失敗,比如 object.friend.stranger 而不是 object.friend。
SoC
關(guān)注點(diǎn)分離[5](SoC,Separation of Concerns)原則建議根據(jù)關(guān)注點(diǎn)的不同將系統(tǒng)分成較小的部分,這里的"關(guān)注點(diǎn)"指的是系統(tǒng)的某個(gè)顯著特征。
例如,如果你正在為某個(gè)領(lǐng)域建模,那么每個(gè)對(duì)象都可以被視為一個(gè)特殊的關(guān)注點(diǎn)。在分層系統(tǒng)中,每一層都有自己的關(guān)注點(diǎn)。在微服務(wù)架構(gòu)中,每個(gè)服務(wù)都有自己的目的。這個(gè)列表可以無(wú)限繼續(xù)下去。
SoC 的主要特點(diǎn)是:
- 確定系統(tǒng)關(guān)注的問(wèn)題;
- 將系統(tǒng)分為不同部分,獨(dú)立解決這些問(wèn)題;
- 通過(guò)明確定義的接口連接這些組件。
在這種方式下,關(guān)注點(diǎn)分離與抽象原則非常相似。遵循 SoC 原則的結(jié)果是:易于理解、模塊化、可重用、基于穩(wěn)定的接口和可測(cè)試代碼。
SOLID
SOLID 原則是Robert Martin提出的五項(xiàng)設(shè)計(jì)原則,旨在明確面向?qū)ο缶幊痰某跏技s束,并使程序更具靈活性和適應(yīng)性。
(1) 單一責(zé)任原則(Single responsibility principle)
"類應(yīng)該有且只有一個(gè)變更的理由。"
換句話說(shuō):
"把因相同原因而變化的事物聚集在一起。把因不同原因而變化的事物分開(kāi)"。
和 SoC 非常像,是吧?這兩個(gè)原則的區(qū)別在于,SRP 的目標(biāo)是類級(jí)分離,而 SoC 則是一種通用方法,同時(shí)適用于高層(如層、系統(tǒng)、服務(wù))和低層(類、函數(shù)等)抽象。
單一責(zé)任原則具有 SoC 的所有優(yōu)點(diǎn),尤其是促進(jìn)了高內(nèi)聚和低耦合,避免了"上帝對(duì)象"的反模式。
(2) 開(kāi)閉原則(Open-closed principle)
"軟件實(shí)體應(yīng)該可以擴(kuò)展,但不可修改。"
實(shí)施新功能時(shí),應(yīng)避免對(duì)現(xiàn)有代碼進(jìn)行破壞性修改。
當(dāng)你可以擴(kuò)展某個(gè)類并添加所需修改時(shí),這個(gè)類就被認(rèn)為是開(kāi)放的。如果某個(gè)類有明確定義的接口,并且將來(lái)不會(huì)改變,即可以被別的代碼所使用,那么這個(gè)類就被認(rèn)為是封閉的。
試想一個(gè)經(jīng)典的 OOP 繼承:你創(chuàng)建一個(gè)父類,然后用附加了功能的子類對(duì)其進(jìn)行擴(kuò)展。然后出于某種原因,你決定改變父類的內(nèi)部結(jié)構(gòu)(例如,添加一個(gè)新字段或刪除某些方法),而這個(gè)結(jié)構(gòu)也可以訪問(wèn)或直接影響派生類。這樣做就違反了這一原則,因?yàn)楝F(xiàn)在你不僅需要修改父類,還需要調(diào)整子類以適應(yīng)新的變化。出現(xiàn)這種情況是因?yàn)闆](méi)有正確應(yīng)用信息隱藏。相反,如果你通過(guò)公共屬性或方法給子類定義穩(wěn)定的契約,那么只要不影響該契約,就可以自由改變內(nèi)部結(jié)構(gòu)。
這一原則鼓勵(lì)客戶端依賴抽象(如接口或抽象類)而非實(shí)現(xiàn)(具體類)。通過(guò)這種方式,依賴抽象的客戶端被認(rèn)為是封閉的,但同時(shí)又是開(kāi)放的,因?yàn)樗蟹铣橄蟮男滦薷亩伎梢詿o(wú)縫集成到客戶端中。
再舉一個(gè)例子。假設(shè)我們正在開(kāi)發(fā)折扣計(jì)算邏輯。到目前為止,只有兩種折扣。在應(yīng)用開(kāi)閉原則之前:
class DiscountCalculator {
public double calculateDiscountedPrice(double amount, DiscountType discount) {
double discountAmount = 15.6;
double percentage = 4.0;
double appliedDiscount;
if (discount == 'fixed') {
appliedDiscount = amount - discountAmount;
}
if (discount == 'percentage') {
appliedDiscount = amount * (1 - (percentage / 100)) ;
}
// logic
}
}
現(xiàn)在,客戶端(DiscountCalculator)依賴于外部 DiscountType。如果添加新的折扣類型,就需要進(jìn)入客戶端邏輯并對(duì)其進(jìn)行擴(kuò)展。這是不可取的行為。
在應(yīng)用了開(kāi)閉原則之后:
interface Discount {
double applyDiscount(double amount);
}
class FixedDiscount implements Discount {
private double discountAmount;
public FixedDiscount(double discountAmount) {
this.discountAmount = discountAmount;
}
public double applyDiscount(double amount) {
return amount - discountAmount;
}
}
class PercentageDiscount implements Discount {
private double percentage;
public PercentageDiscount(double percentage) {
this.percentage = percentage;
}
public double applyDiscount(double amount) {
return amount * (1 - (percentage / 100));
}
}
class DiscountCalculator {
public double calculateDiscountedPrice(double amount, Discount discount) {
double appliedDiscount = discount.applyDiscount(amount);
// logic
}
}
這樣就可以利用開(kāi)閉原則和多態(tài)性,而不是添加多個(gè) if 語(yǔ)句來(lái)確定某些實(shí)體的類型和未來(lái)行為。所有實(shí)現(xiàn) Discount 接口的類在公共 applyDiscount 方法方面都是封閉的,但與此同時(shí),在修改內(nèi)部數(shù)據(jù)時(shí)又是開(kāi)放的。
里氏替代原則(Liskov substitution)
"派生類必須可替代基類。"
或者更正式的表述:
"讓 φ(x) 成為 T 類型對(duì)象 x 的可證明屬性。那么對(duì)于 S 類型的對(duì)象 y,φ(y) 應(yīng)該為真,其中 S 是 T 的子類型"(Barbara Liskov 和 Jeannette Wing,1994 年)
簡(jiǎn)單來(lái)說(shuō),當(dāng)你擴(kuò)展某個(gè)類時(shí),不應(yīng)破壞它所建立的契約。所謂"毀約",是指未能滿足以下要求:
- 不要更改派生類中的參數(shù):子類應(yīng)符合父類的方法簽名,即接受與父類相同的參數(shù),或接受更抽象的參數(shù)。
- 不要改變派生類的返回類型:子類應(yīng)返回與父類相同的類型,或返回更具體的(子類型)參數(shù)。
- 不要在派生類中拋出異常:除非父類拋出異常,否則子類不應(yīng)在其方法中拋出異常。這種情況下,異常類型應(yīng)與父類的異常類型相同或?qū)儆诟割惍惓5淖宇愋汀?/li>
- 不要在派生類中強(qiáng)化先決條件:子類不應(yīng)通過(guò)將其工作限制在某些條件下來(lái)改變預(yù)期客戶端的行為,例如,在父類中,能夠接受任意長(zhǎng)度字符串,但在子類中,只能接受不超過(guò) 100 個(gè)字符的字符串。
- 不要弱化派生類中的后置條件:子類不應(yīng)改變預(yù)期行為,允許減少某些工作,例如,操作后不清理狀態(tài),不關(guān)閉套接字等。
- 不要削弱派生類中的不變性:子類不應(yīng)改變父類中定義的條件,例如,不要重新分配父類的字段,因?yàn)樽宇惪赡軟](méi)有意識(shí)到圍繞該字段的全局邏輯概念。
接口隔離(Interface segregation)
"提供客戶端專用的細(xì)粒度接口。"
任何代碼都不應(yīng)依賴不需要的方法。如果客戶端不使用對(duì)象的某些行為,為什么要強(qiáng)迫它依賴這些行為?同樣,如果客戶端不使用某些方法,為什么要強(qiáng)迫實(shí)現(xiàn)者提供這些功能?
將"胖"的接口分解為更具體的接口。如果更改了具體的接口,不會(huì)影響到無(wú)關(guān)的客戶端。
依賴倒置(Dependency inversion)
"依賴抽象,而非實(shí)現(xiàn)。"
Bob大叔將這一原則描述為嚴(yán)格遵守 OCP 和 LSP:
"在本專欄中,我們將討論 OCP 和 LSP 的結(jié)構(gòu)化定義。嚴(yán)格使用這些原則所產(chǎn)生的結(jié)構(gòu)本身可以概括為一個(gè)原則。我稱之為"依賴倒置原則"(DIP,The Dependency Inversion Principle)"。- Robert Martin
依賴倒置主要包括兩層概念:
- 高層模塊不應(yīng)依賴于低層模塊,兩者都應(yīng)依賴于抽象。
- 抽象不應(yīng)依賴細(xì)節(jié),細(xì)節(jié)應(yīng)該依賴于抽象。
舉例來(lái)說(shuō),假設(shè)我們正在開(kāi)發(fā)一個(gè)用戶管理服務(wù),決定使用 PostgreSQL 作為數(shù)據(jù)持久化。
class UserService {
private PostgresDriver postgresDriver;
public UserService(PostgresDriver postgresDriver) {
this.postgresDriver = postgresDriver;
}
public void saveUser(User user) {
postgresDriver.query("INSERT INTO USER (id, username, email) VALUES (" + user.getId() + ", '" + user.getUsername() + "', '" + user.getEmail() + "')");
}
public User getUserById(int id) {
ResultSet resultSet = postgresDriver.query("SELECT * FROM USER WHERE id = " + id);
User user = null;
try {
if (resultSet.next()) {
user = new User(resultSet.getInt("id"), resultSet.getString("username"), resultSet.getString("email"));
}
} catch (SQLException e) {
e.printStackTrace();
}
return user;
}
// ...
}
目前,UserService 與其依賴關(guān)系(PostgresDriver)緊密耦合。但后來(lái)我們決定遷移到 MongoDB 數(shù)據(jù)庫(kù)。由于 MongoDB 與 PostgreSQL 不同,我們需要重寫 UserService 類中的每個(gè)方法。
解決辦法是引入接口:
interface UserRepository {
void saveUser(User user);
User getUserById(int id);
// ...
}
class UserPGRepository implements UserRepository {
private PostgresDriver driver;
public UserPGRepository(PostgresDriver driver) {
this.driver = driver;
}
public void saveUser(User user) {
// ...
}
public User getUserById(int id) {
// ...
}
// ...
}
class UserMongoRepository implements UserRepository {
private MongoDriver driver;
public UserPGRepository(MongoDriver driver) {
this.driver = driver;
}
public void saveUser(User user) {
// ...
}
public User getUserById(int id) {
// ...
}
// ...
}
class UserService {
private UserRepository repository;
public UserService(UserRepository database) {
this.repository = database;
}
public void saveUser(User user) {
repository.saveUser(user);
}
public User getUserById(int id) {
return repository.getUserById(id);
}
// ...
}
現(xiàn)在,高層模塊(UserService)依賴于抽象模塊(UserRepository),而抽象模塊并不依賴于細(xì)節(jié)(PostgreSQL 的 SQL API 和 MongoDB 的 Query API),而只依賴于為客戶端構(gòu)建的接口。
最后提一下,要實(shí)現(xiàn)依賴反轉(zhuǎn),可以使用依賴注入技術(shù):Demystifying Dependency Injection: An Essential Guide for Software Developers[6]
GRASP
[一般責(zé)任分配原則](https://en.wikipedia.org/wiki/GRASP_(object-oriented_design "一般責(zé)任分配原則"))(GRASP,General Responsibility Assignment Principles)是 Craig Larman 在其著作 Applying UML and Patterns 中提出的9項(xiàng)用于面向?qū)ο笤O(shè)計(jì)的原則。
與 SOLID 類似,這些原則并不是從零開(kāi)始建立,而是在 OOP 的背景下由久經(jīng)考驗(yàn)的編程準(zhǔn)則組成的。
(1) 高內(nèi)聚(High cohesion)
"將相關(guān)功能和職責(zé)集中在一起。"
高內(nèi)聚原則的重點(diǎn)是保持復(fù)雜度可控。在這種情況下,內(nèi)聚度是對(duì)象的職責(zé)緊密程度。如果一個(gè)類的內(nèi)聚度較低,就意味著它在做與其主要目的無(wú)關(guān)的工作,或者在做可以委托給其他子系統(tǒng)的工作。
一般來(lái)說(shuō),高內(nèi)聚設(shè)計(jì)的類只有少量方法,而且方法的功能都高度相關(guān)。這樣做可以提高代碼的可維護(hù)性、可讀性和重用性。
(2) 低耦合(Low coupling)
"減少不穩(wěn)定組件之間的聯(lián)系。"
這一原則旨在降低組件之間的依賴性,從而防止代碼變更帶來(lái)副作用。這里的耦合度是指一個(gè)組件對(duì)另一個(gè)組件的依賴程度(知道或依賴)。
具有高耦合度的程序組件彼此依賴性非常強(qiáng)。如果有一個(gè)耦合度很高的類,它的變化會(huì)導(dǎo)致系統(tǒng)其他部分的局部變化,反之亦然。這樣的設(shè)計(jì)限制了代碼復(fù)用,并且需要花更多時(shí)間來(lái)理解。另一方面,低耦合支持設(shè)計(jì)獨(dú)立性更強(qiáng)的類,從而減少變更帶來(lái)的影響。
耦合和內(nèi)聚原則是相輔相成的。如果兩個(gè)類的內(nèi)聚性很高,那么它們之間的聯(lián)系通常很弱。同樣,如果這些類之間的耦合度較低,顧名思義,它們的內(nèi)聚度也較高。
(3) 信息專家(Information expert)
"以數(shù)據(jù)定責(zé)任。"
信息專家模式回答了應(yīng)該如何分配了解某一信息或完成某些工作的責(zé)任這一問(wèn)題。按照這種模式,可以直接獲取所需信息的對(duì)象被視為該信息的信息專家。
還記得在客戶和銀行之間應(yīng)用得墨忒耳定律的例子嗎?本質(zhì)上是一樣的:
- Wallet 類是了解余額和管理余額的信息專家。
- Customer 類是有關(guān)其內(nèi)部結(jié)構(gòu)和行為的信息專家。
- Bank 類是銀行領(lǐng)域的信息專家。
履行一項(xiàng)職責(zé)往往需要收集系統(tǒng)不同部分的信息。因此,應(yīng)該有中介信息專家。有了他們,對(duì)象就能保存自己的內(nèi)部信息,從而提高封裝性,降低耦合度。
(4) 構(gòu)建者(Creator)
"將構(gòu)建對(duì)象的責(zé)任分配給密切相關(guān)的類。"
誰(shuí)應(yīng)該負(fù)責(zé)構(gòu)建新的對(duì)象實(shí)例?根據(jù)構(gòu)建者模式,要構(gòu)建 x 類的新實(shí)例,構(gòu)建者類應(yīng)具備以下屬性之一:
- 聚合 x;
- 包含 x;
- 記錄 x;
- 密切使用 x;
- 擁有 x 所需的初始化數(shù)據(jù)。
這一原則促進(jìn)了低耦合,因?yàn)槿绻銥槟硞€(gè)對(duì)象找到了合適的構(gòu)建者,即一個(gè)已經(jīng)與該對(duì)象有某種關(guān)聯(lián)的類,就不會(huì)增加彼此之間的關(guān)聯(lián)性。
控制器(Controller)
"將處理系統(tǒng)信息的責(zé)任分配給特定的類。"
控制器是系統(tǒng)對(duì)象,負(fù)責(zé)接收用戶事件并將其委托給領(lǐng)域?qū)印K堑谝粋€(gè)從用戶界面接收服務(wù)請(qǐng)求的元素??刂破魍ǔS糜谔幚眍愃频挠美?,例如 UserController 用于管理用戶實(shí)體交互。
記住,控制器不應(yīng)從事任何業(yè)務(wù)工作??刂破鲬?yīng)盡可能精簡(jiǎn),應(yīng)將工作委托給相應(yīng)的類,而不是自己負(fù)責(zé)。
例如,我們可以在類似 MVC 的設(shè)計(jì)模式中找到控制器。MVC 引入了控制器,作為負(fù)責(zé)處理視圖和模型之間交互的中間部分,而不是讓模型和視圖直接通信。有了這個(gè)組件,模型就不再受外部交互的影響。
(1) 間接(Indirection)
"為了降低耦合,將責(zé)任分配給中介類。"
Butler Lampson 有一句名言:"計(jì)算機(jī)科學(xué)中的所有問(wèn)題都可以通過(guò)加一個(gè)間接層來(lái)解決"。
間接原則與依賴反轉(zhuǎn)原則的理念相同:在兩個(gè)組件之間引入中介,使它們不產(chǎn)生直接交互。這樣做的目的是支持弱耦合,以及其他一些優(yōu)點(diǎn)。
(2) 多態(tài)(Polymorphism)
"當(dāng)類似行為因類型不同而不同時(shí),通過(guò)多態(tài)將職責(zé)分配給不同行為的類型。"
如果看到使用 if/switch 語(yǔ)句檢查對(duì)象類型的代碼,那就說(shuō)明可能欠缺對(duì)多態(tài)性的應(yīng)用。當(dāng)需要加入新功能來(lái)擴(kuò)展代碼時(shí),如果必須在條件檢查的地方添加新的 if 語(yǔ)句,說(shuō)明這是個(gè)糟糕的設(shè)計(jì)。
對(duì)具有類似行為的不同類應(yīng)用多態(tài)性原則,可以統(tǒng)一不同類型,構(gòu)造可互換的軟件組件,每個(gè)組件負(fù)責(zé)特定功能。根據(jù)語(yǔ)言的不同,可以有多種方法,但常見(jiàn)的是實(shí)現(xiàn)相同的接口或使用繼承,特別是為不同對(duì)象的方法賦予相同的名稱。最終可獲得可插拔、易于擴(kuò)展的組件,并且無(wú)需更改無(wú)關(guān)代碼。
與開(kāi)閉原則一樣,正確使用多態(tài)也很重要。只有在確定某些組件將會(huì)或可能發(fā)生變化時(shí),才需要使用多態(tài)。例如,不需要在語(yǔ)言內(nèi)部類或框架之上創(chuàng)建一個(gè)實(shí)現(xiàn)多態(tài)的抽象概念。它們已經(jīng)很穩(wěn)定了,你只是在做無(wú)謂的工作。
(3) 純粹構(gòu)件(Pure Fabrication)
"為了支持高內(nèi)聚,將責(zé)任分配給適當(dāng)?shù)念悺?
有時(shí),為了遵循高內(nèi)聚/低耦合原則,需要實(shí)現(xiàn)現(xiàn)實(shí)世界中并不存在的實(shí)體。從這個(gè)角度看,你是在創(chuàng)造一個(gè)虛構(gòu)的東西,一個(gè)在領(lǐng)域中并不存在的東西。之所以說(shuō)它純粹,是因?yàn)檫@個(gè)實(shí)體的職責(zé)設(shè)計(jì)得很清楚。Craig Larman 建議在信息專家應(yīng)用邏輯錯(cuò)誤時(shí)使用這一原則。
例如,控制器模式就是一種純粹構(gòu)件,DAO 或存儲(chǔ)庫(kù)也是一種構(gòu)件。這些類并不是某些領(lǐng)域獨(dú)有,但卻方便了開(kāi)發(fā)人員。雖然我們可以將數(shù)據(jù)訪問(wèn)邏輯直接放在領(lǐng)域類中(因?yàn)橛羞@方面的專家),但這會(huì)違反高內(nèi)聚,因?yàn)閿?shù)據(jù)管理邏輯與領(lǐng)域?qū)ο蟮男袨榉绞經(jīng)]有直接關(guān)系。同時(shí)因?yàn)樾枰蕾嚁?shù)據(jù)庫(kù)接口,耦合度也會(huì)增加。不同領(lǐng)域?qū)嶓w的數(shù)據(jù)管理邏輯相似,代碼重復(fù)率可能會(huì)很高。換句話說(shuō),這會(huì)導(dǎo)致在一個(gè)地方混合不同的抽象概念。
使用純粹構(gòu)件類的好處是將相關(guān)行為歸類到對(duì)象中,這在現(xiàn)實(shí)世界中是無(wú)可替代的,可以實(shí)現(xiàn)有助于代碼重用的良好設(shè)計(jì),并降低對(duì)不同職責(zé)的依賴性。
(4) 受控變更(Protected variations)
"通過(guò)引入穩(wěn)定的契約來(lái)保護(hù)可預(yù)測(cè)的變更。"
為了在不破壞其他部分的情況下提供未來(lái)的變化,需要引入穩(wěn)定的契約來(lái)阻止不確定的影響。這一原則強(qiáng)調(diào)了前面討論過(guò)的在不同對(duì)象之間分離責(zé)任的原則的重要性:需要通過(guò)間接依賴來(lái)輕松的在不同實(shí)現(xiàn)之間切換,需要通過(guò)信息專家來(lái)決定誰(shuí)應(yīng)該負(fù)責(zé)滿足需求,需要在設(shè)計(jì)系統(tǒng)時(shí)考慮到多態(tài)性,以引入不同的可插拔解決方案,等等。
受控變更原則是一個(gè)核心概念,驅(qū)動(dòng)著其他設(shè)計(jì)模式和原則。
"從一個(gè)層面上講,開(kāi)發(fā)人員或架構(gòu)師的成熟體現(xiàn)在他們對(duì)實(shí)現(xiàn)受控變更的更廣泛機(jī)制的了解不斷增加,能夠選擇值得付出努力的合適的受控變更,并有能力選擇合適的受控變更解決方案。在早期階段,人們學(xué)習(xí)數(shù)據(jù)封裝、接口和多態(tài)性,這些都是實(shí)現(xiàn)受控變更的核心機(jī)制。之后,人們會(huì)學(xué)習(xí)到基于規(guī)則的語(yǔ)言、規(guī)則解釋器、反射和元數(shù)據(jù)設(shè)計(jì)、虛擬機(jī)等技術(shù),所有這些技術(shù)都可以用來(lái)防止某些變更"。- Craig Larman,Applying UML and Patterns。
結(jié)論
我相信,在閱讀本文之前,你已經(jīng)在不知不覺(jué)中運(yùn)用了其中的某些原則?,F(xiàn)在,你知道了更多細(xì)節(jié),溝通起來(lái)就更容易了。
你可能已經(jīng)注意到,其中某些原則有相同的基本理念。本質(zhì)上來(lái)說(shuō),確實(shí)如此。例如,信息專家、SoC、高內(nèi)聚低耦合、SRP、接口隔離等,都有相同的觀點(diǎn),即分離不同軟件組件之間的關(guān)注點(diǎn)。這樣做是為了實(shí)現(xiàn)受控變更、依賴反轉(zhuǎn)和間接依賴,以獲得可維護(hù)、可擴(kuò)展、可理解和可測(cè)試的代碼。
與任何工具一樣,這只是一個(gè)指導(dǎo)方針,而不是嚴(yán)格的規(guī)則。關(guān)鍵是要了解其中的利弊得失,并做出明智的決定。