【譯】Understanding SOLID Principles - Dependency Inversion

Understanding SOLID Principles: Dependency Inversion

這是理解SOLID原則中,關於依賴倒置原則如何幫助我們編寫低耦合和可測試代碼的第一篇文章。

寫在前頭

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

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

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

SOLID原則是由5個基本的原則構成的。這些概念會幫助創造更好(或者說更健壯)的軟件架構。這些原則包含(SOLID是這5個原則的開頭字母組成的縮略詞):

  • S stands for SRP (Single responsibility principle):單一職能原則
  • O stands for OCP (Open closed principle):開閉原則
  • L stands for LSP (Liskov substitution principle):里氏替換原則
  • I stand for ISP ( Interface segregation principle):接口隔離原則
  • D stands for DIP ( Dependency inversion principle):依賴倒置原則

起初這些原則是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中,所有的東西都是動態的,函數是一等公民,是對象,那麼把這些與依賴倒置原則聯繫起來,完全也講的通。我們真正關心的是核心問題是如何解耦,把更多的注意力投入的真正的業務邏輯中去。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章