首頁 >web前端 >js教程 >JavaScript 的堅實原則

JavaScript 的堅實原則

Linda Hamilton
Linda Hamilton原創
2024-12-28 19:34:11342瀏覽

OOP 範式的引入普及了繼承、多型、抽象和封裝等關鍵程式設計概念。 OOP 很快就成為一種廣為接受的程式設計範例,並以多種語言(例如 Java、C、C#、JavaScript 等)實作。隨著時間的推移,物件導向程式系統變得越來越複雜,但其軟體仍然難以改變。為了提高軟體可擴展性並降低程式碼剛性,Robert C. Martin(又名 Bob 叔叔)在 2000 年代初引入了 SOLID 原則。

SOLID 是一個縮寫詞,由一組原則組成——單一責任原則、開閉原則、里氏替換原則、介面隔離原則和依賴倒置原則——幫助軟體工程師設計和編寫可維護、可擴展和靈活的軟體程式碼。它的目的是什麼?提高遵循物件導向程式設計(OOP)範式開發的軟體的品質。

在本文中,我們將深入研究 SOLID 的所有原則,並說明如何使用最受歡迎的 Web 程式語言之一 JavaScript 來實現它們。

單一職責原則(SRP)

SOLID中的第一個字母代表單一責任原則。這原則顯示類別或模組應該只執行一個角色。

簡單地說,一個類別應該有單一的責任或單一的改變理由。如果一個類別處理多個功能,則更新一個功能而不影響其他功能會變得很棘手。隨後的複雜情況可能會導致軟體效能故障。為了避免此類問題,我們應該盡力編寫模組化軟體,其中關注點是分離的。

如果一個類別的職責或功能太多,修改起來就會很頭痛。透過使用單一責任原則,我們可以編寫模組化、更易於維護且不易出錯的程式碼。以人物模型為例:

class Person {
    constructor(name, age, height, country){
      this.name = name
      this.age = age
      this.height = height
      this.country = country
  }
  getPersonCountry(){
    console.log(this.country)    
  }
  greetPerson(){
    console.log("Hi " + this.name)
  }
  static calculateAge(dob) { 
    const today = new Date(); 
    const birthDate = new Date(dob);

    let age = today.getFullYear() - birthDate.getFullYear(); 
    const monthDiff = today.getMonth() - birthDate.getMonth();

    if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { 
      age--; 
    }
    return age; 
  } 
}

上面的程式碼看起來沒問題,對吧?不完全是。範例程式碼違反了單一責任原則。 Person 類別不是可以建立 Person 的其他實例的唯一模型,它還具有其他職責,例如calculateAge、greetPerson 和getPersonCountry。

Person 類別處理的這些額外職責使得僅更改程式碼的一個方面變得困難。例如,如果您嘗試重構calculateAge,您也可能被迫重構Person 模型。根據我們的程式碼庫的緊湊和複雜程度,重新配置程式碼而不導致錯誤可能很困難。

讓我們試著修改錯誤。我們可以將職責分成不同的類,如下:

class Person {
    constructor(name, age, height, country){
      this.name = name
      this.age = age
      this.height = height
      this.country = country
  }
  getPersonCountry(){
    console.log(this.country)    
  }
  greetPerson(){
    console.log("Hi " + this.name)
  }
  static calculateAge(dob) { 
    const today = new Date(); 
    const birthDate = new Date(dob);

    let age = today.getFullYear() - birthDate.getFullYear(); 
    const monthDiff = today.getMonth() - birthDate.getMonth();

    if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { 
      age--; 
    }
    return age; 
  } 
}

正如您從上面的範例程式碼中看到的,我們已經分離了我們的職責。 Person 類別現在是一個模型,我們可以用它來建立一個新的 person 物件。 PersonUtils 類別只有一項職責-計算一個人的年齡。 PersonService 類別處理問候語並向我們顯示每個人的國家。

如果我們願意,我們仍然可以進一步減少這個過程。遵循SRP,我們希望將類別的責任解耦到最低限度,以便在出現問題時,可以輕鬆地進行重構和調試。

透過將功能劃分為單獨的類,我們遵循單一職責原則並確保每個類別負責應用程式的特定方面。

在我們繼續下一個原則之前,應該注意的是,遵守 SRP 並不意味著每個類別應該嚴格包含單一方法或功能。

但是,堅持單一責任原則意味著我們應該有意識地為類別分配功能。一個班級所進行的每件事在任何意義上都應該是密切相關的。我們必須小心,不要讓多個類別分散在各處,並且我們應該盡一切努力避免在程式碼庫中出現臃腫的類別。

開閉原則(OCP)

開閉原則指出軟體元件(類別、函數、模組等)應該對擴充開放,並對修改封閉。我知道你在想什麼——是的,這個想法一開始可能看起來很矛盾。但 OCP 只是要求軟體的設計方式允許擴充而不必修改原始碼。

OCP 對於維護大型程式碼庫至關重要,因為指南允許您引入新功能,而幾乎沒有破壞程式碼的風險。當出現新需求時,您不應修改現有的類別或模組,而應透過新增元件來擴充相關類別。執行此操作時,請務必檢查新組件是否不會為系統引入任何錯誤。

OC 原理可以使用 ES6 類別繼承功能在 JavaScript 中實作。

以下程式碼片段說明如何使用前面提到的 ES6 class 關鍵字在 JavaScript 中實現開閉原則:

class Person {
    constructor(name, dateOfBirth, height, country){
      this.name = name
      this.dateOfBirth = dateOfBirth
      this.height = height
      this.country = country
  }
}

class PersonUtils {
  static calculateAge(dob) { 
    const today = new Date(); 
    const birthDate = new Date(dob);

    let age = today.getFullYear() - birthDate.getFullYear(); 
    const monthDiff = today.getMonth() - birthDate.getMonth();

    if(monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { 
      age--; 
    }
    return age; 
  } 
}

const person = new Person("John", new Date(1994, 11, 23), "6ft", "USA"); 
console.log("Age: " + PersonUtils.calculateAge(person.dateOfBirth));

class PersonService {
  getPersonCountry(){
    console.log(this.country)    
  }
  greetPerson(){
    console.log("Hi " + this.name)
  }
}

上面的程式碼工作正常,但它僅限於計算矩形的面積。現在想像一下有一個新的計算要求。舉例來說,我們需要計算圓的面積。我們必須修改 shapeProcessor 類別來滿足這一點。但是,遵循 JavaScript ES6 標準,我們可以擴展此功能以考慮新形狀的區域,而不必修改 shapeProcessor 類別。

我們可以這樣做:

class Person {
    constructor(name, age, height, country){
      this.name = name
      this.age = age
      this.height = height
      this.country = country
  }
  getPersonCountry(){
    console.log(this.country)    
  }
  greetPerson(){
    console.log("Hi " + this.name)
  }
  static calculateAge(dob) { 
    const today = new Date(); 
    const birthDate = new Date(dob);

    let age = today.getFullYear() - birthDate.getFullYear(); 
    const monthDiff = today.getMonth() - birthDate.getMonth();

    if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { 
      age--; 
    }
    return age; 
  } 
}

在上面的程式碼片段中,我們使用 extends 關鍵字擴充了 Shape 類別的功能。在每個子類別中,我們重寫了area()方法的實作。遵循這個原則,我們可以增加更多的形狀和處理區域,而無需修改 ShapeProcessor 類別的功能。

為什麼 OCP 很重要?

  • 減少錯誤:OCP 透過避免系統修改來幫助避免大型程式碼庫中的錯誤。
  • 鼓勵軟體適應性:OCP 還提高了在不破壞或更改原始程式碼的情況下向軟體添加新功能的便利性。
  • 測試新功能:OCP 提倡程式碼擴充而非修改,使新功能更容易作為一個單元進行測試,而不影響整個程式碼庫。

里氏替換原則

里氏替換原則指出子類別的物件應該能夠替換超類別的物件而不破壞程式碼。讓我們用一個例子來解釋它是如何運作的:如果 L 是 P 的子類,那麼 L 的對象應該替換 P 的對象,而不會破壞系統。這僅僅意味著子類別應該能夠以不破壞系統的方式重寫超類別方法。

在實務上,里氏替換原則確保遵守以下條件:

  • 子類別應該重寫父類別的方法而不破壞程式碼
  • 子類別不應偏離父類別的行為,這表示子類別只能添加功能,而不能更改或刪除父類別的功能
  • 與父類別實例一起工作的程式碼應該與子類別實例一起工作,而不需要知道該類別已更改

是時候用 JavaScript 程式碼範例來說明里氏替換原理了。看看:

class Person {
    constructor(name, dateOfBirth, height, country){
      this.name = name
      this.dateOfBirth = dateOfBirth
      this.height = height
      this.country = country
  }
}

class PersonUtils {
  static calculateAge(dob) { 
    const today = new Date(); 
    const birthDate = new Date(dob);

    let age = today.getFullYear() - birthDate.getFullYear(); 
    const monthDiff = today.getMonth() - birthDate.getMonth();

    if(monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { 
      age--; 
    }
    return age; 
  } 
}

const person = new Person("John", new Date(1994, 11, 23), "6ft", "USA"); 
console.log("Age: " + PersonUtils.calculateAge(person.dateOfBirth));

class PersonService {
  getPersonCountry(){
    console.log(this.country)    
  }
  greetPerson(){
    console.log("Hi " + this.name)
  }
}

在上面的程式碼片段中,我們建立了兩個子類別(Bicycle 和 Car)和一個超類別(Vehicle)。出於本文的目的,我們為超類別實作了一個方法 (OnEngine)。

LSP 的核心條件之一是子類別應該覆蓋父類別的功能而不破壞程式碼。記住這一點,讓我們看看我們剛剛看到的程式碼片段是如何違反里氏替換原則的。實際上,汽車有發動機並且可以打開發動機,但自行車從技術上講沒有發動機,因此無法打開發動機。因此,Bicycle 無法在不破壞程式碼的情況下重寫 Vehicle 類別中的 OnEngine 方法。

我們現在已經確定了違反里氏替換原則的程式碼部分。 Car 類別可以重寫超類別中的 OnEngine 功能,並以區別於其他車輛(例如飛機)的方式實現它,並且程式碼不會中斷。 Car 類滿足里氏替換原則。

在下面的程式碼片段中,我們將說明如何建立符合里氏替換原則的程式碼:

class Person {
    constructor(name, age, height, country){
      this.name = name
      this.age = age
      this.height = height
      this.country = country
  }
  getPersonCountry(){
    console.log(this.country)    
  }
  greetPerson(){
    console.log("Hi " + this.name)
  }
  static calculateAge(dob) { 
    const today = new Date(); 
    const birthDate = new Date(dob);

    let age = today.getFullYear() - birthDate.getFullYear(); 
    const monthDiff = today.getMonth() - birthDate.getMonth();

    if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { 
      age--; 
    }
    return age; 
  } 
}

這是具有通用功能「移動」的 Vehicle 類別的基本範例。人們普遍認為所有車輛都會移動;它們只是透過不同的機制移動。我們要說明 LSP 的一種方法是重寫 move() 方法並以描述特定車輛(例如汽車)如何移動的方式實現它。

為此,我們將創建一個 Car 類別來擴展 Vehicle 類別並重寫 move 方法以適應汽車的移動,如下所示:

class Person {
    constructor(name, dateOfBirth, height, country){
      this.name = name
      this.dateOfBirth = dateOfBirth
      this.height = height
      this.country = country
  }
}

class PersonUtils {
  static calculateAge(dob) { 
    const today = new Date(); 
    const birthDate = new Date(dob);

    let age = today.getFullYear() - birthDate.getFullYear(); 
    const monthDiff = today.getMonth() - birthDate.getMonth();

    if(monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { 
      age--; 
    }
    return age; 
  } 
}

const person = new Person("John", new Date(1994, 11, 23), "6ft", "USA"); 
console.log("Age: " + PersonUtils.calculateAge(person.dateOfBirth));

class PersonService {
  getPersonCountry(){
    console.log(this.country)    
  }
  greetPerson(){
    console.log("Hi " + this.name)
  }
}

我們仍然可以在另一個子車輛類別(例如飛機)中實作 move 方法。

我們的做法如下:

class Rectangle { 
  constructor(width, height) {
    this.width = width; 
    this.height = height; 
  } 
  area() { 
  return this.width * this.height; 
  } 
} 

class ShapeProcessor { 
    calculateArea(shape) { 
    if (shape instanceof Rectangle) { 
    return shape.area(); 
    } 
  }
}  
const rectangle = new Rectangle(10, 20); const shapeProcessor = new ShapeProcessor(); console.log(shapeProcessor.calculateArea(rectangle)); 

在上面的兩個範例中,我們說明了繼承和方法重寫等關鍵概念。

注意:允許子類別實作父類別中已定義的方法的程式設計功能稱為方法重寫。

讓我們做一些家事工作並將所有東西放在一起,就像這樣:

class Shape {
  area() {
    console.log("Override method area in subclass");
  }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }

  area() {
    return this.width * this.height;
  }
}

class Circle extends Shape {
  constructor(radius) {
    super();
    this.radius = radius;
  }

  area() {
    return Math.PI * this.radius * this.radius;
  }
}

class ShapeProcessor {
  calculateArea(shape) {
    return shape.area();
  }
}

const rectangle = new Rectangle(20, 10);
const circle = new Circle(2);
const shapeProcessor = new ShapeProcessor();

console.log(shapeProcessor.calculateArea(rectangle));
console.log(shapeProcessor.calculateArea(circle));

現在,我們有 2 個子類別繼承並重寫父類別的單一功能,並根據它們的要求實現它。這個新的實作不會破壞程式碼。

介面隔離原則(ISP)

介面隔離原則規定,任何客戶端都不應被迫依賴它不使用的介面。它希望我們創建與特定客戶端相關的更小、更具體的接口,而不是擁有一個大型、單一的接口,迫使客戶端實現他們不需要的方法。

保持介面緊湊使得程式碼庫更易於調試、維護、測試和擴充。如果沒有ISP,大型介面的某個部分的更改可能會迫使程式碼庫的不相關部分發生更改,從而導致我們進行程式碼重構,這在大多數情況下取決於程式碼庫的大小可能是一項艱鉅的任務。

JavaScript 與 Java 等基於 C 的程式語言不同,它沒有內建的介面支援。然而,有一些技術可以在 JavaScript 中實作介面。

介面是類別必須實作的一組方法簽章。

在 JavaScript 中,您將介面定義為具有方法名稱和函數簽名的對象,如下所示:

class Person {
    constructor(name, age, height, country){
      this.name = name
      this.age = age
      this.height = height
      this.country = country
  }
  getPersonCountry(){
    console.log(this.country)    
  }
  greetPerson(){
    console.log("Hi " + this.name)
  }
  static calculateAge(dob) { 
    const today = new Date(); 
    const birthDate = new Date(dob);

    let age = today.getFullYear() - birthDate.getFullYear(); 
    const monthDiff = today.getMonth() - birthDate.getMonth();

    if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { 
      age--; 
    }
    return age; 
  } 
}

要在 JavaScript 中實現接口,請創建一個類別並確保它包含與接口中指定的名稱和簽名相同的方法:

class Person {
    constructor(name, dateOfBirth, height, country){
      this.name = name
      this.dateOfBirth = dateOfBirth
      this.height = height
      this.country = country
  }
}

class PersonUtils {
  static calculateAge(dob) { 
    const today = new Date(); 
    const birthDate = new Date(dob);

    let age = today.getFullYear() - birthDate.getFullYear(); 
    const monthDiff = today.getMonth() - birthDate.getMonth();

    if(monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { 
      age--; 
    }
    return age; 
  } 
}

const person = new Person("John", new Date(1994, 11, 23), "6ft", "USA"); 
console.log("Age: " + PersonUtils.calculateAge(person.dateOfBirth));

class PersonService {
  getPersonCountry(){
    console.log(this.country)    
  }
  greetPerson(){
    console.log("Hi " + this.name)
  }
}

現在我們已經弄清楚如何在 JavaScript 中建立和使用介面。我們需要做的下一件事是說明如何在 JavaScript 中隔離接口,以便我們可以看到它們如何組合在一起並使程式碼更易於維護。

在下面的範例中,我們將使用印表機來說明介面隔離的原理。

假設我們有印表機、掃描器和傳真機,讓我們建立一個定義這些物件功能的介面:

class Rectangle { 
  constructor(width, height) {
    this.width = width; 
    this.height = height; 
  } 
  area() { 
  return this.width * this.height; 
  } 
} 

class ShapeProcessor { 
    calculateArea(shape) { 
    if (shape instanceof Rectangle) { 
    return shape.area(); 
    } 
  }
}  
const rectangle = new Rectangle(10, 20); const shapeProcessor = new ShapeProcessor(); console.log(shapeProcessor.calculateArea(rectangle)); 

在上面的程式碼中,我們創建了一系列分離或隔離的接口,以反對使用定義所有這些功能的大型接口。透過將這些功能分解為更小的部分和更具體的接口,我們允許不同的客戶端僅實現他們需要的方法,並保留所有其他部分。

下一步,我們將建立實作這些介面的類別。遵循介面隔離原則,每個類別只會實作它需要的方法。

如果我們想要實作一個只能列印文件的基本印表機,我們可以透過printerInterface實作print()方法,如下所示:

class Shape {
  area() {
    console.log("Override method area in subclass");
  }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }

  area() {
    return this.width * this.height;
  }
}

class Circle extends Shape {
  constructor(radius) {
    super();
    this.radius = radius;
  }

  area() {
    return Math.PI * this.radius * this.radius;
  }
}

class ShapeProcessor {
  calculateArea(shape) {
    return shape.area();
  }
}

const rectangle = new Rectangle(20, 10);
const circle = new Circle(2);
const shapeProcessor = new ShapeProcessor();

console.log(shapeProcessor.calculateArea(rectangle));
console.log(shapeProcessor.calculateArea(circle));

該類別僅實作PrinterInterface。它不實現掃描或傳真方法。透過遵循介面隔離原則,客戶端(在本例中為 Printer 類別)降低了其複雜性並提高了軟體的效能。

依賴倒置原則(DIP)

現在我們的最後一個原則:依賴倒置原則。此原則表示較高層級的模組(業務邏輯)應該依賴抽象,而不是直接依賴較低層級的模組(具體)。它幫助我們減少程式碼依賴性,並為開發人員提供在更高層級修改和擴展應用程式的靈活性,而不會遇到複雜性。

為什麼依賴倒置原則支持抽象而不是直接依賴?這是因為抽象的引入減少了更改的潛在影響,提高了可測試性(模擬抽象而不是具體實現),並在程式碼中實現了更高程度的靈活性。這條規則使得透過模組化方法擴展軟體元件變得更容易,也幫助我們在不影響高層邏輯的情況下修改低層元件。

遵守 DIP 使程式碼更易於維護、擴展和擴展,從而阻止因程式碼變更而可能出現的錯誤。它建議開發人員在類別之間使用鬆散耦合而不是緊密耦合。一般來說,透過採用優先考慮抽象而不是直接依賴的思維方式,團隊將獲得適應和添加新功能或更改舊組件的敏捷性,而不會造成連鎖反應。在 JavaScript 中,我們可以使用依賴注入方法來實作 DIP,如下所示:

class Person {
    constructor(name, age, height, country){
      this.name = name
      this.age = age
      this.height = height
      this.country = country
  }
  getPersonCountry(){
    console.log(this.country)    
  }
  greetPerson(){
    console.log("Hi " + this.name)
  }
  static calculateAge(dob) { 
    const today = new Date(); 
    const birthDate = new Date(dob);

    let age = today.getFullYear() - birthDate.getFullYear(); 
    const monthDiff = today.getMonth() - birthDate.getMonth();

    if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { 
      age--; 
    }
    return age; 
  } 
}

在上面的基本範例中,Application 類別是依賴資料庫抽象的高階模組。我們建立了兩個資料庫類別:MySQLDatabase 和 MongoDBDatabase。資料庫是低階模組,它們的實例被注入到應用程式運行時中,而無需修改應用程式本身。

結論

SOLID 原則是可擴展、可維護和穩健的軟體設計的基本構建塊。這套原則可以幫助開發人員編寫乾淨、模組化且適應性強的程式碼。

SOLID 原則促進內聚功能、無需修改的可擴展性、物件替換、介面分離以及對特定依賴項的抽象化。請務必將 SOLID 原則整合到您的程式碼中,以防止錯誤並獲得其所有好處。


LogRocket:透過了解上下文更輕鬆地調試 JavaScript 錯誤

偵錯程式碼總是一項乏味的任務。但你越了解自己的錯誤,就越容易糾正它們。

LogRocket 讓您以新的、獨特的方式理解這些錯誤。我們的前端監控解決方案追蹤使用者與 JavaScript 前端的互動,使您能夠準確查看使用者的操作導致了錯誤。

SOLID principles for JavaScript

LogRocket 記錄控制台日誌、頁面載入時間、堆疊追蹤、帶有標頭正文的慢速網路請求/回應、瀏覽器元資料和自訂日誌。了解 JavaScript 程式碼的影響從未如此簡單!

免費試用。

以上是JavaScript 的堅實原則的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn