1. 程式人生 > >設計模式 #1(7大設計原則)

設計模式 #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(); } } } ~~~ 上面程式碼違反單一職責原則,同一個方法我們讓它去做檔案讀取,還讓他去做內容分割;當有需求變更(需要更換載入檔案,統計文字檔案中有多少個句子)時,我們需要重寫整個方法。 正例: ~~~java 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); } } ~~~ 遵守單一原則,可以給我們帶來的好處是,提高了程式碼的可重用性,同時還讓得到的資料不再有耦合,可以用來完成我們的個性化需求。 ## 開閉原則 簡述:==對擴充套件(新功能)開放,對修改(舊功能)關閉== 實體類設計: ~~~java 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 + '}'; } } ~~~ ~~~java 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 `方法 ~~~java public void setProd_price(float prod_price) { this.prod_price = prod_price * 0.8f; } ~~~ 違反了開閉原則,在原始碼中修改,對顯示原價這一功能進行了修改。 在開發時,我們應該,必須去考慮可能會變化的需求,屬性在任何時候都可能發生改變,對於需求的變化,==在要求遵守開閉原則的前提下,我們應該在開發中去進行擴充套件,而不是修改原始碼。== 正例: ~~~java public class discountPen extends Pen{ //用重寫方法設定價格 @Override public void setProd_price(float prod_price) { super.setProd_price(prod_price * 0.8f); } } ~~~ ~~~java 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); } } ~~~ 開閉原則並不是必須要一味地死守,需要結合開發場景進行使用,如果需要修改的原始碼是自己寫的,修改之後去完成需求,當然是簡單快速的;但是如果原始碼是別人寫的,或者是別人的架構,修改是存在巨大風險的,這時候應該去遵守開閉原則,防止破壞結構的完整性。 ## 介面隔離原則 簡述:==設計介面的時候,介面的抽象應是具有特定意義的。==需要設計出的是一個內聚的、職責單一的介面。“使用多個專門介面總比使用單一的總介面好。”這一原則不提倡設計出具有“普遍”意義的介面。 反例:動物介面中並不是所有動物都需要的。 ~~~java public interface Animal { void eat(); void fiy(); //泥鰍:你來飛? void swim(); // 大雕:你來遊? } ~~~ ~~~java class Bird implements Animal { @Override public void eat() { System.out.println("用嘴巴吃"); } @Override public void fiy() { System.out.println("用翅膀飛"); } @Override public void swim() { //我是大雕不會游泳 } } ~~~ 介面中的 `swim() `方法在實際開發中,並不適用於該類。 正例:介面抽象出同一層級的特定意義,提供給需要的類去實現。 ~~~java public interface Fly { void fly(); } public interface Eat { void eat(); } public interface Swim { void swim(); } ~~~ ~~~java public class Bird_02 implements Fly,Eat{ @Override public void eat() { System.out.println("用嘴巴吃"); } @Override public void fly() { System.out.println("用翅膀飛"); } //我是大雕不會游泳 } ~~~ 客戶端依賴的介面中不應該存在他所不需要的方法。 如果某一介面太大導致這一情況發生,應該分割這一介面,使用介面的客戶端只需要知道他需要使用的介面及該介面中的方法即可。 ## 依賴倒置原則 簡述:==上層不能依賴於下層,他們都應該依賴於抽象。== 需求:人餵養動物 反例: ~~~java 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](https://i.loli.net/2020/09/12/KBGYWTC1XNgLO7c.png) 這時候,`Person`內部的`feed`方法依賴於`Dog`,是上層方法中又依賴於下層的類。(人竟然依賴於一條狗?這算罵人嗎?) 當有需求變更,人的寵物不止有狗狗,還可以是貓等等,這時候需要修改上層類,這帶來的是重用性的問題,同時還違反上面提到的[**開閉原則**](#開閉原則)。 正例: ![image-20200912204707141](https://i.loli.net/2020/09/12/S7IBqp2d5xGDXsg.png) ~~~java 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`這一抽象類,==都依賴於抽象類==。 這時候,不管是曾經的上層程式碼,還是曾經的下層程式碼,都不會因為需求而改變。 依賴倒轉原則就是指:程式碼要依賴於抽象的類,而不要依賴於具體的類;要針對介面或抽象類程式設計,而不是針對具體類程式設計。通過面向介面程式設計,==**抽象不應該依賴於細節,細節應該依賴於抽象**==。 ## 迪米特法則(最少知道原則) 簡述:==一個類對於其他類知道的越少越好,就是說一個物件應當對其他物件有儘可能少的瞭解,只和朋友通訊,不和陌生人說話。== 反例: ~~~java 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`的很多細節,對於使用者來說不夠友好,而且,使用者還可能會呼叫錯誤,先斷電,再儲存檔案,顯然不符合邏輯,會導致檔案出現未儲存的錯誤。 其實對於使用者來說,知道進行關機就行了。 正例:封裝細節 ~~~java 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. 集合中的泛型 總的來說,只要在自身內定義的就是朋友,通過其他方法得到的都只是朋友的朋友; 但是,==朋友的朋友不是我的朋友==。 舉個反例: ~~~java 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`,替換後如何。 ~~~java 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: ~~~java 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: ~~~java 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`中的這兩個方法的某些方法就會有出錯、崩潰等風險。 這時候,可以得出一些結論: 當我們不屬於繼承父類的開發團隊時,是沒辦法保證父類程式碼不會被修改,或者修改時一定被通知到,這時候,就可能會出現需求滿足有缺陷的情況。所以,但我們去複用父類的程式碼時,避免去重寫或者新建方法,這樣可以防止原始碼結構發生改變帶來的打擊。 也就是說,我們在重用程式碼時,應該是==[組合優於繼承](#)==。 正例: ~~~java 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`由原來的繼承關係改為了低耦合的組合關係。