我們在學習過程中可能並不會關心設計模式,但一旦牽扯到專案和麵試,設計模式就成了我們的短板
這篇文章並不會講到二十三種設計模式,但是會講解設計模式的設計原則以及設計依據和最明顯的圖形表示
或許我們只有先去了解設計模式的來源才能真正理解設計模式吧
我們該篇會提及到以下內容:
既然我們要講述設計模式原理,自然就需要先來了解設計模式了
其實我們軟體的很多概念往往來自於其他專業,設計模式就是其中一個:
我們的設計模式就是根據建築領域中設計模式的概念而產生的:
我們以官方角度來講述設計模式:
簡單來說,設計模式就是前輩們的經驗之談:
設計模式的本質是物件導向設計原則的實際運用,是對類的封裝性、繼承性和多型性以及類的關聯關係和組合關係的充分理解。
正確使用設計模式具有以下優點:
我們在未正式學習設計模式之前先去簡單瞭解一下設計模式的主要三種分類:
建立型模式
用於描述「怎樣建立物件」,它的主要特點是「將物件的建立與使用分離」。
書中提供了單例、原型、工廠方法、抽象工廠、建造者等 5 種建立型模式。
結構型模式
用於描述如何將類或物件按某種佈局組成更大的結構
書中提供了代理、介面卡、橋接、裝飾、外觀、享元、組合等 7 種結構型模式。
行為型模式
用於描述類或物件之間怎樣相互共同作業共同完成單個物件無法單獨完成的任務,以及怎樣分配職責。
書中提供了模板方法、策略、命令、職責鏈、狀態、觀察者、中介者、迭代器、存取者、備忘錄、直譯器等 11 種行為型模式。
統一建模語言(UML)是用來設計軟體的視覺化建模語言。它的特點是簡單、統一、圖形化、能表達軟體設計中的動態與靜態資訊。
UML 從目標系統的不同角度出發,定義了用例圖、類圖、物件圖、狀態圖、活動圖、時序圖、共同作業圖、構件圖、部署圖等 9 種圖。
我們在設計模式中最常用的無非只有類圖:
類圖主要具有以下兩種作用:
首先我們先來介紹UML中類的基本表示:
屬性/方法名稱前加的加號和減號表示了這個屬性/方法的可見性:
那麼我們就可以給出對應的屬性方法的完整表達方式:
我們給出一個簡單的範例圖:
我們可以對上述的類圖進行一個簡單的解析:
類關係表示法大致分為關聯關係,繼承關係,實現關係
首先我們先來介紹一下關聯關係:
在UML類圖中單向關聯用一個帶箭頭的實線表示。
上圖表示每個顧客都有一個地址,這通過讓Customer類持有一個型別為Address的成員變數類實現。
在UML類圖中,雙向關聯用一個不帶箭頭的直線表示。
從上圖中我們很容易看出,所謂的雙向關聯就是雙方各自持有對方型別的成員變數。上圖中在Customer類中維護一個List<Product>,表示一個顧客可以購買多個商品;在Product類中維護一個Customer型別的成員變數表示這個產品被哪個顧客所購買。
自關聯在UML類圖中用一個帶有箭頭且指向自身的線表示。
上圖的意思就是Node類包含型別為Node的成員變數,也就是「自己包含自己」。
在 UML 類圖中,聚合關係可以用帶空心菱形的實線來表示,菱形指向整體。
聚合關係是關聯關係的一種,是強關聯關係,是整體和部分之間的關係。
聚合關係也是通過成員物件來實現的,其中成員物件是整體物件的一部分,但是成員物件可以脫離整體物件而獨立存在。例如,學校與老師的關係,學校包含老師,但如果學校停辦了,老師依然存在。
在 UML 類圖中,組合關係用帶實心菱形的實線來表示,菱形指向整體。
組合表示類之間的整體與部分的關係,但它是一種更強烈的聚合關係。
在組合關係中,整體物件可以控制部分物件的生命週期,一旦整體物件不存在,部分物件也將不存在,部分物件不能脫離整體物件而存在。例如,頭和嘴的關係,沒有了頭,嘴也就不存在了。
在 UML 類圖中,依賴關係使用帶箭頭的虛線來表示,箭頭從使用類指向被依賴的類。
依賴關係是一種使用關係,它是物件之間耦合度最弱的一種關聯方式,是臨時性的關聯。在程式碼中,某個類的方法通過區域性變數、方法的引數或者對靜態方法的呼叫來存取另一個類(被依賴類)中的某些方法來完成一些職責。
繼承關係是物件之間耦合度最大的一種關係,表示一般與特殊的關係,是父類別與子類之間的關係,是一種繼承關係。
在 UML 類圖中,泛化關係用帶空心三角箭頭的實線來表示,箭頭從子類指向父類別。在程式碼實現時,使用物件導向的繼承機制來實現泛化關係。例如,Student 類和 Teacher 類都是 Person 類的子類,其類圖如下圖所示:
實現關係是介面與實現類之間的關係。在這種關係中,類實現了介面,類中的操作實現了介面中所宣告的所有的抽象操作。
在 UML 類圖中,實現關係使用帶空心三角箭頭的虛線來表示,箭頭從實現類指向介面。例如,汽車和船實現了交通工具,其類圖如圖 9 所示。
在軟體開發中,為了提高軟體系統的可維護性和可複用性,增加軟體的可延伸性和靈活性,程式設計師要儘量根據6條原則來開發程式,從而提高軟體開發效率、節約軟體開發成本和維護成本,同時設計模式也是根據這些原則所產生的。
首先我們給出官方解釋:
那麼從我們的視角來看是怎樣的:
我們來給出一個簡單的範例:
我們對上圖進行解釋並講解:
/* 案例介紹 */
【例】`搜狗輸入法` 的面板設計。
分析:
`搜狗輸入法` 的面板是輸入法背景圖片、視窗顏色和聲音等元素的組合。
使用者可以根據自己的喜愛更換自己的輸入法的面板,也可以從網上下載新的面板。
這些面板有共同的特點,可以為其定義一個抽象類(AbstractSkin),而每個具體的面板是其子類。
使用者表單可以根據需要選擇或者增加新的主題,而不需要修改原始碼,所以它是滿足開閉原則的。
/* 程式碼展示 */
/**
* @version v1.0
* @ClassName: Client
* @Description: 測試程式碼
*/
public class Client {
public static void main(String[] args) {
//1,建立搜狗輸入法物件
SougouInput input = new SougouInput();
//2,建立面板物件
//DefaultSkin skin = new DefaultSkin();
HeimaSkin skin = new HeimaSkin();
//3,將面板設定到輸入法中
input.setSkin(skin);
//4,顯示面板
input.display();
}
}
/**
* @version v1.0
* @ClassName: SougouInput
* @Description: 搜狗輸入法
*/
public class SougouInput {
private AbstractSkin skin;
public void setSkin(AbstractSkin skin) {
this.skin = skin;
}
public void display() {
skin.display();
}
}
/**
* @version v1.0
* @ClassName: AbstractSkin
* @Description: 抽象面板類
*/
public abstract class AbstractSkin {
//顯示的方法
public abstract void display();
}
/**
* @version v1.0
* @ClassName: DefaultSkin
* @Description: 預設面板類
*/
public class DefaultSkin extends AbstractSkin {
public void display() {
System.out.println("預設面板");
}
}
/**
* @version v1.0
* @ClassName: HeimaSkin
* @Description: 黑馬程式設計師面板
*/
public class HeimaSkin extends AbstractSkin {
public void display() {
System.out.println("黑馬面板");
}
}
首先我們給出官方解釋:
從我們的視角來解釋:
我們給出一個對比案例來展示里氏代換原則:
我們對問題和上圖進行解釋:
/* 問題展示 */
【例】正方形不是長方形。
在數學領域裡,正方形毫無疑問是長方形,它是一個長寬相等的長方形。
所以,我們開發的一個與幾何圖形相關的軟體系統,就可以順理成章的讓正方形繼承自長方形。
/* 程式碼展示 */
// 但我們可以作用在長方形的方法,如果作用在正方形上卻不可以,這就說明正方形不能繼承長方形
/**
* @version v1.0
* @ClassName: RectangleDemo
* @Description: 測試方法,這裡的resize方法只能作用在長方形,卻不能作用於正方形,但正方形繼承於長方形導致思維錯誤
*/
public class RectangleDemo {
public static void main(String[] args) {
//建立長方形物件
Rectangle r = new Rectangle();
//設定長和寬
r.setLength(20);
r.setWidth(10);
//呼叫resize方法進行擴寬
resize(r);
printLengthAndWidth(r);
System.out.println("==================");
//建立正方形物件
Square s = new Square();
//設定長和寬
s.setLength(10);
//呼叫resize方法進行擴寬
resize(s);
printLengthAndWidth(s);
}
//擴寬方法
public static void resize(Rectangle rectangle) {
//判斷寬如果比長小,進行擴寬的操作
while(rectangle.getWidth() <= rectangle.getLength()) {
rectangle.setWidth(rectangle.getWidth() + 1);
}
}
//列印長和寬
public static void printLengthAndWidth(Rectangle rectangle) {
System.out.println(rectangle.getLength());
System.out.println(rectangle.getWidth());
}
}
/**
* @version v1.0
* @ClassName: Rectangle
* @Description: 長方形類
*/
public class Rectangle {
private double length;
private double width;
public double getLength() {
return length;
}
public void setLength(double length) {
this.length = length;
}
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
}
package com.itheima.principles.demo2.before;
/**
* @version v1.0
* @ClassName: Square
* @Description: 正方形類
*/
public class Square extends Rectangle {
@Override
public void setLength(double length) {
super.setLength(length);
super.setWidth(length);
}
@Override
public void setWidth(double width) {
super.setLength(width);
super.setWidth(width);
}
}
我們對問題和上圖進行解釋:
/* 問題解釋 */
抽象出來一個四邊形介面(Quadrilateral),讓Rectangle類和Square類實現Quadrilateral介面
/* 程式碼展示 */
/**
* @version v1.0
* @ClassName: RectangleDemo
* @Description: 測試類,這裡測試僅對長方形類測試,而不對正方形測試
*/
public class RectangleDemo {
public static void main(String[] args) {
//建立長方形物件
Rectangle r = new Rectangle();
r.setLength(20);
r.setWidth(10);
//呼叫方法進行擴寬操作
resize(r);
printLengthAndWidth(r);
}
//擴寬的方法
public static void resize(Rectangle rectangle) {
//判斷寬如果比長小,進行擴寬的操作
while(rectangle.getWidth() <= rectangle.getLength()) {
rectangle.setWidth(rectangle.getWidth() + 1);
}
}
//列印長和寬
public static void printLengthAndWidth(Quadrilateral quadrilateral) {
System.out.println(quadrilateral.getLength());
System.out.println(quadrilateral.getWidth());
}
}
/**
* @version v1.0
* @ClassName: Quadrilateral
* @Description: 四邊形介面
*/
public interface Quadrilateral {
//獲取長
double getLength();
//獲取寬
double getWidth();
}
/**
* @version v1.0
* @ClassName: Rectangle
* @Description: 長方形類
*/
public class Rectangle implements Quadrilateral {
private double length;
private double width;
public void setLength(double length) {
this.length = length;
}
public void setWidth(double width) {
this.width = width;
}
public double getLength() {
return length;
}
public double getWidth() {
return width;
}
}
/**
* @version v1.0
* @ClassName: Square
* @Description: 正方形
*/
public class Square implements Quadrilateral {
private double side;
public double getSide() {
return side;
}
public void setSide(double side) {
this.side = side;
}
public double getLength() {
return side;
}
public double getWidth() {
return side;
}
}
我們首先用官方話語解釋:
從我們的視角解釋就是:
下面看一個例子來理解依賴倒轉原則:
我們對問題和上圖進行解釋:
/* 問題解釋 */
【例】組裝電腦
現要組裝一臺電腦,需要配件cpu,硬碟,記憶體條。只有這些設定都有了,計算機才能正常的執行。選擇cpu有很多選擇,如Intel,AMD等,硬碟可以選擇希捷,西數等,記憶體條可以選擇金士頓,海盜船等。
/* 程式碼展示 */
// 如果我們直接在Computer中設定對應的cpu,硬碟,記憶體條的實現類也就是特定的型號,那麼我們這臺電腦就只能使用這些型號
/**
* @version v1.0
* @ClassName: ComputerDemo
* @Description: 測試類
*/
public class ComputerDemo {
public static void main(String[] args) {
//建立元件物件
XiJieHardDisk hardDisk = new XiJieHardDisk();
IntelCpu cpu = new IntelCpu();
KingstonMemory memory = new KingstonMemory();
//建立計算機物件
Computer c = new Computer();
//組裝計算機
c.setCpu(cpu);
c.setHardDisk(hardDisk);
c.setMemory(memory);
//執行計算機
c.run();
}
}
/**
* @version v1.0
* @ClassName: Computer
* @Description: 電腦
*/
public class Computer {
// 注意:這裡的屬性直接使用了實現類,也就是我們無法更換硬體型號
private XiJieHardDisk hardDisk;
private IntelCpu cpu;
private KingstonMemory memory;
public XiJieHardDisk getHardDisk() {
return hardDisk;
}
public void setHardDisk(XiJieHardDisk hardDisk) {
this.hardDisk = hardDisk;
}
public IntelCpu getCpu() {
return cpu;
}
public void setCpu(IntelCpu cpu) {
this.cpu = cpu;
}
public KingstonMemory getMemory() {
return memory;
}
public void setMemory(KingstonMemory memory) {
this.memory = memory;
}
public void run() {
System.out.println("執行計算機");
String data = hardDisk.get();
System.out.println("從硬碟上獲取的資料是:" + data);
cpu.run();
memory.save();
}
}
/**
* @version v1.0
* @ClassName: XiJieHardDisk
* @Description: 希捷硬碟
*/
public class XiJieHardDisk {
//儲存資料的方法
public void save(String data) {
System.out.println("使用希捷硬碟儲存資料為:" + data);
}
//獲取資料的方法
public String get() {
System.out.println("使用希捷希捷硬碟取資料");
return "資料";
}
}
/**
* @version v1.0
* @ClassName: IntelCpu
* @Description: Intel cpu
*/
public class IntelCpu {
public void run() {
System.out.println("使用Intel處理器");
}
}
/**
* @version v1.0
* @ClassName: KingstonMemory
* @Description: 金士頓記憶體條類
*/
public class KingstonMemory {
public void save() {
System.out.println("使用金士頓記憶體條");
}
}
我們對問題和上圖進行解釋:
/* 問題解釋 */
目前我們的電腦屬性連線的是介面,而該介面可以去實現多型號的硬體,說明我們的電腦可以採用多種設定方式
/* 程式碼展示 */
/**
* @version v1.0
* @ClassName: ComputerDemo
* @Description: 測試類,為電腦裝配不同的設定
*/
public class ComputerDemo {
public static void main(String[] args) {
//建立計算機的元件物件
HardDisk hardDisk = new XiJieHardDisk();
Cpu cpu = new IntelCpu();
Memory memory = new KingstonMemory();
//建立計算機物件
Computer c = new Computer();
//組裝計算機
c.setCpu(cpu);
c.setHardDisk(hardDisk);
c.setMemory(memory);
//執行計算機
c.run();
}
}
/**
* @version v2.0
* @ClassName: Computer
* @Description: 電腦
*/
public class Computer {
// 注意這裡採用的是介面,我們具體的介面實現可以依賴於不同的實現類
private HardDisk hardDisk;
private Cpu cpu;
private Memory memory;
public HardDisk getHardDisk() {
return hardDisk;
}
public void setHardDisk(HardDisk hardDisk) {
this.hardDisk = hardDisk;
}
public Cpu getCpu() {
return cpu;
}
public void setCpu(Cpu cpu) {
this.cpu = cpu;
}
public Memory getMemory() {
return memory;
}
public void setMemory(Memory memory) {
this.memory = memory;
}
public void run() {
System.out.println("計算機工作");
}
}
// 後續就是硬體的介面和實現類,這裡就不贅述了
首先我們給出官方解釋:
從我們的角度來解釋:
我們同樣給出案例:
我們針對上圖和問題進行解釋:
/* 問題解釋 */
【例】安全門案例
我們需要建立一個`黑馬`品牌的安全門,該安全門具有防火、防水、防盜的功能。可以將防火,防水,防盜功能提取成一個介面,形成一套規範。
現在如果我們還需要再建立一個傳智品牌的安全門,而該安全門只具有防盜、防水功能,很顯然如果實現SafetyDoor介面就違背了介面隔離原則
/* 程式碼展示 */
/**
* @version v1.0
* @ClassName: Client
* @Description: 測試
*/
public class Client {
public static void main(String[] args) {
HeimaSafetyDoor door = new HeimaSafetyDoor();
door.antiTheft();
door.fireProof();
door.waterProof();
}
}
/**
* @version v1.0
* @ClassName: SafetyDoor
* @Description: 多功能介面
*/
public interface SafetyDoor {
//防盜
void antiTheft();
//防火
void fireProof();
//防水
void waterProof();
}
/**
* @version v1.0
* @ClassName: HeimaSafetyDoor
* @Description: 黑馬品牌的安全門
*/
public class HeimaSafetyDoor implements SafetyDoor {
public void antiTheft() {
System.out.println("防盜");
}
public void fireProof() {
System.out.println("防火");
}
public void waterProof() {
System.out.println("防水");
}
}
我們針對上圖和問題進行解釋:
/* 問題解釋 */
現在我們將介面的功能拆分為多介面,我們的類想要使用單個介面就可以直接繼承單個介面
/* 程式碼展示 */
/**
* @version v1.0
* @ClassName: Client
* @Description: TODO(一句話描述該類的功能)
*/
public class Client {
public static void main(String[] args) {
//建立黑馬安全門物件
HeimaSafetyDoor door = new HeimaSafetyDoor();
//呼叫功能
door.antiTheft();
door.fireProof();
door.waterProof();
System.out.println("============");
//建立傳智安全門物件
ItcastSafetyDoor door1 = new ItcastSafetyDoor();
//呼叫功能
door1.antiTheft();
door1.fireproof();
}
}
/**
* @version v1.0
* @ClassName: HeiMaSafetyDoor
* @Description: TODO(一句話描述該類的功能)
*/
public class HeiMaSafetyDoor implements AntiTheft,Fireproof,Waterproof {
public void antiTheft() {
System.out.println("防盜");
}
public void fireproof() {
System.out.println("防火");
}
public void waterproof() {
System.out.println("防水");
}
}
/**
* @version v1.0
* @ClassName: ItcastSafetyDoor
* @Description: 傳智安全門
*/
public class ItcastSafetyDoor implements AntiTheft,Fireproof {
public void antiTheft() {
System.out.println("防盜");
}
public void fireproof() {
System.out.println("防火");
}
}
public interface AntiTheft {
void antiTheft();
}
public interface Fireproof {
void fireproof();
}
public interface Waterproof {
void waterproof();
}
首先我們給出官方解釋:
從我們的視角來解釋:
我們給出一個簡單的案例進行解釋:
我們對上述問題和圖進行解釋:
/* 問題展示 */
【例】明星與經紀人的關係範例
明星由於全身心投入藝術,所以許多日常事務由經紀人負責處理,如和粉絲的見面會,和媒體公司的業務洽淡等。這裡的經紀人是明星的朋友,而粉絲和媒體公司是陌生人,所以適合使用迪米特法則。
/* 程式碼展示 */
/**
* @version v1.0
* @ClassName: Client
* @Description: 測試類
*/
public class Client {
public static void main(String[] args) {
//建立經紀人類
Agent agent = new Agent();
//建立明星物件
Star star = new Star("林青霞");
agent.setStar(star);
//建立粉絲物件
Fans fans = new Fans("李四");
agent.setFans(fans);
//建立媒體公司物件
Company company = new Company("黑馬媒體公司");
agent.setCompany(company);
agent.meeting();//和粉絲見面
agent.business();//和媒體公司洽談業務
}
}
/**
* @version v1.0
* @ClassName: Agent
* @Description: 經紀人類
*/
public class Agent {
private Star star;
private Fans fans;
private Company company;
public void setStar(Star star) {
this.star = star;
}
public void setFans(Fans fans) {
this.fans = fans;
}
public void setCompany(Company company) {
this.company = company;
}
//和粉絲見面的方法
public void meeting() {
System.out.println(star.getName() + "和粉絲" + fans.getName() + "見面");
}
//和媒體公司洽談的方法
public void business() {
System.out.println(star.getName() + "和" + company.getName() + "洽談");
}
}
/**
* @version v1.0
* @ClassName: Company
* @Description: 媒體公司類
*/
public class Company {
private String name;
public String getName() {
return name;
}
public Company(String name) {
this.name = name;
}
}
/**
* @version v1.0
* @ClassName: Fans
* @Description: 粉絲類
*/
public class Fans {
private String name;
public String getName() {
return name;
}
public Fans(String name) {
this.name = name;
}
}
/**
* @version v1.0
* @ClassName: Star
* @Description: 明星類
*/
public class Star {
private String name;
public Star(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
我們給出官方解釋:
通常類的複用分為繼承複用和合成複用兩種。
繼承複用雖然有簡單和易實現的優點,但它也存在以下缺點:
繼承複用破壞了類的封裝性。因為繼承會將父類別的實現細節暴露給子類,父類別對子類是透明的,所以這種複用又稱為「白箱」複用。
子類與父類別的耦合度高。父類別的實現的任何改變都會導致子類的實現發生變化,這不利於類的擴充套件與維護。
它限制了複用的靈活性。從父類別繼承而來的實現是靜態的,在編譯時已經定義,所以在執行時不可能發生變化。
採用組合或聚合複用時,可以將已有物件納入新物件中,使之成為新物件的一部分,新物件可以呼叫已有物件的功能,它有以下優點:
它維持了類的封裝性。因為成分物件的內部細節是新物件看不見的,所以這種複用又稱為「黑箱」複用。
物件間的耦合度低。可以在類的成員位置宣告抽象。
複用的靈活性高。這種複用可以在執行時動態進行,新物件可以動態地參照與成分物件型別相同的物件。
最後我們給出兩張圖來介紹為什麼組合優於繼承:
我們進行簡單的解釋:
/* 問題解釋 */
【例】汽車分類管理程式
汽車按「動力源」劃分可分為汽油汽車、電動汽車等;按「顏色」劃分可分為白色汽車、黑色汽車和紅色汽車等。如果同時考慮這兩種分類,其組合就很多。
/* 圖形介紹 */
繼承圖:我們會發現我們每多一種屬性,就需要建立多個子類,類的建立是非常耗費資源的,上述多類的建立也是資源耗費的一種
組合圖:我們將部分屬性以屬性的形式介入,並採用介面存放,後續我們只需要更新其介面就可以更新多種實體類,節省資源的使用
關於設計模式原理我們就介紹到這裡,後面我會繼續更新二十三種設計模式,希望能給你帶來幫助~
該文章屬於學習內容,具體參考B站黑馬程式設計師的Java設計模式詳解
這裡附上視訊連結:黑馬程式設計師Java設計模式詳解, 23種Java設計模式(圖解+框架原始碼分析+實戰)_嗶哩嗶哩_bilibili