【譯】淺談SOLID原則
SOLID原則是一種編碼的標準,為了避免不良設計,所有的軟體開發人員都應該清楚這些原則。SOLID原則是由Robert C Martin推廣並被廣泛引用於面向物件程式設計中。正確使用這些規範將提升你的程式碼的可擴充套件性、邏輯性和可讀性。
當開發人員按照不好的設計來開發軟體時,程式碼將失去靈活性和健壯性。任何一點點小的修改都非常容易引起bug。因此,我們應該遵循SOLID原則。
首先我們需要花一些時間來瞭解SOLID原則,當你能夠理解這些原則並正確使用時,你的程式碼質量將會得到大幅的提高。同時,它可以幫助你更好的理解一些優秀軟體的設計。
為了理解SOLID原則,你必須清楚介面的用法,如果你還不理解介面的概念,建議你先讀一讀這篇
下面我將用簡單易懂的方式為你描述SOLID原則,希望能幫助你對這些原則有個初步的理解。
單一責任原則
一個類只能因為一個理由被修改。
A class should have one,and only one,reason to change.
一個類應該只為一個目標服務。並不是說每個類都只能有一個方法,但它們都應該與類的責任有直接關係。所有的方法和屬性都應該努力做好同一類事情。當一個類具有多個目標或職責時,就應該建立一個新的類出來。
我們來看一下這段程式碼:
public class OrdersReportService {
public List<OrderVO> getOrdersInfo (Date startDate,Date endDate) {
List<OrderDO> orders = queryDBForOrders(startDate,endDate);
return transform(orders);
}
private List<OrderDO> queryDBForOrders(Date startDate,Date endDate) {
// select * from order where date >= startDate and date < endDate;
}
private List<OrderVO> transform(List<OrderDO> orderDOList) {
//transform DO to VO
}
}
複製程式碼
這段程式碼就違反了單一責任原則。為什麼會在這個類中執行sql語句?這樣的操作應該放到持久化層,持久化層負責處理資料的持久化的相關操作,包括從資料庫中儲存或查詢資料。所以這個職責不應該屬於這個類。
transform方法同樣不應該屬於這個類,因為我們可能需要很多種型別的轉換。
因此我們需要對程式碼進行重構,重構之後的程式碼如下(為了節省篇幅):
public class OrdersReportService {
@Autowired
private OrdersReportDao ordersReportDao;
@Autowired
private Formatter formatter;
public List<OrderVO> getOrdersInfo(Date startDate,Date endDate) {
List<OrderDO> orders = ordersReportDao.queryDBForOrders(startDate,endDate);
return formatter.transform(orders);
}
}
public class OrdersReportDao {
public List<OrderDO> queryDBForOrders(Date startDate,Date endDate) {}
}
public class Formatter {
private List<OrderVO> transform(List<OrderDO> orderDOList) {}
}
複製程式碼
開閉原則
對擴充套件開放,對修改關閉。
Entities should be open for extension,but closed for modification.
軟體實體(包括類、模組、函式等)都應該可擴充套件,而不用因為擴充套件而修改實體的內容。如果我們嚴格遵循這個原則,就可以做到修改程式碼行為時,不需要改動任何原始程式碼。
我們還是以一段程式碼為例:
class Rectangle extends Shape {
private int width;
private int height;
public Rectangle(int width,int height) {
this.width = width;
this.height = height;
}
}
class Circle extends Shape {
private int radius;
public Circle(int radius) {
this.radius = radius;
}
}
class CostManager {
public double calculate(Shape shape) {
double costPerUnit = 1.5;
double area;
if (shape instanceof Rectangle) {
area = shape.getWidth() * shape.getHeight();
} else {
area = shape.getRadius() * shape.getRadius() * pi();
}
return costPerUnit * area;
}
}
複製程式碼
如果你想要計算正方形的面積,那麼我們就需要修改calculate方法的程式碼。這就破壞了開閉原則。根據這個原則,我們不能修改原有程式碼,但是我們可以進行擴充套件。
所以我們可以把計算面積的方法放到Shape類中,再由每個繼承它的子類自己去實現自己的計算方法。這樣就不用修改原有的程式碼了。
里氏替換原則
里氏替換原則是由Barbara Liskov在1987年的“資料抽象“大會上提出的。Barbara Liskov和Jeannette Wing在1994年發表了論文對這一原則進行闡述:
如果φ(x)是型別T的屬性,並且S是T的子型別,那麼φ(y)就是S的屬性。
Let φ(x) be a property provable about objects x of type T. Then φ(y) should be true for objects y of type S where S is a subtype of T.
Barbara Liskov給出了易於理解的版本,但是這一版本更依賴於型別系統:
1. Preconditions cannot be strengthened in a subtype. 2. Postconditions cannot be weakened in a subtype. 3. Invariants of the supertype must be preserved in a subtype.
Robert Martin在1996年提出了更加簡潔、通順的定義:
使用指向基類指標的函式也可以使用子類。
Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it.
更簡單一點講就是子類可以替代父類。
根據里氏替換原則,我們可以在接受抽象類(介面)的任何地方用它的子類(實現類)來替代它們。基本上,我們應該注意在程式設計時不能只關注介面的輸入引數,還需要保證介面實現類的返回值都是同一型別的。
下面這段程式碼就違反了里氏替換原則:
<?php
interface LessonRepositoryInterface
{
/**
* Fetch all records.
*
* @return array
*/
public function getAll();
}
class FileLessonRepository implements LessonRepositoryInterface
{
public function getAll()
{
// return through file system
return [];
}
}
class DbLessonRepository implements LessonRepositoryInterface
{
public function getAll()
{
/*
Violates LSP because:
- the return type is different
- the consumer of this subclass and FileLessonRepository won't work identically
*/
// return Lesson::all();
// to fix this
return Lesson::all()->toArray();
}
}
複製程式碼
譯者注:這裡沒想到Java應該怎麼實現,因此直接用了作者的程式碼,大家理解就好
介面隔離原則
不能強制客戶端實現它不使用的介面。
A client should not be forced to implement an interface that it doesn’t use.
這個規則告訴我們,應該把介面拆的儘可能小。這樣才能更好的滿足客戶的確切需求。
與單一責任原則類似,介面隔離原則也是通過將軟體拆分為多個獨立的部分來最大程度的減少副作用和重複程式碼。
我們來看一個例子:
public interface WorkerInterface {
void work();
void sleep();
}
public class HumanWorker implements WorkerInterface {
public void work() {
System.out.println("work");
}
public void sleep() {
System.out.println("sleep");
}
}
public class RobotWorker implements WorkerInterface {
public void work() {
System.out.println("work");
}
public void sleep() {
// No need
}
}
複製程式碼
在上面這段程式碼中,我們很容易發現問題所在,機器人不需要睡覺,但是由於實現了WorkerInterface介面,它不得不實現sleep方法。這就違背了介面隔離的原則,下面我們一起修復一下這段程式碼:
public interface WorkAbleInterface {
void work();
}
public interface SleepAbleInterface {
void sleep();
}
public class HumanWorker implements WorkAbleInterface,SleepAbleInterface {
public void work() {
System.out.println("work");
}
public void sleep() {
System.out.println("sleep");
}
}
public class RobotWorker implements WorkerInterface {
public void work() {
System.out.println("work");
}
}
複製程式碼
依賴倒置原則
高層模組不應該依賴於低層的模組,它們都應該依賴於抽象。
抽象不應該依賴於細節,細節應該依賴於抽象。
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Abstractions should not depend on details. Details should depend on abstractions.
簡單來講就是:抽象不依賴於細節,而細節依賴於抽象。
通過應用依賴倒置模組,只需要修改依賴模組,其他模組就可以輕鬆得到修改。同時,低層模組的修改是不會影響到高層模組修改的。
我們來看這段程式碼:
public class MySQLConnection {
public void connect() {
System.out.println("MYSQL Connection");
}
}
public class PasswordReminder {
private MySQLConnection mySQLConnection;
public PasswordReminder(MySQLConnection mySQLConnection) {
this.mySQLConnection = mySQLConnection;
}
}
複製程式碼
有一種常見的誤解是,依賴倒置只是依賴注入的另一種表達方式,實際上兩者並不相同。
在上面這段程式碼中,儘管將MySQLConnection類注入了PasswordReminder類,但它依賴於MySQLConnection。而高層模組PasswordReminder是不應該依賴於低層模組MySQLConnection的。因此這不符合依賴倒置原則。
如果你想要把MySQLConnection改成MongoConnection,那就要在PasswordReminder中更改硬編碼的建構函式注入。
要想符合依賴倒置原則,PasswordReminder就要依賴於抽象類(介面)而不是細節。那麼應該怎麼改這段程式碼呢?我們一起來看一下:
public interface ConnectionInterface {
void connect();
}
public class MySQLConnection implements ConnectionInterface {
public void connect() {
System.out.println("MYSQL Connection");
}
}
public class PasswordReminder {
private ConnectionInterface connection;
public PasswordReminder(ConnectionInterface connection) {
this.connection = connection;
}
}
複製程式碼
修改後的程式碼中,如果我們想要將MySQLConnection改成MongoConnection,就不需要修改PasswordReminder類的建構函式注入,因為這裡PasswordReminder類依賴於抽象而非細節。
感謝閱讀!
原文地址
譯者點評
作者對於SOLID原則介紹的還是比較清楚的,但是里氏原則那裡我認為說得還不是很明白,舉的例子似乎也不是很明確。我理解的里氏替換原則是:子類可以擴充套件父類的功能,但不能修改父類方法。因此里氏替換原則可以說是開閉原則的一種實現。當然,這篇文章也只是大概介紹了SOLID的每個原則,大家可以通過查資料來進行更詳細的瞭解。我相信理解了這些設計原則之後,你對程式設計就會有更加深入的認識。後面我也會繼續推送一些關於設計原則的文章,歡迎關注。