設計模式 #1(7大設計原則)

凌丹妙耀發表於2020-09-13

設計模式 #1(7大設計原則)

單一職責原則

簡述:單個類,單個方法或者單個框架只完成某一特定功能。

需求:統計文字檔案中有多少個單詞。

反例:

public class nagtive {
    public static void main(String[] args) {
        try{
            //讀取檔案的內容
            Reader in = new FileReader("E:\\1.txt");
            BufferedReader bufferedReader = new BufferedReader(in);

            String line = null;
            StringBuilder sb = new StringBuilder("");

            while((line =bufferedReader.readLine()) != null){
                sb.append(line);
                sb.append(" ");
            }

            //對內容進行分割
            String[] words = sb.toString().split("[^a-zA-Z]+");
            System.out.println(words.length);

            bufferedReader.close();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

上面程式碼違反單一職責原則,同一個方法我們讓它去做檔案讀取,還讓他去做內容分割;當有需求變更(需要更換載入檔案,統計文字檔案中有多少個句子)時,我們需要重寫整個方法。

正例:

public class postive {
    
    public static StringBuilder loadFile(String fileLocation) throws IOException {
      
            //讀取檔案的內容
            Reader in = new FileReader("E:\\1.txt");
            BufferedReader bufferedReader = new BufferedReader(in);

            String line = null;
            StringBuilder sb = new StringBuilder("");

            while ((line = bufferedReader.readLine()) != null) {
                sb.append(line);
                sb.append(" ");
            }
            
            bufferedReader.close();
            return sb;
    }
    
    public static String[] getWords(String regex, StringBuilder sb){
        //對內容進行分割
        return  sb.toString().split(regex);
    }
    
    public static void main(String[] args) throws IOException {
        
            //讀取檔案的內容
            StringBuilder sb = loadFile("E:\\1.txt");
            
            //對內容進行分割
            String[] words = getWords("[^a-zA-Z]+", sb);
            
            System.out.println(words.length);
    }
}

遵守單一原則,可以給我們帶來的好處是,提高了程式碼的可重用性,同時還讓得到的資料不再有耦合,可以用來完成我們的個性化需求。

開閉原則

簡述:對擴充套件(新功能)開放,對修改(舊功能)關閉

實體類設計:

public class Pen {
    private String prod_name;
    private String prod_origin;
    private float prod_price;

    public String getProd_name() {
        return prod_name;
    }

    public void setProd_name(String prod_name) {
        this.prod_name = prod_name;
    }

    public String getProd_origin() {
        return prod_origin;
    }

    public void setProd_origin(String prod_origin) {
        this.prod_origin = prod_origin;
    }

    public float getProd_price() {
        return prod_price;
    }

    public void setProd_price(float prod_price) {
        this.prod_price = prod_price;
    }

    @Override
    public String toString() {
        return "Pen{" +
                "prod_name='" + prod_name + '\'' +
                ", prod_origin='" + prod_origin + '\'' +
                ", prod_price=" + prod_price +
                '}';
    }
}
public static void main(String[] args) {
        //輸入商品資訊
        Pen redPen = new Pen();
        redPen.setProd_name("英雄牌鋼筆");
        redPen.setProd_origin("廠裡");
        redPen.setProd_price(15.5f);
        //輸出商品資訊
        System.out.println(redPen);
    }

需求:商品搞活動,打折8折銷售。

反例:在實體類的原始碼,修改 setProd_price 方法

public void setProd_price(float prod_price) {
        this.prod_price = prod_price * 0.8f;
    }

違反了開閉原則,在原始碼中修改,對顯示原價這一功能進行了修改。

在開發時,我們應該,必須去考慮可能會變化的需求,屬性在任何時候都可能發生改變,對於需求的變化,在要求遵守開閉原則的前提下,我們應該在開發中去進行擴充套件,而不是修改原始碼。

正例:

public class discountPen extends Pen{
	//用重寫方法設定價格
    @Override
    public void setProd_price(float prod_price) {
        super.setProd_price(prod_price * 0.8f);
    }
}
public class postive {
    public static void main(String[] args) {
        //輸入商品資訊,向上轉型呼叫重寫方法設定價格
        Pen redPen = new discountPen();
        redPen.setProd_name("英雄牌鋼筆");
        redPen.setProd_origin("廠裡");
        redPen.setProd_price(15.5f);
        //輸出商品資訊
        System.out.println(redPen);
    }
}

開閉原則並不是必須要一味地死守,需要結合開發場景進行使用,如果需要修改的原始碼是自己寫的,修改之後去完成需求,當然是簡單快速的;但是如果原始碼是別人寫的,或者是別人的架構,修改是存在巨大風險的,這時候應該去遵守開閉原則,防止破壞結構的完整性。

介面隔離原則

簡述:設計介面的時候,介面的抽象應是具有特定意義的。需要設計出的是一個內聚的、職責單一的介面。“使用多個專門介面總比使用單一的總介面好。”這一原則不提倡設計出具有“普遍”意義的介面。

反例:動物介面中並不是所有動物都需要的。

public interface Animal {
    void eat();
    void fiy(); //泥鰍:你來飛?
    void swim(); // 大雕:你來遊?
}
class Bird implements Animal {
    @Override
    public void eat() {
        System.out.println("用嘴巴吃");
    }

    @Override
    public void fiy() {
        System.out.println("用翅膀飛");
    }

    @Override
    public void swim() {
    //我是大雕不會游泳
    }
}

介面中的 swim() 方法在實際開發中,並不適用於該類。

正例:介面抽象出同一層級的特定意義,提供給需要的類去實現。

public interface Fly {
    void fly();
}

public interface Eat {
    void eat();
}

public interface Swim {
    void swim();
}
public class Bird_02 implements Fly,Eat{
    @Override
    public void eat() {
        System.out.println("用嘴巴吃");
    }

    @Override
    public void fly() {
        System.out.println("用翅膀飛");
    }

    //我是大雕不會游泳
}

客戶端依賴的介面中不應該存在他所不需要的方法。

如果某一介面太大導致這一情況發生,應該分割這一介面,使用介面的客戶端只需要知道他需要使用的介面及該介面中的方法即可。

依賴倒置原則

簡述:上層不能依賴於下層,他們都應該依賴於抽象。

需求:人餵養動物

反例:

public class negtive {

    static class Person {
        public void feed(Dog dog){
            dog.eat();
        }
    }

    static class Dog {
        public void eat() {
            System.out.println("主人餵我了。汪汪汪...");
        }
    }

    public static void main(String[] args) {
        Person person= new Person();
        Dog dog = new Dog();
        person.feed(dog);
    }
}

image-20200912204644913

這時候,Person內部的feed方法依賴於Dog,是上層方法中又依賴於下層的類。(人竟然依賴於一條狗?這算罵人嗎?)

當有需求變更,人的寵物不止有狗狗,還可以是貓等等,這時候需要修改上層類,這帶來的是重用性的問題,同時還違反上面提到的開閉原則

正例:

image-20200912204707141

public class postive {
    static class Person {
        public void feed(Animal animal){
            animal.eat();
        }
    }

    interface Animal{
        public void eat();
    }

    static class Dog implements Animal{
        public void eat() {
            System.out.println("我是狗狗,主人餵我了。汪汪汪...");
        }
    }

    static class Cat implements Animal{
        public void eat() {
            System.out.println("我是貓咪,主人也餵我了。(我為什麼要說也?)喵喵喵...");
        }
    }

    public static void main(String[] args) {
        Person person= new Person();
        Dog dog = new Dog();
        Cat cat = new Cat();
        person.feed(dog);
        person.feed(cat);
    }
}

這時候,Person內部的feed方法不在依賴於依賴於Dog或者Cat,而是不管是Person,還是Dog或者Cat,他們都依賴與Animal這一抽象類,都依賴於抽象類

這時候,不管是曾經的上層程式碼,還是曾經的下層程式碼,都不會因為需求而改變。

依賴倒轉原則就是指:程式碼要依賴於抽象的類,而不要依賴於具體的類;要針對介面或抽象類程式設計,而不是針對具體類程式設計。通過面向介面程式設計,抽象不應該依賴於細節,細節應該依賴於抽象

迪米特法則(最少知道原則)

簡述:一個類對於其他類知道的越少越好,就是說一個物件應當對其他物件有儘可能少的瞭解,只和朋友通訊,不和陌生人說話。

反例:

public class negtive {
    class Computer{
        public  void  closeFile(){
            System.out.println("關閉檔案");
        }
        public  void  closeScreen(){
            System.out.println("關閉螢幕");
        }
        public  void  powerOff(){
            System.out.println("斷電");
        }
    }

    class Person{
        private Computer computer;

        public void offComputer(){
            computer.closeFile();
            computer.closeScreen();
            computer.powerOff();
        }
    }
}

這時候,Person 知道了 Computer的很多細節,對於使用者來說不夠友好,而且,使用者還可能會呼叫錯誤,先斷電,再儲存檔案,顯然不符合邏輯,會導致檔案出現未儲存的錯誤。

其實對於使用者來說,知道進行關機就行了。

正例:封裝細節

public class postive {
    class Computer{
        public  void  closeFile(){
            System.out.println("關閉檔案");
        }
        public  void  closeScreen(){
            System.out.println("關閉螢幕");
        }
        public  void  powerOff(){
            System.out.println("斷電");
        }
        
        public void turnOff(){  //封裝細節
            this.closeFile();
            this.closeScreen();
            this.powerOff();
        }
    }

    class Person{
        private Computer computer;

        public void offComputer(){
            computer.turnOff();
        }
    }
}

前面說的,只和朋友通訊,不和陌生人說話。先來明確一下什麼才叫做朋友:

什麼是朋友?

  1. 類中的欄位
  2. 方法的返回值
  3. 方法的引數
  4. 方法中的例項物件
  5. 物件本身
  6. 集合中的泛型

總的來說,只要在自身內定義的就是朋友,通過其他方法得到的都只是朋友的朋友;

但是,朋友的朋友不是我的朋友

舉個反例:

public class negtive {

     class Market{
        private  Computer computer;
        public Computer getComputer(){
            return this.computer;
        }
    }

    static class Computer{
        public  void  closeFile(){
            System.out.println("關閉檔案");
        }
        public  void  closeScreen(){
            System.out.println("關閉螢幕");
        }
        public  void  powerOff(){
            System.out.println("斷電");
        }
    }

    class Person{
        private Market market;

        Computer computer =market.getComputer(); 
        // //此時的 computer 並不是 Person 的朋友,只是 Market 的朋友。
    }
}

在實際開發中,要完全符合迪米特法則,也會有缺點

  • 在系統裡造出大量的小方法,這些方法僅僅是傳遞間接的呼叫,與系統的業務邏輯無關。

  • 遵循類之間的迪米特法則會是一個系統的區域性設計簡化,因為每一個區域性都不會和遠距離的物件有直接的關聯。但是,這也會造成系統的不同模組之間的通訊效率降低,也會使系統的不同模組之間不容易協調。

因此,前人總結出,一些方法論以供我們參考:

  1. 優先考慮將一個類設定成不變類。

  2. 儘量降低一個類的訪問許可權。

  3. 謹慎使用Serializable

  4. 儘量降低成員的訪問許可權。

雖然規矩很多,但是理論需要深刻理解,實戰需要經驗積累。路還很長。

里氏替換原則

簡述:任何能使用父類物件的地方,都應該能透明地替換為子類物件。

需求:將長方形的寬改成比長大 1 。

反例:在父類Rectangular下,業務場景符合邏輯。現有子類Square,替換後如何。

public class negtive {
    static class Rectangular {
        private Integer width;
        private Integer length;

        public Integer getWidth() {
            return width;
        }

        public void setWidth(Integer width) {
            this.width = width;
        }

        public Integer getLength() {
            return length;
        }

        public void setLength(Integer length) {
            this.length = length;
        }

    }

    static class Square extends Rectangular {
        private Integer sideWidth;

        @Override
        public Integer getWidth() {
            return sideWidth;
        }

        @Override
        public void setWidth(Integer width) {
            this.sideWidth = width;
        }

        @Override
        public Integer getLength() {
            return sideWidth;
        }

        @Override
        public void setLength(Integer length) {
            this.sideWidth = length;
        }
    }


    static class Utils{
        public static void transform(Rectangular graph){
            while ( graph.getWidth() <= graph.getLength() ){
                graph.setWidth(graph.getWidth() + 1);
                System.out.println("長:"+graph.getLength()+" : " +
                        "寬:"+graph.getWidth());
            }
        }
    }

    public static void main(String[] args) {
    // Rectangular graph = new Rectangular();
        Rectangular graph = new Square();
        graph.setWidth(20);
       graph.setLength(30);

        Utils.transform(graph);
    }
}

替換後執行將是無限死迴圈。

要知道,在向上轉型的時候,方法的呼叫只和new的物件有關,才會造成不同的結果。在使用場景下,需要考慮替換後業務邏輯是否受影響。

由此引出里氏替換原則的使用需要考慮的條件:

  • 是否有is-a關係
  • 子類可以擴充套件父類的功能,但是不能改變父類原有的功能。

這樣的反例還有很多,如:鴕鳥非鳥,還有我們們老祖宗早就說過的的春秋戰國時期--白馬非馬說,都是一個道理。

組合優於繼承

簡述:複用別人的程式碼時,不宜使用繼承,應該使用組合。

需求:製作一個組合,該集合能夠記錄下曾經新增過多少元素。(不只是統計某一時刻)

反例 #1:

public class negtive_1 {

    static class MySet extends HashSet{
        private int count = 0;

        public int getCount() {
            return count;
        }

        @Override
        public boolean add(Object o) {
            count++;
            return super.add(o);
        }
    }

    public static void main(String[] args) {
        MySet mySet = new MySet();
        mySet.add("111111");
        mySet.add("22222222222222");
        mySet.add("2333");


        Set hashSet = new HashSet();
        hashSet.add("集合+11111");
        hashSet.add("集合+22222222");
        hashSet.add("集合+233333");
        mySet.addAll(hashSet);

        System.out.println(mySet.getCount());
    }
}

看似解決了需求,add 方法可以成功將count進行自加, addAll方法通過方法內呼叫add,可以成功將count進行增加操作。

缺陷JDK 版本如果未來進行更新,addAll方法不再通過方法內呼叫add,那麼當呼叫addAll進行集合新增元素時,count將不無從進行自加。需求也將無法滿足。

HashMap 就在 1.6 1.7 1.8就分別更新了三次。


反例 #2:

public class negtive_2 {

    static class MySet extends HashSet{
        private int count = 0;

        public int getCount() {
            return count;
        }

        @Override
        public boolean add(Object o) {
            count++;
            return super.add(o);
        }

        @Override
        public boolean addAll(Collection c) {
            boolean modified = false;
            for (Object e : c)
                if (add(e))
                    modified = true;
            return modified;
        }
    }

    public static void main(String[] args) {
        MySet mySet = new MySet();
        mySet.add("111111");
        mySet.add("22222222222222");
        mySet.add("2333");


        Set hashSet = new HashSet();
        hashSet.add("集合+11111");
        hashSet.add("集合+22222222");
        hashSet.add("集合+233333");
        mySet.addAll(hashSet);

        System.out.println(mySet.getCount());
    }
}

親自再重寫addAll方法,確保addAll方法一定能呼叫到add方法,也就能夠對 count進行增加操作。

但是,問題還是有的:

缺陷

  • 如果未來,HashSet新增了一個addSome方法進行元素的新增,那就白給了。
  • 重寫了addAll、add這兩個方法,如果JDK中其他類的某些方法依賴於HashMap中的這兩個方法,那麼JDK中其他類依賴於HashMap中的這兩個方法的某些方法就會有出錯、崩潰等風險。

這時候,可以得出一些結論:

當我們不屬於繼承父類的開發團隊時,是沒辦法保證父類程式碼不會被修改,或者修改時一定被通知到,這時候,就可能會出現需求滿足有缺陷的情況。所以,但我們去複用父類的程式碼時,避免去重寫或者新建方法,這樣可以防止原始碼結構發生改變帶來的打擊。

也就是說,我們在重用程式碼時,應該是組合優於繼承

正例:

public class postive {
    
    class MySet{
        private HashSet hashSet;

        private int count = 0;

        public int getCount() {
            return count;
        }

        public boolean add(Object o) {
            count++;
            return hashSet.add(o);
        }

        public boolean addAll(Collection c) {
            count += c.size();
            return hashSet.addAll(c);
        }
    }

    public static void main(String[] args) {
        negtive_2.MySet mySet = new negtive_2.MySet();
        mySet.add("111111");
        mySet.add("22222222222222");
        mySet.add("2333");


        Set hashSet = new HashSet();
        hashSet.add("集合+11111");
        hashSet.add("集合+22222222");
        hashSet.add("集合+233333");

        mySet.addAll(hashSet);

        System.out.println(mySet.getCount());
    }
}

利用組合,實現解耦,將HashSet和自定義類MySet由原來的繼承關係改為了低耦合的組合關係。


相關文章