一. 什麼是開閉原則?
開放封閉原則(OCP,Open Closed Principle)是所有物件導向原則的核心。軟體設計本身所追求的目標就是封裝變化、降低耦合,而開放封閉原則正是對這一目標的最直接體現。其他的設計原則,很多時候是為實現這一目標服務的.
1.1 先來看開閉原則的定義:
Software entities like classes,modules and functions should be open for extension but closed for modifications
一個軟體實體, 如類, 模組, 函式等應該對擴充套件開放, 對修改封閉.
這也是開放封閉原則的核心思想:對擴充套件開放,對修改封閉.
1.2 這是什麼含義呢?
- 對擴充套件開放,意味著有新的需求或變化時,可以對現有程式碼進行擴充套件,以適應新的情況。
- 對修改封閉,意味著類一旦設計完成,就可以獨立完成其工作,而不要對已有程式碼進行任何修改
二. 如何實現開放封閉原則呢?
“需求總是變化”、“世界上沒有一個軟體是不變的”。這裡投射出的意思是:需求總是變化的, 可是對於軟體設計者來說, 如何才能做到不對原有系統修改的前提下, 實現靈活的擴充套件. 這就是開閉原則要實現的.
我們在設計系統的時候, 不可能設想一次性把需求確定後, 後面就不改變了.這不科學也不現實的. 既然需求是一定會變化的, 那麼我們要如何優雅的面對這種變化呢? 如何設計可以使軟體相對容易修改, 不至於需求一變, 就要把整個程式推到重來?
開封-封閉原則. 設計軟體要容易維護且不容易出問題的最好辦法, 就是多擴充套件, 少修改.
2.1 依賴與抽象
實現開放封閉的核心思想就是面對抽象程式設計,而不是面對具體程式設計,因為抽象相對穩定。
讓類依賴於固定的抽象,所以對修改是封閉的;而通過物件導向的繼承和多型機制,可以實現對抽象體的繼承,通過覆寫其方法來改變固有行為,實現新的擴充套件方法,所以對於擴充套件就是開放的。這是實施開放封閉原則的基本思路。
2.2 如何落地開閉原則
如果當前的設計不符合開放封閉原則,則必須進行重構。常用的設計模式主要有模板方法(Template Method)設計模式和策略(Strategy)設計模式。而封裝變化,是實現這一原則的重要手段,將經常發生變化的部分封裝為一個類。
2.3 開閉原則的重要性
1.開閉原則對測試的影響
開閉原則可是保持原有的測試程式碼仍然能夠正常執行,我們只需要對擴充套件的程式碼進行測試就可以了。
2.開閉原則可以提高複用性
在物件導向的設計中,所有的邏輯都是從原子邏輯組合而來的,而不是在一個類中獨立實現一個業務邏輯。只有這樣程式碼才可以複用,粒度越小,被複用的可能性就越大。
3.開閉原則可以提高可維護性
物件導向開發的要求。
2.4 如何使用開閉原則
1.抽象約束
第一,通過介面或者抽象類約束擴充套件,對擴充套件進行邊界限定,不允許出現在介面或抽象類中不存在的public方法;
第二,引數型別、引用物件儘量使用介面或者抽象類,而不是實現類;
第三,抽象層儘量保持穩定,一旦確定即不允許修改。
2.後設資料(metadata)控制模組行為
後設資料就是用來描述環境和資料的資料,通俗地說就是配置引數,引數可以從檔案中獲得,也可以從資料庫中獲得。
Spring容器就是一個典型的後設資料控制模組行為的例子,其中達到極致的就是控制反轉(Inversion of Control)
3.制定專案章程
在一個團隊中,建立專案章程是非常重要的,因為章程中指定了所有人員都必須遵守的約定,對專案來說,約定優於配置。
4.封裝變化
對變化的封裝包含兩層含義:
第一,將相同的變化封裝到一個介面或者抽象類中;
第二,將不同的變化封裝到不同的介面或抽象類中,不應該有兩個不同的變化出現在同一個介面或抽象類中。
三. 案例分析
案例一: 畫形狀
需求: 有圓形, 有橢圓形, 根據要求畫出相應的形狀
public class GraphicEditor {
public void draw(Shape shape) {
if (shape.m_type == 1) {
drawRectangle();
} else if(shape.m_type == 2) {
drawCircle();
}
}
public void drawRectangle() {
System.out.println("畫長方形");
}
public void drawCircle() {
System.out.println("畫圓形");
}
class Shape {
int m_type;
}
class Rectangle extends Shape {
Rectangle() {
super.m_type=1;
}
}
class Circle extends Shape {
Circle() {
super.m_type=2;
}
}
}
我們來看看, 這個程式碼, 初看是符合要求了, 再想想, 要是我增加一種形狀呢? 比如增加三角形.
首先, 要增加一個三角形的類, 繼承自Shape
第二, 要增加一個畫三角形的方法drawTrriage()
第三, 在draw方法中增加一種型別type=3的處理方案.
這就違背了開閉原則-對擴充套件開發, 對修改關閉. 增加一個型別, 修改了三處程式碼.
我們來看看合適的設計
public class GraphicEditor1 {
public void draw(Shape shape) {
shape.draw();
}
interface Shape {
void draw();
}
class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("畫矩形");
}
}
class Circle implements Shape {
@Override
public void draw() {
System.out.println("畫圓形");
}
}
}
各種型別的形狀自己規範自己的行為, 而GraphicEditor.draw()只負責畫出來. 當增加一種型別三角形. 只需要
第一: 增加一個三角形的類,實現Shape介面
第二, 呼叫draw方法,劃出來就可以了.
整個過程都是在擴充套件, 而沒有修改原來的類. 這個設計是符合開閉原則的.
案例二:
比如現在有一個銀行業務, 存錢, 取錢和轉賬. 最初我們會怎麼思考呢?
- 首先有一個銀行業務類, 用來處理銀行的業務
- 銀行有哪些業務呢? 存錢,取錢,轉賬, 這都是銀行要執行的操作
- 那外部說我要存錢, 我要取錢,我要轉賬, 通過一個型別來告訴我們
程式碼就生成了
package com.lxl.www.designPatterns.sixPrinciple.openclosePrinciple.bank;
/**
* 銀行業務
*/
public class BankBusiness {
public void operate(int type) {
if (type == 1) {
save();
} else if(type == 2) {
take();
} else if(type == 3) {
transfer();
}
}
public void save(){
System.out.println("存錢");
}
public void take(){
System.out.println("取錢");
}
public void transfer() {
System.out.println("轉賬");
}
}
咋一看已經實現了需求. 但是現在有新的需求來了, 銀行要增加功能---理財. 理財是銀行業務的一種, 自然是新增一個方法.
然後在operate()方法裡增加一種型別. 這就是一個糟糕的設計, 增加新功能, 但是卻修改了原來的程式碼
我們設計成介面抽象的形式, 上程式碼
public interface Business {
public void operate();
}
public class Save implements Business{
@Override
public void operate() {
System.out.println("存錢業務");
}
}
public class Take implements Business {
@Override
public void operate() {
System.out.println("取錢業務");
}
}
public class Transfer implements Business {
@Override
public void operate() {
System.out.println("轉賬業務");
}
}
/**
* 銀行業務類
*/
public class BankBusinesses {
/**
* 處理銀行業務
* @param business
*/
public void operate(Business business) {
System.out.println("處理銀行業務");
business.operate();
}
}
通過介面抽象的形式方便擴充套件, 加入要新增理財功能. 只需新增一個理財類, 其他業務程式碼都不需要修改.
其實, 在日常工作中, 經常會遇到這種情況. 因為我們平時寫業務邏輯會更多一些, 而業務就像流水賬, 今天一個明天一個一點一點的增加. 所以,當業務增加到3個的時候, 我們就要思考, 如何寫能夠方便擴充套件j
3.3 關於作答鏈路的思考
作答鏈路包括
拉題-->初始化-->答題-->訂正-->加積分-->主觀題批改-->主觀題批改回傳等流程.
那麼這麼一條鏈路是否可以通過介面抽象的形式規範程式碼,實現開閉原則呢? 不至於後面增加一種型別, 就需要新增一個方法.
其實, 我覺得是可以的.
四. 總結:
- 遵守開閉原則可以提高軟體擴充套件性和維護性。
- 大部分的設計模式和設計原則都是在實現開閉原則。