剛開始物件導向程式設計,不知道SOLID?別擔心,在本文中我將向您解釋它並舉例說明如何在開發程式碼時使用它。
[什麼是 SOLID?](#什麼是 SOLID)
在物件導向程式設計中,術語 SOLID 是五個設計假設的縮寫,旨在促進理解、開發和維護軟體。
當使用這套原則時,可以顯著減少錯誤的產生,提高程式碼質量,產生更有組織的程式碼,減少耦合,改進重構並鼓勵程式碼重複使用。
SRP - 單一職責原則
這原則說一個類別必須有一個且只有一個改變的理由
就是這樣,不要建立具有多種功能和職責的類別。您可能已經完成或遇到一個可以做所有事情的類,例如“God Class”。在那一刻看起來一切都很好,但是當需要對這個類別的邏輯進行任何更改時,問題肯定會開始出現。
God Class - God Class: 在物件導向程式設計中,它是一個知道太多或做太多事情的類別。
class Task { createTask(){/*...*/} updateTask(){/*...*/} deleteTask(){/*...*/}
showAllTasks(){/*...*/}
existsTask(){/*...*/}
TaskCompleter(){/*...*/}
}
這個 **Task** 類別透過執行 **四個** 不同的任務來打破 **SRP** 原則。它正在處理**任務**的資料、顯示、驗證和驗證。
### 這可能導致的問題:
- 「缺乏連結」-一個類別不應該承擔不屬於它自己的責任;
- 「太多的資訊在一起」 - 你的類別將有很多依賴項並且很難進行更改;
- 「實現自動化測試的困難」 - 很難[“mock”](https://pt.wikipedia.org/wiki/Objeto_Mock)這種類型的類別;
現在將 **SRP** 應用於 *Task* 類,讓我們看看這個原則可以帶來的改進:
class TaskHandler{
createTask() {/.../}
updateTask() {/.../}
deleteTask() {/.../}
}
class TaskViewer{
showAllTasks() {/.../}
}
class TaskChecker {
existsTask() {/.../}
}
class TaskCompleter {
completeTask() {/.../}
}
>您可以將建立、更新和刪除放在單獨的類別中,但根據專案的上下文和大小,最好避免不必要的複雜性。
也許您問過自己「我只能將其應用於類別嗎?」不,相反,您也可以將其應用於方法和函數。
//❌
function emailClients(clients: IClient[]) {
clients.forEach((client)=>{
const clientRecord = db.find(client);
if(clientRecord){
sendEmail(client);
}
})
}
//✅
function isClientActive(client: IClient):boolean {
const clientRecord = db.find(client);
return !!clientRecord;
}
function getActiveClients(clients: IClient[]):<IClient | undefined> {
return clients.filter(isClientActive);
}
function emailClients(clients: IClient[]):void {
const activeClients = getActiveClients(clients);
activeClients?.forEach(sandEmail);
}
更美觀、優雅、更有組織的程式碼。這個原則是其他原則的基礎,透過應用它,您將建立優質、易於閱讀和易於維護的程式碼。
## O - 開閉原則
![開閉原則範例](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qk0e24cnjefd5e8r0h6c.png)
>*OCP - 開閉原則*
這個原則說的是**物件或實體必須對擴充功能開放,但對修改關閉**,如果需要加入功能,最好對其進行擴展而不是更改其原始程式碼。
想像一個學校辦公室的小型系統,其中有兩個班級代表學生的課程表:小學和高中。另外還有一個班級,定義了學生的班級。
class EnsinoFundamental {
gradeCurricularFundamental(){}
}
class EnsinoMedio {
gradeCurricularMedio(){}
}
class SecretariaEscola {
aulasDoAluno: string;
cadastrarAula(aulasAluno){
if(aulasAluno instanceof EnsinoFundamental){
this.aulasDoAluno = aulasAluno.gradeCurricularFundamental();
} else if(aulasAluno.ensino instanceof EnsinoMedio){
this.aulasDoAluno = aulasAluno.gradeCurricularMedio();
}
}
}
`SecretariaEscola` 類別負責檢查學生的教育程度,以便在註冊課程時應用正確的業務規則。現在想像一下,這所學校在系統中加入了技術教育和課程表,那麼就需要修改這個課程,對吧?但是,這樣你就會遇到一個問題,那就是違反了*SOLID 的「開閉原則” *。
我想到了什麼解決方案?可能在類別中加入一個“else if”,就這樣,問題解決了。不是小學徒😐,這就是問題所在!
**透過更改現有類別以加入新行為,我們面臨著將錯誤引入到已經執行的內容中的嚴重風險。**
>**記住:** **OCP** 認為課程必須針對更改關閉並針對擴充功能開放。
看看重構程式碼所帶來的美妙之處:
interface gradeCurricular {
gradeDeAulas();
}
class EnsinoFundamental implements gradeCurricular {
gradeDeAulas(){}
}
class EnsinoMedio implements gradeCurricular {
gradeDeAulas(){}
}
class EnsinoTecnico implements gradeCurricular {
gradeDeAulas(){}
}
class SecretariaEscola {
aulasDoAluno: string;
cadastrarAula(aulasAluno: gradeCurricular) {
this.aulasDoAluno = aulasAluno.gradeDeAulas();
}
}
看到 `SecretariaEscola` 類,它不再需要知道要呼叫哪些方法來註冊該類別。它將能夠為建立的任何新型教學模式正確註冊課程表,請注意,我加入了“EnsinoTecnico”,無需更改原始程式碼。
>*自從我實作了 `gradeCurrarily` 介面以來。*
>介面背後的獨立可擴展行為和反向依賴關係。
>鮑伯叔叔
- `開放擴充`:您可以為類別加入一些新功能或行為,而無需更改其原始程式碼。
-「修改關閉」:如果您的類別已經具有不存在任何問題的功能或行為,請勿變更其原始程式碼以新增內容。
## L - 里氏替換原則
![里氏替換原理範例](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eyrl3p96aqzowf72ipcb.png)
>*LSP - 里氏替換原理*
里氏替換原則 — **A** **衍生類別必須可以被其基底類別取代**。
*兄弟* Liskov 在 1987 年的一次會議上介紹的這個原理在閱讀他的解釋時有點難以理解,但是不用擔心,我將向您展示另一個解釋和一個示例來幫助您理解。
> 如果對於 S 類型的每個物件 o1 都有一個 T 類型的物件 o2,這樣,對於用 T 定義的所有程式 P,當 o1 被 o2 取代時 P 的行為不變,那麼 S 是 T 的子類型
你明白了嗎?不,我第一次讀它時(或其他十次)也不明白它,但等等,還有另一種解釋:
> 如果 S 是 T 的子類型,則程式中類型 T 的物件可以用類型 S 的物件替換,而不必變更該程式的屬性。 - [維基百科](https://pt.wikipedia.org/wiki/Princ%C3%ADpio_da_substitui%C3%A7%C3%A3o_de_Liskov)。
如果您更直觀,我有一個程式碼範例:
class Fulano {
falarNome() {
return "sou fulano!";
}
}
class Sicrano extends Fulano {
falarNome() {
return "sou sicrano!";
}
}
const a = new Fulano();
const b = new Sicrano();
function imprimirNome(msg: string) {
console.log(msg);
}
imprimirNome(a.falarNome()); // sou fulano!
imprimirNome(b.falarNome()); // sou sicrano!
父類別和衍生類別作為參數傳遞,並且程式碼繼續按預期工作,神奇嗎?沒什麼,這就是我們利斯科夫兄弟的原則。
### 違規範例:
- 覆蓋/實作一個不執行任何操作的方法;
- 拋出意外的異常;
- 從基底類別傳回不同類型的值;
## I - 介面隔離原則
![範例介面隔離原則](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p88do8ivd00s9aofo5yq.png)
>*ISP - 介面隔離原則*
介面隔離原則 — **不應強迫類別實作它不會使用的介面和方法。**
該原則表明,建立更具體的介面比建立通用介面更好。
在下面的範例中,建立了一個「Animal」接口來抽象化動物行為,然後類別實作該接口,請參閱:
interface Animal {
comer();
dormir();
voar();
}
class Pato implements Animal{
comer(){/faz algo/};
dormir(){/faz algo/};
voar(){/faz algo/};
}
class Peixe implements Animal{
comer(){/faz algo/};
dormir(){/faz algo/};
voar(){/*faz algo*/};
// Esta implementação não faz sentido para um peixe
// ela viola o Princípio da Segregação da Interface
}
通用介面「Animal」強制「Peixe」類別具有有意義的行為,最終違反了 **ISP** 原則和 **LSP** 原則。
使用 **ISP** 解決此問題:
interface Animal {
comer();
dormir();
}
interface AnimalQueVoa extends Animal {
voar();
}
class Peixe implements Animal{
comer(){/faz algo/};
dormir(){/faz algo/};
}
class Pato implements AnimalQueVoa {
comer(){/faz algo/};
dormir(){/faz algo/};
voar(){/faz algo/};
}
現在更好了,“voar()”方法已從“Animal”介面中刪除,我們將其加入到派生介面“AnimalQueVoa”中。這樣,行為就在我們的上下文中被正確隔離,並且我們仍然尊重介面隔離的原則。
## D - 依賴倒置原則
![依賴倒置原則範例](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9s5ywa9xijb41c1z2l1a.png)
> *DIP — 依賴倒置原理*
依賴倒置原則 — **依賴抽象,而不是實現。**
> 1. 高層模組不應該依賴低層模組。兩者都必須依賴抽象。
>
> 2. 抽像不應該依賴細節。細節必須依賴抽象。
>
> - *叔叔鮑伯*
在下面的範例中,我將展示一個簡單的程式碼來說明**DIP**。在此範例中,我們有一個透過不同方式(例如電子郵件和簡訊)發送訊息的通知系統。首先讓我們為這些通知方式建立具體的類別:
class EmailNotification {
send(message) {
console.log(Enviando e-mail: ${message}
);
}
}
class SMSNotification {
send(message) {
console.log(Enviando SMS: ${message}
);
}
}
現在,讓我們建立一個依賴這些具體實作的服務類別:
class NotificationService {
constructor() {
this.emailNotification = new EmailNotification();
this.smsNotification = new SMSNotification();
}
sendNotifications(message) {
this.emailNotification.send(message);
this.smsNotification.send(message);
}
}
在上面的例子中,`NotificationService`直接依賴`EmailNotification`和`SMSNotification`的具體實作。這違反了 DIP,因為高級 `NotificationService` 類別直接依賴低階類別。
讓我們使用 **DIP** 修復此程式碼。高級“NotificationService”類別不應依賴具體實現,而應依賴抽象。讓我們建立一個「Notification」介面作為抽象:
// Abstração para o envio de notificações
interface Notification {
send(message) {}
}
現在,具體的「EmailNotification」和「SMSNotification」實作必須實作此介面:
class EmailNotification implements Notification {
send(message) {
console.log(Enviando e-mail: ${message}
);
}
}
class SMSNotification implements Notification {
send(message) {
console.log(Enviando SMS: ${message}
);
}
}
最後,通知服務類別可以依賴「Notification」抽象:
class NotificationService {
constructor(notificationMethod: Notification) {
this.notificationMethod = notificationMethod;
}
sendNotification(message) {
this.notificationMethod.send(message);
}
}
這樣,「NotificationService」服務類別依賴「Notification」抽象,而不是具體實現,從而滿足**依賴倒置原則**。
## 結論
透過採用這些原則,開發人員可以建立更能適應變化的系統,使維護變得更容易,並隨著時間的推移提高程式碼品質。
所有這些內容都是基於我學習 OOP 期間在網上找到的筆記、其他文章和影片,其中的解釋接近原理的作者,而示例中使用的程式碼是我根據自己對 OOP 的理解建立的。原則。讀者,我希望我對您的學習進程有所幫助。
---
原文出處:https://dev.to/clintonrocha98/era-solid-o-que-me-faltava-bhp