阿川私房教材:學程式,拿 offer!

63 個專案實戰,直接上手!
無需補習,按步驟打造你的面試作品。

立即解鎖你的轉職秘笈

剛開始物件導向程式設計,不知道SOLID?別擔心,在本文中我將向您解釋它並舉例說明如何在開發程式碼時使用它。

什麼是實體?

在物件導向程式設計中,術語 SOLID 是五個設計假設的縮寫,旨在促進理解、開發和維護軟體。

當使用這套原則時,可以顯著減少錯誤的產生,提高程式碼質量,產生更有組織的程式碼,減少耦合,改進重構並鼓勵程式碼重複使用。

S - 單一職責原則

單一責任原則範例

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

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。

阿川私房教材:學程式,拿 offer!

63 個專案實戰,直接上手!
無需補習,按步驟打造你的面試作品。

立即解鎖你的轉職秘笈