如何寫出優雅的 JS 代碼?使用 SOLID 原則
本文轉載自微信公眾號「大遷世界」,轉載本文請聯系大遷世界公眾號。
設計模式的六大原則有:
- Single Responsibility Principle:單一職責原則
- Open Closed Principle:開閉原則
- Liskov Substitution Principle:里氏替換原則
- Law of Demeter:迪米特法則
- Interface Segregation Principle:接口隔離原則
- Dependence Inversion Principle:依賴倒置原則
把這六個原則的首字母聯合起來(兩個 L 算做一個)就是 SOLID (solid,穩定的),其代表的含義就是這六個原則結合使用的好處:建立穩定、靈活、健壯的設計。下面我們來分別看一下這六大設計原則。
單一職責原則(SRP)
單一功能原則 :單一功能原則 認為對象應該僅具有一種單一功能的概念。
換句話說就是讓一個類只做一種類型責任,當這個類需要承擔其他類型的責任的時候,就需要分解這個類。在所有的SOLID原則中,這是大多數開發人員感到最能完全理解的一條。嚴格來說,這也可能是違反最頻繁的一條原則了。單一責任原則可以看作是低耦合、高內聚在面向對象原則上的引申,將責任定義為引起變化的原因,以提高內聚性來減少引起變化的原因。責任過多,可能引起它變化的原因就越多,這將導致責任依賴,相互之間就產生影響,從而極大的損傷其內聚性和耦合度。單一責任,通常意味著單一的功能,因此不要為一個模塊實 現過多的功能點,以保證實體只有一個引起它變化的原因。
「不好的寫法」
- class UserSettings {
- constructor(user) {
- this.user = user;
- }
- changeSettings(settings) {
- if (this.verifyCredentials()) {
- // ...
- }
- }
- verifyCredentials() {
- // ...
- }
- }
「好的寫法」
- class UserAuth {
- constructor(user) {
- this.user = user;
- }
- verifyCredentials() {
- // ...
- }
- }
- class UserSettings {
- constructor(user) {
- this.user = user;
- this.auth = new UserAuth(user);
- }
- changeSettings(settings) {
- if (this.auth.verifyCredentials()) {
- // ...
- }
- }
- }
開放閉合原則 (OCP)
軟件實體應該是可擴展,而不可修改的。也就是說,對擴展是開放的,而對修改是封閉的。這個原則是諸多面向對象編程原則中最抽象、最難理解的一個。
- 通過增加代碼來擴展功能,而不是修改已經存在的代碼。
- 若客戶模塊和服務模塊遵循同一個接口來設計,則客戶模塊可以不關心服務模塊的類型,服務模塊可以方便擴展服務(代碼)。
- OCP支持替換的服務,而不用修改客戶模塊。
說大白話就是:你不是要變化嗎?,那么我就讓你繼承實現一個對象,用一個接口來抽象你的職責,你變化越多,繼承實現的子類就越多。
「不好的寫法」
- class AjaxAdapter extends Adapter {
- constructor() {
- super();
- this.name = "ajaxAdapter";
- }
- }
- class NodeAdapter extends Adapter {
- constructor() {
- super();
- this.name = "nodeAdapter";
- }
- }
- class HttpRequester {
- constructor(adapter) {
- this.adapter = adapter;
- }
- fetch(url) {
- if (this.adapter.name === "ajaxAdapter") {
- return makeAjaxCall(url).then(response => {
- // transform response and return
- });
- } else if (this.adapter.name === "nodeAdapter") {
- return makeHttpCall(url).then(response => {
- // transform response and return
- });
- }
- }
- }
- function makeAjaxCall(url) {
- // request and return promise
- }
- function makeHttpCall(url) {
- // request and return promise
- }
「好的寫法」
- class AjaxAdapter extends Adapter {
- constructor() {
- super();
- this.name = "ajaxAdapter";
- }
- request(url) {
- // request and return promise
- }
- }
- class NodeAdapter extends Adapter {
- constructor() {
- super();
- this.name = "nodeAdapter";
- }
- request(url) {
- // request and return promise
- }
- }
- class HttpRequester {
- constructor(adapter) {
- this.adapter = adapter;
- }
- fetch(url) {
- return this.adapter.request(url).then(response => {
- // transform response and return
- });
- }
- }
里氏替換原則(LSP)
里氏替換原則 :里氏替換原則 認為“程序中的對象應該是可以在不改變程序正確性的前提下被它的子類所替換的”的概念。
LSP則給了我們一個判斷和設計類之間關系的基準:需不需 要繼承,以及怎樣設計繼承關系。
當一個子類的實例應該能夠替換任何其超類的實例時,它們之間才具有is-A關系。繼承對于「OCP」,就相當于多態性對于里氏替換原則。子類可以代替基類,客戶使用基類,他們不需要知道派生類所做的事情。這是一個針對行為職責可替代的原則,如果S是T的子類型,那么S對象就應該在不改變任何抽象屬性情況下替換所有T對象。
客戶模塊不應關心服務模塊的是如何工作的;同樣的接口模塊之間,可以在不知道服務模塊代碼的情況下,進行替換。即接口或父類出現的地方,實現接口的類或子類可以代入。
「不好的寫法」
- class Rectangle {
- constructor() {
- this.width = 0;
- this.height = 0;
- }
- setColor(color) {
- // ...
- }
- render(area) {
- // ...
- }
- setWidth(width) {
- this.width = width;
- }
- setHeight(height) {
- this.height = height;
- }
- getArea() {
- return this.width * this.height;
- }
- }
- class Square extends Rectangle {
- setWidth(width) {
- this.width = width;
- this.height = width;
- }
- setHeight(height) {
- this.width = height;
- this.height = height;
- }
- }
- function renderLargeRectangles(rectangles) {
- rectangles.forEach(rectangle => {
- rectangle.setWidth(4);
- rectangle.setHeight(5);
- const area = rectangle.getArea(); // BAD: Returns 25 for Square. Should be 20.
- rectangle.render(area);
- });
- }
- const rectangles = [new Rectangle(), new Rectangle(), new Square()];
- renderLargeRectangles(rectangles);
「好的寫法」
- class Shape {
- setColor(color) {
- // ...
- }
- render(area) {
- // ...
- }
- }
- class Rectangle extends Shape {
- constructor(width, height) {
- super();
- this.width = width;
- this.height = height;
- }
- getArea() {
- return this.width * this.height;
- }
- }
- class Square extends Shape {
- constructor(length) {
- super();
- this.length = length;
- }
- getArea() {
- return this.length * this.length;
- }
- }
- function renderLargeShapes(shapes) {
- shapes.forEach(shape => {
- const area = shape.getArea();
- shape.render(area);
- });
- }
- const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
- renderLargeShapes(shapes);
接口隔離原則(ISP)
接口隔離原則 :接口隔離原則 認為“多個特定客戶端接口要好于一個寬泛用途的接口”的概念。
不能強迫用戶去依賴那些他們不使用的接口。換句話說,使用多個專門的接口比使用單一的總接口總要好。
這個原則起源于施樂公司,他們需要建立了一個新的打印機系統,可以執行諸如裝訂的印刷品一套,傳真多種任務。此系統軟件創建從底層開始編制,并實現了這些 任務功能,但是不斷增長的軟件功能卻使軟件本身越來越難適應變化和維護。每一次改變,即使是最小的變化,有人可能需要近一個小時的重新編譯和重新部署。這 是幾乎不可能再繼續發展,所以他們聘請羅伯特Robert幫助他們。他們首先設計了一個主要類Job,幾乎能夠用于實現所有任務功能。只要調用Job類的 一個方法就可以實現一個功能,Job類就變動非常大,是一個胖模型啊,對于客戶端如果只需要一個打印功能,但是其他無關打印的方法功能也和其耦合,ISP 原則建議在客戶端和Job類之間增加一個接口層,對于不同功能有不同接口,比如打印功能就是Print接口,然后將大的Job類切分為繼承不同接口的子 類,這樣有一個Print Job類,等等。
「不好的寫法」
- class DOMTraverser {
- constructor(settings) {
- this.settings = settings;
- this.setup();
- }
- setup() {
- thisthis.rootNode = this.settings.rootNode;
- this.animationModule.setup();
- }
- traverse() {
- // ...
- }
- }
- const $ = new DOMTraverser({
- rootNode: document.getElementsByTagName("body"),
- animationModule() {} // Most of the time, we won't need to animate when traversing.
- // ...
- });
「好的寫法」
- class DOMTraverser {
- constructor(settings) {
- this.settings = settings;
- this.options = settings.options;
- this.setup();
- }
- setup() {
- thisthis.rootNode = this.settings.rootNode;
- this.setupOptions();
- }
- setupOptions() {
- if (this.options.animationModule) {
- // ...
- }
- }
- traverse() {
- // ...
- }
- }
- const $ = new DOMTraverser({
- rootNode: document.getElementsByTagName("body"),
- options: {
- animationModule() {}
- }
- });
依賴倒置原則(DIP)
依賴倒置原則:依賴倒置原則 認為一個方法應該遵從“依賴于抽象而不是一個實例” 的概念。依賴注入是該原則的一種實現方式。
依賴倒置原則(Dependency Inversion Principle,DIP)規定:代碼應當取決于抽象概念,而不是具體實現。
- 高層模塊不要依賴低層模塊
- 高層和低層模塊都要依賴于抽象;
- 抽象不要依賴于具體實現
- 具體實現要依賴于抽象
- 抽象和接口使模塊之間的依賴分離
類可能依賴于其他類來執行其工作。但是,它們不應當依賴于該類的特定具體實現,而應當是它的抽象。這個原則實在是太重要了,社會的分工化,標準化都 是這個設計原則的體現。顯然,這一概念會大大提高系統的靈活性。如果類只關心它們用于支持特定契約而不是特定類型的組件,就可以快速而輕松地修改這些低級 服務的功能,同時最大限度地降低對系統其余部分的影響。
「不好的寫法」
- class InventoryRequester {
- constructor() {
- this.REQ_METHODS = ["HTTP"];
- }
- requestItem(item) {
- // ...
- }
- }
- class InventoryTracker {
- constructor(items) {
- this.items = items;
- // BAD: We have created a dependency on a specific request implementation.
- // We should just have requestItems depend on a request method: `request`
- this.requester = new InventoryRequester();
- }
- requestItems() {
- this.items.forEach(item => {
- this.requester.requestItem(item);
- });
- }
- }
- const inventoryTracker = new InventoryTracker(["apples", "bananas"]);
- inventoryTracker.requestItems();
「好的寫法」
- class InventoryTracker {
- constructor(items, requester) {
- this.items = items;
- this.requester = requester;
- }
- requestItems() {
- this.items.forEach(item => {
- this.requester.requestItem(item);
- });
- }
- }
- class InventoryRequesterV1 {
- constructor() {
- this.REQ_METHODS = ["HTTP"];
- }
- requestItem(item) {
- // ...
- }
- }
- class InventoryRequesterV2 {
- constructor() {
- this.REQ_METHODS = ["WS"];
- }
- requestItem(item) {
- // ...
- }
- }
- const inventoryTracker = new InventoryTracker(
- ["apples", "bananas"],
- new InventoryRequesterV2()
- );
- inventoryTracker.requestItems();