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

什麼是實體?

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

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

S - 單一職責原則

單一責任原則範例

SRP - 單一職責原則

這原則說一個類別必須有一個且只有一個改變的理由

就是這樣,不要建立具有多種功能和職責的類別。您可能已經完成或遇到一個可以做所有事情的類,例如“God Class”。在那一刻看起來一切都很好,但是當需要對這個類別的邏輯進行任何更改時,問題肯定會開始出現。

God Class - God Class: 在物件導向程式設計中,它是一個知道太多或做太多事情的類別。

class Task {
    createTask(){/*...*/}
    updateTask(){/*...*/}
    deleteTask(){/*...*/}

    showAllTasks(){/*...*/}

    existsTask(){/*...*/}

    TaskCompleter(){/*...*/}
}

這個 Task 類別透過執行 四個 不同的任務來打破 SRP 原則。它正在處理任務的資料、顯示、驗證和驗證。

這可能導致的問題:

  • 「缺乏連結」-一個類別不應該承擔不屬於它自己的責任;

  • 「太多的資訊在一起」 - 你的類別將有很多依賴項並且很難進行更改;

  • 「實現自動化測試的困難」 - 很難“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 - 開閉原則

開閉原則範例

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 - 里氏替換原則

里氏替換原理範例

LSP - 里氏替換原理

里氏替換原則 — A 衍生類別必須可以被其基底類別取代

兄弟 Liskov 在 1987 年的一次會議上介紹的這個原理在閱讀他的解釋時有點難以理解,但是不用擔心,我將向您展示另一個解釋和一個示例來幫助您理解。

如果對於 S 類型的每個物件 o1 都有一個 T 類型的物件 o2,這樣,對於用 T 定義的所有程式 P,當 o1 被 o2 取代時 P 的行為不變,那麼 S 是 T 的子類型

你明白了嗎?不,我第一次讀它時(或其他十次)也不明白它,但等等,還有另一種解釋:

如果 S 是 T 的子類型,則程式中類型 T 的物件可以用類型 S 的物件替換,而不必變更該程式的屬性。 - 維基百科

如果您更直觀,我有一個程式碼範例:

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 - 介面隔離原則

範例介面隔離原則

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 - 依賴倒置原則

依賴倒置原則範例

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直接依賴EmailNotificationSMSNotification的具體實作。這違反了 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 則留言