設計原則:介面隔離原則(ISP)

Mr於發表於2021-04-21

介面隔離原則的英文是Interface Segregation Principle,縮寫就是ISP。與里氏替換原則一樣其定義同樣有兩種

定義1:

Clients should not be forced to depend upon interfaces that they don'tuse.

(客戶端被強迫不應該依賴它不需要的介面。)

定義2:

The dependency of one class to another one should depend on the smallest possibleinterface.

(類間的依賴關係應該建立在最小的介面上。)

這兩種定義相比較,我更喜歡它的第一種定義。其中最重要的概念就是“介面”,這裡的介面其實不僅僅是指OOP概念中的介面,其小到類所暴露出來的public方法、所提供的公共屬性,大到業務上一組API的組合,甚至系統對外所提供的服務,都可以稱之為介面,介面是一種抽象的約定。而客戶端可以理解為介面的呼叫者或者使用者。

這個表述看起來很容易,但是在真正設計中卻很難,在大多數的專案裡,也是經常被違背的原則之一,因為設計者往往很難站在使用者的角度上去看待問題,甚至有很多設計者根本沒有介面的概念,他們往往只從類的角度上去思考問題,而在類設計完畢後,而為了使用介面象徵性的增加一個介面,然後把類的方法簽名搬到介面裡而已。(我們可以想想,自己在專案中是不是也是先寫類,後寫介面呢?)

2.如何理解並運用好介面隔離原則

介面隔離原則要求我們儘量提供小而美的介面,而不是一個龐大臃腫的介面,以試圖滿足所有的呼叫者使用,它是對介面的的一種規範和約束。

其實在設計中想要運用好介面隔離原則,有一個好的辦法,就是需要我們站在使用者的角度上去思考問題,按需去設計介面,我們可以通過幾個例子來看一下

2.1 OOP中的介面隔離原則

現在,我們有一個商品系統,我想絕對多數的系統中都會按照下面這種方式進行介面的設計

image-20210317082659780

它提供了CRUD操作供客戶端呼叫。隨著業務的不斷髮展壯大,我們發現商品訪問的效能越來越差,資料庫的壓力也越來越大,這時我們需要對商品系統增加快取的功能,但是有些場景下我們又需要能夠實時的查詢到商品系統,這種場景下應該怎麼辦?


public ProductInfo get(String id) {
	if(cache.contains(key)){
		return cache.get(key);
	}
	return productRepository.get()
}

public ProductInfo get(String id,boolean isCache) {
	if(cache.contains(key)&&isCache){
		return cache.get(key);
	}
	return productRepository.get()
}

這時許多人的做法可能是增加一個引數isCache由客戶端傳入來標記是否需要讀取快取,不得不說這真是一個餿主意,因為這違背了一個最基礎的原則——開閉原則,它會給我們後續維護帶來很大的災難。

也有一些人可能會想到我提供一個CacheProductService 也實現一下IProductService 在這個服務裡面做快取的功能,這樣需要快取的客戶端就例項化CacheProductService 不需要快取的客戶端就還是例項化原來的 ProductService

public class CacheProductService implements IProductService {
    @Override
    public ProductInfo get(String id) {
        if(cache.contains(id)){
            return cache.get(id);
        }
        return productRepository.get(id);
    }

    @Override
    public List<ProductInfo> getList() {
        if(cache.contains("product-key")){
            return cache.get("product-key");
        }
        return productRepository.getList();
    }

    @Override
    public void create(ProductInfo productInfo) {    }

    @Override
    public void modify(ProductInfo productInfo) {    }

    @Override
    public void delete(String id) {    }
}

就像我上面寫的這樣,但這樣又有一些問題,首先這樣的設計違背了裡式替換原則,再者增刪改操作並不需要快取。

那麼到底應該如何去做呢?這個時候我們可以利用介面隔離原則,

  1. 把原來的IProductService 拆分成兩個IReadProductService IOperProductService
  2. 然後我們的ProductService實現這兩個介面,而CacheProductService只實現IReadProductService
  3. 需要快取的客戶端使用IReadProductService ,不需要快取的客戶端使用IReadProductService IOperProductService

image-20210318084901258

public class ProductService implements IReadProductService,IOperProductService {
    @Override
    public ProductInfo get(String id) {
        return productRepository.get(id);
    }
    
    @Override
    public List<ProductInfo> getList() {
        return productRepository.getList();
    }
    
    @Override
    public void create(ProductInfo productInfo) {
        productRepository.create(productInfo);
    }
    
    @Override
    public void modify(ProductInfo productInfo) {
        productRepository.modify(productInfo);
    }
    
    @Override
    public void delete(String id) {
        productRepository.delete(id);
    }
}

public class CacheProductService implements IReadProductService {
    @Override
    public ProductInfo get(String id) {
        if(cache.contains(id)){
            return cache.get(id);
        }
        return productRepository.get(id);
    }

    @Override
    public List<ProductInfo> getList() {
        if(cache.contains("product-key")){
            return cache.get("product-key");
        }
        return productRepository.getList();
    }
}

甚至在客戶端和設計需要的情況下我們可以把簡單的CRUD介面拆分最喜歡的拆分成但方法介面ICreate IModify IDelete IRead。介面的規模越小,其複用性和靈活性也就越高,但我們必須注意一點那就是按需設計,因為複用性和靈活性增加的同時,必然也會帶來增加系統的複雜度,降低可讀性等問題,因此我們必須要掌握好設計的度。

public interface ICreate<T> {
    void create(T t);
}

public interface IModify<T> {
    void modify(T t);
}

public interface IDelete {
    void delete(String id);
}

public interface IRead<T> {
    T get(String id);

    List<T> getList();
}

public class ProductService implements ICreate<ProductInfo>,IModify<ProductInfo>,IDelete,IRead<ProductInfo>{

}

2.3 類設計中的介面隔離原則

其實介面隔離原則的應用不應該侷限於OOP中的"介面",我們不應被介面所迷惑,介面隔離原則中的“介面”,更像是一種約定,因此在類的設計中我們同樣應該遵循介面原則,因為類所提供的一些公共方法也是一種約定。

比如我們在監控、統計等系統中通常會用到各個指標的統計,比如均值、求和、最大值、中位數.......,這時我們設計了一個Indicator類,裡面提供了sum avg p50 p95等屬性,然後提供了一個compute方法來計算各個指標的值,最後返回一個Indicator物件。

public class Indicator {

    private Long sum;

    private Long min;

    private Long avg;

    private Long p50;

    private Long p95;

    private Long p99;

    public Indicator compute(List<Long> list){

        Indicator indicator=new Indicator();
		//...
        return indicator;
    }

   
}

我們想一想這樣會不會有問題,如果我們所有的呼叫者都需要所有的指標,這樣設計並沒有什麼問題,但如果有些呼叫者可能僅僅需要其中的某一個或者幾個指標就會有問題了。因為,如果我只需要其中一個指標,但卻計算了所有的指標值,浪費時間效能不說,一旦其中某一個指標計算過程中除了錯誤,就會導致我連其它幾個指標都拿不到。這樣客戶端就依賴了自己所不需要的東西,違背了介面隔離的原則。

這時我們可以考慮把各個指標的計算分開來

   public Long min(List<Long> list){   }

   public Long max(List<Long> list){   }

   public Long avg(List<Long> list){   }

   ......

這樣看起來介面隔離原則跟單一職責原則有些相似,但其實是有不同的,單一職責原則主要是針對模組、類、方法的設計,注重職責的單一,而介面隔離原則更注重站在呼叫者的角度上看約定是否存在自己所不需要的東西,它要求給每個使用者都按需提供介面,而不是建立一個龐大臃腫的介面以供所有呼叫者使用。

2.3 系統設計中的介面隔離原則

不僅僅是在OOP中的介面和類的設計要遵循介面隔離原則,在系統對外所提供的API的設計中,我們同樣應該遵循介面隔離原則。

例如在使用者系統的設計中,多數人都會提供一個使用者API,然後這個API提供了一個大而全的介面列表。createmodifydeleteget .......

image-20210317085523677

但有些場景下並不合理的,因為這是站在服務提供者的角度上進行設計的。如果你的使用者服務僅僅是提供給後臺管理系統使用那麼並沒有問題,但是如果同時也提供給登入系統使用那麼就會有問題了,因為登入系統可能只需要登入註冊兩個操作,那麼對於登入系統的來說使用者服務就提供了它所不需要的介面。這樣以來我們對登入系統暴漏了刪除和修改介面增加了系統風險。

此時,我們應該對登入系統單獨的提供一組API介面。如下圖所示

image-20210317085843149

3 總結

大而全的東西存在了太多的不確定性,在介面的設計中,我們應該遵循介面隔離原則,儘量提供小而美的介面。但同時我們也應該注意設計要適度,因為越小的東西就越靈活,但如果過於小又會增加系統的複雜性。

介面隔離原則強調了客戶端被強迫不應該依賴它不需要的介面。它的應用不應侷限於簡單的OOP介面,小到類、方法的設計,大到系統之間的互動......介面隔離原則都可以指導我們進行更好的設計。

系列文章

設計原則:單一職責(SRP)

設計原則:開閉原則(OCP)

設計原則:裡式替換原則(LSP)

設計原則:介面隔離原則(ISP)

設計原則:依賴倒置原則(DIP)

何謂高質量程式碼?

理解RESTful API

關注下方公眾號,回覆“程式碼的藝術”,可免費獲取重構、設計模式、程式碼整潔之道等提升程式碼質量等相關學習資料

相關文章