設計模式六大原則

2019年2月26日19:41:21html

設計模式六大原則

爲何會有六大原則

有言曰,「無規矩不成方圓」,有「規」才能畫「圓」,那設計模式要遵循的六大原則要畫一個什麼的「圓」呢?前端

這裏要從面向對象編程提及,從面向過程編程到面向對象編程是軟件設計的一大步,封裝、繼承、多態是面向對象的三大特徵,原本這些都是面向對象的好處,可是一旦有人濫用了,就有了壞味道。java

好比,封裝是隱藏對象的屬性和實現細節的,我想到了還沒提倡MVC的時候,一個servlet裏的doGet、doPost方法就完成了全部事情,業務邏輯、數據持久化、頁面渲染等,這樣一來咱們須要修改業務邏輯的時候是修改這個servlet,須要修改數據持久化的是修改這個servlet,甚至頁面修改也是修改這個servlet。這樣可維護性就不好了。數據庫

由於濫用或者不正確的時候致使代碼的壞味道,致使系統的可維護性和複用性等變低,因此面向對象須要遵循一些原則make the code better。如:一個servlet幹全部事情能夠改成MVC,每一層的類作本身負責的事情,遵循單一職責原則。編程

爲了提升系統的可維護性、複用性和高內聚低耦合等,因此有了六大原則。由於設計模式是面向對象實踐出來的經驗,因此這六大原則既是面向對象的六大原則,也是設計模式的六大原則。設計模式

六大原則

設計模式六大原則

先來個圖,整體感覺一下,其實說簡單也簡單,死記硬背這六個名詞不用十分鐘,可是要使用得遊刃有餘,仍是要下一點功夫的。本文也只是紙上談兵,聊聊六大原則的定義、用法、好處等。ide

單一職責原則(Single Responsibility Principle,SRP)

定義:不要存在多於一個致使類變化的緣由(There should never be more than one reason for a class to change.)。函數

就像我前面說到的那個例子,一個servlet幹完了全部事情,這樣致使servlet變化的緣由就不止一個了,因此要將這些事情分給不一樣的類。測試

好比我如今要實現一個登陸的功能,servlet代碼是這樣的:this

public class LoginServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 一、獲取前端傳過來的數據
        // 二、鏈接數據庫,查詢數據
        // 三、比較數據,得出結果
        // 四、封裝結果,返回給前端
    }
}

應用MVC後,代碼修改以下:

public class LoginController {

    private LoginService loginService;

    public ModelAndView Login(HttpServletRequest req, HttpServletResponse resp) {
        // 一、獲取前端傳過來的數據
        loginService.login();
        // 四、封裝結果,返回給前端
        return null;
    }
}

public class LoginService {

    private UserDao userDao;

    public boolean login() {
        userDao.findOne();
        // 三、比較數據,得出結果
        return false;
    }
}

public class UserDao {

    public User findOne(){
        // 二、鏈接數據庫,查詢數據
        return null;
    }
}

有圖以下:
單一職責原則

這樣職責分明,有變動需求只須要找到職責相關的那部分修改就好。好比要修改比較邏輯,就修改Service層代碼;要修改鏈接數據庫,就修改Dao層就能夠了;要修改返回頁面的內容,就修改Controller層就能夠了。

應用場景:在項目開始階段要明確類的職責,若是發現一個類有兩個或以上的職責,那就拆成多個類吧。若是是項目後期,要評估好修改的代價以後在重構。別讓一個類作的事情太多。

好處:實現高內聚、低耦合,增長代碼的複用性。

開閉原則(Open Closed Principle, OCP)

定義:軟件實體,如:類、模塊與函數,對於擴展開放,對修改關閉(Software entities like classes, modules and functions should be open for extension but closed for modifications.)。

從簡單工廠模式到工廠方法模式,完美詮釋了開閉原則的應用場景。有興趣能夠查看本人所寫的《簡單工廠模式》《工廠方法模式》

用類對象實現操做符運算:

簡單工廠模式實現:

public static IOperation createOperation(String op) {
    IOperation operation = null;

    if ("+".equals(op)) {
        operation = new AddOperationImpl();
    } else if ("-".equals(op)) {
        operation = new SubOperationImpl();
    } else if ("*".equals(op)) {
        operation = new MulOperationImpl();
    } else if ("/".equals(op)) {
        operation = new DivOperationImpl();
    }
    
    return operation;
}

這是簡單工廠模式中的工廠角色實現建立全部實例的內部邏輯的方法,調用方法時,根據傳進來的操做符選擇不一樣的實現類,可是若是我要添加一個乘方的話,就須要添加else if結構,沒有對修改關閉,這樣就不符合開閉原則了。

工廠方法模式實現:

// 加
// 建立具體工廠
IOperationFactory operationFactory = new AddOperationFactoryImpl();
// 建立具體產品
IOperation operation = operationFactory.createOperation();
// 調用具體產品的功能
int result = operation.getResult(a, b);

須要什麼運算,就繼承IOperationFactory實現對應的實現類,使用時只須要在須要的地方new這個實現類便可。不用修改工廠類,增長運算就增長抽象工廠類的實現類,符合開閉原則。

應用場景:在系統的任何地方

好處:使得系統在擁有適應性和靈活性的同時具有較好的穩定性和延續性

里氏替換原則(Liskov Substitution Principle,LSP)

定義:使用基類的指針或引用的函數,必須是在不知情的狀況下,可以使用派生類的對象(Functions that use pointers or references to base classes must be able to use objects of derived classes whithout knowing it.)。

爲何叫里氏替換原則? 里氏代換原則由2008年圖靈獎得主、美國第一位計算機科學女博士Barbara Liskov教授和卡內基·梅隆大學Jeannette Wing教授於1994年提出。

里氏替換原則告訴咱們,在軟件中將一個基類對象替換成它的子類對象,程序將不會產生任何錯誤和異常,反過來則不成立,若是一個軟件實體使用的是一個子類對象的話,那麼它不必定可以使用基類對象。例如,我喜歡動物,那我必定喜歡狗,由於狗是動物的子類;可是我喜歡狗,不能據此判定我喜歡動物,由於我並不喜歡老鼠,雖然它也是動物。

上面敘述轉爲代碼以下:

// 動物
public interface Animal {
    public String getName();
}
// 狗
public class Dog implements Animal{
    private String name = "狗";
    @Override
    public String getName() {
        return this.name;
    }
}
// 老鼠
public class Mouse implements Animal{
    private String name = "老鼠";
    @Override
    public String getName() {
        return this.name;
    }
}
// 測試類
public class ISPTest {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal mouse = new Mouse();
        iLoveAnimal(dog);
        iLoveAnimal(mouse);
//        iLoveDog(dog);
//        iLoveDog(mouse);
    }

    public static void iLoveAnimal(Animal animal) {
        System.out.println(animal.getName());
    }

    public static void iLoveDog(Dog dog) {
        System.out.println(dog.getName());
    }

}

其中iLoveAnimal(Animal animal)能夠傳遞的對象參數有Dog和Mouse,由於Dog和Mouse是Animal的子類,因此編譯經過。iLoveDog(Dog dog)不能Mouse爲參數,雖然他們同屬Animal的子類,編譯不能經過。在編譯階段,Java編譯器會檢查一個程序是否符合里氏替換原則,可是Java編譯器的檢查是有侷限的,這只是一個與實現無關、純語法意義上的檢查,在設計上咱們要注意遵循里氏替換原則。

理論上,里氏替換原則是實現開閉原則的重要方式之一,使用基類對象的地方均可以使用子類對象,因此在程序中儘可能使用基類類型來對對象進行定義,而在運行時在肯定其子類類型,當子類類型改變時,就能實現擴展,並無對現有的代碼結構進行修改

實踐中,咱們能作的是:

  • 儘可能將基類設計爲抽象類和接口

  • 子類必須實現父類中聲明的全部方法,且子類的全部方法必須在基類中聲明

最少知識原則(Least Knowledge Principle,LKP)又名迪米特法則(Law of Demeter)

定義:只與你最直接的朋友交流(Only talk to you immediate friends.)。

又名迪米特法則的緣由是:迪米特法則來自於1987年美國東北大學(Northeastern University)一個名爲「Demeter」的研究項目。

根據迪米特法則有,對象O的一個方法M僅能訪問如下類型的對象:

  • 一、當前對象O自身(this)

  • 二、M方法的參數對象(如,toString(Integer i)中對象i

  • 三、當前對象O成員對象(當前對象O直接依賴的對象)

  • 四、M方法中所建立的對象

重要的是,方法M不該該調用這些方法返回對象的方法,就是鏈式調用返回的但返回的並非自身對象的對象的方法。和你朋友說話,而不是和你朋友的朋友,對於你來講是陌生人的人說話。

下面是一個例子:

public class LawOfDelimterDemo {

    /**
     * 這個方法有兩個違反最少知識原則或迪米特法則的地方。
     */
    public void process(Order o) {

        // 這個方法調用符合迪米特法則,由於o是process方法的參數,是類型2的參數
        Message msg = o.getMessage();

        // 這個方法調用違反了迪米特法則,由於使用了msg對象,這個對象是從參數對象中獲得的對象。
        // 咱們應該讓Order去normalize這個Message,例如:o.normalizeMessage(),而不是使用msg對象的方法
        msg.normalize();

        // 這也是違反迪米特法則的,使用了方法鏈代替上面說的msg臨時變量。
        o.getMessage().normalize();

        // 構造函數調用
        Instrument symbol = new Instrument();

        // 這個方法調用是符合迪米特法則的,由於Instrument實例是本地建立的,就是類型4的對象,process方法中所建立的對象
        symbol.populate(); 
    }
}

好處:下降系統的耦合性,增長系統的可維護性和適應性。由於較少依賴於其餘對象的內部結構,其餘對象的修改就從新修改它們的調用者。

壞處:可能會增長對象的方法,引起其餘bug。

接口隔離原則(Interface Segregation Principle, ISP)

定義:一個類與另一個類之間的依賴性,應該依賴於儘量小的接口(The dependency of one class to another one should depend on the smallest possible interface.)。

例子:首先有一個經理,負責管理工人。其次,有兩種類型的工人,一種是在平均水平的工人,一種是高效率的工人,這些工人都須要午休時間來吃飯。最後還有一種機器人在工做,可是機器人不須要午休。

設計實現代碼:

interface IWorker {
    public void work();
    public void eat();
}

// 通常工人
class Worker implements IWorker {
    public void work() {
        // 正常工做
    }
    pubic void eat() {
        // 午休吃飯
    }
}

// 高效率工人
class SuperWorker implements IWorker {
    public void work() {
        // 高效率工做
    }
    public void eat() {
        // 午休吃飯
    }
}

// 機器人
class Rebot implements IWorker {
    public woid work() {
        // 工做
    }
    public void eat() {
        // (實現代碼爲空,什麼也不作)
    }
}

class Manager {
    IWorker worker;

    public void setWorker(IWorker w) {
        worker = w;
    }
    public void manage() {
        worker.work();
        worker.eat();
    }

}

經理去管理工人的時候,調用接口eat方法的時候,機器人什麼也不作。咱們應該讓接口變小,把IWorker接口拆分。

// 工做接口
interface IWorkable {
    public void work();
}
// 吃飯接口
interface IFeedable {
    public void eat();
}

// 通常工人
class Worker implements IWorkable, IFeedable {
    public void work() {
        // 正常工做
    }
    pubic void eat() {
        // 午休吃飯
    }
}

// 高效率工人
class SuperWorker implements IWorkable, IFeedable {
    public void work() {
        // 高效率工做
    }
    public void eat() {
        // 午休吃飯
    }
}

// 機器人
class Rebot implements IWorkable {
    public woid work() {
        // 工做
    }
}

class Manager {
    IWorkable worker;
    IFeedable feed;

    public void setWorker(IWorkable w) {
        worker = w;
    }
    public void setfeed(IFeedable f) {
        feed = f;
    }
    public void manageWork() {
        worker.work();
    }
    public void manageFeed() {
        feed.eat();
    }

}

將IWorker接口拆分紅IWorkable接口和IFeedable接口,將Manager類與工人類交互儘可能依賴與比較小的接口。

在使用接口隔離原則時,咱們須要注意控制接口的粒度,接口不能過小,若是過小會致使系統中接口氾濫,不利於維護;接口也不能太大,太大的接口將違背接口隔離原則,靈活性較差,使用起來很不方便。通常而言,接口中僅包含爲某一類用戶定製的方法便可,不該該強迫客戶依賴於那些它們不用的方法。

依賴倒置原則(Dependence Inversion Principle, DIP)

定義:高層模塊不該該依賴於底層模塊,它們都應該依賴於抽象。抽象不該該依賴於細節,細節應該依賴與抽象(High level modules should not depends upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions.)。

儘可能面對接口編程,而不是面對實現編程。

例子:你如今是一個導演,你要拍一部電影,準備找劉德華作主角。在電影裏,劉德華是一個警察,能夠捉犯人。

// 劉德華
class LiuDeHua {
    public LiuDeHua(){}
    public void catchPrisoner(){}
}

// 劇本
class Play {
    LiuDeHua liuDeHua = new LiuDeHua();
    liuDeHua.catchPrisoner();
}

可是華仔由於檔期來不了,因而找古天樂。

// 古天樂
class GuTianLe {
    public GuTianLe(){}
    public void catchPrisoner(){}
}

// 劇本
class Play {
    GuTianLe guTianLe = new GuTianLe();
    guTianLe.catchPrisoner();
}

古仔說要捐錢建學校,沒空來。因而又說要找劉青雲,編劇心好累……

若是編劇只是面對接口編程,就會變成這樣:

// 警察
interface Police {
    public void catchPrisoner();
}

// 劇本
class Play {
    private Police police;
    public Play(Police p) {
        police = p;
    }
    police.catchPrisoner();
}

這樣不管誰來,只須要實現Police接口就能夠按劇本拍了。

在實現依賴倒置原則時,咱們須要針對抽象層編程,而將具體類的對象經過依賴注入(DependencyInjection, DI)的方式注入到其餘對象中,依賴注入是指當一個對象要與其餘對象發生依賴關係時,經過抽象來注入所依賴的對象。

經常使用的注入方式有三種,分別是:構造注入,設值注入(Setter注入)和接口注入。構造注入是指經過構造函數來傳入具體類的對象,設值注入是指經過Setter方法來傳入具體類的對象,而接口注入是指經過在接口中聲明的業務方法來傳入具體類的對象。這些方法在定義時使用的是抽象類型,在運行時再傳入具體類型的對象,由子類對象來覆蓋父類對象。

在大多數狀況下,開閉原則、里氏替換原則和依賴倒置原則這三個設計原則會同時出現,開閉原則是目標,里氏代換原則是基礎,依賴倒置原則是手段,它們相輔相成,相互補充,目標一致,只是分析問題時所站角度不一樣而已。

參考

Law of Demeter in Java - Principle of least Knowledge

面向對象設計原則之依賴倒轉原則

2019年3月22日14:31:03

相關文章
相關標籤/搜索