02. 原件設計七大原則

課程目標前端

一、經過對節課內容的學習,瞭解設計原則的重要性。java

二、掌握七大設計原則的具體內容。mysql

內容定位sql

學習設計原則,學習設計模式的基礎。在實際開發過程當中,並非必定要求全部代碼都遵循設計原則,咱們要考慮人力、時間、成本、質量,不是刻意追求完美,要在適當的場景遵循設計原則,體現的是一種平衡取捨,幫助咱們設計出更加優雅的代碼結構。數據庫

1、開閉原則

1. 定義

開閉原則(Open-Closed Principle, OCP)是指一個軟件實體如類、模塊和函數應該對擴展開放,對修改關閉。編程

2. 優勢

一、開閉原則,是面向對象設計中最基礎的設計原則。它指導咱們如何創建穩定靈活的系統,例如:咱們版本更新,我儘量不修改源代碼,可是能夠增長新功能設計模式

二、所謂的開閉,也正是對擴展和修改兩個行爲的一個原則。強調的是用抽象構建框架,用實現擴展細節。能夠提升軟件系統的可複用性及可維護性架構

三、實現開閉原則的核心思想就是面向抽象編程oracle

3. 代碼展現

首先建立一個課程接口框架

//課程接口
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());
}

類圖展現:

截圖

2、依賴倒置原則

1. 定義

依賴倒置原則(Dependence Inversion Principle,DIP)是指設計代碼結構時,高層模塊不該該依賴底層模塊,兩者都應該依賴其抽象。

2. 優勢

一、抽象不該該依賴細節,細節應該依賴抽象

二、針對接口編程,不要針對實現編程

三、能夠減小類與類之間的耦合性,提升系統的穩定性,提升代碼的可讀性和可維護性,並可以下降修改程序所形成的風險

3. 代碼展現

首先建立一個類 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();
}

類圖展現:

截圖

3、單一職責原則

1. 定義

單一職責(Simple Responsibility Pinciple,SRP)是指不要存在多於一個致使類變動的緣由

一個類,接口,方法只負責一項職責

2. 優勢

一、下降類的複雜度

二、下降變動引發的風險

三、提升類的可讀性

四、提升系統的可維護性

3. 代碼展現

咱們的課程有直播課和錄播課。直播課不能快進和快退,錄播能夠能夠任意的反覆觀看,功能職責不同。

仍是先建立一個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){
}

這修改以後,開發起來簡單,維護起來也容易。可是,咱們在實際開發中會項目依賴,組合,聚合這些關係,還有還有項目的規模,週期,技術人員的水平,對進度的把控,不少類都不符合單一職責。可是,咱們在編寫代碼的過程,儘量地讓接口和方法保持單一職責,對咱們項目後期的維護是有很大幫助的

4、接口隔離原則

1. 定義

接口隔離原則(Interface Segregation Principle, ISP)是指用多個專門的接口,而不使用單一的總接口,客戶端不該該依賴它不須要的接口

2. 注意

一、一個類對一類的依賴應該創建在最小的接口之上。
二、創建單一接口,不要創建龐大臃腫的接口。
三、儘可能細化接口,接口中的方法儘可能少(不是越少越好,必定要適度)

3. 優勢

符合咱們常說的高內聚低耦合的設計思想,從而使得類具備很好的可讀性、可擴展性和可維護性

4. 代碼展現

//接口
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() {
    }
}

來看下兩種類圖對比,仍是很是清晰明瞭的

截圖截圖

5、迪米特法則

1. 定義

迪米特原則(Law of Demeter LoD)是指一個對象應該對其餘對象保持最少的瞭解,又叫最少知道原則(Least Knowledge Principle,LKP),儘可能下降類與類之間的耦合

2. 優勢

一、下降類之間的耦合

二、迪米特原則主要強調只和朋友交流,不和陌生人說話

三、出如今成員變量、方法的輸入、輸出參數中的類均可以稱之爲成員朋友類,而出如今方法體內部的類不屬於朋友類。

代碼展現

如今來設計一個權限系統,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 已經沒有關聯了

截圖

學習軟件設計原則,千萬不能造成強迫症。碰到業務複雜的場景,咱們須要隨機應變。

6、里氏替換原則

1. 定義

里氏替換原則(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;
    }
}

2. 優勢

一、約束繼承氾濫,開閉原則的一種體現
二、增強程序的健壯性,同時變動時也能夠作到很是好的兼容性,提升程序的維護性、擴展性。下降需求變動時引入的風險。

3. 代碼展現

如今來描述一個經典的業務場景,用正方形、矩形和四邊形的關係說明裏氏替換原則,咱們都知道正方形是一個特殊的長方形

//長方形
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 長方形。固然,咱們在後面的設計模式課程中還會繼續深刻講解。

7、合成複用原則

1. 定義

合成複用原則(Composite/Aggregate Reuse Principle,CARP)是指儘可能使用對象 組合 (has-a) / 聚合 (contanis-a),而不是繼承關係達到軟件複用的目的。可使系統更加靈活,下降類與類之間的耦合度,一個類的變化對其餘類形成的影響相對較少

繼承咱們叫作白箱複用,至關於把全部的實現細節暴露給子類。組合/聚合也稱之爲黑箱複用,對類之外的對象是沒法獲取到實現細節的。要根據具體的業務場景來作代碼設計,其實也都須要遵循 OOP 模型

2. 優勢

可使系統更加靈活,下降類與類之間的耦合,一個類的變化對其它類的影響相對較小

3. 代碼展現

//以數據庫操做爲例,先來建立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 數據庫鏈接";
    }
}

截圖

設計原則總結

學習設計原則,學習設計模式的基礎。在實際開發 過程當中,並非必定要求全部代碼都遵循設計原則,咱們要考慮人力、時間、成本、質量,不是刻意追求完美,要在適當的場景遵循設計原則,體現的是一種平衡取捨,幫助咱們設計出更加優雅的代碼結構。

相關文章
相關標籤/搜索