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

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

單一職責原則

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

需求:統計文本文件中有多少個單詞。編程

反例:設計模式

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();
        }
    }
}

上面代碼違反單一職責原則,同一個方法咱們讓它去作文件讀取,還讓他去作內容分割;當有需求變動(須要更換加載文件,統計文本文件中有多少個句子)時,咱們須要重寫整個方法。架構

正例:app

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);
    }
}

遵照單一原則,能夠給咱們帶來的好處是,提升了代碼的可重用性,同時還讓獲得的數據再也不有耦合,能夠用來完成咱們的個性化需求。框架

開閉原則

簡述:對擴展(新功能)開放,對修改(舊功能)關閉ide

實體類設計:post

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折銷售。ui

反例:在實體類的源代碼,修改 setProd_price 方法this

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由原來的繼承關係改成了低耦合的組合關係。

相關文章
相關標籤/搜索