作者:Grey
原文地址: 設計模式學習筆記
UML和代碼
單例模式
單例模式是創建型模式。
單例的定義:“一個類只允許創建唯一一個對象(或者實例),那這個類就是一個單例類,這種設計模式就叫作單例設計模式,簡稱單例模式。”定義中提到,“一個類只允許創建唯一一個對象”。那對象的唯一性的作用范圍是指進程內只允許創建一個對象,也就是說,單例模式創建的對象是進程唯一的(而非線程)
為什么要使用單例
-
處理資源訪問沖突
比如寫日志的類,如果不使用單例,就必須使用鎖機制來解決日志被覆蓋的問題。
-
表示全局唯一類
比如配置信息類,在系統中,只有一個配置文件,當配置文件加載到內存中,以對象形式存在,也理所應當只有一份。
唯一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();
}
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?
說了這個問題
實際上,只有很低版本的 Java 才會有這個問題。我們現在用的高版本的 Java 已經在 JDK 內部實現中解決了這個問題(解決的方法很簡單,只要把對象 new 操作和初始化操作設計為原子操作,就自然能禁止重排序)。
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
類
/**
* Every Java application has a single instance of class
* <code>Runtime</code> that allows the application to interface with
* the environment in which the application is running. The current
* runtime can be obtained from the <code>getRuntime</code> method.
* <p>
* An application cannot create its own instance of this class.
*
* @author unascribed
* @see java.lang.Runtime#getRuntime()
* @since JDK1.0
*/
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() {}
//....
public void addShutdownHook(Thread hook) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("shutdownHooks"));
}
ApplicationShutdownHooks.add(hook);
}
//...
}
工廠模式
工廠模式是創建型模式。
簡單工廠
這個模式很簡單,比如我們需要制造不同類型的鼠標,我們只需要創建一個鼠標工廠
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圖如下:
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();
}
}
注:單例模式就是一種工廠模式(靜態工廠)
應用
- Spring IOC DI
- 配置解析
- 對象創建(BeansFactory:通過BeanDifinition來創建對象)
- 對象生命周期管理(單例/多例,是否支持懶加載,銷毀對象時候的清理方法)
- Hibernate 換數據庫只需換方言和驅動就可以切換不同數據庫
- java.util.Calender.getInstance()
建造者模式
建造者模式是創建型模式。
我們在對一個實體類進行屬性的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();
構造器模式也適用於類層次結構。抽象類有抽象的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:
package builder;
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();
原型模式
原型模式是創建型模式。
如果對象的創建成本比較大,而同一個類的不同對象之間差別不大(大部分字段都相同),在這種情況下,我們可以利用對已有對象(原型)進行復制(或者叫拷貝)的方式來創建新對象,以達到節省創建時間的目的。這種基於原型來創建對象的方式就叫作原型設計模式(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圖如下:
使用示例
克隆一個巨大的HashMap,如果構建散列表的代價很大,我們可以通過
- HashMap的clone方法(注意:默認的clone方法是淺拷貝,需要遞歸拷貝HashMap里面的內容,直到類型是基礎類型為止)
- 使用序列化
如果只是增量拷貝,可以通過淺拷貝拿到一個新的HashMap,然后拿到增量的數據單獨進行深拷貝即可。
代理模式
代理模式是結構型模式。
靜態代理
舉例說明,假設我們需要在某個類的某段代碼的前后加上日志記錄,我們就可以通過靜態代理的方式實現
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圖如下:
動態代理
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圖如下:
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
- 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示例圖如下:
如果說代理模式是一個類與另一個類的組合,那么橋接模式是一組類和另外一組類的組合。
橋接模式的應用
-
jdbc驅動配置
當我們把具體的 Driver 實現類(比如,com.mysql.jdbc.Driver)注冊到 DriverManager 之后,后續所有對 JDBC 接口的調用,都會委派到對具體的 Driver 實現類來執行。而 Driver 實現類都實現了相同的接口(java.sql.Driver ),這也是可以靈活切換 Driver 的原因。
裝飾器模式
裝飾器模式是一種結構型模式。
顧名思義,就是對某個方法或者對象進行裝飾,舉個簡單的例子,有個圓形類(Circle),我需要把這個圓形的塗上紅色,其實就是新增一個裝飾器來裝飾這個圓形類。如果要讓裝飾器通用一些,可以處理圓形類對應的抽象類
Sharpe,那么對於任意Shape的子類,都可以用紅色裝飾器來塗紅色。
我們先定義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圖如下:
說明:
- 裝飾器類和原始類繼承同樣的父類,這樣我們可以對原始類“嵌套”多個裝飾器類。
- 裝飾器類是對功能的增強,這也是裝飾器模式應用場景的一個重要特點。符合“組合關系”這種代碼結構的設計模式有很多,比如代理模式、橋接模式,還有現在的裝飾器模式。盡管它們的代碼結構很相似,但是每種設計模式的意圖是不同的。就拿比較相似的代理模式和裝飾器模式來說吧,代理模式中,代理類附加的是跟原始類無關的功能,而在裝飾器模式中,裝飾器類附加的是跟原始類相關的增強功能。
實際上,如果去查看 JDK 的源碼,你會發現,BufferedInputStream、DataInputStream 並非繼承自 InputStream,而是另外一個叫 FilterInputStream 的類。那這又是出於什么樣的設計意圖,才引入這樣一個類呢?
因為InputStream 是一個抽象類而非接口,而且它的大部分函數(比如 read()、available())都有默認實現,按理來說,我們只需要在 BufferedInputStream 類中重新實現那些需要增加緩存功能的函數就可以了,其他函數繼承 InputStream 的默認實現。但實際上,這樣做是行不通的。對於即便是不需要增加緩存功能的函數來說,BufferedInputStream 還是必須把它重新實現一遍,簡單包裹對 InputStream 對象的函數調用。那 BufferedInputStream 類就無法將最終讀取數據的任務,委托給傳遞進來的 InputStream 對象來完成,DataInputStream 也存在跟 BufferedInputStream 同樣的問題。為了避免代碼重復,Java IO 抽象出了一個裝飾器父類 FilterInputStream,InputStream 的所有的裝飾器類(BufferedInputStream、DataInputStream)都繼承自這個裝飾器父類。這樣,裝飾器類只需要實現它需要增強的方法就可以了,其他方法繼承裝飾器父類的默認實現。
應用
- Java中的IO流, Read/InputStream ,Write/OutputStream
- JDK中的UnmodifiableCollection
適配器模式
適配器模式是一種結構型模式。
舉例說明,假設又一個播放器,需要根據不同格式以及對應的文件來播放,接口設計如下:
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圖如下:
適配器模式:適配器模式是一種事后的補救策略。適配器提供跟原始類不同的接口,而代理模式、裝飾器模式提供的都是跟原始類相同的接口。
應用
- 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();
}
};
}
門面模式
門面模式是一種結構型模式。
門面模式為子系統提供一組統一的接口,定義一組高層接口讓子系統更易用。
假設建造一個房子需要有如下三個步驟:
- 和泥
- 搬磚
- 砌牆
如果每次我們制造一個房子都要分別調用這三個方法,就會比較麻煩一些,我們可以設置一個門面,這個門面封裝了這三個步驟,后續建造房子,只需要調用這個門面即可。
和泥
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圖如下
-
應用
-
Linux的系統調用和Shell腳本
Linux 系統調用函數就可以看作一種“門面”。它是 Linux 操作系統暴露給開發者的一組“特殊”的編程接口,它封裝了底層更基礎的 Linux 內核調用。再比如,Linux 的 Shell 命令,實際上也可以看作一種門面模式的應用。它繼續封裝系統調用,提供更加友好、簡單的命令,讓我們可以直接通過執行命令來跟操作系統交互。
-
組合模式
組合模式是一種結構型模式。
組合模式中,最常用的一個用法就是目錄層級的遍歷,話不多說,直接上代碼,主方法中
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);
}
}
}
其中BranchNode和LeafNode都實現了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圖如下:
享元模式
享元模式是一種結構型模式。
運用共享技術有效地支持大量細粒度的對象。主要解決:在有大量對象時,有可能會造成內存溢出,我們把其中共同的部分抽象出來,如果有相同的業務請求,直接返回在內存中已有的對象,避免重新創建。
假設我們有一個子彈類,同時我們設計一個子彈池,子彈池負責提供子彈
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圖如下
應用
- 使用對象池對高並發下的內存進行管理
對於開發者來說,垃圾回收是不可控的,而且是無法避免的。但是,我們還是可以通過一些方法來降低垃圾回收的頻率,減少進程暫停的時長。我們知道,只有使用過被丟棄的對象才是垃圾回收的目標,所以,我們需要想辦法在處理大量請求的同時,盡量少的產生這種一次性對象。最有效的方法就是,優化你的代碼中處理請求的業務邏輯,盡量少的創建一次性對象,特別是占用內存較大的對象。比如說,我們可以把收到請求的 Request 對象在業務流程中一直傳遞下去,而不是每執行一個步驟,就創建一個內容和 Request 對象差不多的新對象。這里面沒有多少通用的優化方法。對於需要頻繁使用,占用內存較大的一次性對象,我們可以考慮自行回收並重用這些對象。實現的方法是這樣的:我們可以為這些對象建立一個對象池。收到請求后,在對象池內申請一個對象,使用完后再放回到對象池中,這樣就可以反復地重用這些對象,非常有效地避免頻繁觸發垃圾回收。
- Java中Boolean的valueOf(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);
-
連接池管理
-
IntegerCache 和 String
在 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圖如下
應用
- 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圖如下:
實際應用場景
- 鈎子函數
- Spring中的RestTemplate /JDBCTemplate
- Collections.sort()方法也可以看成模板方法。
策略模式
實例: 假設我們有一個貓類,這個類里面有體重和身高這兩個屬性,給你一個貓的集合,然后需要你按貓的體重從小到大排序
思路: 我們可以把體重從小到大這個看成是一個策略,后續可能衍生其他的策略,比如: 按身高從高到低 按體重從小到大,體重一樣的身高從高到低
以身高從低到高排序這個策略為例
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圖如下
責任鏈模式
責任鏈模式是一種行為型模式。
有一段文本需要過濾敏感字,我們可以通過責任鏈模式來設計這個功能,假設文本是: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圖如下:
應用
- 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圖如下:
迭代器模式
迭代器模式是一種行為型模式。
迭代器最典型的應用是容器遍歷
模仿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_這個內部類,里面其實是實現了 Iterator_ 這個接口,所以 ArrayList_ 的遍歷操作會執行這個內部類中的操作規則來對其進行遍歷。
如何實現一個快照迭代器
我們可以在容器中,為每個元素保存兩個時間戳,一個是添加時間戳 addTimestamp,一個是刪除時間戳 delTimestamp。當元素被加入到集合中的時候,我們將 addTimestamp 設置為當前時間,將 delTimestamp 設置成最大長整型值(Long.MAX_VALUE)。當元素被刪除時,我們將 delTimestamp 更新為當前時間,表示已經被刪除。注意,這里只是標記刪除,而非真正將它從容器中刪除。同時,每個迭代器也保存一個迭代器創建時間戳 snapshotTimestamp,也就是迭代器對應的快照的創建時間戳。當使用迭代器來遍歷容器的時候,只有滿足
addTimestamp < snapshotTimestamp < delTimestamp
的元素,才是屬於這個迭代器的快照。如果元素的
addTimestamp > snapshotTimestamp
說明元素在創建了迭代器之后才加入的,不屬於這個迭代器的快照;
如果元素的
delTimestamp<snapshotTimestamp
說明元素在創建迭代器之前就被刪除掉了,也不屬於這個迭代器的快照。這樣就在不拷貝容器的情況下,在容器本身上借助時間戳實現了快照功能。
訪問者模式
訪問者模式是一種行為型模式。
訪問者模式在結構不變的情況下動態改變對於內部元素的動作,舉例說明:
假設我們需要構造一台電腦,有主板(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圖如下
應用
-
做編譯器的時候,需要生成AST,進行類型檢查 根據抽象語法樹,生成中間代碼
-
XML文件解析
備忘錄模式
備忘錄模式是一種行為型模式。
用於記錄對象的某個瞬間 類似快照 應用實例:
- 后悔葯。
- 打游戲時的存檔。
- Windows 里的 ctri + z。
- IE 中的后退。
- 數據庫的事務管理。
一個簡單的示例
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圖:
命令模式
命令模式是一種行為型模式。
通過調用者調用接受者執行命令,順序:調用者→命令→接受者,比如: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圖如下
命令模式可以
- 結合責任鏈模式實現多次undo[TODO]
- 結合組合模式實現宏命令
- 結合記憶模式實現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);
}
}
中介模式
中介模式是一種行為模式。
舉個簡單的例子,如果一個聊天室里面的用戶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);}
Spring中使用到的設計模式
觀察者模式
定義一個繼承 ApplicationEvent 的事件;定義一個實現了 ApplicationListener 的監聽器;定義一個發送者(DemoPublisher),發送者調用 ApplicationContext 來發送事件消息。
模板方法
適配器模式
Spring 定義了統一的接口 HandlerAdapter,並且對每種 Controller 定義了對應的適配器類。這些適配器類包括:AnnotationMethodHandlerAdapter、SimpleControllerHandlerAdapter、SimpleServletHandlerAdapter 等
策略模式
AopProxy 是策略接口,JdkDynamicAopProxy、CglibAopProxy 是兩個實現了 AopProxy 接口的策略類。策略的創建一般通過工廠方法來實現。對應到 Spring 源碼,AopProxyFactory 是一個工廠類接口,DefaultAopProxyFactory 是一個默認的工廠類,用來創建 AopProxy 對象。
組合模式
CacheManager 組合 Cache
裝飾器模式
TransactionAwareCacheDecorator 增加了對事務的支持,在事務提交、回滾的時候分別對 Cache 的數據進行處理。TransactionAwareCacheDecorator 實現 Cache 接口,並且將所有的操作都委托給 targetCache 來實現,對其中的寫操作添加了事務功能。這是典型的裝飾器模式的應用場景和代碼實現
工廠模式
BeanFactory 類和 ApplicationContext 相關類(AbstractApplicationContext、ClassPathXmlApplicationContext、FileSystemXmlApplicationContext…)