1. 程式人生 > >萬字總結之設計模式七大原則

萬字總結之設計模式七大原則

前言

上篇說了反射,將其作為框架的基礎知識。還沒看過的移至傳送門,萬字總結之反射(框架之魂)。今天我們來看設計模式。話不多說,let's go。

什麼是設計模式?

設計模式是對軟體設計普遍存在的問題,所提出的解決方案。

與專案本身沒有關係,不管是電商,ERP,OA 等,都可以利用設計模式來解決相關問題。

當然如果這個軟體就只有一小部分人用,並且功能非常簡單,在未來可預期的時間內,不會做任何大的修改和新增,即可以不使用設計模式。但是這種的太少了,所以設計模式還是非常重要的。

為什麼要使用設計模式?

使用設計模式的最終目的是“高內聚低耦合”。

  • 程式碼重用性:相同功能的程式碼,不多多次編寫
  • 程式碼可讀性:程式設計規範性,便於其他程式設計師閱讀
  • 程式碼可擴充套件性:當增加新的功能後,對原來的功能沒有影響

設計模式的七大原則

設計模式有7大原則,具體如下,即這些不僅是設計模式的依據,也是我們平常程式設計中應該遵守的原則。

1.單一職責原則

見名知意,我們設計的類儘量負責一項功能,如A類只負責功能A,B類只負責功能B,不要讓A類既負責功能A,又負責功能B,這樣會導致程式碼混亂,容易產生bug。

未使用單一職責原則

Single類:

public class single {
    public static void main(String[] args) {
        Vehicle vehicle = new Vehicle();
        vehicle.run("汽車");
        vehicle.run("輪船");
        vehicle.run("飛機");
    }
}

 

Vehicle類:

public class Vehicle {
    void run(String type){
        System.out.println(type+"在公路上開");
    }
}

 

執行結果:

我們看下執行結果,汽車是在公路上開,但是輪船和飛機並不是在公路上。因為Vehicle類負責了不止一個功能,所以該設計是有問題的。

已使用單一職責原則

對於上面的例子,我們採用單一職責原則重寫一下,將Vehicle類拆分成三個類,分別是Car,Ship,Plane,讓他們各自負責陸地上,水上,空中的交通工具,使其互不影響。

如果我們需要對水上交通做“風級大於8級,禁止出海”的限制,就只需要對Ship類進行修改。

具體程式碼如下:

single類:

public class single {
    public static void main(String[] args) {
        Car car = new Car();
        car.run("汽車");

        Ship ship=new Ship();
        ship.run("輪船");

        Plane plane=new Plane();
        plane.run("飛機");
    }
}

 

Car類:

public class Car {
    void run(String type){
        System.out.println(type+"在公路上開");
    }
}

 

Ship類:

public class Ship {
    void run(String type){
        System.out.println(type+"在水裡開");
    }
} 

 

Plane類:

public class Plane {
    void run(String type){
        System.out.println(type+"在天空開");
    }
}

 

執行結果:

優化

我們可以發現單一職責原則有點程式碼太多了,顯得冗餘。畢竟我們程式設計師是能少寫就少寫,決不能多寫程式碼。那我們對其優化下,上面每個類只有一個方法,我們可以合併為一個類,其中有三個方法,每個方法對應著在公路上,在水上,在天空中的交通工具,將單一職責原則落在方法層面,而不再是類層面,程式碼如下:

single類:

public class single {
    public static void main(String[] args) {
        Vehicle vehicle = new Vehicle();
        vehicle.runOnRoad("汽車");

        vehicle.runOnWater("輪船");

        vehicle.runOnAir("飛機");
    }
}

 

Vehicle類:

 

public class Vehicle {
    void runOnRoad(String type){
        System.out.println(type+"在公路上開");
    }
    void runOnWater(String type){
        System.out.println(type+"在水裡開");
    }
    void runOnAir(String type){
        System.out.println(type+"在天空開");
    }
} 

 

執行結果:

優缺點總結

優點:

  • 降低類的複雜性,一個類只負責一個職責。
  • 提高程式碼的可讀性,邏輯清楚明瞭。
  • 降低風險,只修改一個類,並不影響其他類的功能。

缺點:程式碼量增多。(可將單一職責原則落在方法層面進行優化)

2.介面隔離原則

類不應該依賴他不需要的介面,介面儘量小顆粒劃分。

未使用介面隔離原則

People類:

public interface People {
    void exam();
    void teach();
}

 

Student類:

public class Student implements People {
    @Override
    public void exam() {
        System.out.println("學生考試");
    }

    @Override
    public void teach() {

    }
}

 

Teacher類:

public class Teacher  implements People{
    @Override
    public void exam() {

    }

    @Override
    public void teach() {
        System.out.println("教師教書");
    }
}

 

test類:

public class test {
    public static void main(String[] args){
        People student=new Student();
        student.exam();

        People teacher=new Teacher();
        teacher.teach();
    }
} 

 

執行結果:

注:此處程式碼並沒有報錯,正常執行的,但是看得程式碼冗餘且奇怪。Student只需要實現People的exam方法,而Teacher只需要實現People的teach方法,但是現在Student實現了People介面,就必須重寫exam和teach方法,Teacher也是如此。

已使用介面隔離原則

我們將People介面的兩個方法拆分開,分為兩個介面People1和People2,並且讓Sudent實現People1介面,Teacher實現People2介面,使其互不干擾,具體程式碼如下:

People1類:

public interface People1 {
    void exam();
}

 

People2類:

public interface People2 {
    void teach();
}

 

Student類:

public class Student implements People1 {
    @Override
    public void exam() {
        System.out.println("學生考試");
    }
}

 

Teacher類:

public class Teacher  implements People2 {
    @Override
    public void teach() {
        System.out.println("教師教書");
    }
} 

 

test類:

public class test {
    public static void main(String[] args){
        People1 student=new Student();
        student.exam();

        People2 teacher=new Teacher();
        teacher.teach();
    }
}

 

執行結果:

總結

言歸正傳,如果將多個方法合併為一個介面,再提供給其他系統使用的時候,就必須實現該介面的所有方法,那有些方法是根本不需要的,造成使用者的混淆。

3.依賴倒轉原則

高層模組不應該依賴底層模組,二者都應該依賴介面或抽象類。

其核心就是面向介面程式設計。

依賴倒轉原則主要基於如下的設計理念:相對於細節的多變性,抽象的東西要穩定的多,以抽象為基礎搭建的架構比以細節為基礎的架構要穩定的多。

抽象指介面或抽象類,細節指具體的實現類。

這樣講太乾澀,照搬宣科,沒有靈魂,說了等於沒說。接下來我們用例子來說明。

未使用依賴倒轉原則

由於現在是特殊時期,我們先來一個買菜的例子。如下是傻白甜的例子,未使用到依賴倒轉原則。

Qingcai類:

public class Qingcai {
    public void run(){
        System.out.println("買到了青菜");
    }
} 

 

People類:

public class People {
    public void bug(Qingcai qingcai){
        qingcai.run();
    }
}

 

test類:

public class test {
    public static  void main(String[] args){
        People people=new People();
        people.bug(new Qingcai());
    }
}

  

執行結果:

提出問題,思路轉變(重點)

上述看著沒啥問題,但是如果他不想買青菜,想買蘿蔔怎麼辦?我們當然可以新建一個蘿蔔類,再給他弄一個run方法,但是問題是People並沒有操作蘿蔔類的方法,我們還需要在People新增對蘿蔔類的依賴。這樣程式碼要修改的程式碼量太多了,模組與模組之間的耦合性太高,只要需要稍微有點變化,就要大面積重構,所以該設計不合理,我們看下其類圖,如下:

這種設計是一般設計的思考方式,而依賴倒轉原則中的倒轉是指和平常的思考方式完全相反,先從底部開始,即先從Qingcai和Luobo開始,然後想是否能抽象出什麼。很明顯,他們都是蔬菜,然後我們再回頭重新思考如何來設計,新的設計圖如下:

(請原諒我手殘黨,畫圖都畫不好。。。)

我們可以看到將低層的類抽象出一個介面Shucai,其直接和高層進行互動,而低層的一些類則不參與,這樣能降低程式碼的耦合性,提高穩定性。

已使用依賴倒轉原則

思路有了,那就來程式碼耍耍把。

Shucai類:

public interface Shucai {
    public void run();
}

 

Qingcai類:

public class Qingcai implements Shucai{
    public void run(){
        System.out.println("買到了青菜");
    }
}

 

Luobo類:

public class Luobo implements Shucai {
    @Override
    public void run() {
        System.out.println("買到了蘿蔔");
    }
}

 

People類:

public class People {
    public void bug(Shucai shucai){
        shucai.run();
    }
}

 

test類:

public class test {
    public static  void main(String[] args){
        People people=new People();
        people.bug(new Qingcai());
        people.bug(new Luobo());
    }
}

執行結果:

總結

該原則重點在“倒轉”,要從低層往上思考,儘量抽象抽象類和介面。此例子很好的解釋了“上層模組不應該依賴低層模組,他們都應該依賴於抽象”。在最開始的設計中,上層模組依賴了低層模組,調整後,上層模組和低層模組都依賴於介面Shucai,依賴關係從圖中可以看出來了“倒轉”。

4.里氏替換原則

繼承的優缺點

里氏替換原則是1988年麻省理工姓李的女士提出,它是闡述了對繼承extends的一些看法。

繼承的優點:

  1. 提高程式碼的重用性,子類也有父類的屬性和方法。
  2. 提高程式碼的可擴充套件性,子類有自己特有的方法。

繼承的缺點:

當父類發生改變的時候,要考慮子類的修改。

里氏替換原則是繼承的基礎,只有當子類替換父類時,軟體功能仍然不受到影響,才說明父類真正被複用啦。

使用里氏替換原則1

子類必須實現父類的抽象方法,但不得重寫(覆蓋)父類的非抽象(已實現)方法。

反例

父類A:

public class A {
    public void run(){
        System.out.println("父類執行");
    }
} 

 

子類B:

public class B extends A{
    public void run(){
        System.out.println("子類執行");
    }
} 

 

測試類test:

public class test {
    public static void main(String[] args) {
        A a = new A();
        a.run();
        System.out.println("將子類替換成父類:");
        B b = new B();
        b.run();
    }
}

 

執行結果:

注:我每次使用子類替換父類的時候,還要擔心這個子類有沒有可能導致問題。此處子類不能直接替換成父類,故沒有遵循里氏替換原則。

使用里氏替換原則2

子類中可以增加自己特有的方法

父類A:

public class A {
    public void run(){
        System.out.println("父類執行");
    }
}

 

子類B:

public class B extends A{
    public void runOwn(){
        System.out.println("子類執行");
    }
}

測試類test:

public class test {
    public static void main(String[] args) {
        A a = new A();
        a.run();

        System.out.println("將子類替換成父類:");
        B b = new B();
        b.run();

        b.runOwn();
    }
}

 

執行結果:

注:父類A 有run方法,繼承父類A的子類B有runOwn方法,測試類test先是呼叫A類的run方法,接著用B類替換A類,發現還是執行的是父類A的run方法,最後再呼叫子類B特有的方法runOwn方法。如上,說明該段程式碼已使用了里氏替換原則。

使用里氏替換原則3

當子類覆蓋或實現父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入引數更寬鬆。

父類A:

public class  A {
    public void run(HashMap hashMap){
        System.out.println("父類執行");
    }
}

 

子類B :

public class B extends A{
    public void run(Map map){
        System.out.println("子類執行");
    }
}

 

測試類test:

public class test {
    public static void main(String[] args) {
        A a = new A();
        a.run(new HashMap());

        System.out.println("將子類替換成父類:");
        B b = new B();
        b.run(new HashMap());

    }
}

 

執行結果:

我們可以看到在測試類test中,將父類A替換成子類B的時候,還是顯示的執行結果“父類執行”,我們可以發現他並不是重寫,而是方法過載,因為引數不一樣,所以他其實是對繼承的規範化,為了更好的使用繼承。關於是否為方法過載或重寫,我們從下圖看:

如果是重寫,在上圖示紅的位置會出現箭頭,我們可以看出是實際為過載。

那如果沒有使用這個規則,會是什麼樣?看下面的程式碼:

父類A:

public class  A {
    public void run(Map map){
        System.out.println("父類執行");
    }
}

 

子類B:

public class B extends A{
    public void run(HashMap hashMap){
        System.out.println("子類執行");
    }
}

 

測試test:

public class test {
    public static void main(String[] args) {
        A a = new A();
        a.run(new HashMap());

        System.out.println("將子類替換成父類:");
        B b = new B();
        b.run(new HashMap());

    }
}

 

執行結果:

我們可以看到將子類的範圍比父類大的時候,替換的子類還是執行自己的子類方法。此不符合里氏替換原則。

總結

我們平常好像也沒有遵循這些里氏替換原則,程式還是正常跑。其實如果不遵循里氏替換原則,你寫的程式碼出問題的機率會大大增加。

5.開閉原則(重點)

基本介紹

前面四個原則,單一職責原則,介面遮蔽原則,依賴倒轉原則,里氏替換原則可以說都是為了開閉原則做鋪墊,其是程式設計彙總最基礎,最重要的設計原則,核心為對擴充套件開發,對修改關閉,簡單來說,通過擴充套件軟體的行為來實現變化,而不是通過修改來實現,儘量不修改程式碼,而是擴充套件程式碼。

未使用開閉原則

介面transport:

public interface transport {
    public void run();
}

 

Bus:

public class Bus implements transport {
    @Override
    public void run() {
        System.out.println("大巴在公路上跑");
    }
}

 

當我們修改需求,讓大巴也能有在水裡開的屬性,我們可以對Bus類新增一個方法即可。但是這個已經違背了開閉原則,如果業務複雜,這樣子的修改很容易出問題的。

已使用開閉原則

我們可以新增一個類,實現transport介面,並繼承Bus類,寫自己的需求即可。

public class universalBus extends Bus implements transport {
    @Override
    public void run() {
        System.out.println("大巴既然在公路上開,又能在水裡開");
    }
}

 

6.迪米特原則

介紹

  1. 一個物件應該對其他物件保持最少的瞭解。
  2. 類與類關係越密切,耦合度越大
  3. 一個類對自己依賴的類知道的越少越好。也就是說,對於被依賴的類不管多麼複雜,都儘量將邏輯封裝在類的內部。對外除了提供的public 方法,不對外洩露任何資訊
  4. 迪米特法則還有個更簡單的定義:只與直接(熟悉)的朋友通訊
  5. 直接(熟悉)的朋友:每個物件都會與其他物件有耦合關係,只要兩個物件之間有耦合關係, 我們就說這兩個物件之間是朋友關係。耦合的方式很多,依賴,關聯,組合,聚合等。
    其中,我們稱出現成員變數,方法引數,方法返回值中的類為直接的朋友,而出現在區域性變數中的類不是直接的朋友。也就是說,陌生的類最好不要以區域性變數 的形式出現在類的內部。
 

把上面的概念一一翻譯成人話就是:

  1. 我們這個類姑娘啊,因為太矜持了不善於社交,所以對其他類夥伴們不怎麼熟悉。
  2. 類姑娘實在是太害羞了,一旦與別人多說幾句話就會緊張的不知所措,頻頻犯錯。
  3. 矜持的類姑娘儘管心思很活躍,愛多想。但是給別人的感覺都是純潔的像一張白紙。
  4. 因為類姑娘太過於矜持,害怕陌生人,認為陌生人都是壞人,所以只與自己熟悉的朋友交流。
  5. 類姑娘熟悉的朋友有:成員變數,方法引數,方法返回值的物件。而出現在其他地方的類都是陌生人,壞人!本姑娘拒絕與你交流!!!

哈哈,這樣應該大家都能理解了。總而言之就一句話:一個類應該儘量不要知道其他類太多的東西,不要和陌生的類有太多接觸

未使用迪米特原則

總公司員工Employee類:

public class Employee {
    private String id;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
}

 

分公司員工SubEmployee類:

public class SubEmployee {
    private String id;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
}

 

總公司員工管理EmployeeManager類:

public class EmployeeManager {
    public List<Employee> setValue(){
        List<Employee> employees=new ArrayList<Employee>();
        for(int i=0;i<10;i++){
            Employee employee=new Employee();
            employee.setId("總公司"+i);
            employees.add(employee);
        }
        return  employees;
    }

    public void printAllEmployee(SubEmployeeManager sub){
        List<SubEmployee> list1 = sub.setValue();
        for(SubEmployee e:list1){
            System.out.println(e.getId());
        }

        List<Employee> list2 = this.setValue();
        for(Employee e:list2){
            System.out.println(e.getId());
        }
    }

}

 

分公司員工管理SubEmployeeManager類:

public class SubEmployeeManager {
    public List<SubEmployee> setValue(){
        List<SubEmployee> subEmployees=new ArrayList<SubEmployee>();
        for(int i=0;i<10;i++){
            SubEmployee subEmployee=new SubEmployee();
            subEmployee.setId("分公司"+i);
            subEmployees.add(subEmployee);
        }
        return subEmployees;
    }
}

 

測試類:

public class test {
    public static  void main(String[] args){
        EmployeeManager employeeManager=new EmployeeManager();
        SubEmployeeManager subEmployeeManager=new SubEmployeeManager();
        employeeManager.printAllEmployee(subEmployeeManager);
    }
} 

 

執行結果:

上面的程式碼是正常執行的,但是可以看到一個問題,EmployeeManager類的printAllEmployee方法中使用的區域性變數SubEmployee是不符合迪米特法則的,其是陌生朋友,應該拒絕溝通。

已使用迪米特原則

EmployeeManager類:

public class EmployeeManager {
    public List<Employee> setValue() {
        List<Employee> employees = new ArrayList<Employee>();
        for (int i = 0; i < 10; i++) {
            Employee employee = new Employee();
            employee.setId("總公司" + i);
            employees.add(employee);
        }
        return employees;
    }

    public void printAllEmployee(SubEmployeeManager sub) {
        sub.printAllSubEmployee();

        List<Employee> list2 = this.setValue();
        for (Employee e : list2) {
            System.out.println(e.getId());
        }
    }

}

 

SubEmployeeManager類:

public class SubEmployeeManager {
    public List<SubEmployee> setValue(){
        List<SubEmployee> subEmployees=new ArrayList<SubEmployee>();
        for(int i=0;i<10;i++){
            SubEmployee subEmployee=new SubEmployee();
            subEmployee.setId("分公司"+i);
            subEmployees.add(subEmployee);
        }
        return subEmployees;
    }

    public void printAllSubEmployee(){
        List<SubEmployee> list1 = setValue();
        for(SubEmployee e:list1){
            System.out.println(e.getId());
        }
    }
}

 

我們將EmployeeManager類printAllEmployee方法中的列印分公司的程式碼移到了分公司的管理類SubEmployeeManager類中,再在方法中顯示的呼叫SubEmployeeManager類的方法,這符合迪米特法則的。

7.合成複用原則

儘量使用合成/集合,不要用繼承。

如果使用繼承,會使得耦合性加強,儘量作為方法的輸入引數或類的成員變數,這樣可以避免耦合。

結語

所有的原則只是規範,為了程式碼更加優雅,為了讓人一目瞭然。如果一定不遵循原則,那程式碼還是可以跑的,只是日後出bug的可能性提高。

以上,簡單來說,主要包括兩點:

1.找出應用中需要變化的獨立出來,不要和固定的混合在一起。

2.面向介面程式設計,而不是面向實現程式設計。

參考資料

設計模式六大原則(一):單一職責原則

設計模式的七大原則(1) --單一職責原則

六大設計原則之依賴倒置原則(DIP) 

設計模式之里氏替換原則