Java設計模式
Java設計模式入門
先看幾個經典的面試題
原型設計模式問題
- 請使用UML類圖畫出原型模式核心角色。
- 原型設計模式的深拷貝和淺拷貝是什麼,並寫出深拷貝的兩種方式的源碼【重寫clone方法實現深拷貝、使用序列化來實現深拷貝】。
- 在Spring框架中哪裏使用到原型模式,並對源碼進行分析。
beans.xml 文件中
<bean id=“id01” class=“com.atguigu.spring.bean.Monster” scope=“prototype”/>
- Spring中原型bean的創建,就是原型模式的應用。
- 代碼分析+Debug源碼
設計模式的七大原則
- 要求:① 七大設計原則核心思想;② 能夠以類圖的說明設計原則;③ 在項目實際開發中,你在哪裏使用到了ocp原則。
- 設計模式常用的七大原則:單一職責原則、接口隔離原則、依賴倒轉原則、里氏替換原則、開閉原則 ocp、迪米特法則、合成複用原則。
金融借貸平臺項目
- 借貸平臺的訂單,有審覈-發佈-搶單 等等 步驟,隨着操作的不同,會改變訂單的狀態, 項目中的這個模塊實現就會使用到狀態模式,請你使用狀態模式進行設計,並完成實際代碼。
- 問題分析:這類代碼難以應對變化,在添加一種狀態時,我們需要手動添加if/else,在添加一種功能時, 要對所有的狀態進行判斷。因此代碼會變得越 來越臃腫,並且一旦沒有處理某個狀態,便會 發生極其嚴重的BUG,難以維護。
解釋器設計模式
- 介紹解釋器設計模式是什麼?
- 畫出解釋器設計模式的UML類圖,分析設計模式中的各個角色是什麼?
- 請說明Spring的框架中,哪裏使用到了解釋器設計模式,並做源碼級別的分析。
單例設計模式
- 單例設計模式一共有幾種實現方式?請分別用代碼實現,並說明各個實現方式的優點和缺點?
- 單例設計模式一共有8種寫法:餓漢式兩種、懶漢式三種、雙重檢查、靜態內部類、枚舉。
設計模式的重要性
- 軟件工程中,設計模式(design pattern)是對軟件設計中普遍存在(反覆出現)的各種問題,所提出的解決方案。這個術語是由埃裏希·伽瑪(Erich Gamma)等人在1990年代從建築設計領域引入到計算機科學的。
- 大廈 VS 簡易房:
- 拿實際工作經歷來說,當一個項目開發完後,如果客戶提出增新功能,怎麼辦?
- 如果項目開發完後,原來程序員離職,你接手維護該項目怎麼辦? (維護性[可讀性、規範性])。
- 目前程序員門檻越來越高,一線IT公司(大廠),都會問你在實際項目中使用過什麼設計模式,怎樣使用的,解決了什麼問題。
- 設計模式在軟件中哪裏?面向對象(oo)=>功能模塊[設計模式+算法(數據結構)]=>框架[使用到多種設計模式]=>架構 [服務器集羣]
- 如果想成爲合格軟件工程師,那就花時間來研究下設計模式是非常必要的。
設計模式七大原則
設計模式的目的
編寫軟件過程中,程序員面臨着來自 耦合性、內聚性以及可維護性、可擴展性、重用性、靈活性 等多方面的挑戰,設計模式是爲了讓程序(軟件),具有更好的:
- 代碼重用性:相同功能的代碼,不用多次編寫
- 可讀性:編程規範性,便於其他程序員的閱讀和理解
- 可擴展性:當需要增加新的功能時,非常的方便,稱爲可維護
- 可靠性:當我們增加新的功能後,對原來的功能沒有影響
- 使程序呈現高內聚,低耦合的特性
設計模式七大原則
設計模式原則,其實就是程序員在編程時,應當遵守的原則,也是各種設計模式的基礎(即:設計模式爲什麼這樣設計的依據)
- 單一職責原則
- 接口隔離原則
- 依賴倒轉(倒置)原則
- 里氏替換原則
- 開閉原則
- 迪米特法則
- 合成複用原則
單一職責原則
基本介紹
對類來說的,即一個類應該只負責一項職責。如類A負責兩個不同職責:職責1、職責2。當職責1需求變更而改變A時,可能造成職責2執行錯誤,所以需要將類A的粒度分解爲A1,A2。
應用實例
/**
* 單一職責原則
*
* @author yangwei
* @date 2020-06-25 21:42
*/
public class SingleResponsibilityCase {
/**
* 方式一:違反了單一職責原則
* 解決方案:根據交通工具的不同,分解成不同的類
*/
static class VehicleUtil {
static void run(String vehicle) {
System.out.println(vehicle + " 在公路上跑...");
}
}
/**
* 方式二:遵循單一職責原則,即將類分解,同時修改客戶端
*/
static class RoadVehicleUtil {
static void run(String vehicle) {
System.out.println(vehicle + " 在公路上跑...");
}
}
static class AirVehicleUtil {
static void run(String vehicle) {
System.out.println(vehicle + " 在天空中飛行...");
}
}
/**
* 方式三:沒有對原有的類做大的修改,在類上沒有遵循單一職責原則,但是在方法上遵循單一職責原則
*/
static class VehicleUtil2 {
static void run(String vehicle) {
System.out.println(vehicle + " 在公路上跑...");
}
static void runAir(String vehicle) {
System.out.println(vehicle + " 在天空中飛行...");
}
static void runWater(String vehicle) {
System.out.println(vehicle + " 在水中運行...");
}
}
public static void main(String[] args) {
System.out.println("方式一:");
VehicleUtil.run("汽車");
VehicleUtil.run("摩托車");
VehicleUtil.run("飛機");
System.out.println("方式二:");
RoadVehicleUtil.run("汽車");
RoadVehicleUtil.run("摩托車");
AirVehicleUtil.run("飛機");
System.out.println("方式三:");
VehicleUtil2.run("汽車");
VehicleUtil2.runWater("輪船");
VehicleUtil2.runAir("飛機");
}
}
單一職責原則注意事項和細節
- 降低類的複雜度,一個類只負責一項職責。
- 提高類的可讀性,可維護性。
- 降低變更引起的風險。
- 通常情況下,我們應當遵守單一職責原則,只有邏輯足夠簡單,纔可以在代碼級違反單一職責原則;只有類中方法數量足夠少,可以在方法級別保持單一職責原則。
接口隔離原則
基本介紹
- 客戶端不應該依賴它不需要的接口,即一個類對另一個類的依賴應該建立在最小的接口上,看下圖:
- 類A通過接口Interface1依賴類B,類C通過 接口Interface1依賴類D,如果接口 Interface1對於類A和類C來說不是最小接口,那麼類B和類D必須去實現他們不需要的方 法。
- 按隔離原則應當這樣處理:將接口Interface1拆分爲獨立的幾個接口, 類A和類C分別與他們需要的接口建立依賴關係,也就是採用接口隔離原則。
應用實例
public class InterfaceSegregationCase {
interface Interface1 {
void operation1();
}
interface Interface2 {
void operation2();
void operation3();
}
interface Interface3 {
void operation4();
void operation5();
}
static class B implements Interface1, Interface2 {
public void operation1() {
System.out.println("B 實現了 operation1");
}
public void operation2() {
System.out.println("B 實現了 operation2");
}
public void operation3() {
System.out.println("B 實現了 operation3");
}
}
static class D implements Interface1, Interface3 {
public void operation1() {
System.out.println("D 實現了 operation1");
}
public void operation4() {
System.out.println("D 實現了 operation4");
}
public void operation5() {
System.out.println("D 實現了 operation5");
}
}
static class A {
void depend1(Interface1 i) {
i.operation1();
}
void depend2(Interface2 i) {
i.operation2();
}
void depend3(Interface2 i) {
i.operation3();
}
}
static class C {
void depend1(Interface1 i) {
i.operation1();
}
void depend4(Interface3 i) {
i.operation4();
}
void depend5(Interface3 i) {
i.operation5();
}
}
public static void main(String[] args) {
A a = new A();
// A 類通過接口去依賴 B 類
a.depend1(new B());
a.depend2(new B());
a.depend3(new B());
C c = new C();
// C 類通過接口去依賴 D 類
c.depend1(new D());
c.depend4(new D());
c.depend5(new D());
}
}
應傳統方法的問題和使用接口隔離原則改進
依賴倒轉原則
基本介紹
- 高層模塊不應該依賴低層模塊,二者都應該依賴其抽象。
- 抽象不應該依賴細節,細節應該依賴抽象。
- 依賴倒轉(倒置)的中心思想是面向接口編程。
- 依賴倒轉原則是基於這樣的設計理念:相對於細節的多變性,抽象的東西要穩定的 多。以抽象爲基礎搭建的架構比以細節爲基礎的架構要穩定的多。在java中,抽象 指的是接口或抽象類,細節就是具體的實現類。
- 使用接口或抽象類的目的是制定好規範,而不涉及任何具體的操作,把展現細節的 任務交給他們的實現類去完成。
應用實例
public class DependenceInversionCase {
//region -- 方式一
// /**
// * 方式一
// * 優點:簡單
// * 缺點:如果我們通過 微信、短信 等獲取,則需要新增類,同時Person類也需要增加相應的接收方法
// */
// static class Email {
// String getInfo() {
// return "電子郵件信息:Hello World";
// }
// }
// static class Person {
// void receive(Email email) {
// System.out.println(email.getInfo());
// }
// }
//endregion
//region
/**
* 方式二
* 引入一個抽象的接口 IReceiver, 表示接收者, 這樣 Person 類與接口 IReceiver 發生依賴
* 因爲 Email, WeiXin 等等屬於接收的範圍,他們各自實現 IReceiver 接口就可以了,這樣我們就符號依賴倒轉原則
*/
interface IReceiver {
String getInfo();
}
static class Email implements IReceiver {
public String getInfo() {
return "電子郵件信息:Hello World";
}
}
static class WeChat implements IReceiver {
public String getInfo() {
return "微信信息:來紅包啦!";
}
}
static class Person {
// 這裏是對接口的依賴
void receive(IReceiver receiver) {
System.out.println(receiver.getInfo());
}
}
//endregion
public static void main(String[] args) {
Person person = new Person();
person.receive(new Email());
person.receive(new WeChat());
}
}
依賴關係傳遞的三種方式
- 接口傳遞
- 構造方法傳遞
- setter方式傳遞
public class DependencyPass {
public static void main(String[] args) {
// // 通過接口傳遞實現依賴
// OpenAndClose openAndClose = new OpenAndClose();
// openAndClose.open(new ChangHong());
// // 通過構造器進行依賴傳遞
// OpenAndClose2 openAndClose = new OpenAndClose2(new ChangHong());
// openAndClose.open();
// 通過 setter 方法進行依賴傳遞
OpenAndClose3 openAndClose = new OpenAndClose3();
openAndClose.setTv(new ChangHong());
openAndClose.open();
}
}
/**
* 電視接口
*/
interface ITV {
void play();
}
/**
* 開關接口
*/
interface IOpenAndClose {
void open(ITV tv);
}
/**
* 方式 1: 通過接口傳遞實現依賴
*/
class ChangHong implements ITV {
public void play() {
System.out.println("長虹電視機,打開");
}
}
class OpenAndClose implements IOpenAndClose {
public void open(ITV tv) {
tv.play();
}
}
/**
* 方式 2: 通過構造方法依賴傳遞
*/
interface IOpenAndClose2 {
void open();
}
class OpenAndClose2 implements IOpenAndClose2 {
private ITV tv;
public OpenAndClose2(ITV tv) {
this.tv = tv;
}
public void open() {
this.tv.play();
}
}
/**
* 方式 3: 通過 setter 方法傳遞
*/
interface IOpenAndClose3 {
void open();
void setTv(ITV tv);
}
class OpenAndClose3 implements IOpenAndClose3 {
private ITV tv;
public void open() {
this.tv.play();
}
public void setTv(ITV tv) {
this.tv = tv;
}
}
依賴倒轉原則的注意事項和細節
- 低層模塊儘量都要有抽象類或接口,或者兩者都有,程序穩定性更好。
- 變量的聲明類型儘量是抽象類或接口, 這樣我們的變量引用和實際對象間,就存在一個緩衝層,利於程序擴展和優化。
- 繼承時遵循里氏替換原則
里氏替換原則
OO中的繼承性的思考和說明
- 繼承包含這樣一層含義:父類中凡是已經實現好的方法,實際上是在設定規範和契約,雖然它不強制要求所有的子類必須遵循這些契約,但是如果子類對這些已經實現的方法任意修改,就會對整個繼承體系造成破壞。
- 繼承在給程序設計帶來便利的同時,也帶來了弊端。比如使用繼承會給程序帶來侵入性,程序的可移植性降低,增加對象間的耦合性,如果一個類被其他的類所繼承,則當這個類需要修改時,必須考慮到所有的子類,並且父類修改後,所有涉及到子類的功能都有可能產生故障。
- 問題提出:在編程中,如何正確的使用繼承? => 里氏替換原則
基本介紹
- 里氏替換原則(Liskov Substitution Principle)在1988年,由麻省理工學院的以爲姓裏的女士提出的。
- 如果對每個類型爲T1的對象o1,都有類型爲T2的對象o2,使得以T1定義的所有程序在所有的對象o1都代換成o2時,程序P的行爲沒有發生變化,那麼類型T2是類型T1的子類型。換句話說,所有引用基類的地方必須能透明地使用其子類的對象。
- 在使用繼承時,遵循里氏替換原則,在子類中儘量不要重寫父類的方法。
- 里氏替換原則告訴我們,繼承實際上讓兩個類耦合性增強了,在適當的情況下,可以通過聚合、組合、依賴 來解決問題。
一個程序引出的問題和思考
public class LiskovSubstitutionCase {
public static void main(String[] args) {
A a = new A();
System.out.println("11-3=" + a.func1(11, 3));
System.out.println("1-8=" + a.func1(1, 8));
System.out.println("-----------------------");
B b = new B();
System.out.println("11-3=" + b.func1(11, 3));
System.out.println("1-8=" + b.func1(1, 8));
System.out.println("11+3+9=" + b.func2(11, 3));
}
}
class A {
public int func1(int num1, int num2) { return num1 - num2; }
}
class B extends A {
@Override
public int func1(int a, int b) { return a + b; }
public int func2(int a, int b) { return func1(a, b) + 9; }
}
//結果輸出
//11-3=8
//1-8=-7
//-----------------------
//11-3=14
//1-8=9
//11+3+9=23
- 我們發現原來運行正常的相減功能發生了錯誤。原因就是類B無意中重寫了父類的方法,造成原有功能出現錯誤。在實際編程中,我們常常會通過重寫父類的方法完成新的功能,這樣寫起來雖然簡單,但整個繼承體系的複用性會比較差。特別是運行多態比較頻繁的時候.
- 通用的做法是:原來的父類和子類都繼承一個更通俗的基類,原有的繼承關係去掉,採用依賴、聚合、組合等關係代替。
方案改進
class Base {}
class A extends Base {
public int func1(int num1, int num2) { return num1 - num2; }
}
class B extends Base {
// 如果B需要使用A類的方法,使用組合關係
private A a = new A();
public int func1(int a, int b) { return a + b; }
public int func2(int a, int b) { return func1(a, b) + 9; }
public int func3(int a, int b) { return this.a.func1(a, b); }
}
public class LiskovSubstitutionCase {
public static void main(String[] args) {
B b = new B();
// System.out.println("11-3=" + b.func1(11, 3));
// System.out.println("1-8=" + b.func1(1, 8));
// System.out.println("11+3+9=" + b.func2(11, 3));
// 因爲B不在集成A類,因此調用者不會再用func1來求減法,而是用func3
System.out.println("11-3=" + b.func3(11, 3));
System.out.println("1-8=" + b.func3(1, 8));
System.out.println("11+3+9=" + b.func2(11, 3));
}
}
開閉原則OCP
基本介紹
- 開閉原則(Open Closed Principle)是編程中最基礎、最重要的設計原則。
- 一個軟件實體如類,模塊和函數應該對擴展開放(對提供方),對修改關閉(對使用方)。用抽象構建框架,用實現擴展細節。
- 當軟件需要變化時,儘量通過擴展軟件實體的行爲來實現變化,而不是通過修改已有的代碼來實現變化。
- 編程中遵循其它原則,以及使用設計模式的目的就是遵循開閉原則。
看一段代碼
public class OpenClosedCase {
public static void main(String[] args) {
GraphicEditor graphicEditor = new GraphicEditor();
graphicEditor.drawShape(new Rectangle());
graphicEditor.drawShape(new Circle());
graphicEditor.drawShape(new Triangle());
}
}
class GraphicEditor {
public void drawShape(Shape s) {
if (s.type == 1) {
drawRectangle(s);
} else if (s.type == 2) {
drawCircle(s);
} else if (s.type == 3) {
// 這裏需要改動
drawTriangle(s);
}
}
private void drawRectangle(Shape r) {
System.out.println("繪製矩形");
}
private void drawCircle(Shape r) {
System.out.println("繪製圓形");
}
/**
* 這裏也需要新增方法
*/
private void drawTriangle(Shape r) {
System.out.println("繪製三角形");
}
}
class Shape {
int type;
}
class Rectangle extends Shape {
Rectangle() {
super.type = 1;
}
}
class Circle extends Shape {
Circle() {
super.type = 2;
}
}
/**
* 新增畫三角形
*/
class Triangle extends Shape {
Triangle() {
super.type = 3;
}
}
- 這種方式的優點是比較好理解,簡單易操作。缺點是違反了設計模式的ocp原則,即對擴展開放(提供方),對修改關閉(使用方)。即當我們給類增加新功能的時候,儘量不修改代碼,或者儘可能少修改代碼。比如我們這時要新增加一個圖形種類三角形,我們需要做如下修改,修改的地方較多。
方案改進
- 思路:把創建Shape類做成抽象類,並提供一個抽象的draw方法,讓子類去實現即可,這樣我們有新的圖形種類時,只需要讓新的圖形類繼承Shape,並實現draw方法即可,使用方的代碼就不需要修 -> 滿足了開閉原則
public class OpenClosedCase {
public static void main(String[] args) {
GraphicEditor graphicEditor = new GraphicEditor();
graphicEditor.drawShape(new Rectangle());
graphicEditor.drawShape(new Circle());
graphicEditor.drawShape(new Triangle());
}
}
/**
* 改進方案:
*/
abstract class Shape {
int type;
abstract void draw();
}
class Rectangle extends Shape {
Rectangle() {
super.type = 1;
}
@Override
public void draw() {
System.out.println("繪製矩形");
}
}
class Circle extends Shape {
Circle() {
super.type = 2;
}
@Override
public void draw() {
System.out.println("繪製圓形");
}
}
class Triangle extends Shape {
Triangle() {
super.type = 3;
}
@Override
public void draw() {
System.out.println("繪製三角形");
}
}
class GraphicEditor {
public void drawShape(Shape s) {
s.draw();
}
}
迪米特法則
基本介紹
- 一個對象應該對其他對象保持最少的瞭解,類與類關係越密切,耦合度越大。
- 迪米特法則(Demeter Principle)又叫最少知道原則,即一個類對自己依賴的類知道的越少越好。也就是說,對於被依賴的類不管多麼複雜,都儘量將邏輯封裝在類的內部。對外除了提供的public 方法,不對外泄露任何信息。
- 迪米特法則還有個更簡單的定義:只與直接的朋友通信。
- 直接的朋友:每個對象都會與其他對象有耦合關係,只要兩個對象之間有耦合關係,我們就說這兩個對象之間是朋友關係。耦合的方式很多,依賴,關聯,組合,聚合等。其中,我們稱出現成員變量,方法參數,方法返回值中的類爲直接的朋友,而出現在局部變量中的類不是直接的朋友。也就是說,陌生的類最好不要以局部變量的形式出現在類的內部。
應用實例
- 需求:有一個學校,下屬有各個學院和總部,現要求打印出學校總部員工ID和學院員工的id。
- 代碼實現:
public class DemeterCase {
public static void main(String[] args) {
SchoolManager schoolManager = new SchoolManager();
schoolManager.printAllEmployee(new CollegeManager());
}
}
/**
* 學校總部員工
*/
class Employee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
/**
* 學校學院員工
*/
class CollegeEmployee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
/**
* 管理學校學院員工的類
*/
class CollegeManager {
public List<CollegeEmployee> getAllEmployee() {
List<CollegeEmployee> list = new ArrayList<CollegeEmployee>();
for (int i = 0; i < 10; i++) {
CollegeEmployee emp = new CollegeEmployee();
emp.setId("學校學院員工 id= " + i);
list.add(emp);
}
return list;
}
}
/**
* 管理學校總部員工的類
* 分析:SchoolManager 類的直接朋友有哪些【Employee、CollegeManager】
* 但是 CollegeEmployee 類並不是 SchoolManager 類的直接朋友,這樣就違背了迪米特法則
*/
class SchoolManager {
public List<Employee> getAllEmployee() {
List<Employee> list = new ArrayList<Employee>();
for (int i = 0; i < 5; i++) {
Employee emp = new Employee();
emp.setId("學校總部員工 id= " + i);
list.add(emp);
}
return list;
}
void printAllEmployee(CollegeManager sub) {
List<CollegeEmployee> list1 = sub.getAllEmployee();
System.out.println("------------學校學院員工------------");
for (CollegeEmployee e : list1) {
System.out.println(e.getId());
}
List<Employee> list2 = this.getAllEmployee();
System.out.println("------------學校總部員工------------");
for (Employee e : list2) {
System.out.println(e.getId());
}
}
}
- 改進方案:
迪米特法則注意事項和細節
- 迪米特法則的核心是降低類之間的耦合。
- 但是注意:由於每個類都減少了不必要的依賴,因此迪米特法則只是要求降低類間(對象間)耦合關係, 並不是要求完全沒有依賴關係
合成複用原則
基本介紹
- 原則是儘量使用合成/聚合的方式,而不是使用繼承。
應用實例
設計原則核心思想
- 找出應用中可能需要變化之處,把它們獨立出來,不要和那些不需要變化的代碼混在一起。
- 針對接口編程,而不是針對實現編程。
- 爲了交互對象之間的松耦合設計而努力。
UML類圖
UML基本介紹
- UML——Unified modeling language UML(統一建模語言),是一種用於軟件系統分析和設計的語言工具,它用於幫助軟件開發人員進行思考和記錄思路的結果。
- UML本身是一套符號的規定,就像數學符號和化學符號一樣,這些符號用於描述軟件模型中的各個元素和他們之間的關係,比如類、接口、實現、泛化、依賴、組合、聚合等,如圖:
- 使用UML來建模,常用的工具有 Rational Rose , 也可以使用一些插件來建模。
UML圖
畫UML圖與寫文章差不多,都是把自己的思想描述給別人看,關鍵在於思路和條理,UML圖分類:
- ① 用例圖(usecase)
- ② 靜態結構圖:類圖、對象圖、包圖、組件圖、部署圖
- ③ 動態行爲圖:交互圖(時序圖與協作圖)、狀態圖、活動圖
- 類圖是描述類與類之間的關係的,是UML圖中最核心的
UML類圖
- 用於描述系統中的類(對象)本身的組成和類(對象)之間的各種靜態關係。
- 類之間的關係:依賴、泛化(繼承)、實現、關聯、聚合與組合
- 類圖簡單舉例:
public class Person{ //代碼形式->類圖 private Integer id;
private String name;
public void setName(String name){
this.name = name;
}
public String getName(){
return name;
}
}
類圖—依賴關係(Dependence)
- 只要是在類中用到了對方,那麼他們之間就存在依賴關係。如果沒有對方,連編繹都通過不了。
- 案例:
public class PersonServiceBean {
private PersonDao personDao;//類
public void save(Person person){}
public IDCard getIDCard(Integer personid){}
public void modify(){
Department department = new Department();
}
}
public class PersonDao{}
public class IDCard{}
public class Person{}
public class Department{}
- 小結
* 類中用到了對方
* 如果是類的成員屬性
* 如果是方法的返回類型
* 是方法接收的參數類型
* 方法中使用到
類圖—泛化關係(generalization)
- 泛化關係實際上就是繼承關係,他是依賴關係的特例,如果A類繼承了B類,我們就說A和B存在泛化關係。
類圖—實現關係(Implementation)
- 實現關係實際上就是A類實現B接口,他是依賴關係的特例
類圖—關聯關係(Association)
- 關聯關係實際上就是類與類之間的聯繫,他是依賴關係的特例
- 關聯具有導航性:即雙向關係或單向關係
- 關係具有多重性:如“1”(表示有且僅有一個),“0…”(表示0個或者多個),“0,1”(表示0個或者一個),“n…m”(表示n到 m個都可以),“m…*”(表示至少m個)
- ① 單向一對一關係
- ② 雙向一對一關係
類圖—聚合關係(Aggregation)
- 聚合關係(Aggregation)表示的是整體和部分的關係,整體與部分可以分開。聚合關係是關聯關係的特例,所以他具有關聯的導航性與多重性。
- 如:一臺電腦由鍵盤(keyboard)、顯示器(monitor),鼠標等組成;組成電腦的各個配件是可以從電腦上分離出來的,使用帶空心菱形的實線來表示:
- 如果我們讓 Mouse、Monitor 和 Computer 是不可分離的,則升級爲組合關係。
類圖—組合關係(Composition)
- 組合關係:也是整體與部分的關係,但是整體與部分不可以分開。
- 再看一個案例:在程序中我們定義實體 Person 與 IDCard、Head,那麼 Head 和 Person 就是 組合,IDCard 和 Person 就是聚合。
- 但是如果在程序中 Person 實體中定義了對 IDCard 進行級聯刪除,即刪除 Person 時連同 IDCard 一起刪除,那麼 IDCard 和 Person 就是組合了。
Java設計模式概述
掌握設計模式的層次
- 第1層:剛開始學編程不久,聽說過什麼是設計模式。
- 第2層:有很長時間的編程經驗,自己寫了很多代碼,其中用到了設計模式,但是自己卻不知道。
- 第3層:學習過了設計模式,發現自己已經在使用了,並且發現了一些新的模式挺好用的。
- 第4層:閱讀了很多別人寫的源碼和框架,在其中看到別人設計模式,並且能夠領會設計模式的精妙和帶來的好處。
- 第5層:代碼寫着寫着,自己都沒有意識到使用了設計模式,並且熟練的寫了出來。
設計模式介紹
- 設計模式是程序員在面對同類軟件工程設計問題所總結出來的有用的經驗,模式不是代碼,而是某類問題的通用解決方案,設計模式(Design pattern)代表了最佳的實踐。這些解決方案是衆多軟件開發人員經過相當長的一段時間的試驗和錯誤總結出來的。
- 設計模式的本質提高軟件的維護性,通用性和擴展性,並降低軟件的複雜度。
- <<設計模式>>是經典的書,作者是ErichGamma、RichardHelm、Ralph Johnson 和 John Vlissides Design(俗稱 “四人組 GOF”)。
- 設計模式並不侷限於某種語言,java、php、c++ 都有設計模式。
設計模式類型
設計模式分爲三種類型,共23種。
- 創建型模式:單例模式、抽象工廠模式、原型模式、建造者模式、工廠模式。
- 結構型模式:適配器模式、橋接模式、裝飾模式、組合模式、外觀模式、享元模式、代理模式。
- 行爲型模式:模版方法模式、命令模式、訪問者模式、迭代器模式、觀察者模式、中介者模式、備忘錄模式、解釋器模式(Interpreter模式)、狀態模式、策略模式、職責鏈模式(責任鏈模式)。
23種設計模式
創建型模式
單例模式
- 所謂類的單例設計模式,就是採取一定的方法保證在整個的軟件系統中,對某個類只能存在一個對象實例,並且該類只提供一個取得其對象實例的方法(靜態方法)。
- 比如Hibernate的SessionFactory,它充當數據存儲源的代理,並負責創建Session對象。SessionFactory並不是輕量級的,一般情況下,一個項目通常只需要一個SessionFactory就夠,這是就會使用到單例模式。
- UML類圖:
單例設計模式八種方式
- 餓漢式(靜態常量)
- 餓漢式(靜態代碼塊)
- 懶漢式(線程不安全)
- 懶漢式(線程安全,同步方法)
- 懶漢式(線程安全,同步代碼塊)
- 雙重檢查
- 靜態內部類
- 枚舉
餓漢式(靜態常量)
public class SingletonCase01 {
public static void main(String[] args) {
Singleton1 instance1 = Singleton1.getInstance();
Singleton1 instance2 = Singleton1.getInstance();
System.out.println(instance1 == instance2);
}
}
class Singleton1 {
/**
* 1、構造器私有化(防止 new)
*/
private Singleton1(){}
/**
* 2、類的內部創建對象
*/
private static final Singleton1 INSTANCE = new Singleton1();
/**
* 3、向外暴露一個靜態的公共方法
*/
public static Singleton1 getInstance() {
return INSTANCE;
}
}
- 優點:這種寫法比較簡單,就是在類裝載的時候就完成實例化。避免了線程同步問題。
- 缺點:在類裝載的時候就完成實例化,沒有達到 Lazy Loading 的效果。如果從始至終從未使用過這個實例,則會造成內存的浪費。
- 這種方式基於classloder機制避免了多線程的同步問題,不過,instance在類裝載時就實例化,在單例模式中大多數都是調用getInstance方法, 但是導致類裝載的原因有很多種,因此不能確定有其他的方式(或者其他的靜態方法)導致類裝載,這時候初始化instance就沒有達到 lazy loading 的效果
- 結論:這種單例模式可用,可能造成內存浪費。
餓漢式(靜態代碼塊)
public class SingletonCase02 {
public static void main(String[] args) {
Singleton2 instance1 = Singleton2.getInstance();
Singleton2 instance2 = Singleton2.getInstance();
System.out.println(instance1 == instance2);
}
}
class Singleton2 {
/**
* 1、構造器私有化(防止 new)
*/
private Singleton2(){}
/**
* 2、類的內部創建對象:在靜態代碼塊執行時,創建單例對象
*/
private static Singleton2 INSTANCE;
static {
INSTANCE = new Singleton2();
}
/**
* 3、向外暴露一個靜態的公共方法
*/
public static Singleton2 getInstance() {
return INSTANCE;
}
}
- 這種方式和上面的方式其實類似,只不過將類實例化的過程放在了靜態代碼塊中,也是在類裝載的時候,就執行靜態代碼塊中的代碼,初始化類的實例。優缺點和上面是一樣的。
- 結論:這種單例模式可用,但是可能造成內存浪費。
懶漢式(線程不安全)
public class SingletonCase03 {
public static void main(String[] args) {
// Singleton3 instance1 = Singleton3.getInstance();
// Singleton3 instance2 = Singleton3.getInstance();
// System.out.println(instance1 == instance2);
for (int i = 0; i < 50; i++) {
new Thread(() -> System.out.println(Singleton3.getInstance().hashCode())).start();
}
}
}
class Singleton3 {
private Singleton3(){}
private static Singleton3 INSTANCE;
/**
* 提供一個靜態的公有方法,當使用到該方法時,纔去創建 instance
*/
public static Singleton3 getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton3();
}
return INSTANCE;
}
}
- 起到了Lazy Loading的效果,但是隻能在單線程下使用。
- 如果在多線程下,一個線程進入了if (singleton == null)判斷語句塊,還未來得及往下執行,另一個線程也通過了這個判斷語句,這時便會產生多個實例。所以在多線程環境下不可使用這種方式。
- 結論:在實際開發中,不要使用這種方式。
懶漢式(線程安全,同步方法)
public class SingletonCase04 {
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
new Thread(() -> System.out.println(Singleton4.getInstance().hashCode())).start();
}
}
}
class Singleton4 {
private Singleton4(){}
private static Singleton4 INSTANCE;
/**
* 加synchronized,解決多線程不安全問題
*/
public static synchronized Singleton4 getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton4();
}
return INSTANCE;
}
}
- 解決了線程不安全問題。
- 但是效率太低了,每個線程在想獲得類的實例時候,執行getInstance()方法都要進行同步。而其實這個方法只執行一次實例化代碼就夠了,後面的想獲得該類實例,直接return就行了。方法進行同步效率太低。
- 結論:在實際開發中,不推薦使用這種方式。
懶漢式(線程安全,同步代碼塊)
public class SingletonCase05 {
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
new Thread(() -> System.out.println(Singleton5.getInstance().hashCode())).start();
}
}
}
class Singleton5 {
private Singleton5(){}
private static Singleton5 INSTANCE;
/**
* 加synchronized,解決多線程不安全問題
*/
public static Singleton5 getInstance() {
if (INSTANCE == null) {
synchronized (Singleton5.class) {
INSTANCE = new Singleton5();
}
}
return INSTANCE;
}
}
- 這種方式,本意是想對第四種實現方式的改進,因爲前面同步方法效率太低,改爲同步產生實例化的的代碼塊。
- 但是這種同步並不能起到線程同步的作用。跟第3種實現方式遇到的情形一致,假如一個線程進入了if (singleton == null)判斷語句塊,還未來得及往下執行,另一個線程也通過了這個判斷語句,這時便會產生多個實例。
- 結論:在實際開發中,不能使用這種方式
雙重檢查單例
public class SingletonCase06 {
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
new Thread(() -> System.out.println(Singleton6.getInstance().hashCode())).start();
}
}
}
class Singleton6 {
private Singleton6(){}
/**
* 這裏的 volatile 一定不能夠少,防止獲取到半初始化狀態的實例
*/
private static volatile Singleton6 INSTANCE;
/**
* 加synchronized,解決多線程不安全問題
*/
public static Singleton6 getInstance() {
if (INSTANCE == null) {
synchronized (Singleton6.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton6();
}
}
}
return INSTANCE;
}
}
- Double-Check概念是多線程開發中常使用到的,如代碼中所示,我們進行了兩次if (singleton == null)檢查,這樣就可以保證線程安全了。
- 這樣,實例化代碼只用執行一次,後面再次訪問時,判斷if (singleton == null),直接return實例化對象,也避免的反覆進行方法同步。
- 線程安全、延遲加載、效率較高。
- 結論:在實際開發中,推薦使用這種單例設計模式。
靜態內部類單例
public class SingletonCase07 {
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
new Thread(() -> System.out.println(Singleton7.getInstance().hashCode())).start();
}
}
}
class Singleton7 {
private Singleton7() {}
/**
* 使用靜態內部類,在Singleton7被加載時,不會被加載
* 只有在getInstance()時被加載,並且加載時是線程安全的
*/
private static class SingletonInstance {
private static final Singleton7 INSTANCE = new Singleton7();
}
public static Singleton7 getInstance() {
return SingletonInstance.INSTANCE;
}
}
- 這種方式採用了類裝載的機制來保證初始化實例時只有一個線程。
- 靜態內部類方式在Singleton類被裝載時並不會立即實例化,而是在需要實例化時,調用getInstance方法,纔會裝載SingletonInstance類,從而完成Singleton的實例化。
- 類的靜態屬性只會在第一次加載類的時候初始化,所以在這裏,JVM幫助我們保證了線程的安全性,在類進行初始化時,別的線程是無法進入的。
- 優點:避免了線程不安全,利用靜態內部類特點實現延遲加載,效率高。
- 結論:推薦使用。
枚舉單例
public class SingletonCase08 {
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
new Thread(() -> System.out.println(Singleton8.getInstance().hashCode())).start();
}
}
}
class Singleton8 {
private Singleton8() {}
static enum SingletonInstance {
INSTANCE;
private Singleton8 singleton8;
// 私有化枚舉的構造函數
private SingletonInstance() {
singleton8 = new Singleton8();
}
public Singleton8 getInstance() {
return singleton8;
}
}
public static Singleton8 getInstance() {
return SingletonInstance.INSTANCE.getInstance();
}
}
- 這藉助JDK1.5中添加的枚舉來實現單例模式。不僅能避免多線程同步問題,而且還能防止反序列化重新創建新的對象。
- 這種方式是Effective Java作者Josh Bloch 提倡的方式。
- 結論:推薦使用。
單例模式在JDK 應用的源碼分析
- 我們JDK中,java.lang.Runtime就是經典的單例模式(餓漢式)
單例模式注意事項和細節說明
- 單例模式保證了系統內存中該類只存在一個對象,節省了系統資源,對於一些需要頻繁創建銷燬的對象,使用單例模式可以提高系統性能。
- 當想實例化一個單例類的時候,必須要記住使用相應的獲取對象的方法,而不是使用new。
- 單例模式使用的場景:需要頻繁的進行創建和銷燬的對象、創建對象時耗時過多或耗費資源過多(即:重量級對象),但又經常用到的對象、工具類對象、頻繁訪問數據庫或文件的對象(比如數據源、session工廠等)。
工廠設計模式
- 看一個披薩的項目:要便於披薩種類的擴展,要便於維護。
- 披薩的種類很多(比如 GreekPizz、CheesePizz 等),披薩的製作有 prepare、bake、cut、box,完成披薩店訂購功能。
使用傳統的方式來完成
/**
* 定義一個披薩的抽象類
*/
public abstract class Pizza {
/**
* 披薩名字
*/
protected String name;
/**
* 原材料準備
*/
public abstract void prepare();
public void bake(){
System.out.println(name + " baking;");
}
public void cut() {
System.out.println(name + " cutting;");
}
public void box() {
System.out.println(name + " boxing;");
}
public Pizza(String name) {
this.name = name;
}
}
// 奶酪披薩
public class CheesePizza extends Pizza {
public CheesePizza() {
super("奶酪披薩");
}
@Override
public void prepare() {
System.out.println("給製作奶酪披薩準備原材料");
}
}
// 希臘披薩
public class GreekPizza extends Pizza {
public GreekPizza() {
super("希臘披薩");
}
@Override
public void prepare() {
System.out.println("給希臘披薩準備原材料");
}
}
/**
* 披薩訂購
*/
public class PizzaOrder {
Pizza pizza = null;
public PizzaOrder() {
String orderType = "";
do {
orderType = getType();
if (orderType.equals("greek")) {
pizza = new GreekPizza();
} else if (orderType.equals("cheese")) {
pizza = new CheesePizza();
} else {
break;
}
// 輸出披薩製作過程
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
} while (true);
}
/**
* 獲取客戶希望訂購披薩的種類
*/
private String getType() {
try {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
System.out.println("input pizza type: ");
return in.readLine();
} catch (IOException e) {
System.out.println(e.getMessage());
return "";
}
}
}
// 披薩店
public class PizzaStore {
public static void main(String[] args) {
new PizzaOrder();
}
}
- 優點是比較好理解,簡單易操作。
- 缺點是違反了設計模式的ocp原則,即對擴展開放,對修改關閉。即當我們給類增加新功能的時候,儘量不修改代碼,或者儘可能少修改代碼。
- 比如我們這時要新增加一個Pizza的種類(Pepper披薩),我們就需要修改代碼。
- 改進:把創建Pizza對象封裝到一個類中,這樣我們有新的Pizza種類時,只需要修改該類就可,其它有創建到Pizza對象的代碼就不需要修改了-> 簡單工廠模式。
簡單工廠模式
基本介紹
- 簡單工廠模式是屬於創建型模式,是工廠模式的一種。簡單工廠模式是由一個工廠對象決定創建出哪一種產品類的實例。簡單工廠模式是工廠模式家族中最簡單實用的模式。
- 簡單工廠模式:定義了一個創建對象的類,由這個類來封裝實例化對象的行爲。
- 在軟件開發中,當我們會用到大量的創建某種、某類或者某批對象時,就會使用到工廠模式。
- UML類圖:
代碼實現
public class SimpleFactory {
public Pizza createPizza(String orderType) {
Pizza pizza = null;
if (orderType.equals("greek")) {
pizza = new GreekPizza();
} else if (orderType.equals("cheese")) {
pizza = new CheesePizza();
}
if (pizza != null) {
// 輸出披薩製作過程
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
}
return pizza;
}
}
public class PizzaOrder {
SimpleFactory simpleFactory;
public PizzaOrder(SimpleFactory simpleFactory) {
do {
this.simpleFactory = simpleFactory;
simpleFactory.createPizza(getType());
} while (true);
}
/**
* 獲取客戶希望訂購披薩的種類
*/
private String getType() {
try {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
System.out.println("input pizza type: ");
return in.readLine();
} catch (IOException e) {
System.out.println(e.getMessage());
return "";
}
}
}
public class PizzaStore {
public static void main(String[] args) {
new PizzaOrder(new SimpleFactory());
}
}
工廠方法模式
基本介紹
- 需求:披薩項目新的需求:客戶在點披薩時,可以點不同口味的披薩,比如 北京的奶酪pizza、北京的胡椒pizza 或者是倫敦的奶酪pizza、倫敦的胡椒pizza。
- 使用簡單工廠模式,創建不同的簡單工廠類,比如BJPizzaSimpleFactory、LDPizzaSimpleFactory 等等。從當前這個案例來說,也是可以的,但是考慮到項目的規模,以及軟件的可維護性、可擴展性並不是特別好。
- 工廠方法模式設計方案:將披薩項目的實例化功能抽象成抽象方法,在不同的口味點餐子類中具體實現。定義了一個創建對象的抽象方法,由子類決定要實例化的類,工廠方法模式將對象的實例化推遲到子類。
- UML類圖:
代碼實現
public abstract class PizzaOrder {
public PizzaOrderFactory(String desc) {
System.out.println(desc);
do {
Pizza pizza = createPizza(getType());
if (pizza != null) {
// 輸出披薩製作過程
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
} else {
System.out.println("沒有這種披薩,訂購失敗");
break;
}
} while (true);
}
/**
* 交給工廠子類完成
*/
abstract Pizza createPizza(String orderType);
/**
* 獲取客戶希望訂購披薩的種類
*/
private String getType() {
try {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
System.out.println("input pizza type: ");
return in.readLine();
} catch (IOException e) {
System.out.println(e.getMessage());
return "";
}
}
}
public class BJPizzaOrderFactory extends PizzaOrderFactory {
BJPizzaOrderFactory() {
super("開始訂購北京披薩");
}
@Override
Pizza createPizza(String orderType) {
if (orderType.equals("cheese")) {
return new BJCheesePizza();
} else if (orderType.equals("pepper")) {
return new BJPepperPizza();
}
return null;
}
}
public class LDPizzaOrderFactory extends PizzaOrderFactory {
LDPizzaOrderFactory() {
super("開始訂購倫敦披薩");
}
@Override
Pizza createPizza(String orderType) {
if (orderType.equals("cheese")) {
return new LDCheesePizza();
} else if (orderType.equals("pepper")) {
return new LDPepperPizza();
}
return null;
}
}
抽象工廠模式
基本介紹
- 抽象工廠模式:定義了一個interface用於創建相關或有依賴關係的對象簇,而無需指明具體的類。
- 抽象工廠模式可以將簡單工廠模式和工廠方法模式進行整合。
- 從設計層面看,抽象工廠模式就是對簡單工廠模式的改進(或者稱爲進一步的抽象)。
- 將工廠抽象成兩層,AbsFactory(抽象工廠) 和 具體實現的工廠子類。程序員可以根據創建對象類型使用對應的工廠子類。這樣將單個的簡單工廠類變成了工廠簇,更利於代碼的維護和擴展。
- UML類圖:
代碼實現
public abstract class AbstractPizzaOrderFactory {
AbstractPizzaOrderFactory(String desc) {
System.out.println(desc);
}
abstract Pizza createPizza(String orderType);
}
public class BJPizzaOrderFactory extends AbstractPizzaOrderFactory {
BJPizzaOrderFactory() {
super("開始訂購北京披薩");
}
@Override
Pizza createPizza(String orderType) {
if (orderType.equals("cheese")) {
return new BJCheesePizza();
} else if (orderType.equals("pepper")) {
return new BJPepperPizza();
}
return null;
}
}
public class LDPizzaOrderFactory extends AbstractPizzaOrderFactory {
LDPizzaOrderFactory() {
super("開始訂購倫敦披薩");
}
@Override
Pizza createPizza(String orderType) {
if (orderType.equals("cheese")) {
return new LDCheesePizza();
} else if (orderType.equals("pepper")) {
return new LDPepperPizza();
}
return null;
}
}
工廠模式在JDK-Calendar 應用的源碼分析
- JDK中的Calendar類中,就使用了簡單工廠模式
public class JDKFactory {
public static void main(String[] args) {
Calendar cal = Calendar.getInstance();
}
}
↓↓↓↓↓
public static Calendar getInstance()
{
return createCalendar(TimeZone.getDefault(), Locale.getDefault(Locale.Category.FORMAT));
}
↓↓↓↓↓
private static Calendar createCalendar(TimeZone zone, Locale aLocale)
{
CalendarProvider provider =
LocaleProviderAdapter.getAdapter(CalendarProvider.class, aLocale)
.getCalendarProvider();
if (provider != null) {
try {
return provider.getInstance(zone, aLocale);
} catch (IllegalArgumentException iae) {
// fall back to the default instantiation
}
}
Calendar cal = null;
// 下面就是工廠模式
if (aLocale.hasExtensions()) {
String caltype = aLocale.getUnicodeLocaleType("ca");
if (caltype != null) {
switch (caltype) {
case "buddhist":
cal = new BuddhistCalendar(zone, aLocale);
break;
case "japanese":
cal = new JapaneseImperialCalendar(zone, aLocale);
break;
case "gregory":
cal = new GregorianCalendar(zone, aLocale);
break;
}
}
}
if (cal == null) {
if (aLocale.getLanguage() == "th" && aLocale.getCountry() == "TH") {
cal = new BuddhistCalendar(zone, aLocale);
} else if (aLocale.getVariant() == "JP" && aLocale.getLanguage() == "ja"
&& aLocale.getCountry() == "JP") {
cal = new JapaneseImperialCalendar(zone, aLocale);
} else {
cal = new GregorianCalendar(zone, aLocale);
}
}
return cal;
}
工廠模式小結
- 工廠模式的意義:將實例化對象的代碼提取出來,放到一個類中統一管理和維護,達到和主項目的依賴關係的解耦。從而提高項目的擴展和維護性。
- 三種工廠模式:簡單工廠模式、工廠方法模式、抽象工廠模式。
- 設計模式的依賴抽象原則:創建對象實例時,不要直接 new 類, 而是把這個new 類的動作放在一個工廠的方法中,並返回。有的書上說,變量不要直接持有具體類的引用。不要讓類繼承具體類,而是繼承抽象類或者是實現interface(接口)。不要覆蓋基類中已經實現的方法。
原型模式
克隆羊問題
- 現在有一隻羊 Tom,姓名爲Tom,年齡爲1,顏色爲白色,請編寫程序創建和 Tom 羊,屬性完全相同的10只羊。
- 傳統方式解決克隆羊問題:
public class SheepCase {
public static void main(String[] args) {
// 傳統方法
Sheep sheep = new Sheep("Tom", 1, "白色");
Sheep sheep2 = new Sheep(sheep.getName(), sheep.getAge(), sheep.getColor());
Sheep sheep3 = new Sheep(sheep.getName(), sheep.getAge(), sheep.getColor());
Sheep sheep4 = new Sheep(sheep.getName(), sheep.getAge(), sheep.getColor());
Sheep sheep5 = new Sheep(sheep.getName(), sheep.getAge(), sheep.getColor());
// ...
System.out.println(sheep);
System.out.println(sheep2);
System.out.println(sheep3);
System.out.println(sheep4);
System.out.println(sheep5);
}
}
class Sheep {
private String name;
private int age;
private String color;
Sheep(String name, int age, String color) {
this.name = name;
this.age = age;
this.color = color;
}
// setter、getter
}
- 優點是比較好理解,簡單易操作。
- 在創建新的對象時,總是需要重新獲取原始對象的屬性,如果創建的對象比較複雜時,效率較低。
- 總是需要重新初始化對象,而不是動態地獲得對象運行時的狀態, 不夠靈活。
- 改進的思路:Java中Object類是所有類的根類,Object類提供了一個clone()方法,該方法可以將一個Java對象複製一份,但是需要實現clone的Java類必須要實現一個接口Cloneable,該接口表示該類能夠複製且具有複製的能力 => 原型模式。
原型模式-基本介紹
- 原型模式(Prototype模式)是指:用原型實例指定創建對象的種類,並且通過拷貝這些原型,創建新的對象。
- 原型模式是一種創建型設計模式,允許一個對象再創建另外一個可定製的對象,無需知道如何創建的細節。
- 工作原理是:通過將一個原型對象傳給那個要發動創建的對象,這個要發動創建的對象通過請求原型對象拷貝它們自己來實施創建,即 對象.clone()。
- 形象的理解:孫大聖拔出猴毛,變出其它孫大聖。
- UML類圖:
代碼改進
class Sheep implements Cloneable {
private String name;
private int age;
private String color;
Sheep(String name, int age, String color) {
this.name = name;
this.age = age;
this.color = color;
}
// setter、getter
@Override
public String toString() {
return "Sheep: {name: " + name + ", age: " + age + ", color: " + color + "}";
}
@Override
protected Object clone() {
try {
Sheep sheep = (Sheep) super.clone();
return super.clone();
} catch (Exception e) {
System.out.println(e.getMessage());
}
return null;
}
public Sheep getCopy() {
return (Sheep) clone();
}
}
原型模式在Spring框架中源碼分析
深入討論-淺拷貝和深拷貝
淺拷貝的介紹
- 對於數據類型是基本數據類型的成員變量,淺拷貝會直接進行值傳遞,也就是將該屬性值複製一份給新的對象。
- 對於數據類型是引用數據類型的成員變量,比如說成員變量是某個數組、某個類的對象等,那麼淺拷貝會進行引用傳遞,也就是隻是將該成員變量的引用值(內存地址)複製一份給新的對象。因爲實際上兩個對象的該成員變量都指向同一個實例。在這種情況下,在一個對象中修改該成員變量會影響到另一個對象的該成員變量值。
- 前面我們克隆羊就是淺拷貝,淺拷貝是使用默認的 clone()方法來實現。
深拷貝基本介紹
- 複製對象的所有基本數據類型的成員變量值。
- 爲所有引用數據類型的成員變量申請存儲空間,並複製每個引用數據類型成員變量所引用的對象,直到該對象可達的所有對象。也就是說,對象進行深拷貝要對整個對象進行拷貝。
- 深拷貝實現方式1:重寫clone方法來實現深拷貝。
- 深拷貝實現方式2:通過對象序列化實現深拷貝(推薦)。
/**
* 深淺複製
*/
public class DeepShallowCopy implements Cloneable, Serializable {
private static final long serialVersionUID = 1L;
private String string;
private SerializableObject obj;
public String getString() {
return string;
}
public void setString(String string) {
this.string = string;
}
public SerializableObject getObj() {
return obj;
}
public void setObj(SerializableObject obj) {
this.obj = obj;
}
/**
* 淺複製
*/
public Object shallowClone() throws CloneNotSupportedException{
DeepShallowCopy proto = (DeepShallowCopy) super.clone();
return proto;
}
/**
* 深複製
* @throws ClassNotFoundException
*/
public Object deepClone() throws IOException, ClassNotFoundException{
// 寫入當前對象的二進制流
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
// 讀出二進制流產生的新對象
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return ois.readObject();
}
}
class SerializableObject implements Serializable{
private static final long serialVersionUID = 1L;
}
原型模式的注意事項和細節
- 創建新的對象比較複雜時,可以利用原型模式簡化對象的創建過程,同時也能夠提高效率。
- 不用重新初始化對象,而是動態地獲得對象運行時的狀態。
- 如果原始對象發生變化(增加或者減少屬性),其它克隆對象的也會發生相應的變化,無需修改代碼。
- 在實現深克隆的時候可能需要比較複雜的代碼。
- 缺點:需要爲每一個類配備一個克隆方法,這對全新的類來說不是很難,但對已有的類進行改造時,需要修改其源代碼,違背了ocp原則。
建造者模式
蓋房項目需求
- 需要建房子:這一過程爲打樁、砌牆、封頂,房子有各種各樣的,比如普通房,高樓,別墅,各種房子的過程雖然一樣,但是要求不要相同的。
- 傳統方式解決蓋房需求:
public abstract class AbstractHouse {
abstract void buildBasic();
abstract void buildWalls();
abstract void roofed();
public void build() {
buildBasic();
buildWalls();
roofed();
System.out.println("房子建造完成~~");
}
}
public class CommonHouse extends AbstractHouse {
@Override
void buildBasic() {
System.out.println("普通房子 打地基");
}
@Override
void buildWalls() {
System.out.println("普通房子 砌牆");
}
@Override
void roofed() {
System.out.println("普通房子 蓋屋頂");
}
}
public class BuildHouseCase {
public static void main(String[] args) {
CommonHouse commonHouse = new CommonHouse();
commonHouse.build();
}
}
- 優點是比較好理解,簡單易操作。
- 設計的程序結構,過於簡單,沒有設計緩存層對象,程序的擴展和維護不好。也就是說,這種設計方案,把產品(即房子) 和創建產品的過程(即:建房子流程) 封裝在一起,耦合性增強了。
- 解決方案:將產品和產品建造過程解耦 => 建造者模式。
建造者模式基本介紹
- 建造者模式(Builder Pattern)又叫生成器模式,是一種對象構建模式。它可以將複雜對象的建造過程抽象出來(抽象類別),使這個抽象過程的不同實現方法可以構造出不同表現(屬性)的對象。
- 建造者模式是一步一步創建一個複雜的對象,它允許用戶只通過指定複雜對象的類型和內容就可以構建它們,用戶不需要知道內部的具體構建細節。
建造者模式的四個角色
- Product(產品角色):一個具體的產品對象。
- Builder(抽象建造者):創建一個Product對象的各個部件指定的接口/抽象類。
- ConcreteBuilder(具體建造者):實現接口,構建和裝配各個部件。
- Director(指揮者):構建一個使用Builder接口的對象。它主要是用於創建一個複雜的對象。它主要有兩個作用,一是隔離了客戶與對象的生產過程,二是負責控制產品對象的生產過程。
- UML類圖:
建造者模式解決蓋房需求
// 產品
public class House {
private String basic;
private String wall;
private String roofed;
// setter、getter
@Override
public String toString() {
return "House: {basic: " + basic + ", wall: " + wall + ", roofed: " + roofed + "}";
}
}
// 房子構建抽象類
public abstract class HouseBuilder {
protected House house = new House();
abstract void buildBasic();
abstract void buildWalls();
abstract void roofed();
public House getHouse() {
return house;
}
}
// 房子的具體構建者-普通房子
public class CommonHouse extends HouseBuilder {
@Override
void buildBasic() {
System.out.println("普通房子 打地基 5 米");
house.setBasic("5 米");
}
@Override
void buildWalls() {
System.out.println("普通房子 砌牆 10 釐米");
house.setWall("10 釐米");
}
@Override
void roofed() {
System.out.println("普通房子屋頂");
house.setRoofed("普通屋頂");
}
}
// 房子的具體構建者-高樓
public class HighBuildingHouse extends HouseBuilder {
@Override
void buildBasic() {
System.out.println("高樓 打地基 100 米");
house.setBasic("100 米");
}
@Override
void buildWalls() {
System.out.println("高樓 砌牆 25 釐米");
house.setWall("25 釐米");
}
@Override
void roofed() {
System.out.println("高樓屋頂");
house.setRoofed("高樓屋頂");
}
}
// 指揮者:去指定建造房子的流程
public class HouseDirector {
private HouseBuilder builder;
HouseDirector(HouseBuilder builder) {
this.builder = builder;
}
public House build() {
builder.buildBasic();
builder.buildWalls();
builder.roofed();
return builder.getHouse();
}
}
- 測試:
public class BuildHouseCase {
public static void main(String[] args) {
CommonHouse commonHouse = new CommonHouse();
HouseDirector director = new HouseDirector(commonHouse);
House house1 = director.build();
System.out.println(house1);
HighBuildingHouse highBuildingHouse = new HighBuildingHouse();
director = new HouseDirector(highBuildingHouse);
House house2 = director.build();
System.out.println(house2);
}
}
建造者模式在JDK的應用和源碼分析
- java.lang.StringBuilder中的建造者模式
public class JDKBuilder {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append("Hello, World");
System.out.println(sb);
}
}
↓↓↓↓↓
// public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, CharSequence
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
↓↓↓↓↓
// abstract class AbstractStringBuilder implements Appendable, CharSequence
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
- Appendable 接口定義了多個append方法(抽象方法), 即Appendable 爲抽象建造者, 定義了抽象方法。
- AbstractStringBuilder 實現了 Appendable 接口方法,這裏的AbstractStringBuilder 已經是建造者,只是不能實例化。
- StringBuilder 即充當了指揮者角色,同時充當了具體的建造者,建造方法的實現是由 AbstractStringBuilder 完成,而StringBuilder 繼承了AbstractStringBuilder。
建造者模式的注意事項和細節
- 客戶端(使用程序)不必知道產品內部組成的細節,將產品本身與產品的創建過程解耦,使得相同的創建過程可以創建不同的產品對象。
- 每一個具體建造者都相對獨立,而與其他的具體建造者無關,因此可以很方便地替換具體建造者或增加新的具體建造者, 用戶使用不同的具體建造者即可得到不同的產品對象。
- 可以更加精細地控制產品的創建過程。將複雜產品的創建步驟分解在不同的方法中,使得創建過程更加清晰,也更方便使用程序來控制創建過程。
- 增加新的具體建造者無須修改原有類庫的代碼,指揮者類針對抽象建造者類編程,系統擴展方便,符合 “開閉原則”。
- 建造者模式所創建的產品一般具有較多的共同點,其組成部分相似,如果產品之間的差異性很大,則不適合使用建造者模式,因此其使用範圍受到一定的限制。
- 如果產品的內部變化複雜,可能會導致需要定義很多具體建造者類來實現這種變化,導致系統變得很龐大,因此在這種情況下,要考慮是否選擇建造者模式。
- 抽象工廠模式VS建造者模式:抽象工廠模式實現對產品家族的創建,一個產品家族是這樣的一系列產品:具有不同分類維度的產品組合,採用抽象工廠模式不需要關心構建過程,只關心什麼產品由什麼工廠生產即可。而建造者模式則是要求按照指定的藍圖建造產品,它的主要目的是通過組裝零配件而產生一個新產品。