萬字總結之設計模式(掃盲篇)

目錄

前言

什麼是設計模式?

爲什麼要使用設計模式?

設計模式的七大原則

1.單一職責原則

未使用單一職責原則

已使用單一職責原則

優化

優缺點總結

2.接口隔離原則

未使用接口隔離原則

已使用接口隔離原則

總結

3.依賴倒轉原則

未使用依賴倒轉原則

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

已使用依賴倒轉原則

總結

4.里氏替換原則

繼承的優缺點

使用里氏替換原則1

使用里氏替換原則2

使用里氏替換原則3

總結

5.開閉原則(重點)

基本介紹

未使用開閉原則

已使用開閉原則

6.迪米特原則

介紹

未使用迪米特原則

已使用迪米特原則

7.合成複用原則

結語

參考資料


前言

上篇說了反射,將其作爲框架的基礎知識。還沒看過的移至傳送門,萬字總結之反射(框架之魂)。今天我們來看設計模式。話不多說,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) 

設計模式之里氏替換原則

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章