設計模式學習筆記

Grey Zeng發表於2022-01-07

作者:Grey

原文地址: 設計模式學習筆記

UML和程式碼

UML圖

程式碼

軟體設計七大原則

設計原則 一句話歸納 目的
開閉原則 對擴充套件開放,對修改關閉 降低維護帶來的新風險
依賴倒置原則 高層不應該依賴低層 更利於程式碼結構的升級擴充套件
單一職責原則 一個類只幹一件事 便於理解,提高程式碼的可讀性
介面隔離原則 一個介面只幹一件事 功能解耦,高聚合,低耦合
迪米特法則 不該知道的不要知道 只和朋友交流,不和陌生人說話,減少程式碼臃腫
里氏替換原則 子類重寫方法功能發生改變,不應該影響父類方法的含義 防止繼承氾濫
合成複用原則 儘量使用組合實現程式碼複用,而不使用繼承 降低程式碼耦合

單例模式

單例模式是建立型模式。

單例的定義:“一個類只允許建立唯一一個物件(或者例項),那這個類就是一個單例類,這種設計模式就叫作單例設計模式,簡稱單例模式。”定義中提到,“一個類只允許建立唯一一個物件”。那物件的唯一性的作用範圍是指程式內只允許建立一個物件,也就是說,單例模式建立的物件是程式唯一的(而非執行緒)

image

為什麼要使用單例

  1. 處理資源訪問衝突

    比如寫日誌的類,如果不使用單例,就必須使用鎖機制來解決日誌被覆蓋的問題。

  2. 表示全域性唯一類

    比如配置資訊類,在系統中,只有一個配置檔案,當配置檔案載入到記憶體中,以物件形式存在,也理所應當只有一份。

    唯一ID生成器也是類似的機制。如果程式中有兩個物件,那就會存在生成重複 ID 的情況,所以,我們應該將 ID 生成器類設計為單例。

餓漢式

類載入的時候就會初始化這個例項,JVM保證唯一例項,執行緒安全,但是可以通過反射破壞

方式一

public class Singleton1 {
    private final static Singleton1 INSTANCE = new Singleton1();

    private Singleton1() {
    }

    public static Singleton1 getInstance() {
        return INSTANCE;
    }
}

方式二

public class Singleton2 {
    private static final Singleton2 INSTANCE;

    static {
        INSTANCE = new Singleton2();
    }
    private Singleton2() {
    
    }
    public static Singleton2 getInstance() {
        return INSTANCE;
    }
}

這種方式不支援延遲載入,如果例項佔用資源多(比如佔用記憶體多)或初始化耗時長(比如需要載入各種配置檔案),提前初始化例項是一種浪費資源的行為。最好的方法應該在用到的時候再去初始化。不過,如果初始化耗時長,那最好不要等到真正要用它的時候,才去執行這個耗時長的初始化過程,這會影響到系統的效能,我們可以將耗時的初始化操作,提前到程式啟動的時候完成,這樣就能避免在程式執行的時候,再去初始化導致的效能問題。如果例項佔用資源多,按照 fail-fast 的設計原則(有問題及早暴露),那我們也希望在程式啟動時就將這個例項初始化好。如果資源不夠,就會在程式啟動的時候觸發報錯(比如 Java中的PermGen Space OOM),我們可以立即去修復。這樣也能避免在程式執行一段時間後,突然因為初始化這個例項佔用資源過多,導致系統崩潰,影響系統的可用性。

這兩種方式都可以通過反射方式破壞,例如:

Class<?> aClass=Class.forName("singleton.Singleton2",true,Thread.currentThread().getContextClassLoader());
Singleton2 instance1=(Singleton2)aClass.newInstance();
Singleton2 instance2=(Singleton2)aClass.newInstance();
System.out.println(instance1==instance2);

輸出:false

懶漢式

雖然可以實現按需初始化,但是執行緒不安全, 因為在判斷 INSTANCE == null的時候,如果是多個執行緒操作的話, 一個執行緒還沒有把 INSTANCE初始化好,另外一個執行緒判斷 INSTANCE==null得到true,就會繼續初始化

public class Singleton3 {
    private static Singleton3 INSTANCE;

    private Singleton3() {
    }

    public static Singleton3 getInstance() {
        if (INSTANCE == null) {
            // 模擬初始化物件需要的耗時操作
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Singleton3();
        }
        return INSTANCE;
    }
}

為了防止執行緒不安全,可以在 getInstance方法上加鎖,這樣既實現了按需初始化,又保證了執行緒安全,

但是加鎖可能會導致一些效能的問題:我們給 getInstance()這個方法加了一把大鎖,導致這個函式的併發度很低。量化一下的話,併發度是 1,也就相當於序列操作了。而這個函式是在單例使用期間,一直會被呼叫。如果這個單例類偶爾會被用到,那這種實現方式還可以接受。但是,如果頻繁地用到,那頻繁加鎖、釋放鎖及併發度低等問題,會導致效能瓶頸,這種實現方式就不可取了。

public class Singleton4 {
    private static Singleton4 INSTANCE;

    private Singleton4() {
    }

    public static synchronized Singleton4 getInstance() {
        if (INSTANCE == null) {
            // 模擬初始化物件需要的耗時操作
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Singleton4();
        }
        return INSTANCE;
    }
}

為了提升一點點效能,可以不給 getInstance()整個方法加鎖,而是對 INSTANCE判空這段程式碼加鎖, 但是又帶來了執行緒不安全的問題

public class Singleton5 {
    private static Singleton5 INSTANCE;

    private Singleton5() {
    }

    public static Singleton5 getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton5.class) {
                // 模擬初始化物件需要的耗時操作
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                INSTANCE = new Singleton5();
            }
        }
        return INSTANCE;
    }
}

Double Check Locking模式,就是雙加鎖檢查模式,這種方式中,Volatile是必需的,目的為了防止指令重排,生成一個半初始化的的例項,導致生成兩個例項。

具體可參考 雙重檢索(DCL)的思考: 為什麼要加volatile?
說了這個問題。

public class Singleton6 {
    private volatile static Singleton6 INSTANCE;

    private Singleton6() {
    }

    public static Singleton6 getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton6.class) {
                if (INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Singleton6();
                }
            }
        }
        return INSTANCE;
    }
}

以下兩種更為優雅的方式,既保證了執行緒安全,又實現了按需載入。

方式一:靜態內部類方式,JVM保證單例,載入外部類時不會載入內部類,這樣可以實現懶載入

public class Singleton7 {
    private Singleton7() {
    }

    public static Singleton7 getInstance() {
        return Holder.INSTANCE;
    }

    private static class Holder {
        private static final Singleton7 INSTANCE = new Singleton7();
    }

}

方式二: 使用列舉, 這是實現單例模式的最佳方法。它更簡潔,自動支援序列化機制,絕對防止多次例項化,這種方式是 Effective Java 作者
Josh Bloch 提倡的方式,它不僅能避免多執行緒同步問題,而且還自動支援序列化機制,防止反序列化重新建立新的物件,絕對防止多次例項化。

public enum Singleton8 {
    INSTANCE;
}

單例模式的替代方案

使用靜態方法

   // 靜態方法實現方式
public class IdGenerator {
    private static AtomicLong id = new AtomicLong(0);
   
    public static long getId() { 
       return id.incrementAndGet();
    }
}

// 使用舉例
long id = IdGenerator.getId();

使用依賴注入

   
   // 1. 老的使用方式
   public demofunction() {
     //...
     long id = IdGenerator.getInstance().getId();
     //...
   }
   
   // 2. 新的使用方式:依賴注入
   public demofunction(IdGenerator idGenerator) {
     long id = idGenerator.getId();
   }
   // 外部呼叫demofunction()的時候,傳入idGenerator
   IdGenerator idGenerator = IdGenerator.getInsance();
   demofunction(idGenerator);

執行緒單例

通過一個 HashMap來儲存物件,其中 key 是執行緒 ID,value 是物件。這樣我們就可以做到,不同的執行緒對應不同的物件,同一個執行緒只能對應一個物件。實際上,Java 語言本身提供了 ThreadLocal工具類,可以更加輕鬆地實現執行緒唯一單例。不過,ThreadLocal底層實現原理也是基於下面程式碼中所示的 HashMap


public class IdGenerator {
  private AtomicLong id = new AtomicLong(0);

  private static final ConcurrentHashMap<Long, IdGenerator> instances = new ConcurrentHashMap<>();

  private IdGenerator() {}

  public static IdGenerator getInstance() {
    Long currentThreadId = Thread.currentThread().getId();
    instances.putIfAbsent(currentThreadId, new IdGenerator());
    return instances.get(currentThreadId);
  }

  public long getId() {
    return id.incrementAndGet();
  }
}

叢集模式下單例

我們需要把這個單例物件序列化並儲存到外部共享儲存區(比如檔案)。程式在使用這個單例物件的時候,需要先從外部共享儲存區中將它讀取到記憶體,並反序列化成物件,然後再使用,使用完成之後還需要再儲存回外部共享儲存區。為了保證任何時刻,在程式間都只有一份物件存在,一個程式在獲取到物件之後,需要對物件加鎖,避免其他程式再將其獲取。在程式使用完這個物件之後,還需要顯式地將物件從記憶體中刪除,並且釋放對物件的加鎖。

如何實現一個多例模式

“單例”指的是一個類只能建立一個物件。對應地,“多例”指的就是一個類可以建立多個物件,但是個數是有限制的,比如只能建立 3 個物件。多例的實現也比較簡單,通過一個 Map 來儲存物件型別和物件之間的對應關係,來控制物件的個數。

單例模式的應用舉例

  • JDK的 Runtime
public class Runtime {
  private static Runtime currentRuntime = new Runtime();

  public static Runtime getRuntime() {
    return currentRuntime;
  }
  
  /** Don't let anyone else instantiate this class */
  private Runtime() {}
.......
}
  • Spring中 AbstractBeanFactory中包含的兩個功能。
    • 從快取中獲取單例Bean
    • 從Bean的例項中獲取物件

工廠模式

工廠模式是建立型模式。

簡單工廠

這個模式很簡單,比如我們需要製造不同型別的滑鼠,我們只需要建立一個滑鼠工廠

public class MouseFactory {
    public static Mouse createMouse(int type) {
        switch (type) {
            case 1:
                return new HpMouse();
            case 2:
                return new LenovoMouse();
            case 0:
            default:
                return new DellMouse();
        }
    }

    public static void main(String[] args) {
        Mouse mouse = MouseFactory.createMouse(1);
        mouse.sayHi();
    }
}

根據不同的type來建立不同的滑鼠即可。這個模式的缺點很明顯:違反了開閉原則 ,所以我們引入工廠方法

工廠方法

工廠方法中,我們可以定義對應產品的對應工廠,以上面這個滑鼠的例子為例,我們可以增加工廠的介面

public interface MouseFactory {
    Mouse createMouse();
}

不同型別的滑鼠工廠實現這個工廠即可,以Dell滑鼠工廠為例

public class DellMouseFactory implements MouseFactory {
    @Override
    public Mouse createMouse() {
        return new DellMouse();
    }
}

主函式在呼叫的時候,直接指定工廠即可製造對應的產品了:

public class FactoryMethodDemo {
    public static void main(String[] args) {
        MouseFactory mf = new HpMouseFactory();
        Mouse mouse = mf.createMouse();
        mouse.sayHi();
    }
}

工廠方法的優點是符合開閉原則,但是缺點也很明顯,就是在增加子類的時候,同時要增加一個子類的工廠,而且,只支援同一類產品的建立,不適用於同一產品族

抽象工廠

舉例,現在需要通過工廠來製造交通工具,如果是現代的工廠,製造的就是汽車,如果是古代的工廠,製造的就是馬車, 我們可以先把工廠抽象出來,

package factory.abstractfactory;

/**
 * @author Grey
 * @date 2020/4/13
 */
public abstract class AbstractFactory {
    /**
     * 子類實現
     *
     * @return
     */
    protected abstract Transportation createTransportation();

    /**
     * 子類實現
     *
     * @return
     */
    protected abstract WritingInstrument createWritingInstrument();
}

交通工具我們也可以抽象出來

public abstract class Transportation {
    protected abstract void go();
}

對於馬車和汽車來說,只需要繼承這個Transportation類,實現對應的go方法即可,以汽車為例

public class Car extends Transportation {
    @Override
    protected void go() {
        System.out.println("car go");
    }
}

對於現代工廠還是古代工廠,我們只需要繼承AbstractFactory這個類,實現createTransportation方法即可,以現代工廠為例

package factory.abstractfactory;

/**
 * @author Grey
 * @date 2020/4/13
 */
public class ModernFactory extends AbstractFactory {

    @Override
    protected Transportation createTransportation() {
        return new Car();
    }

    @Override
    protected WritingInstrument createWritingInstrument() {
        return new Pen();
    }
}

主方法在呼叫的時候,只需要

public class Main {
    public static void main(String[] args) {
        AbstractFactory factory = new ModernFactory();
        factory.createTransportation().go();
    }
}

抽象工廠的UML圖如下:

image

Java8提供了Supplier這個函式式介面,我們可以通過這個介面很方便的實現工廠類,舉例:

我們可以定義一個 MovableFactory,裡面的 create方法,傳入的是一個 Supplier,你可以把所有 Movable的子類實現傳給這個引數,示例如下:

public class MovableFactory {
    public static Movable create(Supplier<? extends Movable> supplier) {
        return supplier.get();
    }

    public static void main(String[] args) {
        MovableFactory.create(Car::new).go();
        MovableFactory.create(() -> new Ship()).go();
    }
}

注:單例模式就是一種工廠模式(靜態工廠)

工廠模式應用

  • JDK中 Calendar.getInstance()方法
  • LogBack中 LoggerFactory.getLogger()方法
  • 在Spring中,所有工廠都是 BeanFactory的子類。通過對 BeanFactory的實現,我們可以從Spring的容器訪問Bean。根據不同的策略呼叫 getBean()方法,從而獲得具體物件。
  • Hibernate換資料庫只需換方言和驅動就可以切換不同資料庫

建造者模式

建造者模式是建立型模式。

我們在對一個實體類進行屬性的get/set的時候,可以通過封裝一些常用的構造方法來簡化實體類的構造。

比如 Effective Java中文版(第3版) 中舉到到這個例子

package builder;

// Effective Java 3th examples
public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;

    public static class Builder {
        // Required parameters
        private final int servingSize;
        private final int servings;

        // Optional parameters - initialized to default values
        private int calories      = 0;
        private int fat           = 0;
        private int sodium        = 0;
        private int carbohydrate  = 0;

        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings    = servings;
        }

        public Builder calories(int val) { 
            calories = val;  
            return this;
        }

        public Builder fat(int val) { 
           fat = val;   
           return this;
        }

        public Builder sodium(int val) { 
           sodium = val;  
           return this; 
        }

        public Builder carbohydrate(int val) { 
           carbohydrate = val;  
           return this; 
        }

        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }

    private NutritionFacts(Builder builder) {
        servingSize  = builder.servingSize;
        servings     = builder.servings;
        calories     = builder.calories;
        fat          = builder.fat;
        sodium       = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
}

其中Builder就是一個內部類,用於構造NutritionFacts的必要資訊,外部呼叫NutritionFacts的構造方法時候,可以這樣使用:

NutritionFacts cocaCola=new NutritionFacts.Builder(240,8).calories(100).sodium(35).carbohydrate(27).build();

image

構造器模式也適用於類層次結構。抽象類有抽象的Builder,具體類有具體的Builder。Effective Java中文版(第3版)
中還有一個例子, 假設我們抽象出一個披薩類,各種各樣的披薩均可以繼承披薩這個抽象類來實現自己的具體型別的披薩。

Pizza抽象類如下:

package builder;

import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;

// Effective Java 3th examples
public abstract class Pizza {
    public enum Topping {HAM, MUSHROOM, ONION, PEPPER, SAUSAGE}
    final Set<Topping> toppings;
  
    abstract static class Builder<T extends Builder<T>> {
        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);

        public T addTopping(Topping topping) {
            toppings.add(Objects.requireNonNull(topping));
            return self();
        }
  
        abstract Pizza build();
  
        // Subclasses must override this method to return "this"
        protected abstract T self();
    }

    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone(); // See Item 50
    }
}

其中的Builder方法是abstract的,所以子類需要實現具體的Builder策略,

一種披薩的具體實現:NyPizza

import java.util.Objects;

public class NyPizza extends Pizza {
    public enum Size {SMALL, MEDIUM, LARGE}

    private final Size size;

    public static class Builder extends Pizza.Builder<Builder> {
        private final Size size;

        public Builder(Size size) {
            this.size = Objects.requireNonNull(size);
        }

        @Override
        public NyPizza build() {
            return new NyPizza(this);
        }

        @Override
        protected Builder self() {
            return this;
        }
    }

    private NyPizza(Builder builder) {
        super(builder);
        size = builder.size;
    }
}

另一種披薩的具體實現Calzone:

public class Calzone extends Pizza {
    private final boolean sauceInside;

    public static class Builder extends Pizza.Builder<Builder> {
        private boolean sauceInside = false; // Default

        public Builder sauceInside() {
            sauceInside = true;
            return this;
        }

        @Override
        public Calzone build() {
            return new Calzone(this);
        }

        @Override
        protected Builder self() {
            return this;
        }
    }

    private Calzone(Builder builder) {
        super(builder);
        sauceInside = builder.sauceInside;
    }
}

我們在具體呼叫的時候,可以通過如下方式:

NyPizza pizza=new NyPizza.Builder(SMALL).addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzone=new Calzone.Builder().addTopping(HAM).sauceInside().build();

實際應用有非常多,很多元件都提供這樣的構造方式,比如OkHttpClient的構造方法:

    public static OkHttpClient create(long connectTimeOut) {
        return new OkHttpClient().newBuilder().connectionSpecs(Arrays.asList(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS, ConnectionSpec.CLEARTEXT)).connectTimeout(connectTimeOut, TimeUnit.SECONDS).readTimeout(30, TimeUnit.SECONDS).writeTimeout(30, TimeUnit.SECONDS).connectionPool(CONNECTION_POOL).retryOnConnectionFailure(true).followRedirects(true).followSslRedirects(true).hostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String s, SSLSession sslSession) {
                return true;
            }
        }).cookieJar(new CookieJar() {
            private List<Cookie> cookies;

            @Override
            public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
                this.cookies = cookies;
            }

            @Override
            public List<Cookie> loadForRequest(HttpUrl url) {
                if (cookies != null) {
                    return cookies;
                }
                return Collections.emptyList();
            }
        }).build();
    }

應用

  • JDK中的Calender
Calendar calendar = new Calendar.Builder().build();
  • MyBatis中 CacheBuilder.build()SqlSessionFactoryBuilder.build()

  • Spring中 BeanDefinitionBuilder.getBeanDefinition()方法

原型模式

原型模式是建立型模式。

如果物件的建立成本比較大,而同一個類的不同物件之間差別不大(大部分欄位都相同),在這種情況下,我們可以利用對已有物件(原型)進行復制(或者叫拷貝)的方式來建立新物件,以達到節省建立時間的目的。這種基於原型來建立物件的方式就叫作原型設計模式(Prototype Design Pattern),簡稱原型模式。

實際上,建立物件包含的申請記憶體、給成員變數賦值這一過程,本身並不會花費太多時間,或者說對於大部分業務系統來說,這點時間完全是可以忽略的。應用一個複雜的模式,只得到一點點的效能提升,這就是所謂的過度設計,得不償失。但是,如果物件中的資料需要經過複雜的計算才能得到(比如排序、計算雜湊值),或者需要從 RPC、網路、資料庫、檔案系統等非常慢速的 IO 中讀取,這種情況下,我們就可以利用原型模式,從其他已有物件中直接拷貝得到,而不用每次在建立新物件的時候,都重複執行這些耗時的操作。

原型模式用原型例項指定建立物件的種類,並且通過拷貝這些原型建立新的物件,典型的應用是物件的克隆方法

public class Person implements Cloneable {
    String name = "lisa";
    int age = 1;
    Location loc = new Location("xy", 10);

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Person p = (Person) super.clone();
        p.loc = (Location) loc.clone();
        return p;
    }

    @Override
    public String toString() {
        return "Person{" + "name='" + name + '\'' + ", age=" + age + ", loc=" + loc + '}';
    }
}
public class Location implements Cloneable {
    private String street;
    private int roomNo;

    public Location(String street, int roomNo) {
        this.street = street;
        this.roomNo = roomNo;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public String toString() {
        return "Location{" + "street='" + street + '\'' + ", roomNo=" + roomNo + '}';
    }
}
public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person p = new Person();
        System.out.println(p);
        Person p2 = (Person) p.clone();
        System.out.println(p2);
    }
}

UML圖如下:

image

注:Java自帶的 clone()方法進行的就是淺克隆。而如果我們想進行深克隆,可以直接在 super.clone()後,手動給克隆物件的相關屬性分配另一塊記憶體,不過如果當原型物件維護很多引用屬性的時候,手動分配會比較煩瑣。因此,在Java中,如果想完成原型物件的深克隆,則通常使用序列化(Serializable)的方式。

使用示例

克隆一個巨大的HashMap,如果構建雜湊表的代價很大,我們可以通過

  1. HashMap的clone方法(注意:預設的clone方法是淺拷貝,需要遞迴拷貝HashMap裡面的內容,直到型別是基礎型別為止)
  2. 使用序列化方式克隆

如果只是增量拷貝,可以通過淺拷貝拿到一個新的HashMap,然後拿到增量的資料單獨進行深拷貝即可。

Spring中建立物件的方式預設採用單例模式,可以通過設定 @Scope("prototype")註解將其改為原型模式。

代理模式

代理模式是結構型模式。

靜態代理

舉例說明,假設我們需要在某個類的某段程式碼的前後加上日誌記錄,我們就可以通過靜態代理的方式實現

public class Main {
    public static void main(String[] args) {
        new Tank().move();
    }
}

假設我們需要在move()方法的前後都加上日誌記錄,我們可以設定一個代理類

public class TankLogProxy implements Moveable {
    private Moveable m;

    public TankLogProxy(Moveable m) {
        this.m = m;
    }

    @Override
    public void move() {
        System.out.println("log before");
        m.move();
        System.out.println("log after");
    }
}

這樣的話,原先的呼叫就改成了:

public class Main {
    public static void main(String[] args) {
        new TankLogProxy(new Tank()).move();
    }
}

即可實現在move方法呼叫前後加入日誌記錄的操作。

UML圖如下:

image

動態代理

JDK自帶

如果需要通過動態代理(jdk自帶的方式)的方式來完成上述功能,我們可以這樣來做

public class MovableProxy implements InvocationHandler {
    private Movable movable;

    public MovableProxy(Movable movable) {
        this.movable = movable;
    }

    public void before() {
        System.out.println("before , do sth");
    }

    public void after() {
        System.out.println("after , do sth");
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object o = method.invoke(movable, args);
        after();
        return o;
    }
}

主方法呼叫的時候:

package proxy.dynamic.jdk;

import java.lang.reflect.Proxy;

/**
 * @author Grey
 * @date 2020/4/15
 */
public class Main {
    public static void main(String[] args) {
        Movable tank = new Tank();

        //reflection 通過二進位制位元組碼分析類的屬性和方法

        Movable m = (Movable) Proxy.newProxyInstance(Movable.class.getClassLoader(),
                new Class[]{Movable.class},
                new MovableProxy(tank)
        );

        m.move();
        m.go();
    }
}

UML圖如下:

image

Cglib

JDK自帶的方式實現動態代理需要被代理物件實現一個介面,Cglib不需要,使用示例:

其中被代理的Tank類無需實現介面

public class Tank {
    public void move() {
        System.out.println("tank move");
    }
    public void go() {
        System.out.println("tank go");
    }
}
import net.sf.cglib.proxy.Enhancer;

public class Main {

    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        //設定目標類的位元組碼檔案
        enhancer.setSuperclass(Tank.class);
        //設定回撥函式
        enhancer.setCallback(new MyMethodInterceptor());

        //這裡的creat方法就是正式建立代理類
        Tank m = (Tank) enhancer.create();
        m.move();
        m.go();
    }
}
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class MyMethodInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        before();
        Object o = proxy.invokeSuper(obj, args);
        after();
        return o;
    }

    public void before() {
        System.out.println("before , do sth");
    }

    public void after() {
        System.out.println("after , do sth");
    }
}

實際應用

  • 在業務系統中開發一些非功能性需求,比如:監控、統計、鑑權、限流、事務、冪等、日誌。我們將這些附加功能與業務功能解耦,放到代理類中統一處理。
  • RPC框架可以看成一種代理模式。
  • 為介面增加快取能力。
  • Spring AOP
    • JdkDynamicAopProxy
    • CglibAopProxy
    • 可以使用 <aop:aspectj-autoproxy proxy-target-class="true">配置強制使用Cglib動態代理
  • jdk自帶
    • ASM操作二進位制碼
    • Java Instrumentation
    • 必須面向介面
  • cglib
    • final類不行,代理類的子類 底層也是ASM

橋接模式

橋接模式是一種結構型模式。

使用橋接模式,可以將抽象和具體的發展單獨分支(抽象中持有一個具體的引用 )
舉例說明:

GG在追MM的時候,可以送書和花兩種禮物

public class GG {
    public void chase(MM mm) {
        Gift g = new WarmGift(new Flower());
        give(mm, g);
    }

    public void give(MM mm, Gift g) {
        System.out.println(g + "gived!");
    }
}

如上程式碼,Flower被包裝成了一個WarmGift送給MM,WarmGift和WildGift都是Gift的一種抽象,Flower和Book都算Gift的一種具體實現, 我們讓Gift這個抽象類中,持有一個GiftImpl的引用

public abstract class Gift {
    protected GiftImpl impl;
}
public class Flower extends GiftImpl {
}
public class WarmGift extends Gift {
    public WarmGift(GiftImpl impl) {
        this.impl = impl;
    }
}

UML示例圖如下:

image

如果說代理模式是一個類與另一個類的組合,那麼橋接模式是一組類和另外一組類的組合。

橋接模式的應用

  • jdbc驅動配置

當我們把具體的 Driver 實現類(比如: com.mysql.jdbc.Driver)註冊到DriverManager之後,後續所有對JDBC介面的呼叫,都會委派到對具體的Driver實現類來執行。而Driver實現類都實現了相同的介面(java.sql.Driver),這也是可以靈活切換 Driver 的原因。

裝飾器模式

裝飾器模式是一種結構型模式。

顧名思義,就是對某個方法或者物件進行裝飾,舉個簡單的例子,有個圓形類(Circle),我需要把這個圓形的塗上紅色,其實就是新增一個裝飾器來裝飾這個圓形類。如果要讓裝飾器通用一些,可以處理圓形類對應的抽象類Sharp,那麼對於任意Sharp的子類,都可以用紅色裝飾器來塗紅色。

我們先定義Sharp這個抽象類:

public abstract class Sharp {
    protected abstract void draw();
}

然後我們定義Sharp的裝飾類SharpDecorator,這個類是所有裝飾器類的抽象類,後續的裝飾器只需要實現這個抽象類就可以對Sharp進行各種裝飾了,

public abstract class SharpDecorator extends Sharp {
    protected Sharp decoratedSharp;

    public SharpDecorator(Sharp decoratedSharp) {
        this.decoratedSharp = decoratedSharp;
    }
}

紅色裝飾器實現這個抽象類即可:

public class RedSharpDecorator extends SharpDecorator {
    public RedSharpDecorator(Sharp decoratedSharp) {
        super(decoratedSharp);
    }

    private static void redIt() {
        System.out.println("[RED]");
    }

    @Override
    protected void draw() {
        redIt();
        this.decoratedSharp.draw();
        redIt();
    }
}

主方法呼叫的時候只需要:

new RedSharpDecorator(new Circle()).draw();

UML圖如下:

image

說明:

  1. 裝飾器類和原始類繼承同樣的父類,這樣我們可以對原始類“巢狀”多個裝飾器類。

  2. 裝飾器類是對功能的增強,這也是裝飾器模式應用場景的一個重要特點。符合“組合關係”這種程式碼結構的設計模式有很多,比如代理模式、橋接模式,還有現在的裝飾器模式。儘管它們的程式碼結構很相似,但是每種設計模式的意圖是不同的。就拿比較相似的代理模式和裝飾器模式來說吧,代理模式中,代理類附加的是跟原始類無關的功能,而在裝飾器模式中,裝飾器類附加的是跟原始類相關的增強功能。

實際上,如果去檢視 JDK 的原始碼,你會發現,BufferedInputStreamDataInputStream 並非繼承自 InputStream,而是另外一個叫 FilterInputStream 的類。那這又是出於什麼樣的設計意圖,才引入這樣一個類呢?

因為 InputStream 是一個抽象類而非介面,而且它的大部分函式(比如 read()available())都有預設實現,按理來說,我們只需要在 BufferedInputStream 類中重新實現那些需要增加快取功能的函式就可以了,其他函式繼承 InputStream的預設實現。但實際上,這樣做是行不通的。對於即便是不需要增加快取功能的函式來說,BufferedInputStream還是必須把它重新實現一遍,簡單包裹對 InputStream 物件的函式呼叫。那 BufferedInputStream 類就無法將最終讀取資料的任務,委託給傳遞進來的 InputStream 物件來完成,DataInputStream也存在跟 BufferedInputStream同樣的問題。為了避免程式碼重複,Java IO 抽象出了一個裝飾器父類 FilterInputStreamInputStream 的所有的裝飾器類(BufferedInputStreamDataInputStream)都繼承自這個裝飾器父類。這樣,裝飾器類只需要實現它需要增強的方法就可以了,其他方法繼承裝飾器父類的預設實現。

裝飾器模式的應用

  • Java中的IO流, Read/InputStream ,Write/OutputStream

  • JDK中的 UnmodifiableCollection

  • Spring中的 HttpHeadResponseDecorator, 還有對 Cache的裝飾類 TransactionAwareCacheDecorator

介面卡模式

介面卡模式是一種結構型模式。

舉例說明,假設又一個播放器,需要根據不同格式以及對應的檔案來播放,介面設計如下:

public interface MediaPlayer {
    void play(String type, String fileName);
}

不同型別的播放器只需要實現這個介面即可,比如我們有一個ClassicMediaPlayer,這個只能播放mp3型別的檔案

public class ClassicMediaPlayer implements MediaPlayer {
    @Override
    public void play(String type, String fileName) {
        if ("mp3".equalsIgnoreCase(type)) {
            System.out.println("play mp3");
        } else {
            System.out.println("not supported format");
        }
    }
}

如果我想擴充套件,我們可以增加一個介面卡:

public class PlayerAdapter implements MediaPlayer {
    private AdvanceMediaPlayer advanceMediaPlayer;

    public PlayerAdapter(String type) {
        if ("mp4".equalsIgnoreCase(type)) {
            advanceMediaPlayer = new MP4Player();
        } else if ("AVI".equalsIgnoreCase(type)) {
            advanceMediaPlayer = new AVIPlayer();
        }
    }

    @Override
    public void play(String type, String fileName) {
        if ("mp4".equalsIgnoreCase(type)) {
            advanceMediaPlayer.playMP4(fileName);
        } else if ("AVI".equalsIgnoreCase(type)) {
            advanceMediaPlayer.playAVI(fileName);
        } else {
            new ClassicMediaPlayer().play(type, fileName);
        }
    }
}

這個介面卡就是根據不同型別來構造不同的播放器的,然後定義一個ExtendMediaPlayer,在裡面持有PlayAdapter,這樣,ExtendMediaPlayer就擁有了播放不同型別檔案的能力,所以我們在呼叫的時候,只需要:

ExtendMediaPlayer audioPlayer=new ExtendMediaPlayer();
audioPlayer.play("mp3","beyond the horizon.mp3");
audioPlayer.play("mp4","alone.mp4");
audioPlayer.play("avi","far far away.vlc");

UML圖如下:

image

介面卡模式:介面卡模式是一種事後的補救策略。介面卡提供跟原始類不同的介面,而代理模式、裝飾器模式提供的都是跟原始類相同的介面。

介面卡模式的應用

  • java.io

  • jdbc-odbc bridge

  • ASM transformer

  • 老版本的 JDK 提供了Enumeration類來遍歷容器。新版本的 JDK 用 Iterator 類替代 Enumeration 類來遍歷容器。

/**
 * Returns an enumeration over the specified collection.  This provides
 * interoperability with legacy APIs that require an enumeration
 * as input.
 *
 * @param  <T> the class of the objects in the collection
 * @param c the collection for which an enumeration is to be returned.
 * @return an enumeration over the specified collection.
 * @see Enumeration
 */
public static <T> Enumeration<T> enumeration(final Collection<T> c) {
  return new Enumeration<T>() {
    private final Iterator<T> i = c.iterator();

    public boolean hasMoreElements() {
      return i.hasNext();
    }

    public T nextElement() {
      return i.next();
    }
  };
}

使用Enumeration遍歷容器方法示例

public class TestEnumeration {
    public static void main(String[] args) {
        Vector<String> v = new Vector<>();
        v.addElement("Lisa");
        v.addElement("Billy");
        v.addElement("Mr Brown");
        Enumeration<String> e = v.elements();// 返回Enumeration物件
        while (e.hasMoreElements()) {
            String value = (String) e.nextElement();// 呼叫nextElement方法獲得元素
            System.out.print(value);
        }
    }
}

門面模式

門面模式是一種結構型模式。

門面模式為子系統提供一組統一的介面,定義一組高層介面讓子系統更易用。

假設建造一個房子需要有如下三個步驟:

第一步,和泥

第二步,搬磚

第三步,砌牆

如果每次我們製造一個房子都要分別呼叫這三個方法,就會比較麻煩一些,我們可以設定一個門面,這個門面封裝了這三個步驟,後續建造房子,只需要呼叫這個門面即可。

和泥

public class Mason {
    public void mix() {
        System.out.println("我和好泥了!");
    }
}

搬磚

public class BrickWorker {
    public void carry() {
        System.out.println("我搬好磚了!");
    }
}

砌牆

public class BrickLayer {
    public void neat() {
        System.out.println("我砌好牆了!");
    }
}

門面

public class LabourConstractor {
    private Mason work1 = new Mason();
    private BrickWorker work2 = new BrickWorker();
    private BrickLayer work3 = new BrickLayer();

    public void buildHouse() {
        work1.mix();
        work2.carry();
        work3.neat();
    }
}

這樣主函式只需要呼叫門面的buildHourse()方法,就可以建造一個房子了

public class Client {
    public static void main(String[] args) {
        LabourConstractor labour = new LabourConstractor();
        labour.buildHouse();
    }
}

門面模式的UML圖如下

image

門面模式應用

  • Linux的系統呼叫和Shell指令碼

Linux 系統呼叫函式就可以看作一種“門面”。它是 Linux 作業系統暴露給開發者的一組“特殊”的程式設計介面,它封裝了底層更基礎的 Linux 核心呼叫。再比如,Linux 的 Shell 命令,實際上也可以看作一種門面模式的應用。它繼續封裝系統呼叫,提供更加友好、簡單的命令,讓我們可以直接通過執行命令來跟作業系統互動。

  • Spring JDBC中的 JdbcUtils類,包裝了JDBC相關的所有操作。

  • Tomcat中的 RequestFacade, ResponseFacade, StandardSessionFacade

組合模式

組合模式是一種結構型模式。

組合模式中,最常用的一個用法就是目錄層級的遍歷,話不多說,直接上程式碼,主方法中

public class Main {
    public static void main(String[] args) {
        BranchNode root = new BranchNode("root");
        BranchNode branch1 = new BranchNode("branch1");
        BranchNode branch2 = new BranchNode("branch2");
        branch1.addNode(new LeafNode("leaf1"));
        root.addNode(branch1);
        root.addNode(branch2);
        tree(root, 0);
    }
}

其中,BranchNode為分支節點,LeafNode是葉子節點 達到的效果就是列印如下的形式

root
--branch1
----leaf1
--branch2

遞迴方法

    static void tree(Node node, int depth) {
        for (int i = 0; i < depth; i++) {
            System.out.print("--");
        }
        node.print();
        if (node instanceof BranchNode) {
            for (Node n : ((BranchNode) node).getNodes()) {
                tree(n, depth + 1);
            }
        }
    }

其中BranchNodeLeafNode都實現了Node介面,Node介面(也可以為定義抽象類)僅提供了一個屬性(content:標識節點內容)和一個列印方法:

public abstract class Node {
    protected String content;

    protected abstract void print();
}

BranchNode下可以包含多個Node,因為一個分支下面可以有多個分支(這個分支可以是任意的Node子類)

public class BranchNode extends Node {
    private List<Node> nodes = new ArrayList<>();

    public BranchNode(String content) {
        this.content = content;
    }

    @Override
    public void print() {
        System.out.println(content);
    }    // get..set方法略 
}

組合模式的UML圖如下:

image

組合模式的應用

MyBatis解析各種Mapping檔案中的SQL語句時,設計了一個非常關鍵的類叫作SqlNode,XML中的每一個Node都會被解析為一個SqlNode物件,最後把所有SqlNode都拼裝到一起,就成為一條完整的SQL語句。

享元模式

享元模式是一種結構型模式。

運用共享技術有效地支援大量細粒度的物件。主要解決:在有大量物件時,有可能會造成記憶體溢位,我們把其中共同的部分抽象出來,如果有相同的業務請求,直接返回在記憶體中已有的物件,避免重新建立。

假設我們有一個子彈類,同時我們設計一個子彈池,子彈池負責提供子彈

public class BulletPool {
    List<Bullet> bullets = new ArrayList<>();
    {
        for (int i = 0; i < 10; i++) {
            bullets.add(new Bullet(true));
        }
    }
    public Bullet getBullet() {
        for (int i = 0; i < bullets.size(); i++) {
            if (bullets.get(i).living) {
                return bullets.get(i);
            }
        }
        return new Bullet(true);
    }
}

可以看到getBullet邏輯,如果池子中有子彈,就拿池中的子彈,如果沒有,就new一個新的子彈返回。

UML圖如下

image

享元模式應用

  • 使用物件池對高併發下的記憶體進行管理

對於開發者來說,垃圾回收是不可控的,而且是無法避免的。但是,我們還是可以通過一些方法來降低垃圾回收的頻率,減少程式暫停的時長。我們知道,只有使用過被丟棄的物件才是垃圾回收的目標,所以,我們需要想辦法在處理大量請求的同時,儘量少的產生這種一次性物件。最有效的方法就是,優化你的程式碼中處理請求的業務邏輯,儘量少的建立一次性物件,特別是佔用記憶體較大的物件。比如說,我們可以把收到請求的 Request 物件在業務流程中一直傳遞下去,而不是每執行一個步驟,就建立一個內容和 Request 物件差不多的新物件。這裡面沒有多少通用的優化方法。對於需要頻繁使用,佔用記憶體較大的一次性物件,我們可以考慮自行回收並重用這些物件。實現的方法是這樣的:我們可以為這些物件建立一個物件池。收到請求後,在物件池內申請一個物件,使用完後再放回到物件池中,這樣就可以反覆地重用這些物件,非常有效地避免頻繁觸發垃圾回收。

  • Java中BooleanvalueOf(boolean b) 方法 ,這個方法返回的Boolean物件不會新new出來,而是複用的同一個, 原始碼如下:
public static Boolean valueOf(boolean b){
    return(b?TRUE:FALSE);
}
public static final Boolean TRUE=new Boolean(true);
public static final Boolean FALSE=new Boolean(false);

在 Java Integer 的實現中,-128 到 127 之間的整型物件會被事先建立好,快取在 IntegerCache 類中。當我們使用自動裝箱或者 valueOf() 來建立這個數值區間的整型物件時,會複用 IntegerCache 類事先建立好的物件。這裡的 IntegerCache 類就是享元工廠類,事先建立好的整型物件就是享元物件。在 Java String 類的實現中,JVM 開闢一塊儲存區專門儲存字串常量,這塊儲存區叫作字串常量池,類似於 Integer 中的 IntegerCache。不過,跟 IntegerCache 不同的是,它並非事先建立好需要共享的物件,而是在程式的執行期間,根據需要來建立和快取字串常量

注:Java提供了兩個配置IntegerCache的引數

//方法一:
-Djava.lang.Integer.IntegerCache.high=255
//方法二:
-XX:AutoBoxCacheMax=255

觀察者模式

觀察者模式是一種行為型模式。在物件之間定義一個一對多的依賴,當一個物件狀態改變的時候,所有依賴的物件都會自動收到通知。

一般可以用做事件處理往往和責任鏈模式搭配使用, 舉個例子 按鈕上一般都可以繫結事件,當我們按下按鈕的時候,可以觸發這些事件的執行,這裡就可以用觀察者模式來做, 我們先定義按鈕這個物件

public class Button {
    private List<ActionListener> listeners = new ArrayList<>();

    public void addActionListener(ActionListener listener) {
        this.listeners.add(listener);
    }

    @Override
    public String toString() {
        return "Button{" + "listeners=" + listeners + '}';
    }

    public void buttonPressed() {
        ActionEvent event = new ActionEvent(System.currentTimeMillis(), this);
        listeners.forEach(item -> item.actionPerformed(event));
    }
}

由上可知,Button中持有了一個列表,這個列表裡面裝的就是所有事件的列表,我們可以把事件繫結到這個按鈕的事件列表中,這樣就可以實現按鈕執行press操作的時候,把對應的事件觸發執行了

public interface ActionListener {
    void actionPerformed(ActionEvent event);
}

模擬兩個監聽事件

public class Listener1 implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent event) {
        System.out.println("Listener 1 listened it source: [" + event.getSource() + "], when is [" + event.getWhen() + "]");
    }
}
public class Listener2 implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent event) {
        System.out.println("Listener 2 listened it source: [" + event.getSource() + "], when is [" + event.getWhen() + "]");
    }
}

主方法在呼叫的時候

public class Main {
    public static void main(String[] args) {
        Button button = new Button();
        button.addActionListener(new Listener1());
        button.addActionListener(new Listener2());
        button.buttonPressed();
    }
}

當執行

button.buttonPressed()

的時候,對應的listener1和listener2就可以執行了。

UML圖如下

image

觀察者模式的應用

  • Spring ApplicationEvent

  • 郵件訂閱、RSS Feeds,本質上都是觀察者模式。

  • Google Guava EventBus

模板方法

模板方法是一種行為型模式。

假設我們要實現一個遊戲,這個遊戲有初始化,啟動,結束三個方法,我們可以定義一個遊戲的模板:

public abstract class Game {
    protected abstract void init();

    protected abstract void start();

    protected abstract void end();

    protected final void play() {
        init();
        start();
        end();
    }
}

每種類似這樣結構(有初始化,啟動,結束)的遊戲都可以繼承這個類來實現這三個方法,比如BasketballGame

public class BasketballGame extends Game {
    @Override
    protected void init() {
        System.out.println("basketball init");
    }

    @Override
    protected void start() {
        System.out.println("basketball start");
    }

    @Override
    protected void end() {
        System.out.println("basketball end");
    }
}

FootballGame

public class FootballGame extends Game {
    @Override
    protected void init() {
        System.out.println("football init");
    }

    @Override
    protected void start() {
        System.out.println("football start");
    }

    @Override
    protected void end() {
        System.out.println("football end");
    }
}

主方法在呼叫的時候,直接:

Game basketballGame=new BasketballGame();
basketballGame.play();
Game footballGame=new FootballGame();
footballGame.play();

另外一個例子:

public abstract class TestCase {
    public void run() {
        if (doTest()) {
            System.out.println("Test succeed.");
        } else {
            System.out.println("Test failed.");
        }
    }

    public abstract boolean doTest();
}

public class JunitApplication {
    private static final List<TestCase> testCases = new ArrayList<>();

    public static void register(TestCase testCase) {
        testCases.add(testCase);
    }

    public static final void main(String[] args) {
        for (TestCase c : testCases) {
            c.run();
        }
    }
}

public class UserServiceTest extends TestCase {

    @Override
    public boolean doTest() {
        System.out.println("do test...");
        return false;
    }

}

UML圖如下:

image

模板方法實際應用場景

  • 鉤子函式

  • Spring中的RestTemplate /JDBCTemplate

  • Collections.sort()方法也可以看成模板方法。

  • AbstractList定義了一些模板方法,ArrayList作為子類實現了對應的模板方法。

  • MyBatis中的BaseExecutor定義了模板方法,子類ReuseExecutor、SimpleExecutor、BatchExecutor和ClosedExecutor實現了對應的模板方法

策略模式

例項: 假設我們有一個貓類,這個類裡面有體重和身高這兩個屬性,給你一個貓的集合,然後需要你按貓的體重從小到大排序

思路: 我們可以把體重從小到大這個看成是一個策略,後續可能衍生其他的策略,比如: 按身高從高到低 按體重從小到大,體重一樣的身高從高到低

以身高從低到高排序這個策略為例

public class CatSortStrategy implements Comparator<Cat> {
    @Override
    public int compare(Cat o1, Cat o2) {
        return o1.getHeight() - o2.getHeight();
    }
}

假設我們定義貓排序的方法是: sort 那麼這個方法必然需要傳入一個排序策略的引數(否則我怎麼知道要怎麼排序貓?) 所以定義的sort方法可以是:

public class Sorter {
    public Cat[] sort(Cat[] items, Comparator<Cat> strategy) {
        int length = items.length;
        for (int i = 0; i < length; i++) {
            for (int j = i + 1; j < length; j++) {
                if (strategy.compare(items[i], items[j]) > 0) {
                    Cat tmp = items[i];
                    items[i] = items[j];
                    items[j] = tmp;
                }
            }
        }
        return items;
    }
}

進一步抽象,如果我想讓Sorter這個工具類不僅可以對貓進行各種策略的排序(基於比較的排序演算法),還可以對狗進行各種策略的排序(基於比較排序演算法),可以將Sorter定義成泛型

public class Sorter<T> {
    public T[] sort(T[] items, Comparator<T> strategy) {
        int length = items.length;
        for (int i = 0; i < length; i++) {
            for (int j = i + 1; j < length; j++) {
                if (strategy.compare(items[i], items[j]) > 0) {
                    T tmp = items[i];
                    items[i] = items[j];
                    items[j] = tmp;
                }
            }
        }
        return items;
    }
}

呼叫的時候, 泛型版本的Sorter可以對貓和狗都進行基於特定排序策略的排序。

Sorter<Cat> sorter = new Sorter<>();
Cat[] sortedCats = sorter.sort(cats,new CatSortStrategy());
Sorter<Dog> sorter = new Sorter<>();
Dog[] sortedCats = sorter.sort(dogs,new DogSortStrategy());

策略模式UML圖如下

image

策略模式的應用

  • Spring中的Resource介面

責任鏈模式

責任鏈模式是一種行為型模式。

有一段文字需要過濾敏感字,我們可以通過責任鏈模式來設計這個功能,假設文字是:scripts Hell World! 996

我們有多個過濾規則,比如第一個規則是:過濾 scripts 這個關鍵字(實際的規則可能很複雜,目前只是舉這個簡單例子來說明情況)
第二個規則是:過濾 996 這個關鍵字

我們可以抽象一個Filter介面,各種過濾規則無非就是實現這個介面即可

public interface Filter {
    boolean doFilter(Msg msg);
}

過濾 996 的規則:

public class SensitiveFilter implements Filter {
    @Override
    public boolean doFilter(Msg msg) {
        msg.setContent(msg.getContent().replace("996", ""));
        return true;
    }
}

過濾 scripts 的規則:

public class HTMLFilter implements Filter {
    @Override
    public boolean doFilter(Msg msg) {
        msg.setContent(msg.getContent().replace("scripts", ""));
        return true;
    }
}

主方法呼叫的時候,就直接New 相應的Filter來處理即可:

Msg msg=new Msg();
msg.setContent("scripts Hell World! 996");
System.out.println("before filter , the content is : "+msg.getContent());
Filter html=new HTMLFilter();
Filter sensitive=new SensitiveFilter();
html.doFilter(msg);
sensitive.doFilter(msg);
System.out.println("after filter , the content is : "+msg.getContent());

不過,更為優雅的一種方式是設計一個FilterChain,我們把所有的Filter都加入到這個FilterChain裡面,對於Msg直接去呼叫FilterChain的過濾方法即可把FilterChain中的所有Filter都執行(
而且還可以很靈活指定Filter順序)

package cor;

import java.util.ArrayList;
import java.util.List;

/**
 * @author Grey
 * @date 2020/4/13
 */
public class FilterChain implements Filter {
    private List<Filter> filters = new ArrayList<>();

    public FilterChain addFilter(Filter filter) {
        filters.add(filter);
        return this;
    }

    @Override
    public boolean doFilter(Msg msg) {
        for (Filter filter : filters) {
            if (!filter.doFilter(msg)) {
                return false;
            }
        }
        return true;
    }
}

那麼主方法在呼叫的時候,可以直接通過如下的方式:

public class Main {
    public static void main(String[] args) {
        FilterChain filterChain = new FilterChain();
        filterChain.addFilter(new HTMLFilter()).addFilter(new SensitiveFilter());
        Msg msg = new Msg();
        msg.setContent("scripts Hell World! 996");
        System.out.println("before filter , the content is : " + msg.getContent());
        filterChain.doFilter(msg);
        System.out.println("after filter , the content is : " + msg.getContent());
    }
}

UML圖如下:

image

責任鏈模式應用

  • Servlet filter

  • Structs interceptor

  • SpringMVC interceptor

  • Dubbo Filter

  • Netty ChannelPipeline

狀態模式

狀態模式是一種行為型模式。

物件的行為依賴於它的狀態(屬性),並且可以根據它的狀態改變而改變它的相關行為。

舉個例子,Person有Cry, Smile, Say三種行為,但是在不同狀態(SadState, HappyState)下,這三種行為不一樣,

public class Person {
    private State state;

    public Person(State state) {
        this.state = state;
    }

    void cry() {
        state.cry();
    }

    void smile() {
        state.smile();
    }

    void say() {
        state.say();
    }
}

在Sad狀態下,行為可能是:

public class SadState implements State {
    @Override
    public void cry() {
        System.out.println("Sad cry");
    }

    @Override
    public void smile() {
        System.out.println("Sad smile");
    }

    @Override
    public void say() {
        System.out.println("Sad say");
    }
}

Happy狀態下同理,那麼主方法在呼叫的時候:

public class Main {
    public static void main(String[] args) {
        Person person = new Person(new SadState());
        person.cry();
        person.say();
        person.smile();
        person = new Person(new HappyState());
        person.cry();
        person.say();
        person.smile();
    }
}

Person就可以根據不同的狀態來執行cry,say,smile的行為了

UML圖如下:

image

狀態模式的應用

  • Spring中的StateMachine

迭代器模式

迭代器模式是一種行為型模式。

迭代器最典型的應用是容器遍歷

image

模仿JDK的容器,我們自定義一個容器並實現iterator方法 我們先定義一個容器介面:Collection_.java

public interface Collection_<E> {
    int size();

    void add(E element);

    Iterator_<E> iterator();
}

裡面包括了一個iterator方法,所以每個實現這個容器介面的具體容器型別,都必須自定義iterator方法, 然後定義一個Iterator介面Iterator_.java, 具體容器中可以增加一個內部類來專門實現這個介面,比如我們的具體容器類是ArrayList_.java

package Iterator;

import static java.lang.System.arraycopy;

/**
 * @author Grey
 * @date 2020/4/15
 */
public class ArrayList_<E> implements Collection_<E> {
    private E[] objects = (E[]) new Object[10];
    private int index = 0;

    @Override
    public int size() {
        return index;
    }

    @Override
    public void add(E element) {
        if (objects.length == size()) {
            // 滿了就擴容為原來的兩倍
            E[] newObjects = (E[]) new Object[objects.length * 2];
            arraycopy(objects, 0, newObjects, 0, objects.length);
            objects = newObjects;
        }
        objects[index] = element;
        index++;
    }

    @Override
    public Iterator_<E> iterator() {
        return new ArrayListIterator_<>();
    }

    private class ArrayListIterator_<E> implements Iterator_<E> {
        private int currentIndex = 0;

        @Override
        public boolean hasNext() {
            return currentIndex < index;
        }

        @Override
        public E next() {
            E o = (E) objects[currentIndex];
            currentIndex++;
            return o;
        }
    }

}

我們主要看 ArrayListIterator_.java這個內部類,裡面其實是實現了 Iterator_ 這個介面,所以 ArrayList_ 的遍歷操作會執行這個內部類中的操作規則來對其進行遍歷。

如何實現一個快照迭代器

我們可以在容器中,為每個元素儲存兩個時間戳,一個是新增時間戳 addTimestamp,一個是刪除時間戳 delTimestamp。當元素被加入到集合中的時候,我們將 addTimestamp 設定為當前時間,將 delTimestamp 設定成最大長整型值(Long.MAX_VALUE)。當元素被刪除時,我們將 delTimestamp 更新為當前時間,表示已經被刪除。注意,這裡只是標記刪除,而非真正將它從容器中刪除。同時,每個迭代器也儲存一個迭代器建立時間戳 snapshotTimestamp,也就是迭代器對應的快照的建立時間戳。當使用迭代器來遍歷容器的時候,只有滿足

addTimestamp < snapshotTimestamp < delTimestamp

的元素,才是屬於這個迭代器的快照。如果元素的

addTimestamp > snapshotTimestamp

說明元素在建立了迭代器之後才加入的,不屬於這個迭代器的快照;

如果元素的

delTimestamp<snapshotTimestamp

說明元素在建立迭代器之前就被刪除掉了,也不屬於這個迭代器的快照。這樣就在不拷貝容器的情況下,在容器本身上藉助時間戳實現了快照功能。

迭代器模式應用

MyBatis中的DefaultCursor,它實現了Cursor介面,而且定義了一個成員變數cursorIterator,其定義的型別為CursorIterator。繼續檢視CursorIterator類的原始碼實現,它是DefaultCursor的一個內部類,並且實現了JDK中的Iterator介面。

訪問者模式

訪問者模式是一種行為型模式。

訪問者模式在結構不變的情況下動態改變對於內部元素的動作,舉例說明:

假設我們需要構造一臺電腦,有主機板(Board),CPU,記憶體(Memory),但是針對企業使用者和個人使用者,電腦元件的價格是不一樣的,我們需要根據不同客戶獲取一臺電腦的總價格。

我們先抽象出電腦元件這個類

public abstract class ComputerPart {
    abstract void accept(Visitor visitor);

    abstract int getPrice();
}

每個具體元件會繼承這個抽象類,以主機板(Board)為例

public class Board extends ComputerPart {
    @Override
    void accept(Visitor visitor) {
        visitor.visitBoard(this);
    }

    @Override
    int getPrice() {
        return 20;
    }
}

抽象出一個訪問者(Visitor)介面,

public interface Visitor {
    void visitCPU(CPU cpu);

    void visitBoard(Board board);

    void visitMemory(Memory memory);
}

每個具體型別的訪問者實現這個介面,然後定義其不同的價格策略,以公司訪問者為例(CorpVisitor)

public class CorpVisitor implements Visitor {
    private int totalPrice;

    @Override
    public void visitCPU(CPU cpu) {
        totalPrice += cpu.getPrice() - 1;
    }

    @Override
    public void visitBoard(Board board) {
        totalPrice += board.getPrice() - 2;
    }

    @Override
    public void visitMemory(Memory memory) {
        totalPrice += memory.getPrice() - 3;
    }

    public int getTotalPrice() {
        return totalPrice;
    }
}

個人訪問者(PersonalVisitor)類似

主方法呼叫

package visitor;

/**
 * @author Grey
 * @date 2020/4/16
 */
public class Main {
    public static void main(String[] args) {
        ComputerPart cpu = new CPU();
        ComputerPart memory = new Memory();
        ComputerPart board = new Board();
        PersonalVisitor personalVisitor = new PersonalVisitor();
        cpu.accept(personalVisitor);
        memory.accept(personalVisitor);
        board.accept(personalVisitor);
        System.out.println(personalVisitor.getTotalPrice());

        ComputerPart cpu2 = new CPU();
        ComputerPart memory2 = new Memory();
        ComputerPart board2 = new Board();
        CorpVisitor corpVisitor = new CorpVisitor();
        cpu2.accept(corpVisitor);
        memory2.accept(corpVisitor);
        board2.accept(corpVisitor);
        System.out.println(corpVisitor.getTotalPrice());
    }
}

UML圖如下

image

訪問者模式應用

  • 做編譯器的時候,需要生成AST,進行型別檢查 根據抽象語法樹,生成中間程式碼

  • XML檔案解析

  • JDK中的FileVisitor

  • Spring中的BeanDefinitionVisitor

備忘錄模式

備忘錄模式是一種行為型模式。

用於記錄物件的某個瞬間 類似快照 應用例項:

  1. 遊戲中的後悔藥。

  2. 打遊戲時的存檔。

  3. Windows 裡的 ctri + z。

  4. IE 中的後退。

  5. 資料庫的事務管理。

一個簡單的示例

public class Main {
    public static void main(String[] args) {
        Person person = new Person();
        person.name = "zhangsan";
        person.age = 12;
        new Main().save(person);
        new Main().load();
    }

    public void save(Person person) {
        File c = new File("/tank.data");
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(c));) {
            oos.writeObject(person);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void load() {
        File c = new File("/tank.data");
        try (ObjectInputStream oos = new ObjectInputStream(new FileInputStream(c));) {
            Person myTank = (Person) oos.readObject();
            System.out.println(myTank);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

UML圖:

image

備忘錄模式應用

Spring中StateManageableMessageContext.createMessagesMemento()

命令模式

命令模式是一種行為型模式。

通過呼叫者呼叫接受者執行命令,順序:呼叫者→命令→接受者,比如:CopyCommand中的doit方法,就是執行這個copy的命令,undo就是撤銷上一次執行的命令,我們可以抽象出Command這個介面:

public interface Command {
    void doit();

    void undo();
}

CopyCommand實現這個介面,並實現doit和undo這兩個方法,其他的命令也可以類似的實現出來

public class CopyCommand implements Command {
    private Content content;

    public CopyCommand(Content content) {
        this.content = content;
    }

    @Override
    public void doit() {
        content.msg = content.msg + content.msg;
    }

    @Override
    public void undo() {
        content.msg = content.msg.substring(0, content.msg.length() / 2);
    }
}

UML圖如下

image

命令模式應用

  1. 結合責任鏈模式實現多次undo

  2. 結合組合模式實現巨集命令

  3. 結合記憶模式實現transaction回滾

直譯器模式

直譯器模式是一種行為型模式。

直譯器模式為某個語言定義它的語法(或者叫文法)表示,並定義一個直譯器用來處理這個語法。

一般用於指令碼語言直譯器

示例:如何實現一個自定義介面告警規則功能?

一般來講,監控系統支援開發者自定義告警規則,比如我們可以用下面這樣一個表示式,來表示一個告警規則,它表達的意思是:每分鐘 API 總出錯數超過 100 或者每分鐘 API 總呼叫數超過 10000 就觸發告警。

api_error_per_minute > 100 || api_count_per_minute > 10000

在監控系統中,告警模組只負責根據統計資料和告警規則,判斷是否觸發告警。至於每分鐘 API 介面出錯數、每分鐘介面呼叫數等統計資料的計算,是由其他模組來負責的。其他模組將統計資料放到一個 Map 中(資料的格式如下所示),傳送給告警模組。接下來,我們只關注告警模組。

Map<String, Long> apiStat = new HashMap<>();
apiStat.put("api_error_per_minute", 103);
apiStat.put("api_count_per_minute", 987);

為了簡化講解和程式碼實現,我們假設自定義的告警規則只包含“||、&&、>、<、”這五個運算子,其中,“>、<、”運算子的優先順序高於“||、&&”運算子,“&&”運算子優先順序高於“||”。在表示式中,任意元素之間需要通過空格來分隔。除此之外,使用者可以自定義要監控的 key,比如前面的 api_error_per_minute、api_count_per_minute。


public class AlertRuleInterpreter {

  // key1 > 100 && key2 < 1000 || key3 == 200
  public AlertRuleInterpreter(String ruleExpression) {
    //TODO:由你來完善
  }

  //<String, Long> apiStat = new HashMap<>();
  //apiStat.put("key1", 103);
  //apiStat.put("key2", 987);
  public boolean interpret(Map<String, Long> stats) {
    //TODO:由你來完善
  }

}

public class DemoTest {
  public static void main(String[] args) {
    String rule = "key1 > 100 && key2 < 30 || key3 < 100 || key4 == 88";
    AlertRuleInterpreter interpreter = new AlertRuleInterpreter(rule);
    Map<String, Long> stats = new HashMap<>();
    stats.put("key1", 101l);
    stats.put("key3", 121l);
    stats.put("key4", 88l);
    boolean alert = interpreter.interpret(stats);
    System.out.println(alert);
  }
}

實際上,我們可以把自定義的告警規則,看作一種特殊“語言”的語法規則。我們實現一個直譯器,能夠根據規則,針對使用者輸入的資料,判斷是否觸發告警。利用直譯器模式,我們把解析表示式的邏輯拆分到各個小類中,避免大而複雜的大類的出現。


public interface Expression {
  boolean interpret(Map<String, Long> stats);
}

public class GreaterExpression implements Expression {
  private String key;
  private long value;

  public GreaterExpression(String strExpression) {
    String[] elements = strExpression.trim().split("\\s+");
    if (elements.length != 3 || !elements[1].trim().equals(">")) {
      throw new RuntimeException("Expression is invalid: " + strExpression);
    }
    this.key = elements[0].trim();
    this.value = Long.parseLong(elements[2].trim());
  }

  public GreaterExpression(String key, long value) {
    this.key = key;
    this.value = value;
  }

  @Override
  public boolean interpret(Map<String, Long> stats) {
    if (!stats.containsKey(key)) {
      return false;
    }
    long statValue = stats.get(key);
    return statValue > value;
  }
}

// LessExpression/EqualExpression跟GreaterExpression程式碼類似,這裡就省略了

public class AndExpression implements Expression {
  private List<Expression> expressions = new ArrayList<>();

  public AndExpression(String strAndExpression) {
    String[] strExpressions = strAndExpression.split("&&");
    for (String strExpr : strExpressions) {
      if (strExpr.contains(">")) {
        expressions.add(new GreaterExpression(strExpr));
      } else if (strExpr.contains("<")) {
        expressions.add(new LessExpression(strExpr));
      } else if (strExpr.contains("==")) {
        expressions.add(new EqualExpression(strExpr));
      } else {
        throw new RuntimeException("Expression is invalid: " + strAndExpression);
      }
    }
  }

  public AndExpression(List<Expression> expressions) {
    this.expressions.addAll(expressions);
  }

  @Override
  public boolean interpret(Map<String, Long> stats) {
    for (Expression expr : expressions) {
      if (!expr.interpret(stats)) {
        return false;
      }
    }
    return true;
  }

}

public class OrExpression implements Expression {
  private List<Expression> expressions = new ArrayList<>();

  public OrExpression(String strOrExpression) {
    String[] andExpressions = strOrExpression.split("\\|\\|");
    for (String andExpr : andExpressions) {
      expressions.add(new AndExpression(andExpr));
    }
  }

  public OrExpression(List<Expression> expressions) {
    this.expressions.addAll(expressions);
  }

  @Override
  public boolean interpret(Map<String, Long> stats) {
    for (Expression expr : expressions) {
      if (expr.interpret(stats)) {
        return true;
      }
    }
    return false;
  }
}

public class AlertRuleInterpreter {
  private Expression expression;

  public AlertRuleInterpreter(String ruleExpression) {
    this.expression = new OrExpression(ruleExpression);
  }

  public boolean interpret(Map<String, Long> stats) {
    return expression.interpret(stats);
  }
} 

直譯器模式的應用

  • Spring中的ExpressionParser

中介模式

中介模式是一種行為模式。

舉個簡單的例子,如果一個聊天室裡面的使用者1和使用者2要聊天,聊天室就相當於中介的地位,使用者1和使用者2只管呼叫發訊息方法,聊天室即可把訊息給對方

public class ChatRoom {
    public static void showMessage(User user, String content) {
        System.out.println("user :" + user.getName() + " send a message, content is " + content);
    }
}

以上程式碼表示,聊天室將user說的content展示出來

主方法只需要如下呼叫即可:

public class Main {
    public static void main(String[] args) {
        User user = new User("Peter");
        user.sendMessage("Hello ");
        user = new User("Harry");
        user.sendMessage("Hi");
    }
}

User中的sendMessage方法

public void sendMessage(String content){ChatRoom.showMessage(this,content);}

image

中介模式應用

  • JDK中的 Timer.schedule()

Spring中使用到的設計模式

觀察者模式

定義一個繼承ApplicationEvent的事件;定義一個實現了ApplicationListener的監聽器;定義一個傳送者DemoPublisher,傳送者呼叫ApplicationContext來傳送事件訊息。

模板方法

image

介面卡模式

Spring 定義了統一的介面HandlerAdapter,並且對每種Controller定義了對應的介面卡類。這些介面卡類包括:AnnotationMethodHandlerAdapterSimpleControllerHandlerAdapterSimpleServletHandlerAdapter

策略模式

AopProxy是策略介面,JdkDynamicAopProxyCglibAopProxy是兩個實現了 AopProxy介面的策略類。策略的建立一般通過工廠方法來實現。對應到 Spring 原始碼,AopProxyFactory是一個工廠類介面,DefaultAopProxyFactory是一個預設的工廠類,用來建立AopProxy物件。

組合模式

CacheManager 組合 Cache

裝飾器模式

TransactionAwareCacheDecorator增加了對事務的支援,在事務提交、回滾的時候分別對Cache的資料進行處理。TransactionAwareCacheDecorator實現Cache介面,並且將所有的操作都委託給targetCache來實現,對其中的寫操作新增了事務功能。這是典型的裝飾器模式的應用場景和程式碼實現

工廠模式

BeanFactory 類和ApplicationContext相關類(AbstractApplicationContextClassPathXmlApplicationContextFileSystemXmlApplicationContext等)

參考資料

相關文章