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),即使抽象層內容簡單,還是需要時間以及人力維護。
所以應該在面對系統中最頻繁出現的變化位置再考慮使用多型以及使用相依性注入的解決方案。