1. 程式人生 > >讀懂 SOLID 的「依賴倒置」原則

讀懂 SOLID 的「依賴倒置」原則

寫在前頭

當我們在讀書,或者在和一些別的開發者聊天的時候,可能會談及或者聽到術語SOILD。在這些討論中,一些人會提及它的重要性,以及一個理想中的系統,應當包含它所包含的5條原則的特性。

我們在每次的工作中,你可能沒有那麼多時間思考關於架構這個比較大的概念,或者在有限的時間內或督促下,你也沒有辦法實踐一些好的設計理念。

但是,這些原則存在的意義不是讓我們“跳過”它們。軟體工程師應當將這些原則應用到他們的開發工作中。所以,在你每一次敲程式碼的時候,如何能夠正確的將這些原則付諸於行,才是真正的問題所在。如果可以那樣的話,你的程式碼會變得更優雅。

SOLID原則是由5個基本的原則構成的。這些概念會幫助創造更好(或者說更健壯)的軟體架構。這些原則包含(SOLID

是這5個原則的開頭字母組成的縮略詞):

起初這些原則是Robert C. Martin在1990年提出的,遵循這些原則可以幫助我們更好的構建,低耦合、高內聚的軟體架構,同時能夠真正的對現實中的業務邏輯進行恰到好處的封裝。

不過這些原則並不會使一個差勁的程式設計師轉變為一個優秀的程式設計師。這些法則取決於你如何應用它們,如果你是很隨意的應用它們,那等同於你並沒有使用它們一樣。

關於原則和模式的知識能夠幫助你決定在何時何地正確的使用它們。儘管這些原則僅僅是啟示性的,它們是常見問題的常規解決方案。實踐中,這些原則的正確性已經被證實了很多次,所以它們應當成為一種常識。

依賴倒置原則是什麼

  • 高階模組不應當依賴於低階模組。它們都應當依賴於抽象。
  • 抽象不應當依賴於實現,實現應當依賴於抽象。

這兩句話的意思是什麼呢?

一方面,你會抽象一些東西。在軟體工程和電腦科學中,抽象是一種關於規劃計算機系統中的複雜性的技術。它的工作原理一般是在一個人與系統互動的複雜環境中,隱藏當前級別下的更復雜的實現細節,同時它的範圍很廣,常常會覆蓋多個子系統。這樣,當我們在與一個以高階層面作為抽象的系統協作時,我們僅僅需要在意,我們能做什麼,而不是我們如何做。

另外,你會針對你的抽象,有一寫低級別的模組或者具體實現邏輯。這些東西與抽象是相反的。它們是被用於解決某些特定問題所編寫的程式碼。它們的作用域僅僅在某個單元和子系統中。比如,建立一個與MySQL資料庫的連線就是一個低級別的實現邏輯,因為它與某個特定的技術領域所繫結。

現在仔細讀這兩句話,我們能夠得到什麼暗示呢?

依賴倒置原則存在的真正意義是指,我們需要將一些物件解耦,它們的耦合關係需要達到當一個物件依賴的物件作出改變時,物件本身不需要更改任何程式碼。

這樣的架構可以實現一種鬆耦合的狀態的系統,因為系統中所有的元件,彼此之間都瞭解很少或者不需要了解系統中其餘元件的具體定義和實現細節。它同時實現了一種可測試和可替換的系統架構,因為在鬆耦合的系統中,任何元件都可以被提供相同服務的元件所替換。

但是相反的,依賴倒置也有一些缺點,就是你需要一個用於處理依賴倒置邏輯的容器,同時,你還需要配置它。容器通常需要具備能夠在系統中注入服務,這些服務需要具備正確的作用域和引數,還應當被注入正確的執行上下文中。

以提供Websocket連線服務為例子

舉個例子,我們可以在這個例子中學到更多關於依賴倒置的知識,我們將使用Inversify.js作為依賴倒置的容器,通過這個依賴倒置容器,我們可以看看如何針對提供Websocket連線服務的業務場景,提供服務。

比如,我們有一個web伺服器提供WebSockets連線服務,同時客戶端想要連線伺服器,同時接受更新的通知。當前我們有若干種解決方案來提供一個WebSocket服務,比如說Socket.ioSocks或者使用瀏覽器提供的關於原生的WebSocket介面。每一套解決方案,都提供不同的介面和方法供我們呼叫,那麼問題來了,我們是否可以在一個介面中,將所有的解決方案都抽象成一個提供WebSocket連線服務的提供者?這樣,我們就可以根據我們的實際需求,使用不同的WebSocket服務提供者。

首先,我們來定義我們的介面:

export interface WebSocketConfiguration {
  uri: string;
  options?: Object;
}
export interface SocketFactory {
  createSocket(configuration: WebSocketConfiguration): any;
}

注意在介面中,我們沒有提供任何的實現細節,因此它既是我們所擁有的抽象

接下來,如果我們想要一個提供Socket.io服務工廠:

import {Manager} from 'socket.io-client';

class SocketIOFactory implements SocketFactory {
  createSocket(configuration: WebSocketConfiguration): any {
    return new Manager(configuration.uri, configuration.opts);
  }
}

這裡已經包含了一些具體的實現細節,因此它不再是抽象,因為它聲明瞭一個從Socket.io庫中匯入的Manager物件,它是我們的具體實現細節。

我們可以通過實現SocketFactory介面,來增加若干工廠類,只要我們實現這個介面即可。

我們在提供一個關於客戶端連線例項的抽象:

export interface SocketClient {
  connect(configuration: WebSocketConfiguration): Promise<any>;
  close(): Promise<any>;
  emit(event: string, ...args: any[]): Promise<any>;
  on(event: string, fn: Function): Promise<any>;
}

然後再提供一些實現細節:

class WebSocketClient implements SocketClient {
  private socketFactory: SocketFactory;
  private socket: any;
  public constructor(webSocketFactory: SocketFactory) {
    this.socketFactory = webSocketFactory;
  }
  public connect(config: WebSocketConfiguration): Promise<any> {
    if (!this.socket) {
      this.socket = this.socketFactory.createSocket(config);
    }
    return new Promise<any>((resolve, reject) => {
      this.socket.on('connect', () => resolve());
      this.socket.on('connect_error', (error: Error) => reject(error));
    });
  }
  public emit(event: string, ...args: any[]): Promise<any> {
    return new Promise<string | Object>((resolve, reject) => {
      if (!this.socket) {
        return reject('No socket connection.');
      }
      return this.socket.emit(event, args, (response: any) => {
        if (response.error) {
          return reject(response.error);
        }
        return resolve();
      });
    });
  }
  public on(event: string, fn: Function): Promise<any> {
    return new Promise<any>((resolve, reject) => {
      if (!this.socket) {
        return reject('No socket connection.');
      }
      this.socket.on(event, fn);
      resolve();
    });
  }
  public close(): Promise<any> {
    return new Promise<any>((resolve) => {
      this.socket.close(() => {
        this.socket = null;
        resolve();
      });
    });
  }
}

值得注意的是,這裡我們在建構函式中,傳入了一個型別是SocketFactory的引數,這是為了滿足關於依賴倒置原則的第一條規則。對於第二條規則,我們需要一種方式來提供這個不需要了解內部實現細節的、可替換的、易於配置的引數。

這也是為什麼我們要使用Inversify這個庫的原因,我們來加入一些額外的程式碼和註解(裝飾器):

import {injectable} from 'inversify';
const webSocketFactoryType: symbol = Symbol('WebSocketFactory');
const webSocketClientType: symbol = Symbol('WebSocketClient');
let TYPES: any = {
    WebSocketFactory: webSocketFactoryType,
    WebSocketClient: webSocketClientType
};

@injectable()
class SocketIOFactory implements SocketFactory {...}
...
@injectable()
class WebSocketClient implements SocketClient {
public constructor(@inject(TYPES.WebSocketFactory) webSocketFactory: SocketFactory) {
  this.socketFactory = webSocketFactory;
}

這些註釋(裝飾器)僅僅會在程式碼執行時,在如何提供這些元件例項時,提供一些元資料,接下來我們僅僅需要建立一個依賴倒置容器,並將所有的物件按正確的型別繫結起來,如下:

import {Container} from 'inversify';
import 'reflect-metadata';
import {TYPES, SocketClient, SocketFactory, SocketIOFactory, WebSocketClient} from '@web/app';
const provider = new Container({defaultScope: 'Singleton'});
// Bindings
provider.bind<SocketClient>(TYPES.WebSocketClient).to(WebSocketClient);
provider.bind<SocketFactory>(TYPES.WebSocketFactory).to(SocketIOFactory);
export default provider;

讓我們來看看我們如何使用我們提供連線服務的客戶端例項:

var socketClient = provider.get<SocketClient>(TYPES.WebSocketClient);

當然,使用Inversify可以提供一些更簡單易用的繫結,可以通過瀏覽它的網站來了解。

譯者注

一般說到依賴倒置原則,往往第一個想到的術語即是依賴注入,這種在各個技術棧都有應用,之後又會馬上想到springng等前後端框架。

我們確實是通過使用這些框架熟知這個概念的,但是如果你仔細想想的話,是否還有其他的一些場景也使用了類似的概念呢?

比如:

  • 一些使用外掛和中介軟體的框架,如expressredux
  • js中this的動態繫結
  • js中的回撥函式

也許有的人會不同意我的觀點,會說依賴注入一般都是面向類和介面來講的,這確實有一定的道理,但是我認為沒有必要侷限在一種固定的模式中去理解依賴倒置,畢竟它是一種思想,一種模式,在js中,所有的東西都是動態的,函式是一等公民,是物件,那麼把這些與依賴倒置原則聯絡起來,完全也講的通。我們真正關心的是核心問題是如何解耦,把更多的注意力投入的真正的業務邏輯中去。