分類
Design Principle

Inversion Of Control

IoC 是什麼 ?

IoC 是一種設計原則,主要的用途是降低物件之間的耦合,特別是在建立物件的耦合關係。

為什麼要使用 IoC ?

系統為了運作,其中的物件必定有依賴關係,當系統越大越複雜,這些依賴關係也越來越嚴重。因此希望透過由外界提供(注入)物件而不是主動建立物件的方式來降低耦合關係。


如何使用 IoC ?

IoC 比較常見的方法為 DI (Dependency injection),而 DI 的具體實作有 Setter 注入,建構式注入,介面注入。以下使用範例來描述 DI 的具體實作。

當 Client 依賴於 Server,主動建立物件的方式就是使用 new 關鍵字建立需要的物件,如下在 Client 的建構式中建立了 Server 物件。

public class Client {

private Server mServer;

public Client() {
mServer = new Server();
}
}

Setter 注入就是提供一個公開的方法傳入 Server。

public void setServer(Server server) {
mServer = server;
}

建構式注入則是換成從建構式傳入 Server。

public Client(Server server) {
mServer = server;
}

介面注入會另外建立介面(IServer)並提供方法來傳入 Server。

public interface IServer {

public void setServer(Server server);
}
public class Client implements IServer {

private Server mServer;

@Override
public void setServer(Server server) {
mServer = server;
}
}


Simple Factory Pattern 簡單工廠模式

以上的三種注入方式適用於簡單的依賴情況。
若是產生依賴物件的過程較複雜,可以考慮套用工廠系列模式,一個非常簡單的簡單工廠如下

public class ServerFactory {

public static Server makeServer(){
return new Server();
}
}

而 Client 就從 ServerFactory 來取得 Server

public class Client {

private Server mServer = ServerFactory.makeServer();

}

乍看之下 ServerFactory 似乎只做了很簡單的工作,但這個工廠卻可以隔離產生Server 的變化對 Client 的影響。

首先是產生 Server 的變化
假設在系統中有大量產生 Server 的呼叫點(new Server()),但沒有工廠隔開。萬一因為需求變化導致 Server 的建構式需要改變(多傳入一個參數),就代表系統中所有產生 Server 的呼叫點都要修改。若是使用工廠,便可把變化封裝在工廠中。

第二是測試 Client 的難度
若 Server 本身是重量級物件,建立過程需要一堆繁瑣的過程或元件(如存取資料庫或網路元件等等)。那麼測試 Client 的難度會變得非常高,甚至無法測試。而透過 ServerFactory,可以很簡單的建立 MockServer 來測試 Client 的邏輯。


DI 框架

以上的描述都是屬於自己動手來建立 IoC 機制,這些方式對於簡單的依賴關係是足夠的。

但若情況再複雜一些,比如建立 Server 需要再建立 HttpConnection 物件而 HttpConnection 物件需要再建立 Authorization 物件,Authorization 物件又需要傳入 Account 物件…。

因為這些依賴關係之間層層相依且具有順序,靠手動方式建立 IoC 不僅容易出錯還需要寫出一堆樣板代碼,這種情況就可以考慮透過 DI 框架來自動管理。

分類
Design Principle

Law of Demeter (LOD) (迪米特法則)

Law of Demeter (LOD)

定義:

也稱最少知識原則(Principle of Least Knowledge)。
模組應該儘可能的減少其他模組交互,目的在於降低彼此之間的依賴。
 

說明:

以下為必須遵循 LOD 的條件
類別 O 的任何方法 m 只能呼叫屬於以下情況的方法
1. 類別 O 本身的方法。
2. 傳入 m 的參數的方法。
3. 在 m 中建立對象的方法。
4. 任何直接持有的對象方法。
 
Example Code :

public class Man {
    private Vehicle mVehicle;
    private void methodInMan(){}
    public void driveVehicle(Vehicle vehicle){
    	methodInMan();// Rule 1
        vehicle.drive();// Rule 2
        new Car().drive();// Rule 3
        mVehicle = new Bike();
        mVehicle.drive();//Rule 4
        new Bike().getClass().getName(); //violation at getName()
    }
}

如何修改遵循 LOD ,可以在 Bike 中直接建立方法來取得 class name,如下

public class Bike implements Vehicle{
    ...
    public String getClassName(){
        return getClass().getName();
    }
}

client 端呼叫

public class Man {
    private Vehicle mVehicle;
    private void methodInMan(){}
    public void driveVehicle(Vehicle vehicle){
    	methodInMan();// Rule 1
        vehicle.drive();// Rule 2
        new Car().drive();// Rule 3
        mVehicle = new Bike();
        mVehicle.drive();//Rule 4
        new Bike().getClassName(); //Rule 3
    }
}

 
注意:
在重構中的程式碼壞味道第15點 Message Chain (過度耦合的消息鏈) 其本身也是描述相同的問題:
一個對象請求另一個對象,然後再向後者請求一個對象。
代表對象之間的結構緊密耦合,一旦對象關係發生變化,客戶端不得不做出相應修改。
可以使用 Hide Delegate 來重構!!
 

分類
Design Principle

Interface Segregation Principle (ISP) (介面隔離原則)

Interface Segregation Principle (ISP)

 

定義:

客戶端不應該被強迫依賴不需要的方法。(介面應該只提供客戶端需要的方法)
 
 

說明:

一個過大的 interface ,通常代表其中有某些功能是客戶端不需要的,如果客戶端實作了不需要的功能 ,這些功能會造成不必要的耦合
我們可以把過大的 interface 分離,將其中某些功能拆離到另一個 interface 中。
 

Example Code:

回到 OCP 的初始範例,其中 Bike , Car 都有類似的方法( driveBike , driveCar)。

public class Bike {
    public void driveBike(){
        System.out.println("drive Bike");
    }
}
public class Car {
    public void driveCar(){
        System.out.println("drive Car");
    }
    public void openWindow(){
        System.out.println("open window");
    }
}

現在必須建立抽象層 Vehicle,和 OCP 不同的是我們把 openWindow 也放到 Vehicle 中。

public interface Vehicle {
    public void drive();
    public void openWindow();
}

對 Bike 而言,必須實作 openWindow 行為,但該行為對 Bike 是沒有意義,也違反了 ISP。

public class Bike implements Vehicle{
    public void drive(){
        System.out.println("drive Bike");
    }
    @Override
    public void openWindow() {
        // unmeaning action for Bike
    }
}

為了遵循 ISP,可以另外建立 Window 介面,並把 openWindow 移到 Window 介面中。

public interface Window {
    public void openWindow();
}

現在 Bike 可以不必實作 openWindow 方法。

public class Bike implements Vehicle{
    public void drive(){
        System.out.println("drive Bike");
    }
}

 
 
 

分類
Design Principle

Dependence Inversion Principle (DIP) (相依反向原則)

Dependence Inversion Principle (DIP)

 

定義:

高層模組不應該相依於低層模組,兩者都應該相依於抽象。
抽象不應該相依於細節,細節應該相依於抽象。
 

說明:

傳統的軟體開發方法,如結構化分析與設計傾向於創造高階模組相依於低階模組。目標就是描述高階模組如何呼叫低階模組的子程式結構
相較於以傳統程序化方法的相依結構,設計良好的物件導向程式的相依結構應該是反向的
高層指的是依賴者 , 低層指的是被依賴者。如下圖
dip_before
模組之間的依賴應該透過抽象,而不應該透過具體的方式(針對介面寫程式)
對象的引用盡量是抽象型態而不是具體型態
因此使用DIP之後應該為
dip_after
HighLevel 和 LowLevel 都相依於 abstract level。
 

注意:

若是具體類別已經相當穩定,不太會變化,依賴於該具體類別也是無妨
 
Example Code:
以先前的 LSP 的範例 來說明

public interface Vehicle {
    public void drive();
}
public class Bike implements Vehicle{
    public void drive(){
        System.out.println("drive Bike");
    }
}
public class Car implements Vehicle{
    public void drive(){
        System.out.println("drive car");
    }
    public void openWindow(){
        System.out.println("open window");
    }
}

若用一個 Man 來代表使用者,並讓 Man 可以操縱 Bike,在未加考慮的情況會直接寫出以下 code,並違反了 DIP。
在 driveBike 的方法參數,並沒有依賴抽象(Vehicle),相反的依賴具體型態(Bike)。

public class Man {
    public void driveBike(Bike bike){
        bike.drive();
    }
}

修改的方法很簡單,把參數型態改為 Vehicle。
另一個依賴抽象的好處是可以減少多餘的code,在依賴具體 Bike 的情況下, 若我們想讓 Man 可以駕駛 Car,  必須還要寫出另一個 driveCar 方法。

public class Man {
    public void driveBike(Bike bike){
        bike.drive();
    }
    public void driveCar(Car car){
        car.drive();
    }
}

修改 Man 的 driveVehicle 方法,讓其參數型態依賴抽象(Vehicle)。

public class Man {
    public void driveVehicle(Vehicle vehicle){
        vehicle.drive();
    }
}

調用端呼叫 driveVehicle 方法可以根據傳入參數型態的不同,具有不同的行為。

public class Main {
    public static void main(String[] args) {
        Man man = new Man();
        man.driveVehicle(new Bike());
        man.driveVehicle(new Car());
    }
}

 
另一種依賴具體的形式為基本型別。如

public int getSummary(int a, int b)
{
...
}

如果需要支援多種 Summary 算法,可以引入抽象層為參數。

public int getSummary(CustomType a, CustomType b){
...
}
public interface CustomType{
...
}

 
 
 
 
 
 
 

分類
Design Principle

Liskov Substitution Principle (LSP) (Liskov 替換原則)

Liskov Substitution Principle (LSP)

 

定義:

衍生類別(Sub Type)必須能夠替換成他們的基礎類別(Base Type)。
衍生類別應該可以替換任何基楚類別出現的位置,且程序還能正常工作。
 

說明:

1. 不能僅用 is-a 的關係就建立繼承,必須考慮是否在基礎類別中有些方法對衍生類別而言是不需要或是無意義的。
這些沒有意義的方法會造成不可預期的結果。
2.一些違反 LSP 觀察點(基本上從衍生類別刪除基礎類別的功能就代表了無法取代基礎類別,因而違反了LSP)
1.退化函式(不一定絕對違反LSP,但需要檢查一下),基本上退化函式就是繼承基礎類別的函式,但在該函式中沒有做任何事,這種函式就是沒有意義的。

public class BaseClass {
  public void method(){
    //do something
  }
}
public class DeriveClass extends BaseClass{
  @Override
  public void method() {
    //do nothing
  }
}

DeriveClass 的 method 可能違反了LSP
2.在衍生類別丟出基礎類別沒有的異常

注意:

必須要有繼承關係才需要考慮 LSP ,而 LSP 是讓設計達到 OCP 的規則之一 。
 

Example Code :

Vehicle 有2個簡單的方法, 分別是 drive (駕駛)和 openWindow (打開車窗)。

public class Vehicle {
    public void drive(){
        System.out.println("drive vehicle");
    }
    public void openWindow(){
        System.out.println("open window");
    }
}

若單以語意而言 Vehicle 和 Bike  有 is-a 的關係,也有共同的方法 drive , 但 openWindow 對 Bike 而言是沒有意義的。
若強制讓 Bike 繼承 Vehicle 會有2種情況。
1. Bike 不覆寫 openWindow 方法

public class Bike extends Vehicle{
    public void drive(){
        System.out.println("drive Bike");
    }
}

2. Bike 覆寫 openWindow 方法,但方法內容令人迷惑。

public class Bike extends Vehicle{
    public void drive(){
        System.out.println("drive Bike");
    }
    public void openWindow(){
        System.out.println("Bike can't open window");
    }
}

不論哪種方法都無法到達呼叫端預期的結果 e.g.

Vehicle bike = new Bike();
bike.openWindow();// why bike can open window?

因此 Bike 繼承 Vehicle 不符合 LSP 。
 
如何讓 Bike 符合 LSP?
我們可以重新建立抽象層 Vehicle 並提取 Bike 和 Car 共同的部份並放入 Vehicle 中,並讓2者都派生 Vehicle。

public interface Vehicle {
    public void drive();
}

Bike 實作 Vehicle

public class Bike implements Vehicle{
    public void drive(){
        System.out.println("drive Bike");
    }
}

Car 實作 Vehicle

public class Car implements Vehicle{
    public void drive(){
        System.out.println("drive car");
    }
    public void openWindow(){
        System.out.println("open window");
    }
}

最後以語意來說 Car , Bike 都是 Vehicle。 Vehicle 的方法對於 Car , Bike 都是有意義的。
 

分類
Design Principle

Open Close Principle (OCP) (開放關閉原則)

Open-Close Principle(OCP) 

 

定義:

軟體實體(Class, Method, Module等等)應該針對擴展開放,針對修改關閉
換句話說應該盡量透過擴展的方式來實現變化,而不是透過修改原架構的方式。
 

說明:

OCP 的2個特徵:
1.針對擴展而開放:
當需求發生變化時,必須可以改變模組以符合需求的變化。
2.針對修改而關閉:
當改變模組時,不必改動原有的架構。
ㄧ開始看起來1跟2是互相矛盾的,一般來說改變模組就是代表修改架構,但在OOP中可以藉由抽象來達到。
如何利用抽象來達到OCP呢?
模組依賴於固定的抽象型別,就是代表面對修改而關閉,讓需求發生變化的部份實現在該抽象型別的子類別中,就是面對擴展而開放。
對有相似行為類別的建立抽象層,如 abstract class, 或是 interface
將公共屬性或方法提取到抽象層中,當需要擴展行為(新增)時只需要建立新的子類別並繼承抽象層,不必修改原有的行為
 

注意:

實作 OCP 抽象層需要花費時間和精力,也可能會造成複雜度的上升,OCP 應該只運用在程序中頻繁發生的變化上
在敏捷軟件開發:原則,樣式及實務(Agile software development: principles, Patterns, and Practices)中提到
可以運用 “第一顆子彈” 來說明運用時機。
第一顆子彈:最初編寫代碼時假設變化不會發生,因此不做出用不到的抽象層,直到變化發生,再加入抽象層隔離同類變化。

 

Example Code :

Car , Bike 都有類似的方法(driveBike , driveCar)但並沒有為其建立抽象層。

public class Bike {
    public void driveBike(){
        System.out.println("drive Bike");
    }
}
public class Car {
    public void driveCar(){
        System.out.println("drive Car");
    }
    public void openWindow(){
        System.out.println("open window");
    }
}

 
Man 類別會依賴於 Bike 和 Car 類別。

public class Man {
    private List mVehicle = new LinkedList();
    public Man() {
        mVehicle.add(new Bike());
        mVehicle.add(new Car());
    }
    public void driveVehicle() {
        for (int i = 0; i < mVehicle.size(); ++i) {
            Object object = mVehicle.get(i);
            if( object instanceof Car){
                ((Car) object).driveCar();
            }else if( object instanceof Bike){
                ((Bike) object).driveBike();
            }
        }
    }
}

注意第10行的 driveVehicle 方法,因為必須判斷各物件的型態,所以不得不加入長串的if-else判斷式。
若之後新增 Boat 類別,就必須修改 driveVehicle 方法並加入 object instanceof Boat 判斷式,造成不符合 OCP 設計
(思考的方向為當出現第N種車輛時,對於Man類別來說要如何設計,以達到不需要修改Man的架構,就可以加入新的車輛。)
 
 
如何修改為符合 OCP 設計?
首先建立介面 Vehicle , 並把公共方法提取到抽象層中。

public interface Vehicle {
    public void drive();
}

接著 Car , Bike 實作 Vehicle 介面。

public class Bike implements Vehicle{
    @Override
    public void drive() {
        System.out.println("drive Bike");
    }
}
public class Car implements Vehicle{
    @Override
    public void drive() {
        System.out.println("drive Car");
    }
    public void openWindow(){
        System.out.println("open window");
    }
}

 
在 driveVehicle 方法中不必判斷各物件的型別, 之後即使新增任何交通工具,只要讓新增的類別實作 Vehicle 介面 ,driveVehicle 都不需要改變。

public class Man {
    private List<Vehicle> mVehicle = new LinkedList<Vehicle>();
    public Man() {
        mVehicle.add(new Bike());
        mVehicle.add(new Car());
    }
    public void driveVehicle() {
        for (int i = 0; i < mVehicle.size(); ++i) {
            mVehicle.get(i).drive();
        }
    }
}

以上使用多型的方式去除型別判斷(instanceof)的條件式達到OCP的作法。
 
事實上最常見違反OCP的是另一種根據布林變數來判斷行為的條件式。 e.g.

        if(conditionA){
            statementA;
        }else if(conditionB){
            statementB;
        }else if(conditionC){
            statementC;
        }

 
這種案例的修改必須借助多型,但還需要加上相依性注入(Dependency Injection)
範例如下
假設有個紅綠燈系統,參與的類別各為 GreenLight.java , RedLight.java , YellowLight.java , 以及 LightActionManager.java,如下

public class GreenLight
{
    public void lightOn()
    {
        System.out.println("green light action");
    }
}
public class RedLight
{
    public void lightOn()
    {
        System.out.println("red light action");
    }
}
public class YellowLight
{
    public void lightOn()
    {
        System.out.println("yellow light action");
    }
}

以及 LightActionManager.java

public class LightActionManager
{
    private boolean mIsGreenLightOn;
    private boolean mIsYellowLightOn;
    private boolean mIsRedLightOn;
    private GreenLight mGreenLight = new GreenLight();
    private YellowLight mYellowLight = new YellowLight();
    private RedLight mRedLight = new RedLight();
    public LightActionManager(boolean greenLightOn, boolean redLightOn, boolean yellowLightOn) {
        mIsGreenLightOn = greenLightOn;
        mIsYellowLightOn = yellowLightOn;
        mIsRedLightOn = redLightOn;
    }
    public void lightAction()
    {
        if (mIsGreenLightOn) {
            mGreenLight.lightOn();
        } else if (mIsYellowLightOn) {
            mYellowLight.lightOn();
        } else if (mIsRedLightOn) {
            mRedLight.lightOn();
        }
    }
}

違反OCP的位置在於第17行的 lightAction 函式中使用多個邏輯判斷。
假設之後需要加入綠燈左轉燈號(GreenLightLeft.java),綠燈右轉燈號(GreenLightRight.java)就必須修改if-else判斷 e.g.

    public void lightAction()
    {
        if (mIsGreenLightOn) {
            mGreenLight.lightOn();
        } else if (mIsYellowLightOn) {
            mYellowLight.lightOn();
        } else if (mIsRedLightOn) {
            mRedLight.lightOn();
        } else if (mIsGreenLightLeftOn) {
            mGreenLightLeft.lightOn();
        } else if (mIsGreenLightRightOn) {
            mGreenLightRight.lightOn();
        }
    }

 
如何藉由多型以及相依性注入來改善呢?
首先建立interface (ILight)來達到多型,並讓 GreenLight.java , RedLight.java , YellowLIght.java 實作。

public interface ILight
{
    public void lightOn();
}
public class GreenLight implements ILight
{
    public void lightOn()
    {
        System.out.println("green light action");
    }
}
public class YellowLight implements ILight
{
    public void lightOn()
    {
        System.out.println("yellow light action");
    }
}
public class RedLight implements ILight
{
    public void lightOn()
    {
        System.out.println("red light action");
    }
}

接著修改LightActionManager.java,如下

public class LightActionManager
{
    private ILight mLight;
    public LightActionManager(ILight light) {
        mLight = light;
    }
    public void setLight(ILight light){
        mLight = light;
    }
    public void lightAction()
    {
        if(mLight != null){
            mLight.lightOn();
        }
    }
}

第4行需要持有1個ILight 型別的欄位。
第6行建立LightActionManager的時候就必須傳入Light實體。
第10行即為相依性注入的函式,這個部份相當重要,它是取代條件判斷式的一個重點。
第14行為提供給外部呼叫的函式。
接著來看看外部如何呼叫LightActionManager.java

        LightActionManager actionManager = new LightActionManager(new GreenLight());
        actionManager.lightAction();
        actionManager.setLight(new YellowLight());
        actionManager.lightAction();

我們藉由相依性注入函式(setLight())設定不同的Light,接著才呼叫lightAction()。
另一個好處是藉由提供ILight interface , 可以讓其他開發者實作自己的燈號類別(XXXLight),只要記得實作ILight(implements ILight)即可。
但缺點是引入一層抽象層(ILight),即使抽象層內容簡單,還是需要時間以及人力維護。
所以應該在面對系統中最頻繁出現的變化位置再考慮使用多型以及使用相依性注入的解決方案。
 
 
 
 
 

分類
Design Principle

Single Responsibility Principle (SRP) (單一責任原則)

Single Responsibility Principle (SRP)

 

定義:

對一個類別而言,應該僅有一個引起它變化的原因(職責)
 

說明:

若一個類別有多重職責,職責之間會互相耦合,一個職責的變化可能會影響該類別完成其他職責的能力
注意:運用本原則以及其他設計原則必須在實際發生的情況下才使用,不要預先做目前不需要的設計。
 

1. 以變化的參與性決定。

若需求的出現導致類別中所有部份發生變化,代表所有職責可視為同一個職責,不必分離。如果分離反而會導致不必要的複雜度
相反的,若需求的出現導致類別中某一部份(欄位或函式)發生變化,代表某一部份為不同職責,必須分離,否則會造成僵化性。
 

2. 以變化的頻率和因素決定

變化頻率而言,商業邏輯經常變化,資料儲存變化較少。
變化因素而言,商業邏輯和資料儲存的變化因素多不相同。
所以商業邏輯和資料儲存不應該同屬於一個類別
 

3. 以重構(Refactoring)來分離多重職責

在重構中程式碼的壞味道(bad smell)中,第3點過大的類別(Large Class),以及第5點發散式變化(Divergent Change)都是關於多重職責的問題。
可使用 Extract Class,Extract Subclass 來切割類別以分離多重職責。
 

4. 在無暇的程式碼(Clean Code)提到,類別若具有多重職責代表該類別內聚力低,為了保持類別的高內聚力,

可以拆解內容過長的方法並觀察拆解後的方法是否具有變化的參與性,若變化總是在某些方法中出現,就把它們提取出來吧。