目錄
1、什麼是設計模式?設計模式有什麼用?
設計模式(Design pattern)是一套被反覆使用、多數人知曉的、經過分類編目的、代碼設計經驗的總結。使用設計模式是爲了可重用代碼、讓代碼更容易被他人理解、保證代碼可靠性。 毫無疑問,設計模式於己於他人於系統都是多贏的,設計模式使代碼編制真正工程化,設計模式是軟件工程的基石,如同大廈的一塊塊磚石一樣。項目中合理地運用設計模式可以完美地解決很多問題,每種模式在現實中都有相應的原理來與之對應,每種模式都描述了一個在我們周圍不斷重複發生的問題,以及該問題的核心解決方案。
2、設計模式應該遵循的面向對象設計原則
1994年,在由設計模式四人幫GOF出版的著作Design Patterns - Elements of Reusable Object-Oriented Software(中文譯名:設計模式 - 可複用的面向對象軟件元素)中提出,設計模式應該遵循以下兩條面向對象設計原則:
- 對接口編程而不是對實現編程
- 優先使用對象組合而不是繼承
“對接口編程而不是對實現編程”,對於這句話我的理解就是:要善於使用多態。變量的聲明儘量使用超類型(父類),而不是某個具體的子類,超類型中的各個具體方法的實現都是寫在不同的子類中。程序在執行時能夠根據不同的情況來調用到不同的子類方法,這樣做更加靈活,並且我們在聲明一個變量時無需關心以後執行時的真正的數據類型是哪種(某個子類類型),這是種解耦合(鬆耦合)的思想。實例代碼如下:
package Test;
public interface Animal {
public void makenoise();
}
package Test;
public class Dog implements Animal {
@Override
public void makenoise() {
System.out.println("汪汪汪!");
}
}
class Cat implements Animal{
@Override
public void makenoise() {
System.out.println("喵喵喵!");
package Test;
public class AnimalTest {
public static void hearnoise(Animal animal){
animal.makenoise();
}
public static void main(String[] args) {
AnimalTest.hearnoise(new Dog());
AnimalTest.hearnoise(new Cat());
}
}
執行結果:
汪汪汪!
喵喵喵!
3、設計模式的六大原則
- 開閉原則(Open Close Prinprinciple),開閉原則的意思是:對擴展開放,對修改關閉。在程序需要進行拓展的時候,不能去修改原有的代碼,實現一個熱插拔的效果。簡言之,是爲了使程序的擴展性好,易於維護和升級。想要達到這樣的效果,我們需要使用接口和抽象類。
- 里氏代換原則(Liskov Substitution Principle),里氏代換原則是面向對象設計的基本原則之一。 里氏代換原則中說,任何基類可以出現的地方,子類一定可以出現。LSP 是繼承複用的基石,只有當派生類可以替換掉基類,且軟件單位的功能不受到影響時,基類才能真正被複用,而派生類也能夠在基類的基礎上增加新的行爲。里氏代換原則是對開閉原則的補充。實現開閉原則的關鍵步驟就是抽象化,而基類與子類的繼承關係就是抽象化的具體實現,所以里氏代換原則是對實現抽象化的具體步驟的規範(LSP我曾經在另一篇文章重新思考接口和抽象類中舉了一個例子)。
- 依賴倒轉原則(Dependence Inversion Principle),這個原則是開閉原則的基礎,具體內容:針對接口編程,依賴於抽象而不依賴於具體。
- 接口隔離原則(Interface Segregation Principle),使用多個隔離的接口,比使用單個接口要好。它還有另外一個意思是:降低類之間的耦合度。由此可見,其實設計模式就是從大型軟件架構出發、便於升級和維護的軟件設計思想,它強調降低依賴,降低耦合。
- 迪米特法則,又稱最少知道原則(Demeter Principle),一個實體應當儘量少地與其他實體之間發生相互作用,使得系統功能模塊相對獨立。
- 合成複用原則(Composite Reuse Principle),儘量使用合成/聚合的方式,而不是使用繼承。
4、設計模式的四種類型(包括J2EE設計模式)
創建型模式,這些設計模式提供了一種在創建對象的同時隱藏創建邏輯的方式,而不是使用 new 運算符直接實例化對象。這使得程序在判斷針對某個給定實例需要創建哪些對象時更加靈活。創建者模式包括以下幾種設計模式:
- 工廠模式
- 抽象工廠模式
- 單例模式
- 建造者模式
- 原型模式
結構型模式,這些設計模式關注類和對象的組合。繼承的概念被用來組合接口和定義組合對象獲得新功能的方式。結果型模式包括以下幾種設計模式:
- 適配器模式
- 橋接模式
- 過濾器模式
- 組合模式
- 裝飾器模式
- 外觀模式
- 享元模式
- 代理模式
行爲型模式,這些設計模式特別關注對象之間的通信。行爲型模式包括以下幾種設計模式:
- 責任鏈模式
- 命令模式
- 解釋器模式
- 迭代器模式
- 中介者模式
- 備忘錄模式
- 觀察者模式
- 狀態模式
- 空對象模式
- 策略模式
- 模板模式
- 訪問者模式
J2EE模式,這些設計模式特別關注表現層,這些模式是由Sun Java Center鑑定的。J2EE模式包括以下幾種設計模式:
- MVC模式
- 業務代表模式
- 組合實體模式
- 數據訪問對象模式
- 前端控制器模式
- 攔截過濾器模式
- 服務器定位器模式
- 傳輸對象模式
5、幾種常見的設計模式
5.1、工廠模式
工廠模式(Factory Pattern)是Java中最常見的設計模式之一,屬於創建者模式。顧名思義,它的思路是設計一個對象生產工廠,它提供了一種絕佳的創建對象的方式。在工廠模式中,我們在創建對象時不會對客戶端暴露創建邏輯,並且是通過一個共同的接口來創建對象。
主要解決的問題:解決接口選擇的問題。定義一個創建對象的接口,讓其子類自己決定實例化哪一個工廠類,工廠模式使其創建過程延遲到子類進行。
優點:
- 一個調用者想創建一個實例對象,只需要知道其名字就行。
- 擴展性高,如果想增加一個產品,只要擴展工廠類就行。
- 屏蔽了產品的具體實現,調用者只關心產品的接口。
缺點:
- 每次增加一個產品,都需要增加一個具體實現類和對象實現工廠,使得系統中類的個數成倍增加,在一定程度上增加了系統的複雜性,同時也增加了系統具體類的依賴,這並不是什麼好事。
實現示例:就以一家生產多個不同品牌的汽車生產廠爲例,先創建一個Car接口和三個實現類BENZ、BMW、TOYOTA,再定義一個工廠類CarFactory。我們使用這個工廠類CarFactory來生產不同品牌的汽車。
package FactoryDemo;
public interface Car {
public void Brand();
}
public class BENZ implements Car{
@Override
public void Brand() {
System.out.println("生產一輛奔馳");
}
}
public class BMW implements Car{
@Override
public void Brand() {
System.out.println("生產一輛寶馬");
}
}
public class TOYOTA implements Car{
@Override
public void Brand() {
System.out.println("生產一輛豐田");
}
}
//用來生成汽車的工廠類
//equalsIgnoreCase()方法只能比較字符串,equals()可以比較字符串和對象,且equalsIgnoreCase()
//中不區別大小寫,A-Z和a-z是一樣的
public class CarFactory {
public Car getcar(String carbrand) {
if (carbrand.equalsIgnoreCase("BENZ")) {
return new BENZ();
} else if (carbrand.equalsIgnoreCase("BMW")) {
return new BMW();
} else if (carbrand.equalsIgnoreCase("TOYOTA")) {
return new TOYOTA();
} else
System.out.println("對不起我們不生產這輛車");
return null;
}
}
//實例化工廠類
public class CarFactoryTest {
public static void main(String[] args) {
CarFactory carfactory = new CarFactory();
Car car1 = carfactory.getcar("Benz");
car1.Brand();
Car car2 = carfactory.getcar("Bmw");
car2.Brand();
Car car3 = carfactory.getcar("toyota");
car3.Brand();
}
}
執行結果:
生產一輛奔馳
生產一輛寶馬
生產一輛豐田
5.2、抽象工廠模式
抽象工廠模式是圍繞一個超級工廠創建其他工廠,這個超級工廠是生產其他工廠的工廠。這種設計模式屬於創建型模式,它提供了一種創建對象的最佳方式。在抽象工廠模式中,接口是負責創建一個相關對象的工廠,不需要顯式地指定它們的類。每個生成的工廠都能按照工廠模式提供對象。
主要解決的問題:主要解決接口選擇的問題。系統的產品有多於一個的產品族,而系統只消費其中某一族的產品。
優點:當一個產品族中的多個對象被設計在一起工作時,它能保證客戶端始終只使用一個產品族中的對象。
缺點:產品族的擴展非常困難,要增加一個系列的某個產品,不但要在創造工廠裏新增大量代碼,還要在具體實現里加代碼,產品族難以擴展。
實現示例:還是以上一個實例爲基礎來說明抽象工廠模式。一家汽車集團公司,旗下有兩個工廠,一家是生產國外品牌車(BENZ、BMW、TOYATA),另一家是生產國內品牌車(JAC、BYD、ROEWE)(這就相當於兩個產品族)。集團有一家自營銷售門店,採取顧客下單後再生產的經營策略(這就相當於系統只在一個時刻消費某一族的產品)。有兩個接口:ForeignCar(國外品牌車)、DomesticCar(國內品牌車),汽車實現類:Benz、Bmw、Toyota、Jac、Byd、Roewe。一個工廠抽象類AbstractFactory,兩個工廠類繼承自這個抽象類:ForeigncarFactory、DomesticcarFactory。最後還有一個工廠生產者FactoryProducer和一個測試類AbstractFactoryTest。具體代碼實現如下:
國外品牌車接口及實現類
public interface ForeignCar {
public void Brand();
}
public class BNEZ implements ForeignCar{
@Override
public void Brand() {
System.out.println("生產一輛奔馳");
}
}
public class BMW implements ForeignCar{
@Override
public void Brand() {
System.out.println("生產一輛寶馬");
}
}
public class TOYOTA implements ForeignCar{
@Override
public void Brand() {
System.out.println("生產一輛豐田");
}
}
國內品牌車接口及實現類
public interface DomesticCar {
public void Brand();
}
public class BYD implements DomesticCar{
@Override
public void Brand() {
System.out.println("生產一輛比亞迪");
}
}
public class JAC implements DomesticCar{
@Override
public void Brand() {
System.out.println("生產一輛江淮");
}
}
public class ROEWE implements DomesticCar{
@Override
public void Brand() {
System.out.println("生產一輛榮威");
}
}
工廠的抽象類及兩個工廠實現類
public abstract class AbstractFactory {
public abstract ForeignCar getforeigncar(String brand);
public abstract DomesticCar getdomesticcar(String brand);
}
public class ForeigincarFactory extends AbstractFactory{
@Override
public ForeignCar getforeigncar(String brand) {
if (brand.equalsIgnoreCase("Benz")){
return new BNEZ();
}
else if (brand.equalsIgnoreCase("Bmw")){
return new BMW();
}
else if (brand.equalsIgnoreCase("Toyota")){
return new TOYOTA();
}
else
System.out.println("我們沒有這個品牌授權");
return null;
}
@Override
public DomesticCar getdomesticcar(String brand) {
return null;
}
}
public class DomesticcarFactory extends AbstractFactory{
@Override
public ForeignCar getforeigncar(String brand) {
return null;
}
@Override
public DomesticCar getdomesticcar(String brand) {
if (brand.equalsIgnoreCase("Jac")){
return new JAC();
}
else if (brand.equalsIgnoreCase("Byd")){
return new BYD();
}
else if (brand.equalsIgnoreCase("Roewe")){
return new ROEWE();
}
else
System.out.println("我們沒有這個品牌授權");
return null;
}
}
工廠生產者及測試類
public class FactoryProducer {
public static AbstractFactory CreateFactory(String choice){
if (choice.equalsIgnoreCase("ForeignCar")){
return new ForeigincarFactory();
}
else if (choice.equalsIgnoreCase("DomesticCar")){
return new DomesticcarFactory();
}
else
System.out.println("我們沒有這個工廠");
return null;
}
}
public class AbstractFactoryTest {
public static void main(String[] args) {
AbstractFactory abstractFactory1 = FactoryProducer.CreateFactory("ForeignCar");
ForeignCar car1 = abstractFactory1.getforeigncar("toyota");
car1.Brand();
AbstractFactory abstractFactory2 = FactoryProducer.CreateFactory("DomesticCar");
DomesticCar car2 = abstractFactory2.getdomesticcar("byd");
car2.Brand();
}
}
執行結果
生產一輛豐田
生產一輛比亞迪
5.3、單例模式
單例模式(Siningleton Pattern)屬於創建型模式,它提供了一種創建對象的最佳方式。這種模式設計到一個單一的類,該類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一對象的方式,可以直接訪問,不需要實例化該類的對象。單例模式需要注意一下三點:
- 單例類只能有一個實例。
- 單例類必須自己創建自己的唯一實例。
- 單例類必須給所有其他對象提供這一實例。
主要解決的問題:一個全局使用的類頻繁地創建和銷燬。當使用者想控制實例的數量,節省系統資源的時候就可以使用單例模式。
優點:
- 在內存中只有一個實例,減少了內存的開銷,尤其是頻繁地創建和銷燬實例。
- 避免對資源的多重佔用(比如寫文件操作)。
缺點:
- 沒有接口,不能繼承,與單一職責原則衝突,一個類應該只關係內部實現邏輯,而不應該關注外部怎麼來實例化。
常見使用場景:
- 要求生產唯一序列號。
- WEB中的計數器,不必每一次刷新都在數據庫中加1,可以用單例先緩存起來。
- 創建一個對象需要消耗的資源過多,比如I/O與數據庫的連接等。
實現示例:SingletonObject是一個單例類,SingletonPatternTest是測試類。
package SingletonDemo;
public class SingletonObject {
//創建這個類的唯一對象
private static SingletonObject instance = new SingletonObject();
//讓構造函數私有化,確保外部不能調用這個類的構造器來實例化對象
private SingletonObject(){
}
//獲取這個唯一可用對象
public static SingletonObject getInstance(){
return instance;
}
public void showmessage(){
System.out.println("這是單例模式");
}
}
package SingletonDemo;
public class SingletonPatternTest {
public static void main(String[] args) {
SingletonObject object = SingletonObject.getInstance();
object.showmessage();
}
}
單例模式的實現有多種方式(懶漢式和餓漢式有什麼區別?爲什麼要這麼叫?區別就在於創建對象的時機不同,懶漢式是當你需要時纔去創建對象,而餓漢式是不管你需不需要,一開始就會創建對象):
- 懶漢式(線程不安全)
- 懶漢式(線程安全)
- 餓漢式
- 雙檢鎖/雙重校驗鎖(DCL,即double-checked locking)
- 登記式/靜態內部類
- 枚舉
懶漢式(線程不安全)
這種方式是最基本的實現方式,它最大的問題就是多線程不安全。原因是沒有加鎖synchronized,所以嚴格意義上來說它不屬於單例模式,這種方式lazy loading(延遲加載)很明顯,不要求線程安全,在多線程不能正常工作。
public class Singleton{
private static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
懶漢式(線程安全)
這種方式具備很好的lazy loading,能夠在多線程中很好的工作,但是效率很低,99%的情況下不需要同步。第一次調用才初始化,避免了內存浪費。但是它必須加鎖才能夠保證單例,加鎖必定會影響效率。
public class Singleton {
private static Singleton instance;
private Singleton(){
}
public static synchronized Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
餓漢式(線程安全)
它是基於classloader機制避免了多線程的同步問題,不過instance在類裝載時就實例化,雖然導致類裝載的原因有很多種,在單例模式中都是調用getInstance()方法,但是也不能確定有其他的方式導致類裝載,這是初始化顯然沒有達到lazy loading的效果。這種方式比較常用,但是容易產生垃圾對象。優點是沒有加鎖,執行效率高。缺點是類加載時就初始化,浪費內存。
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return instance;
}
}
雙檢鎖/雙重校驗鎖(DCL,即double-checked locking)
採用雙檢鎖的方式,安全且多線程情況下能保持高性能。這種方式中getInstance()的性能對應用程序很關鍵。
public class Singleton {
private volatile static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
/**
*爲什麼instance要使用volatile關鍵字修飾?
*主要是爲了防止發生指令重排
*在 instance = new Singleton() 中主要有三步:
*1、爲Singleton()對象分配內存空間
*2、初始化Singleton()對象
*3、將instance指向Singleton()對象的內存地址
*如果cpu進行優化,發生指令重排的話,那麼步驟可能就會變成132
*就有可能出現線程中A指令執行完1和3兩步,另一個線程B發現instance不爲空,於是直接拿去用,
*但是這時Singleton()對象還沒有初始化
**/
還有兩種實現方式就不一一列出了,當明確要求實現lazy loading效果時,要使用“登記式/靜態內部類”方式;如果涉及到反序列化創建對象時,可以嘗試使用枚舉。第一種和第二種懶漢式不推薦使用,一般都是使用第三種餓漢式。
5.4、適配器模式
適配器模式是作爲兩個不兼容的接口之間的橋樑,這種設計模式屬於結構性模式,它結合了兩個獨立接口的功能。這種模式涉及到一個單一的類,該類負責加入獨立的或不兼容的接口功能。舉個真實例子,讀卡器是作爲內存卡和筆記本之間的適配器,將內存卡插入讀卡器,再將讀卡器插入筆記本,這樣就能通過筆記本來讀取內存卡了。特別注意:適配器模式不是在詳細設計時添加的,而是針對解決正在服役的項目的問題。
主要解決:主要解決在軟件系統中,常常要將一些“現存的對象”放到新的環境中,而新環境要求的接口是現對象不能滿足的。
使用場景:
- 系統需要使用現有的類,而此類的接口不符合系統的需要。
- 想要建立一個可以重複使用的類,用於與一些彼此之間沒有太大關聯的一些類,包括一些可能在將來引進的類一起工作,這些源類不一定有一致的接口。
- 通過接口轉換,將一個類插入另一個類系中(比如老虎和飛禽,現在要增加一個飛虎,在不增加實體的需求下,增加一個適配器,在裏面包容老虎對象,實現飛的接口)。
優點:
- 可以讓兩個沒有任何關聯的類一起運行。
- 提高了類的複用。
- 增加了類的透明度。
- 靈活性好。
缺點:
- 過多的使用適配器會讓系統非常的零亂,不易整體把控。比如,明明看到的是A接口,內部被適配成了B接口的實現,一個系統如果出現太多這樣的問題,那麼無異於異常災難。因此如果不是特別有必要的話,可以不使用適配器,而是對系統直接進行重構。
- 由於Java至多隻能繼承一個類,所以至多隻能適配一個適配器類,並且目標類必須是抽象類。
實現示例:我們有一個Mp3接口和一個實現了該接口的實現類Mp3Player,默認情況下Mp3Player可以播放mp3格式的音頻文件;還有另一個MediaPlayer接口和實現了該接口的實現類Mp4Player及VlcPlayer,這兩個實現類分別可以播放vlc和mp4格式的文件。我們想讓Mp3Player來播放vlc和mp4格式的音頻文件,那麼就需要創建一個實現了Mp3接口的適配器類MediaAdapter,並且使用Mp4Player對象來播放所需要的格式音頻文件。
Mp3Player使用適配器類MediaAdapter傳遞所需的音頻類型,不需要知道能播放所需音頻格式的實際類。我們在演示類AdapterPatternDemo來使用Mp3Player來播放各種音頻格式。
Mp3接口及MediaPlayer接口:
public interface Mp3 {
//傳入參數:音頻類型,音頻文件名
public void play(String audioType, String filename);
}
public interface MediaPlayer {
public void playVlc(String filename);
public void playMp4(String filename);
}
MediaPlayer接口的實現類Mp4Player、VlcPlayer:
public class Mp4Player implements MediaPlayer{
@Override
public void playVlc(String filename) {
}
@Override
public void playMp4(String filename) {
System.out.println("Playing the MP4 file,filename: " + filename);
}
}
public class VlcPlayer implements MediaPlayer {
@Override
public void playVlc(String filename) {
System.out.println("Playing the Vlc file,filename: " + filename);
}
@Override
public void playMp4(String filename) {
}
}
適配器類MediaAdapter:
public class MediaAdapter implements Mp3{
//定義一個接口類型的引用變量來引來實現了該接口的實現類實例對象
MediaPlayer mediaPlayer;
public MediaAdapter(String audiotype){
if (audiotype.equalsIgnoreCase("vlc")){
mediaPlayer = new VlcPlayer();
}else if (audiotype.equalsIgnoreCase("mp4")){
mediaPlayer = new Mp4Player();
}
}
@Override
public void play(String audioType, String filename) {
if (audioType.equalsIgnoreCase("vlc")){
mediaPlayer.playVlc(filename);
}else if (audioType.equalsIgnoreCase("mp4")){
mediaPlayer.playMp4(filename);
}
}
}
Mp3接口的實現類Mp3Player:
public class Mp3Player implements Mp3{
//定義一個接口類型的引用變量來引來實現了該接口的實現類實例對象
MediaAdapter mediaAdapter;
@Override
public void play(String audioType, String filename) {
if (audioType.equalsIgnoreCase("mp3")){
System.out.println("Playing the MP3 file,filename: " + filename);
}else if (audioType.equalsIgnoreCase("mp4") || audioType.equalsIgnoreCase("vlc")){
mediaAdapter = new MediaAdapter(audioType);
mediaAdapter.play(audioType,filename);
}else{
System.out.println("該設備不支持這種音頻格式播放!");
}
}
}
測試類AdapterPatternDemo:
public class AdapterPatternDemo {
public static void main(String[] args) {
Mp3Player mp3Player = new Mp3Player();
mp3Player.play("vlc","vlcfile1");
mp3Player.play("mp4","mp4file1");
mp3Player.play("mp3","mp3file1");
mp3Player.play("xml","xmlfile1");
}
}
執行結果:
Playing the Vlc file,filename: vlcfile1
Playing the MP4 file,filename: mp4file1
Playing the MP3 file,filename: mp3file1
該設備不支持這種音頻格式播放!
5.5、裝飾器模式
裝飾器模式(Decorator Pattern)允許向一個現有的對象添加新的功能,同時又不改變其結構。這種設計模式屬於結構型模式,它是作爲現有類的一個包裝。裝飾器模式創建了一個裝飾類,用來包裝原有的類,並且在保持類方法簽名完整性的前提下,提供額外的功能。
主要解決:一般來說,我們爲擴展一個類經常使用繼承的方式實現,但由於繼承爲類引入了靜態特性,並且隨着擴展功能的增多,子類會很膨脹。所以當我們不想增加很多子類時我們就可以使用裝飾器模式。
優點:裝飾類和被裝飾類可以獨立發展,不會相互耦合,裝飾模式是繼承的一個替代模式,裝飾模式可以動態擴展一個實現類的功能。
缺點:多層裝飾比較複雜。
使用場景:
- 擴展一個類的功能。
- 動態增加功能、動態撤銷。
實現示例:把一個形狀裝飾上不同的顏色,同時又不改變形狀類。創建一個Shape接口和實現了該接口的實體類,然後創建一個實現了Shape接口的抽象裝飾類ShapeDecorator,並把Shape對象作爲它的實例變量。RedShapeDecorator是實現了ShapeDecorator的實體類。我們在演示類DecoratorPatternDemo中使用RedShapeDecorator來裝飾Shape對象。
Shape接口及實現類Circle、Rectangle:
public interface Shape {
public void draw();
}
public class Circle implements Shape{
@Override
public void draw() {
System.out.println("這是一個圓形!");
}
}
public class Rectangle implements Shape{
@Override
public void draw() {
System.out.println("這是一個長方形!");
}
}
抽象的裝飾器類ShapeDecorator:
public abstract class ShapeDecorator implements Shape{
protected Shape decoratorShape;
public ShapeDecorator(Shape decoratorShape){
this.decoratorShape = decoratorShape;
}
public void draw(){
decoratorShape.draw();
}
}
抽象類的實現類RedShapeDecorator:
public class RedShapeDecorator extends ShapeDecorator{
public RedShapeDecorator(Shape decoratorShape) {
super(decoratorShape);
}
@Override
public void draw(){
decoratorShape.draw();
setRedBorder(decoratorShape);
}
public void setRedBorder(Shape decoratedShape){
System.out.println("邊框顏色:紅色!");
}
}
測試類DecoratorPatternDemo:
public class DecoratoePatternDemo {
public static void main(String[] args) {
Shape circle = new Circle();
Shape redcircle = new RedShapeDecorator(new Circle());
Shape redrectangle = new RedShapeDecorator(new Rectangle());
System.out.println("圓形邊框顏色正常!");
circle.draw();
System.out.println("\n紅色邊框的圓形");
redcircle.draw();
System.out.println("\n紅色邊框的長方形");
redrectangle.draw();
}
}
執行結果:
圓形邊框顏色正常!
這是一個圓形!
紅色邊框的圓形
這是一個圓形!
邊框顏色:紅色!
紅色邊框的長方形
這是一個長方形!
邊框顏色:紅色!
5.6、代理模式
在代理模式(Proxy Pattern)中,一個類代表另一個類的功能。這種設計模式屬於結構型模式。在代理模式中我們創建具有現有對象的對象,以便向外界提供功能接口。
主要解決:在直接訪問對象時帶來的問題,比如說:要訪問的對象在遠程機器上。在面向對象系統中,有些對象由於某些原因(比如對象創建開銷很大,或者某些操作需要安全控制,或者需要進程外的訪問),直接訪問會給使用者帶來很多麻煩,我們可以在訪問此對象時加上一個對此對象的訪問層。主要解決思路就是加上一個中間層。
何時使用:想在訪問一個類時做一些控制。
優點:
- 職責清晰。
- 高擴展性。
- 智能化。
缺點:
- 由於在客戶端和真實主題之間增加了代理對象,因此有些類型的代理模式可能會造成請求的處理速度變慢。
- 實現代理模式需要額外的工作,有些代理模式的實現非常複雜。
常見使用場景:
- 遠程代理、虛擬代理、Copy-on-Write代理、保護代理、Cache代理、防火牆代理、同步化代理、智能引用代理。
- Spring aop。
注意事項:
- 和適配器模式的區別:適配器模式主要改變所考慮對象的接口,而代理模式不能改變所代理類的接口。
- 和裝飾器模式的區別:裝飾器模式是爲了增強功能,而代理模式是爲了加以控制。
實現示例:經紀人和明星就是一個很明顯的代理關係。一家公司想請明星來公司參加開業活動,那麼公司一定是和經紀人取得聯繫,通過經紀人來安排明星出席開業活動,就相當於通過經紀人來控制明星的行爲。定義一個接口Activity,明星類Star和經紀人Proxy都實現了這個接口,然後在演示類ProxyPatternDemo中來使用Proxy對象控制Star對象(經紀人接下這個活動實際是由明星來參加)。
Activity接口及實現類Star、Proxy:
public interface Activity {
public void campaign();
}
public class Star implements Activity{
private String activityname;
public Star(String activityname){
this.activityname = activityname;
}
@Override
public void campaign() {
System.out.println("我是明星,我來參加" + activityname +"活動!");
}
}
public class Proxy implements Activity{
private Star star;
private String activityname;
public Proxy(String activityname){
this.activityname = activityname;
}
@Override
public void campaign() {
if (star == null){
star = new Star(activityname);
}
star.campaign();
}
}
演示類ProxyPatternDemo:
public class ProxyPatternDemo {
public static void main(String[] args) {
Activity activity = new Proxy("九九隆開業慶典");
//經紀人接下這個活動,由明星來參加
activity.campaign();
}
}
執行結果:
我是明星,我來參加九九隆開業慶典活動!
5.7、策略模式
在策略模式(Strategy Pattern)中,一個類的行爲或算法可以在運行時更改。這種設計模式屬於行爲型模式。在策略模式中,我們創建表示各種策略的對象和一個行爲隨着策略對象改變而改變的context對象,策略對象改變context對象的執行算法。
主要解決:在有多種算法相似的情況下,使用if...else所帶來的複雜和難以維護。
何時使用:一個系統中有很多很多類,而區分它們的只是他們直接的行爲。
如何解決:將這些算法封裝成一個一個的類,任意地替換。關鍵是要實現同一個接口。
優點:
- 算法可以自由切換。
- 避免使用多重條件判斷。
- 擴展性良好。
缺點:
- 策略類會增多。
- 策略類會對外暴露。
常見使用場景:
- 旅遊出行的交通方式,選擇騎自行車、坐汽車、搭乘飛機,每一種出行方式就是一種策略。
- 如果在一個系統中有許多類,它們之間的卻別僅在於它們的行爲,那麼使用策略模式可以動態地讓一個對象在許多種行爲中選擇一種行爲。
- 一個系統需要動態地在幾種算法中選擇一種。
- 如果一個對象有很多行爲,如果不用恰當的模式,這些行爲就只好使用多重的條件選擇語句來實現。
注意事項:如果一個系統的策略多於4個,就需要考慮使用混合模式,解決策略類膨脹的問題。
實現示例:定義一個數字運算接口Strategy,以及三個實現了該接口的實現類OperationAdd、OperationSub、OperationMul,這三個實現類代表了三個不同的算法策略。定義一個Context類來選擇執行策略,並在StrategyPatternDemo演示類中進行演示。
Strategy接口以及三個實現類:
public interface Strategy {
public int doOperation(int num1, int num2);
}
public class OperationAdd implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 + num2;
}
}
public class OperationSub implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 - num2;
}
}
public class OperationMul implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 * num2;
}
}
策略選擇類Context:
public class Context {
private Strategy strategy;
public Context(Strategy strategy){
this.strategy = strategy;
}
public int executeStrategy(int num1, int num2){
return strategy.doOperation(num1, num2);
}
}
演示類StrategyPatternDemo:
public class StrategyPatternDemo {
public static void main(String[] args) {
Context context = new Context(new OperationAdd());
System.out.println("3 + 8 = " + context.executeStrategy(3, 8));
context = new Context(new OperationSub());
System.out.println("3 - 8 = " + context.executeStrategy(3, 8));
context = new Context(new OperationMul());
System.out.println("3 * 8 = " + context.executeStrategy(3, 8));
}
}
執行結果:
3 + 8 = 11
3 - 8 = -5
3 * 8 = 24
5.8、觀察者模式
當對象存在一對多關係時,則使用觀察者模式(Observer Pattern)。比如當一個對象被修改時,則自動通知它的依賴對象。觀察者模式屬於行爲型模式(定義對象間一對多的依賴關係,當一個對象的狀態發生改變時,所有依賴於它的對象都得到通知並自動更新)。比如說在拍賣時,拍賣師要觀察出價最高的人,然後通知給其他參與競價的人。
主要解決:一個對象狀態改變給其他對象通知的問題,而且要考慮到易用和低耦合,保證高度的協作。
關鍵代碼:在抽象類中有一個ArrayList存放觀察者們。
優點:
- 觀察者和被觀察者是抽象耦合的。
- 建立一套觸發機制。
缺點:
- 如果一個被觀察者有很多直接或間接的觀察者的話,將所有觀察者都通知到是一件很麻煩的事,要花費很多時間。
- 如果在觀察者和觀察目標之間有循環依賴的話,觀察目標會觸發它們之間的循環調用,可能調至系統崩潰。
- 觀察者模式沒有相應的機制讓觀察者知道觀察目標是如何發生變化的,而僅僅是知道觀察目標發生了變化。
常見使用場景:
- 一個抽象模型有兩個方面,其中一個方面依賴於另一個方面。將這些方面封裝在獨立的對象中使它們獨立地改變和複用。
- 一個對象的改變將導致其他一個或多個對象也發生變化,而不知道具體有多少對象將發生改變,可以降低對象之間的耦合度。
- 一個對象必須通知其他對象,而不知道這些對象是誰。
注意事項:
- Java中已經有了對觀察者模式的支持類。
- 避免循環引用。
- 如果順序執行,某一觀察者錯誤會導致系統卡殼,一般採用異步方式。
實現示例:觀察者模式使用三個類Subject、Observer、Client。Subject對象帶有綁定觀察者到Clinet對象和從Client對象解綁觀察者的方法。我們創建Subject類、Observer抽象類和擴展了抽象類Observer的實體類。在演示類ObserverPatternDemo中使用Subject和實體類對象來演示觀察者模式。
觀察目標Subject類:
import java.util.ArrayList;
import java.util.List;
//Subject是觀察目標,當subject的私有屬性值state發生改變時,它的觀察者們也做出相應的改變
public class Subject {
//將觀察者們添加到ArrayList集合中
private List<Observer> observers = new ArrayList<Observer>();
//表示要改變的值
private int state;
public int getState(){
return state;
}
public void setState(int state){
this.state = state;
notifyAllObservers();
}
//增加觀察者對象
public void attach(Observer observer){
observers.add(observer);
}
//遍歷ArrayList中的觀察者對象並執行觀察者們的update()方法
public void notifyAllObservers(){
for (Observer observer : observers){
observer.update();
}
}
}
觀察者模板,抽象類Observer:
//構建觀察者模板
public abstract class Observer {
//這是觀察目標
protected Subject subject;
//更新方法
public abstract void update();
}
觀察者BinaryObserver類:
public class BinaryObserver extends Observer{
//構造函數中傳入觀察目標,並將自己添加到觀察目標subject保存的觀察者集合中
public BinaryObserver(Subject subject){
this.subject = subject;
this.subject.attach(this);
}
//Integer.toBinaryString()就是將輸入的數字轉換成二進制數,但是轉換輸出的是String類型的字符串
@Override
public void update() {
System.out.println("轉成二進制數是 :" + Integer.toBinaryString(subject.getState()));
}
}
觀察者OctalObserver類:
public class OctalObserver extends Observer{
public OctalObserver(Subject subject){
this.subject = subject;
this.subject.attach(this);
}
//Integer.toOctalString()是將十進制數轉換成八進制數並以字符串的類型輸出
@Override
public void update() {
System.out.println("轉成八進制數是 :" + Integer.toOctalString(subject.getState()));
}
}
觀察者HexaObserver類:
public class HexaObserver extends Observer{
public HexaObserver(Subject subject){
this.subject = subject;
this.subject.attach(this);
}
//Integer.toHexString()是將十進制數轉成十六進制並以字符串類型輸出
//toUpperCase()是將小寫字符轉換成大寫字符
@Override
public void update() {
System.out.println("轉成十六進制數是 :" + Integer.toHexString(subject.getState()).toUpperCase());
}
}
演示類ObserverPatternDemo:
public class ObserverPatternDemo {
public static void main(String[] args) {
Subject subject = new Subject();
new BinaryObserver(subject);
new OctalObserver(subject);
new HexaObserver(subject);
System.out.println("第一次state值改變爲: 15");
subject.setState(15);
System.out.println("\n第二次state值改變爲: 10");
subject.setState(10);
}
}
執行結果:
第一次state值改變爲: 15
轉成二進制數是 :1111
轉成八進制數是 :17
轉成十六進制數是 :F
第二次state值改變爲: 10
轉成二進制數是 :1010
轉成八進制數是 :12
轉成十六進制數是 :A