課程目標前端
一、經過對節課內容的學習,瞭解設計原則的重要性。java
二、掌握七大設計原則的具體內容。mysql
內容定位sql
學習設計原則,學習設計模式的基礎。在實際開發過程當中,並非必定要求全部代碼都遵循設計原則,咱們要考慮人力、時間、成本、質量,不是刻意追求完美,要在適當的場景遵循設計原則,體現的是一種平衡取捨,幫助咱們設計出更加優雅的代碼結構。數據庫
開閉原則(Open-Closed Principle, OCP)是指一個軟件實體如類、模塊和函數應該對擴展開放,對修改關閉。編程
一、開閉原則,是面向對象設計中最基礎的設計原則。它指導咱們如何創建穩定靈活的系統,例如:咱們版本更新,我儘量不修改源代碼,可是能夠增長新功能設計模式
二、所謂的開閉,也正是對擴展和修改兩個行爲的一個原則。強調的是用抽象構建框架,用實現擴展細節。能夠提升軟件系統的可複用性及可維護性架構
三、實現開閉原則的核心思想就是面向抽象編程oracle
首先建立一個課程接口框架
//課程接口 public interface ICourse { Integer getId(); String getName(); Double getPrice(); }
整個課程生態有 Java 架構、大數據、人工智能、前端、軟件測試等,咱們來建立 Java架構課程
//Java架構課程 public class JavaCourse implements ICourse{ private Integer id; private String name; private Double price; public Integer getId() { return this.id; } public String getName() { return this.name; } public Double getPrice() { return this.price; } public JavaCourse(Integer id, String name, Double price) { this.id = id; this.name = name; this.price = price; } }
//測試 public static void main(String[] args) { ICourse iCourse = new JavaDisCountCourse(1, "Java架構", 11800D); System.out.println("課程ID:" + iCourse.getId() + "\n課程標題:《" + iCourse.getName() + "》" + "\n課程售價:" + iCourse.getPrice()); }
一、如今咱們要給 Java 架構課程作活動,價格優惠。若是修改 JavaCourse 中的 getPrice() 方法,則會存在必定的風險,可能影響其餘地方的調用結果。
二、咱們如何在不修改原有代碼前提早下,實現價格優惠這個功能呢?如今,咱們再寫一個處理優惠邏輯的類 JavaDiscountCourse (思考一下爲何要叫JavaDiscountCourse,而不叫 DiscountCourse)
//處理優惠邏輯類 public class JavaDisCountCourse extends JavaCourse{ public JavaDisCountCourse(Integer id, String name, Double price) { super(id, name, price); } //原價拿不到 /*@Override public Double getPrice() { return super.getPrice() * 0.6; }*/ public Double getDisCountPrice(){ return super.getPrice() * 0.6; } }
//測試 public static void main(String[] args) { ICourse iCourse = new JavaDisCountCourse(1, "Java架構", 11800D); JavaDisCountCourse course = (JavaDisCountCourse) iCourse; System.out.println("課程ID:" + course.getId() + "\n課程標題:《" + course.getName() + "》" + "\n課程原價:" + course.getPrice() + "\n課程售價:" + course.getDisCountPrice()); }
類圖展現:
依賴倒置原則(Dependence Inversion Principle,DIP)是指設計代碼結構時,高層模塊不該該依賴底層模塊,兩者都應該依賴其抽象。
一、抽象不該該依賴細節,細節應該依賴抽象
二、針對接口編程,不要針對實現編程
三、能夠減小類與類之間的耦合性,提升系統的穩定性,提升代碼的可讀性和可維護性,並可以下降修改程序所形成的風險
首先建立一個類 Tom
public class Tom { public void studyJavaCourse(){ System.out.println("Tom正在學習Java課程"); } public void studyPythonCourse(){ System.out.println("Tom正在學習Python課程"); } }
調用一下
public static void main(String[] args) { //應用層爲高層 Tom tom = new Tom(); tom.studyJavaCourse(); tom.studyPythonCourse(); tom.studyAICourse(); }
Tom 熱愛學習,目前正在學習 Java 課程和 Python 課程。你們都知道,學習也是會上癮的。隨着學習興趣的暴漲,如今 Tom 還想學習 AI 人工智能的課程。這個時候,業務擴展,咱們的代碼要從底層到高層(調用層)一次修改代碼。在 Tom 類中增長 studyAICourse() 的方法,在高層也要追加調用。如此一來,系統發佈之後,其實是很是不穩定的,在修改代碼的同時也會帶來意想不到的風險
來咱們優化代碼,建立一個課程的抽象ICourse 接口:
public interface ICourse { void study(); }
而後寫 JavaCourse 類
public class JavaCourse implements ICourse{ public void study() { System.out.println("Tom正在學習Java課程"); } }
再實現 PythonCourse 類
public class PythonCourse implements ICourse{ public void study() { System.out.println("Tom正在學習Python課程"); } }
修改 Tom 類
public class Tom { public void study(ICourse iCourse){ iCourse.study(); } }
來調一下
public static void main(String[] args) { //應用層爲高層 Tom tom = new Tom(); //注入的方式實現依賴倒置 tom.study(new JavaCourse()); tom.study(new PythonCourse()); }
咱們這時候再看來代碼,Tom 的興趣不管怎麼暴漲,對於新的課程,我只須要新建一個類,經過傳參的方式告訴Tom,而不須要修改底層代碼。實際上這是一種你們很是熟悉的方式,叫依賴注入。
注入的方式還有構造器方式和setter 方式。咱們來看構造器注入方式:
//構造器注入 public class Tom { private ICourse iCourse; public Tom(ICourse iCourse) { this.iCourse = iCourse; } public void study(){ iCourse.study(); } } //調用 public static void main(String[] args) { Tom tom = new Tom(new JavaCourse()); tom.study(); }
根據構造器方式注入,在調用時,每次都要建立實例。那麼,若是Tom 是全局單例,則咱們就只能選擇用Setter 方式來注入,繼續修改Tom 類的代碼:
//set注入 public class Tom { private ICourse iCourse; public void setiCourse(ICourse iCourse) { this.iCourse = iCourse; } public void study(){ iCourse.study(); } } //調用 public static void main(String[] args) { Tom tom = new Tom(); tom.setiCourse(new JavaCourse()); tom.study(); }
類圖展現:
單一職責(Simple Responsibility Pinciple,SRP)是指不要存在多於一個致使類變動的緣由
一個類,接口,方法只負責一項職責
一、下降類的複雜度
二、下降變動引發的風險
三、提升類的可讀性
四、提升系統的可維護性
咱們的課程有直播課和錄播課。直播課不能快進和快退,錄播能夠能夠任意的反覆觀看,功能職責不同。
仍是先建立一個Course 類:
public class Course { public void study(String courseName){ if("直播課".equals(courseName)){ System.out.println("不能快進"); }else{ System.out.println("任意播放"); } } } //代碼調用 public static void main(String[] args) { Course course = new Course(); course.study("直播課"); course.study("錄播課"); }
從上面代碼來看,Course 類承擔了兩種處理邏輯。假如,如今要對課程進行加密,那麼直播課和錄播課的加密邏輯都不同,必需要修改代碼。而修改代碼邏輯勢必會相互影響容易形成不可控的風險。
咱們對職責進行分離解耦,來看代碼,分別建立兩個類 ReplayCourse 和 LiveCourse:
//直播 public class LiveCourse { public void study(String courseName){ System.out.println("不能快進"); } } //錄播 public class ReplayCourse { public void study(String courseName){ System.out.println("任意播放"); } } //調用代碼 public static void main(String[] args) { LiveCourse liveCourse = new LiveCourse(); liveCourse.study("直播課"); ReplayCourse replayCourse = new ReplayCourse(); replayCourse.study("錄播課"); }
業務繼續發展,課程要作權限。沒有付費的學員能夠獲取課程基本信息,已經付費的學員能夠得到視頻流,即學習權限。那麼對於控制課程層面上至少有兩個職責。咱們能夠把展現職責和管理職責分離開來,都實現同一個抽象依賴。
設計一個頂層接口,建立ICourse 接口
public interface ICourse { String getCourseName();//獲取基本信息 byte[] getCourseVideo();//獲取視頻流 void studyCourse();//學習課程 void refundCourse();//退款 }
咱們能夠把這個接口拆成兩個接口,建立一個接口 ICourseInfo 和 ICourseManager:
//信息 public interface ICourseInfo { String getCourseName(); byte[] getCourseVideo(); } //管理 public interface ICourseManager { void studyCourse(); void refundCourse(); }
來看下類圖:
下面咱們來看一下方法層面的單一職責設計。有時候,咱們爲了偷懶,一般會把一個方法寫成下面這樣
public void modifyUserInfo(String userName,String address){ userName = "Tom"; address = "Changsha"; } //或 private void modifyUserInfo(String userName,String ... fileds){ } //或 private void modifyUserInfo(String userName,String address,boolean bool){ if(bool){ }else{ } }
顯然上面的 modifyUserInfo() 方法中都承擔了多個職責,既能夠修改 userName,也能夠修改 address,甚至更多,明顯不符合單一職責。那麼咱們作以下修改,把這個方法拆成兩個:
private void modifyUserName(String userName){ } private void modifyAddress(String address){ }
這修改以後,開發起來簡單,維護起來也容易。可是,咱們在實際開發中會項目依賴,組合,聚合這些關係,還有還有項目的規模,週期,技術人員的水平,對進度的把控,不少類都不符合單一職責。可是,咱們在編寫代碼的過程,儘量地讓接口和方法保持單一職責,對咱們項目後期的維護是有很大幫助的
接口隔離原則(Interface Segregation Principle, ISP)是指用多個專門的接口,而不使用單一的總接口,客戶端不該該依賴它不須要的接口
一、一個類對一類的依賴應該創建在最小的接口之上。
二、創建單一接口,不要創建龐大臃腫的接口。
三、儘可能細化接口,接口中的方法儘可能少(不是越少越好,必定要適度)
符合咱們常說的高內聚低耦合的設計思想,從而使得類具備很好的可讀性、可擴展性和可維護性
//接口 public interface IAnimal { void eat(); void fly(); void swim(); } //鳥 public class Bird implements IAnimal{ public void eat() { } public void fly() { } public void swim() { } } //狗 public class Dog implements IAnimal{ public void eat() { } public void fly() { } public void swim() { } }
能夠看出,Bird 的swim()方法可能只能空着,Dog 的fly()方法顯然也是不可能的。這時候,咱們針對不一樣動物行爲來設計不一樣的接口,分別設計IEatAnimal,IFlyAnimal 和ISwimAnimal 接口
//接口 public interface IEatAnimal { void eat(); } public interface IFlyAnimal { void fly(); } public interface ISwimAnimal { void swim(); } //鳥 public class Bird implements IEatAnimal,IFlyAnimal{ public void eat() { } public void fly() { } } //狗 public class Dog implements IEatAnimal,ISwimAnimal{ public void eat() { } public void swim() { } }
來看下兩種類圖對比,仍是很是清晰明瞭的
迪米特原則(Law of Demeter LoD)是指一個對象應該對其餘對象保持最少的瞭解,又叫最少知道原則(Least Knowledge Principle,LKP),儘可能下降類與類之間的耦合
一、下降類之間的耦合
二、迪米特原則主要強調只和朋友交流,不和陌生人說話
三、出如今成員變量、方法的輸入、輸出參數中的類均可以稱之爲成員朋友類,而出如今方法體內部的類不屬於朋友類。
如今來設計一個權限系統,TeamLeader 須要查看目前發佈到線上的課程數量。這時候,TeamLeader要找到員工Employee 去進行統計,Employee 再把統計結果告訴TeamLeader
//課程 public class Course { } //員工 public class Employee { public void checkNumberOfCourse(List<Course> courseList){ System.out.println("目前發佈的課程數量爲:" + courseList.size()); } } //領導 public class TeamLeader { public void commandCheckNumber(Employee employee){ List<Course> courseList = new ArrayList<Course>(); for (int i = 0; i < 20; i++) { courseList.add(new Course()); } employee.checkNumberOfCourse(courseList); } } //測試 public static void main(String[] args) { TeamLeader teamLeader = new TeamLeader(); Employee employee = new Employee(); teamLeader.commandCheckNumber(employee); }
寫到這裏,其實功能已經都已經實現,代碼看上去也沒什麼問題。根據迪米特原則,TeamLeader 只想要結果,不須要跟Course 產生直接的交流。而Employee 統計須要引用Course 對象。TeamLeader 和 Course 並非朋友,從下面的類圖就能夠看出來:
下面來對代碼進行改造
//員工 public class Employee { public void checkNumberOfCourses(){ List<Course> courseList = new ArrayList<Course>(); for (int i= 0; i < 20 ;i ++){ courseList.add(new Course()); } System.out.println("目前已發佈的課程數量是:"+courseList.size()); } } //領導 public class TeamLeader { public void commandCheckNumber(Employee employee){ employee.checkNumberOfCourses(); } } }
再來看下面的類圖,Course 和TeamLeader 已經沒有關聯了
學習軟件設計原則,千萬不能造成強迫症。碰到業務複雜的場景,咱們須要隨機應變。
里氏替換原則(Liskov Substitution Principle,LSP)是指若是對每個類型爲 T1 的對象 o1,都有類型爲 T2 的對象 o2,使得以 T1 定義的全部程序 P 在全部的對象 o1 都替換成 o2 時,程序 P 的行爲沒有發生變化,那麼類型T2 是類型T1 的子類型
定義看上去仍是比較抽象,咱們從新理解一下,能夠理解爲一個軟件實體若是適用一個父類的話,那必定是適用於其子類,全部引用父類的地方必須能透明地使用其子類的對象,子類對象可以替換父類對象,而程序邏輯不變
引伸含義:子類能夠擴展父類的功能,但不能改變父類原有的功能。
一、子類能夠實現父類的抽象方法,但不能覆蓋父類的非抽象方法。
二、子類中能夠增長本身特有的方法。
三、當子類的方法重載父類的方法時,方法的前置條件(即方法的輸入/入參)要比父類方法的輸入參數更寬鬆。
四、當子類的方法實現父類的方法時(重寫/重載或實現抽象方法),方法的後置條件(即方法的輸出/返回值)要比父類更嚴格或相等。
在前面講開閉原則的時候埋下了一個伏筆,咱們記得在獲取折後時重寫覆蓋了父類的getPrice()方法,增長了一個獲取源碼的方法getOriginPrice(),顯然就違背了里氏替換原則。咱們修改一下代碼,不該該覆蓋getPrice()方法,增長getDiscountPrice()方法:
public class JavaDiscountCourse extends JavaCourse { public JavaDiscountCourse(Integer id, String name, Double price) { super(id, name, price); } public Double getDiscountPrice(){ return super.getPrice() * 0.61; } }
一、約束繼承氾濫,開閉原則的一種體現
二、增強程序的健壯性,同時變動時也能夠作到很是好的兼容性,提升程序的維護性、擴展性。下降需求變動時引入的風險。
如今來描述一個經典的業務場景,用正方形、矩形和四邊形的關係說明裏氏替換原則,咱們都知道正方形是一個特殊的長方形
//長方形 public class Rectangle { private long height; private long width; public long getHeight() { return height; } public void setHeight(long height) { this.height = height; } public long getWidth() { return width; } public void setWidth(long width) { this.width = width; } } //正方形繼承長方形 public class Square extends Rectangle { private long length; public long getLength() { return length; } public void setLength(long length) { this.length = length; } @Override public long getHeight() { return getLength(); } @Override public void setHeight(long height) { setLength(height); } @Override public long getWidth() { return getLength(); } @Override public void setWidth(long width) { setLength(width); } } //在測試類中建立resize()方法,根據邏輯長方形的寬應該大於等於高,咱們讓高一直自增,知道高等於寬變成正方形: public static void resize(Rectangle rectangle){ while (rectangle.getWidth() >= rectangle.getHeight()){ rectangle.setHeight(rectangle.getHeight() + 1); System.out.println("Width:" +rectangle.getWidth() +",Height:" + rectangle.getHeight()); } System.out.println("Resize End,Width:" +rectangle.getWidth() +",Height:" + rectangle.getHeight()); } //測試 public static void main(String[] args) { Rectangle rectangle = new Rectangle(); rectangle.setWidth(20); rectangle.setHeight(10); resize(rectangle); } /* 結果 width:20,height11 width:20,height12 width:20,height13 width:20,height14 width:20,height15 width:20,height16 width:20,height17 width:20,height18 width:20,height19 width:20,height20 width:20,height21 */
發現高比寬還大了,在長方形中是一種很是正常的狀況。如今咱們再來看下面的代碼,把長方形 Rectangle 替換成它的子類正方形Square,修改測試代碼:
public static void main(String[] args) { Square square = new Square(); square.setLength(10); resize(square); }
這時候咱們運行的時候就出現了死循環,違背了里氏替換原則,將父類替換爲子類後,程序運行結果沒有達到預期。所以,咱們的代碼設計是存在必定風險的。里氏替換原則只存在父類與子類之間,約束繼承氾濫。咱們再來建立一個基於長方形與正方形共同的抽象四邊形 Quadrangle 接口
public interface QuadRangle { long getWidth(); long getHeight(); } //修改長方形 public class Rectangle implements QuadRangle { private long height; private long width; public long getHeight() { return height; } public void setHeight(long height) { this.height = height; } public long getWidth() { return width; } public void setWidth(long width) { this.width = width; } } //修改正方形 public class Square implements QuadRangle { private long length; public long getLength() { return length; } public void setLength(long length) { this.length = length; } public long getWidth() { return length; } public long getHeight() { return length; } }
此時,若是咱們把resize()方法的參數換成四邊形Quadrangle 類,方法內部就會報錯。由於正方形 Square 已經沒有了 setWidth() 和setHeight() 方法了。所以,爲了約束繼承氾濫,resize() 的方法參數只能用 Rectangle 長方形。固然,咱們在後面的設計模式課程中還會繼續深刻講解。
合成複用原則(Composite/Aggregate Reuse Principle,CARP)是指儘可能使用對象 組合 (has-a) / 聚合 (contanis-a),而不是繼承關係達到軟件複用的目的。可使系統更加靈活,下降類與類之間的耦合度,一個類的變化對其餘類形成的影響相對較少
繼承咱們叫作白箱複用,至關於把全部的實現細節暴露給子類。組合/聚合也稱之爲黑箱複用,對類之外的對象是沒法獲取到實現細節的。要根據具體的業務場景來作代碼設計,其實也都須要遵循 OOP 模型
可使系統更加靈活,下降類與類之間的耦合,一個類的變化對其它類的影響相對較小
//以數據庫操做爲例,先來建立DBConnection 類 public class DBConnection { public String getConnection(){ return "MySQL 數據庫鏈接"; } } //Dao類 public class ProductDao{ private DBConnection dbConnection; public void setDbConnection(DBConnection dbConnection) { this.dbConnection = dbConnection; } public void addProduct(){ String conn = dbConnection.getConnection(); System.out.println("使用"+conn+"增長產品"); } }
這就是一種很是典型的合成複用原則應用場景。可是,目前的設計來講,DBConnection 還不是一種抽象,不便於系統擴展。目前的系統支持 MySQL 數據庫鏈接,假設業務發生變化,數據庫操做層要支持 Oracle 數據庫。固然,咱們能夠在 DBConnection 中增長對 Oracle 數據庫支持的方法。可是違背了開閉原則。其實咱們能夠沒必要修改 Dao 的代碼,將 DBConnection 修改成abstract
public abstract class DBConnection { public abstract String getConnection(); } //mysql public class MySQLConnection extends DBConnection { @Override public String getConnection() { return "MySQL 數據庫鏈接"; } } //oracle public class OracleConnection extends DBConnection { @Override public String getConnection() { return "Oracle 數據庫鏈接"; } }
設計原則總結
學習設計原則,學習設計模式的基礎。在實際開發 過程當中,並非必定要求全部代碼都遵循設計原則,咱們要考慮人力、時間、成本、質量,不是刻意追求完美,要在適當的場景遵循設計原則,體現的是一種平衡取捨,幫助咱們設計出更加優雅的代碼結構。