剛開始接觸物件導向編程,對SOLID感到有點迷失?不用擔心,在本文中,我將向您解釋它並提供如何在程式碼開發中使用它的範例。
在物件導向程式設計中, SOLID是五個設計原則的縮寫,旨在增強對軟體的理解、開發和維護。
透過應用這組原則,您應該注意到錯誤的減少、程式碼品質的提高、程式碼組織性更強、耦合性降低、重構增強以及程式碼重用的鼓勵。讓我們來看看他們。
SRP - 單一職責原則
這確實很簡單,但非常重要:一個類別應該有一個且只有一個改變的理由。
不再建立具有多種功能和職責的類,是嗎?您可能遇到甚至建立了一個可以完成所有操作的類,即所謂的God Class 。目前看起來似乎沒問題,但是當您需要更改該類別的邏輯時,肯定會出現問題。
上帝類別:在OOP中,這是一個
do
或knows
太多事情的類別。
class ProfileManager {
authenticateUser(username: string, password: string): boolean {
// Authenticate logic
}
showUserProfile(username: string): UserProfile {
// Show user profile logic
}
updateUserProfile(username: string): UserProfile {
// Update user profile logic
}
setUserPermissions(username: string): void {
// Set permission logic
}
}
此ProfileManager類別執行四個不同的任務,違反了 SRP 原則。它正在驗證和更新資料、進行演示,最重要的是,它正在設定權限,所有這些都是同時進行的。
Lack of cohesion -
一個類別不應該承擔不屬於它自己的責任;
Too much information in one place -
你的類別最終會產生許多依賴性並且難以進行更改;
Challenges in implementing automated tests -
很難模擬這樣的類別。
現在,將SRP應用到ProfileManager
類別中,讓我們來看看這個原則可以帶來的改進:
class AuthenticationManager {
authenticateUser(username: string, password: string): boolean {
// Authenticate logic
}
}
class UserProfileManager {
showUserProfile(username: string): UserProfile {
// Show user profile logic
}
updateUserProfile(username: string): UserProfile {
// Update user profile logic
}
}
class PermissionManager {
setUserPermissions(username: string): void {
// Set permission logic
}
}
您可能想知道, can I apply this only to classes?
答案是:完全不是。您也可以(並且應該)將其應用於方法和函數。
// ❌
function processTasks(taskList: Task[]): void {
taskList.forEach((task) => {
// Processing logic involving multiple responsibilities
updateTaskStatus(task);
displayTaskDetails(task);
validateTaskCompletion(task);
verifyTaskExistence(task);
});
}
// ✅
function updateTaskStatus(task: Task): Task {
// Logic for updating task status
return { ...task, completed: true };
}
function displayTaskDetails(task: Task): void {
// Logic for displaying task details
console.log(`Task ID: ${task.id}, Description: ${task.description}`);
}
function validateTaskCompletion(task: Task): boolean {
// Logic for validating task completion
return task.completed;
}
function verifyTaskExistence(task: Task): boolean {
// Logic for verifying task existence
return tasks.some((t) => t.id === task.id);
}
美麗、優雅、有組織的程式碼。這個原則是其他原則的基礎;透過應用它,您應該建立高品質、可讀且可維護的程式碼。
OCP-開閉原則
物件或實體應該對擴充開放,但對修改關閉。如果您需要加入功能,最好擴展而不是修改原始程式碼。
想像一下,您需要一個類別來計算某些多邊形的面積。
class Circle {
radius: number;
constructor(radius: number) {
this.radius = radius;
}
area(): number {
return Math.PI * this.radius ** 2;
}
}
class Square {
sideLength: number;
constructor(sideLength: number) {
this.sideLength = sideLength;
}
calculateArea(): number {
return this.sideLength ** 2;
}
}
class areaCalculator {
totalArea(shapes: Shape[]): number {
let total = 0;
shapes.forEach((shape) => {
if (shape instanceof Square) {
total += (shape as any).calculateArea();
} else {
total += shape.area();
}
});
return total;
}
}
areaCalculator
類別的任務是計算不同多邊形的面積,每個多邊形都有自己的面積邏輯。如果您,'lil dev,需要加入新形狀,例如三角形或矩形,您會發現自己需要更改此類來進行更改,對吧?這就是你遇到問題的地方,違反了Open-Closed Principle
。
我想到了什麼解決方案?可能會在類別中加入另一個方法並完成,問題解決了🤩。不完全是,年輕學徒😓,這就是問題所在!
修改現有類別以新增行為會帶來嚴重的風險,可能會將錯誤引入到已執行的內容中。
請記住:OCP 堅持認為類別應該對修改關閉,對擴展開放。
看看重構程式碼帶來的美妙之處:
interface Shape {
area(): number;
}
class Circle implements Shape {
radius: number;
constructor(radius: number) {
this.radius = radius;
}
area(): number {
return Math.PI * this.radius ** 2;
}
}
class Square implements Shape {
sideLength: number;
constructor(sideLength: number) {
this.sideLength = sideLength;
}
area(): number {
return this.sideLength ** 2;
}
}
class AreaCalculator {
totalArea(shapes: Shape[]): number {
let total = 0;
shapes.forEach((shape) => {
total += shape.area();
});
return total;
}
}
查看AreaCalculator
類別:它不再需要知道要呼叫哪些方法來註冊該類別。它可以透過呼叫介面強加的契約來正確地呼叫區域方法,這是它唯一需要的。
只要它們實作了 Shape 接口,一切就可以正常運作。
<br/>
分離介面背後的可擴展行為並反轉依賴關係。
>
Open for extension:
您可以為類別新增功能或行為,而無需變更其原始程式碼。
Closed for modification:
如果您的類別已經具有可以正常工作的功能或行為,請不要更改其原始程式碼以加入新內容。
LSP - 里氏替換原理
里氏替換原則指出衍生類別必須可替換其基底類別。
這個原則由 Barbara Liskov 在 1987 年提出,閱讀她的解釋可能會有點複雜。不過,不用擔心,我將提供另一個解釋和範例來幫助您理解。
如果對於 S 類型的每個物件 o1 都有一個 T 類型的物件 o2,使得對於所有用 T 定義的程式 P,當 o1 取代 o2 時 P 的行為保持不變,則 S 是 T 的子類型。
>
芭芭拉‧利斯科夫,1987
你做對了?不,可能不是。是的,我第一次讀時不明白(接下來的一百遍也不明白),但等等,還有另一種解釋:
如果 S 是 T 的子類型,則程式中類型 T 的物件可以用類型 S 的物件替換,而不改變該程式的屬性。
>
如果您更喜歡視覺學習者,請不要擔心,這裡有一個例子:
class Person {
speakName() {
return "I am a person!";
}
}
class Child extends Person {
speakName() {
return "I am a child!";
}
}
const person = new Person();
const child = new Child();
function printName(message: string) {
console.log(message);
}
printName(person.speakName()); // I am a person!
printName(child.speakName()); // I am a child!
父類別和衍生類別作為參數傳遞,程式碼繼續按預期工作。魔法?是的,這就是我們的朋友倒鉤的魔力。
重寫/實作一個不執行任何操作的方法;
從基類傳回不同類型的值。
拋出意外的異常;
ISP-介面隔離原則
這句話說不應該強迫一個類別實作它不使用的介面和方法。建立更具體的介面比建立大而通用的介面更好。
在下面的範例中,建立一個Book介面來抽象化書籍行為,然後類別實作該介面:
interface Book {
read(): void;
download(): void;
}
class OnlineBook implements Book {
read(): void {
// does something
}
download(): void {
// does something
}
}
class PhysicalBook implements Book {
read(): void {
// does something
}
download(): void {
// This implementation doesn't make sense for a book
// it violates the Interface Segregation Principle
}
}
通用Book
介面迫使PhysicalBook
類別做出毫無意義的行為(或者我們在 Matrix 中下載實體書籍? )並且違反了ISP和LSP原則。
使用ISP解決此問題:
interface Readable {
read(): void;
}
interface Downloadable {
download(): void;
}
class OnlineBook implements Readable, Downloadable {
read(): void {
// does something
}
download(): void {
// does something
}
}
class PhysicalBook implements Readable {
read(): void {
// does something
}
}
現在好多了。我們從Book
介面中刪除了download()
方法,並將其加入到派生介面Downloadable
中。這樣,行為就可以在我們的上下文中正確隔離,並且我們仍然尊重介面隔離原則。
DIP - 依賴倒置原理
這個是這樣的:依賴抽象而不是實現。
高層模組不應該依賴低層模組。兩者都應該依賴抽象。
>
抽像不應該依賴細節。細節應該取決於抽象。
>
鮑伯叔叔
現在我將展示一個簡單的程式碼來說明 DIP。在此範例中,有一個從資料庫取得使用者的服務。首先,讓我們建立一個與資料庫連接的具體類別:
// Low-level module
class MySQLDatabase {
getUserData(id: number): string {
// Logic to fetch user data from MySQL database
}
}
現在,讓我們建立一個取決於具體實作的服務類別:
// High-level module
class UserService {
private database: MySQLDatabase;
constructor() {
this.database = new MySQLDatabase();
}
getUser(id: number): string {
return this.database.getUserData(id);
}
}
在上面的範例中, UserService
直接依賴MySQLDatabase
的具體實作。這違反了DIP ,因為高級類別 UserService 直接依賴低階類別。
如果我們想要切換到不同的資料庫系統(例如PostgreSQL),我們需要修改UserService類,這AWFUL
了!
讓我們使用DIP修復此程式碼。高級類別UserService
不應依賴特定實現,而應依賴抽象。讓我們建立一個Database
介面作為抽象:
// Abstract interface (abstraction) for the low-level module
interface Database {
getUserData(id: number): string;
}
現在, MySQLDatabase
和PostgreSQLDatabase
的具體實作應該要實作這個介面:
class MySQLDatabase implements Database {
getUserData(id: number): string {
// Logic to fetch user data from MySQL database
}
}
// Another low-level module implementing the Database interface
class PostgreSQLDatabase implements Database {
getUserData(id: number): string {
// Logic to fetch user data from PostgreSQL database
}
}
最後,UserService 類別可以依賴Database
抽象:
class UserService {
private database: Database;
constructor(database: Database) {
this.database = database;
}
getUser(id: number): string {
return this.database.getUserData(id);
}
}
這樣, UserService
類別依賴Database
抽象,而不是具體實現,滿足依賴倒置原則。
透過採用這些原則,開發人員可以建立更能適應變化的系統,使維護變得更容易,並隨著時間的推移提高程式碼品質。
本文的全部內容源自各種其他文章、我的個人筆記以及我在深入研究物件導向程式設計 (OOP) 領域時遇到的數十個線上影片。範例中使用的程式碼片段是基於我對這些原則的解釋和理解而建立的。我真的希望,我的小學徒,我能為促進你的理解和學習進步做出貢獻。
衷心希望您喜歡這篇文章,別忘了關注!
註:圖片取自本文
原文出處:https://dev.to/lukeskw/solid-principles-theyre-rock-solid-for-good-reason-31hn