前言
軟件設計中通常有很多的設計模式,設計模式是軟件開發中面對某一類型問題的通用解決方案,這些解決方式是由於有經驗的開發人員在軟件開發的多年經驗中整理、總結出來的,設計模式的目的是爲了讓代碼提升代碼的可讀性、可擴展性、可維護性,以及提供代碼的複用率,從而提升代碼的整體穩定性。而設計模式通常需要遵循一些設計原則,在設計原則的基礎之上衍生出了各種各樣的設計模式。設計原則是設計要求,設計模式是設計方案,使用設計模式的代碼則是具體的實現。
一、設計模式六大設計原則
設計模式中主要有六大設計原則,簡稱爲SOLID,是由於各個原則的首字母簡稱合併的來,六大設計原則分別如下:
1、單一職責原則(Single Responsibitity Principle)
2、開放封閉原則(Open Close Principle)
3、里氏替換原則(Liskov Substitution Principle)
4、接口分離原則(Interface Segregation Principle)
5、依賴倒置原則(Dependence Inversion Principle)
6、迪米特法則(Law Of Demter)
二、設計原則詳細描述
2.1、單一職責原則
定義:當需要修改某個對象時,原因有且只有一個;每個類或方法的職責只有一個
一個類或方法只有一個職責,職責單一就可以職責解耦。當一個類或方法的職責越多,被複用的概率就越小,且當某個職責發生變化時很容易會引起其他職責受影響。
優點:
1、代碼簡單,可讀性高,可擴展性強
2、代碼複雜度降低,且職責解耦,降低某個職責變化導致影響其他職責的風險
3、代碼可複用性增高;
案例如下:
業務場景:實現一個用戶註冊的Http接口
破壞單一原則示範:
1 public class UserController { 2 3 /** 用戶註冊*/ 4 @RequestMapping(value = "regist") 5 public String userRegist(String userName, String password) throws SQLException { 6 /** 第一步:校驗參數*/ 7 if(StringUtils.isEmpty(userName)||StringUtils.isEmpty(password)){ 8 return "error"; 9 } 10 /** 第二布:獲取數據庫連接*/ 11 Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test","user","password");; 12 /** 第三步:構造SQL語句*/ 13 String sql = ""; 14 PreparedStatement preparedStatement = connection.prepareStatement(sql); 15 /** 第四布:執行SQL*/ 16 preparedStatement.execute(); 17 return "success"; 18 } 19 }
定義了一個UserController類定義了一個用戶註冊的接口,並且在該方法中完成了用戶註冊時校驗和執行註冊SQL的完成流程,初步看上去是沒有什麼問題,但是該方法userRegist中完成了全部流程的具體實現,就會出現幾個問題:
1、代碼比較臃腫,代碼複用性不高,新增用戶登錄、查詢用戶等功能時,還是需要獲取數據庫連接,構造SQL語句,執行SQL的過程;
2、職責高耦合,用戶註冊的邏輯和數據庫的執行邏輯耦合度過高;
3、職責變化風險過大,當數據庫發生變化時,就會導致整個用戶註冊功能都需要發生變化,變化的成功過大;
遵循單一原則示範:
1 public class UserController { 2 3 @Resource 4 private UserService userService; 5 6 /** 用戶註冊*/ 7 @RequestMapping(value = "regist") 8 public String userRegist(String userName, String password) throws SQLException { 9 /** 第一步:校驗參數*/ 10 checkParam(userName, password); 11 /** 第二布:保存用戶*/ 12 userService.addUser(userName, password); 13 return "success"; 14 } 15 16 private void checkParam(String... params) { 17 18 } 19 }
1 public interface UserService { 2 3 public void addUser(String userName, String password); 4 5 }
將參數校驗邏輯抽離成單獨的方法,當需要驗證更多的參數時只需要修改checkParam方法即可;將保存用戶的邏輯交給UserService去實現,UserController不需要關係UserService是將用戶信息保存到哪一個數據庫中,將職責分給UserService去具體實現。
這樣無論是參數變化還是數據庫層發生變化,接口層都無需發生任何變化。每個方法都只需要完成自己單獨的職責即可。
2.2、開放封閉原則
定義:對擴展開放,對修改關閉
對擴展開放和對修改關閉表示當一個類或一個方法有新需求或者需求發生改變時應該採用擴展的方式而不應該採用修改原有邏輯的方式來實現。因爲擴展了新的邏輯如果有問題只會影響新的業務,不會因爲老業務;而如果採用修改的方式,很有可能就會影響到老業務受影響。開閉原則是所有設計模式的最核心目標,也是最難實現的目標,但是所有的軟件設計都應該以開閉原則當作標準,才能使軟件更加的穩定和健壯。
優點:
1、新老邏輯解耦,需求發生改變不會影響老業務的邏輯
2、改動成本最小,只需要追加新邏輯,不需要改的老邏輯
3、提供代碼的穩定性和可擴展性
案例如下:
業務場景:現有一個將Date類型轉化成年月日字符串格式的工具類如下:
1 /** 將Date轉化成年-月-日格式的字符串*/ 2 public static String formatDate(Date date){ 3 SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); 4 return format.format(date); 5 }
現在需求發生改變,需要在年月日的基礎上添加時分秒
破壞開閉原則示範一:
1 /** 將Date轉化成年-月-日 時:分:秒格式的字符串*/ 2 public static String formatDate(Date date){ 3 SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 4 return format.format(date); 5 }
直接將格式替換成年月日時分秒的格式,雖然改動最快,但是涉及範圍最大,因爲這個工具類可以已經被多個地方調用了,而部分調用方的需求還是要年月日歌手的,這樣就會導致其他業務受到了影響
破壞開閉原則示範二:
1 /** 將Date轉化成指定格式的字符串*/ 2 public static String formatDate(Date date, String dateFormat){ 3 SimpleDateFormat format = new SimpleDateFormat(dateFormat); 4 return format.format(date); 5 }
將時間格式採用參數傳入的方式,將Date類型轉換成指定格式的字符串,雖然這樣更靈活也不會影響其他業務邏輯變化,但是就會導致其他業務的代碼需要發生變化,假設這個方法已經被100個地方調用了,那麼就需要將100個調用方都一一修改增加一個參數,很顯然修改代碼的成功非常高,而代碼修改的越多,出問題的風險就越高。
遵循開閉原則示範:
1 /** 將Date轉化成年-月-日格式的字符串*/ 2 public static String formatDate(Date date){ 3 SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); 4 return format.format(date); 5 } 6 7 /** 將Date轉化成年-月-日 時:分:秒格式的字符串*/ 8 public static String formatTime(Date date){ 9 SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 10 return format.format(date); 11 }
保留原有方法的同時,新增將Date轉換成年月日時分秒格式的方法,這樣雖然代碼變多了,但是不會導致老業務發生改變,風險最小
2.3、里氏替換原則
定義:一個子類實例在任何時刻都可以替換父類實例,從而形成IS-A關係
所有引用基類的地方都可以被必須透明的被子類所替換,也就是說子類可以在任何時候替換父類的對象,並且不會引起程序執行結構的變化。
表示在繼承關係中,子類可以重寫父類的抽象方法,對於父類已經實現了的方法,子類不應該重寫;子類相當於父類而言只應該擴展業務,不應該修改父類的業務,否則就不是一個正確的繼承關係。
優點:
1.提高了代碼的複用率
2.使繼承關係更完整,避免丟失父類的特性
案例如下:
業務場景:給兩個用戶類型進行排序,父類邏輯是按用戶ID進行排序,邏輯如下:
1 public class UserCompare { 2 3 private User user; 4 5 public int compareUser(User otherUser){ 6 if(user.getUserId() > otherUser.getUserId()){ 7 return -1; 8 }else if(user.getUserId() < otherUser.getUserId()){ 9 return 1; 10 }else { 11 return 0; 12 } 13 } 14 15 public User getUser() { 16 return user; 17 } 18 19 public void setUser(User user) { 20 this.user = user; 21 } 22 }
此時當用戶有了身份,比如學生,需要根據用戶的年齡來進行排序,此時衍生學生比較子類StudentCompare繼承之UserCompare
破壞里氏替換原則:
1 public class StudentCompare extends UserCompare{ 2 3 @Override 4 public int compareUser(User otherUser){ 5 if(user.getAge() > otherUser.getAge()){ 6 return -1; 7 }else if(user.getAge() < otherUser.getAge()){ 8 return 1; 9 }else { 10 return 0; 11 } 12 } 13 }
這裏直接將父類的方法進行重寫,雖然可以滿足需求,但是會導致父類的邏輯丟失,如果此時再需要根據用戶的ID進行排序,通過子類就無法實現。如果將父類對象替換成子類對象,很顯然排序的結果就不是預期的結果
遵循里氏替換原則:
1 public class StudentCompare extends UserCompare{ 2 3 public int compareUserByAge(User otherUser){ 4 if(user.getAge() > otherUser.getAge()){ 5 return -1; 6 }else if(user.getAge() < otherUser.getAge()){ 7 return 1; 8 }else { 9 return 0; 10 } 11 } 12 }
這裏既保留了父類的根據用戶ID排序的邏輯,又擴展了新的排序邏輯,此時就可以用子類對象再任何地方替換父類的對象。
2.4、接口分離原則
定義:實現類不應該依賴不需要的接口方法,採用多個專用的接口來替換單個複雜的總接口
客戶端實現接口的方法不應該依賴和實現不需要的方法,實現者只需要實現特定的接口方法即可。表示定義接口時應該將不同職責功能定義在不同的接口中,同一個接口中的職責都是一樣的,這樣纔可以低耦合高內聚
優點:
1、規避接口中包含不同職責的方法,每個接口的職責均單一
2、接口之間低耦合,接口本身高內聚,接口的責任劃分更清晰
案例如下:
業務場景:
電商系統中的訂單模塊,需要保存訂單、查詢訂單、訂單支付等職責
破壞接口分離原則:
1 public interface OrderService { 2 3 /** 保存訂單 */ 4 public void addOrder(); 5 6 /** 查詢訂單 */ 7 public void queryOrder(); 8 9 /** 支付 */ 10 public void payOrder(); 11 12 /** 退款*/ 13 public void refundOrder(); 14 }
這裏將訂單的職責和支付的職責都放在了OrderService接口中,這就需要訂單服務的實現者還需要實現和訂單模塊耦合不高的支付功能,這樣就使得訂單和支付的耦合度非常高;如果有以下的場景就會導致代碼改動非常大
1、訂單需要支持積分支付的方式,很顯然就不能直接使用payOrder方法了
2、有其他業務模塊設計資金業務,也需要調用支付和退款方法時,就需要依賴訂單服務的接口,就導致本身不同的業務之間互相耦合
遵循接口分離原則:
public interface OrderService { /** 保存訂單 */ public void addOrder(); /** 查詢訂單 */ public void queryOrder(); }
public interface PayService { /** 支付 */ public void pay(); /** 退款*/ }
將接口方法按模塊進行拆分,訂單服務僅提供和訂單本身相關的職責,將支付職責交給支付的服務提供,這樣將不同職責的服務完成拆分開,互相之間不會耦合。
2.5、依賴倒置原則
定義:高層模塊不應該依賴於底層模塊,二者都應該依賴於抽象;抽象不應該依賴於細節,細節應該依賴於抽象
1、高層模塊不應該依賴底層模塊,二者都應該依賴抽象。
2、抽象不應該依賴細節,細節應該依賴抽象。
3、依賴倒置的中心思想是面向接口編程。
4、依賴倒置原則是基於這樣的設計理念:相對於細節的多變性,抽象的東西要穩定的多。以抽象爲基礎搭建的架構比以細節爲基礎搭建的架構要穩定的多。
5、使用接口或抽象類的目的是指定好規範,而不涉及任何具體的操作,把展現細節的任務交給他們的實現類來完成。
因爲抽象改動的概率不大,比較穩定,而實現往往會隨着需求的變化而變化,所以抽象的定義不能依賴於實現,否則抽象的穩定性會大打折扣
案例如下:
定義一個給ArrayList和HashSet排序的接口
破壞依賴倒置原則:
1 public interface SortService { 2 3 /** 將ArrayList進行排序*/ 4 public void sortList(ArrayList list); 5 6 /** 將HashSet進行排序*/ 7 public void sortSet(HashSet set); 8 }
很明顯SortService這個抽象的接口的定義了依賴了ArrayList和HashSet這兩個具體的實現,這樣的缺點是一旦ArrayList的邏輯有變動就會導致SortService也進行對應的修改;而且這樣的設計可複用性不強,因爲無法滿足其他的List的排序,比如LinkedList
遵循依賴倒置原則:
1 public interface SortService { 2 3 /** 將List進行排序*/ 4 public void sortList(List list); 5 6 /** 將Set進行排序*/ 7 public void sortSet(Set set); 8 }
抽象的集合排序接口定義只定義了兩個給List和Set排序的方法,但是沒有定義給哪一種List和Set進行排序,這樣就可以在實現時實現任何List的實現的排序功能和Set的實現的排序功能。
2.6、迪米特法則
定義:一個對象應該對其他對象有最少的瞭解,不和陌生對象交流
迪米特法則也叫做最少知道法則(least knowledge Principle),一個類應該儘量少的依賴其他類,降低各個類之間的耦合,類之間耦合度越低,代碼的複用性就越高。
迪米特法則中的朋友是指:當前對象本身、當前對象的成員對象、當前對象所創建的對象、當前對象的方法參數等,這些對象存在關聯、聚合或組合關係,可以直接訪問這些對象的方法
優點:
1、降低類之間的耦合度,提高模塊的相對獨立性。
2、由於親和度降低,從而提高了類的可複用率和系統的擴展性。
遵循迪米特法則雖然會將兩個有關聯的類進行解耦,但是往往會伴隨一箇中介角色來進行互相調用的工作。
案例如下:
業務場景,某集團需要給全公司所有部門的員工發送一份通知
破壞迪米特法則:
1 public class Company { 2 3 private String companyName; 4 5 private List<Department> departmentList; 6 7 /** 發送通知 */ 8 public void sendNotice(){ 9 for (Department department : departmentList){ 10 List<Employee> employeeList = department.getEmployeeList(); 11 for(Employee employee : employeeList){ 12 System.out.println("發送通知給:" + employee.getUserName()); 13 } 14 } 15 } 16 17 public String getCompanyName() { 18 return companyName; 19 } 20 21 public void setCompanyName(String companyName) { 22 this.companyName = companyName; 23 } 24 25 public List<Department> getDepartmentList() { 26 return departmentList; 27 } 28 29 public void setDepartmentList(List<Department> departmentList) { 30 this.departmentList = departmentList; 31 } 32 }
在這裏集團本應該是隻需要和部門交互的即可,但是由於需要發送給所有人,便把Employee引入到了Company類中,這樣Company就需要依賴Employee,增加了Company和Employee之間的耦合
遵循迪米特法則:
1 public class Company { 2 3 private String companyName; 4 5 private List<Department> departmentList; 6 7 /** 發送通知 */ 8 public void sendNotice(){ 9 for (Department department : departmentList){ 10 department.sendNotice(); 11 } 12 } 13 14 public String getCompanyName() { 15 return companyName; 16 } 17 18 public void setCompanyName(String companyName) { 19 this.companyName = companyName; 20 } 21 22 public List<Department> getDepartmentList() { 23 return departmentList; 24 } 25 26 public void setDepartmentList(List<Department> departmentList) { 27 this.departmentList = departmentList; 28 } 29 }
1 public class Department { 2 3 private String deptName; 4 5 private List<Employee> employeeList; 6 7 public void sendNotice() { 8 for(Employee employee : employeeList){ 9 System.out.println("發送通知給:" + employee.getUserName()); 10 } 11 } 12 13 public String getDeptName() { 14 return deptName; 15 } 16 17 public void setDeptName(String deptName) { 18 this.deptName = deptName; 19 } 20 21 public List<Employee> getEmployeeList() { 22 return employeeList; 23 } 24 25 public void setEmployeeList(List<Employee> employeeList) { 26 this.employeeList = employeeList; 27 } 28 }
這樣Company只會和Department交互,而Department負責和Employee交互,這樣就去除了Company和Employee之間的依賴關係,降低了耦合度
三、總結
設計原則本質上算是一種思想,並沒有要求代碼設計時必須要按什麼的代碼規範來進行設計,也並非說完全安全設計原則來進行代碼設計就肯定能大幅度提升代碼的性能和穩定性。設計原則的主要目的是從代碼的可讀性、穩定性、可擴展性等方面對代碼進行了提升,並且重要是一個思想是代碼塊之間達到低耦合和高內聚的設計思想。