點擊上方 "程式設計師小樂"關注, 星標或置頂一起成長
第一時間與你相約
每日英文
One of the most important things in the world is to know oneself. Therefore occasional isolation and meditation are necessary for him.
人生最重要的事之一是發現自己,所以有必要偶爾與孤獨、沉思為伍。
每日掏心話
人生最可怕的不是眼睛看不見了,而是心失去了方向。幸福就是只要牽對了手,就算失去了方向,你也不會再害怕。
來源:fangjian0423 | 責編:樂樂
連結:fangjian0423.github.io/2017/03/26/design-pattern/
程式設計師小樂(ID:study_tech)第 716 次推文 圖片來自網絡
往日回顧:你的電腦被黑客黑過嗎?
正文
記錄一下自己理解的一些設計模式,並儘量使用表達清楚的例子進行講解。
策略模式
策略模式應該是最基礎的一個設計模式,它是對行為的一個抽象。jdk中的Comparator比較器就是一個使用策略設計模式的策略。
比如有一個Student學生類,有name和age兩個屬性。如果有個需求需要列印學生名單,並按照字母順序排序,可以使用Comparator接口並在內部使用name進行比較即可。如果哪一天需要按照年齡進行排序,那麼只需要修改Comparator即可,也就是使用一個新的策略,其它完全不變。
工廠模式
工廠模式的意義在於對象的創建、管理可以使用工廠去管理,而不是創建者自身。最典型的工廠模式使用者就是Spring,Spring內部的容器就是一個工廠,所有的bean都由這個容器管理,包括它們的創建、銷毀、注入都被這個容器管理。
工廠模式分簡單工廠和抽象工廠。它們的區別在於抽象工廠抽象程度更高,把工廠也抽象成了一個接口,這樣可以再每添加一個新的對象的時候而不需要修改工廠的代碼。
比如有個Repository接口,用於存儲數據,有DatabaseRepository,CacheRepository,FileRepository分別在資料庫,緩存,文件中存儲數據,定義如下:
public interface Repository {
void save(Object obj);
}
class DatabaseRepository implements Repository {
@Override
public void save(Object obj) {
System.out.println("save in database");
}
}
class CacheRepository implements Repository {
@Override
public void save(Object obj) {
System.out.println("save in cache");
}
}
class FileRepository implements Repository {
@Override
public void save(Object obj) {
System.out.println("save in file");
}
}
簡單工廠的使用
public class RepositoryFactory {
public Repository create(String type) {
Repository repository = null;
switch (type) {
case "db":
repository = new DatabaseRepository();
break;
case "cache":
repository = new CacheRepository();
break;
case "file":
repository = new FileRepository();
break;
}
return repository;
}
public static void main(String[] args) {
RepositoryFactory factory = new RepositoryFactory();
factory.create("db").save(new Object());
factory.create("cache").save(new Object());
factory.create("file").save(new Object());
}
}
簡單工廠的弊端在於每添加一個新的Repository,都必須修改RepositoryFactory中的代碼。
抽象工廠的使用
public interface RepositoryFactoryProvider {
Repository create();
}
class DatabaseRepositoryFactory implements RepositoryFactoryProvider {
@Override
public Repository create() {
return new DatabaseRepository();
}
}
class CacheRepositoryFactory implements RepositoryFactoryProvider {
@Override
public Repository create() {
return new CacheRepository();
}
}
class FileRepositoryFactory implements RepositoryFactoryProvider {
@Override
public Repository create() {
return new FileRepository();
}
}
抽象工廠的測試:
RepositoryFactoryProvider dbProvider = new DatabaseRepositoryFactory();
dbProvider.create().save(new Object());
RepositoryFactoryProvider cacheProvider = new CacheRepositoryFactory();
cacheProvider.create().save(new Object());
RepositoryFactoryProvider fileProvider = new FileRepositoryFactory();
fileProvider.create().save(new Object());
抽象工廠把工廠也進行了抽象話,所以添加一個新的Repository的話,只需要新增一個RepositoryFactory即可,原有代碼不需要修改。
裝飾者模式
裝飾者模式的作用就在於它可以在不改變原有類的基礎上動態地給類添加新的功能。之前寫過一篇通過源碼分析MyBatis的緩存文章,mybatis中的query就是使用了裝飾者設計模式。
用一段簡單的代碼來模擬一下mybatis中query的實現原理:
@Data
@AllArgsConstructor
@ToString
class Result { // 查詢結果類,相當於一個domain
private Object obj;
private String sql;
}
public interface Query { // 查詢接口,有簡單查詢和緩存查詢
Result query(String sql);
}
public class SimpleQuery implements Query { // 簡單查詢,相當於直接查詢資料庫,這裡直接返回Result,相當於是資料庫查詢的結果
@Override
public Result query(String sql) {
return new Result(new Object(), sql);
}
}
public class CacheQuery implements Query { // 緩存查詢,如果查詢相同的sql,不直接查詢資料庫,而是返回map中存在的Result
private Query query;
private Map<String, Result> cache = new HashMap<>();
public CacheQuery(Query query) {
this.query = query;
}
@Override
public Result query(String sql) {
if(cache.containsKey(sql)) {
return cache.get(sql);
}
Result result = query.query(sql);
cache.put(sql, result);
return result;
}
}
測試:
Query simpleQuery = new SimpleQuery();
System.out.println(simpleQuery.query("select * from t_student") == simpleQuery.query("select * from t_student")); // false
Query cacheQuery = new CacheQuery(simpleQuery);
System.out.println(cacheQuery.query("select * from t_student") == cacheQuery.query("select * from t_student")); // true
這裡CacheQuery就是一個裝飾類,SimpleQuery是一個被裝飾者。我們通過裝飾者設計模式動態地給SimpleQuery添加了緩存功能,而不需要修改SimpleQuery的代碼。
當然,裝飾者模式也有缺點,就是會存在太多的類。
如果我們需要添加一個過濾的查詢(sql中有敏感字的就直接返回null,而不查詢資料庫),只需要可以添加一個FilterQuery裝飾者即可:
public class FilterQuery implements Query {
private Query query;
private List<String> words = new ArrayList<>();
public FilterQuery(Query query) {
this.query = query;
words.add("fuck");
words.add("sex");
}
@Override
public Result query(String sql) {
for(String word : words) {
if(sql.contains(word)) return null;
}
return query.query(sql);
}
}
Query filterQuery = new FilterQuery(simpleQuery);
System.out.println(filterQuery.query("select * from t_student where name = 'fuck'")); // null
System.out.println(filterQuery.query("select * from t_student where name = 'format'")); // Result(obj=java.lang.Object@1b4fb997, sql=select * from t_student where name = 'format')
代理模式
代理模式的作用是使用一個代理類來代替原先類進行操作。比較常見的就是aop中就是使用代理模式完成事務的處理。
代理模式分靜態代理和動態代理,靜態代理的原理就是對目標對象進行封裝,最後調用目標對象的方法即可。
動態代理跟靜態代理的區別就是動態代理中的代理類是程序運行的時候生成的。Spring中對於接口的代理使用jdk內置的Proxy和InvocationHandler實現,對於類的代理使用cglib完成。
以1個UserService為例,使用jdk自帶的代理模式完成計算方法調用時間的需求:
// UserService接口
public interface IUserService {
void printAll();
}
// UserService實現類
class UserService implements IUserService {
@Override
public void printAll() {
System.out.println("print all users");
}
}
// InvocationHandler策略,這裡列印了方法調用前後的時間
@AllArgsConstructor
class UserInvocationHandler implements InvocationHandler {
private IUserService userService;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("start : " + System.currentTimeMillis());
Object result = method.invoke(userService, args);
System.out.println("end : " + System.currentTimeMillis());
return result;
}
}
測試:
IUserService userService = new UserService();
UserInvocationHandler uih = new UserInvocationHandler(userService);
IUserService proxy = (IUserService) Proxy.newProxyInstance(userService.getClass().getClassLoader(), new Class[] {IUserService.class}, uih);
proxy.printAll(); // 列印出start : 1489665566456 print all users end : 1489665566457
組合模式
組合模式經常跟策略模式配合使用,用來組合所有的策略,並遍歷這些策略找出滿足條件的策略。之前寫過一篇SpringMVC關於json、xml自動轉換的原理研究文章,裡面springmvc把返回的返回值映射給用戶的response做了一層抽象,封裝到了HandlerMethodReturnValueHandler策略接口中。
在HandlerMethodReturnValueHandlerComposite類中,使用存在的HandlerMethodReturnValueHandler對返回值進行處理,在HandlerMethodReturnValueHandlerComposite內部的代碼如下:
// 策略集合
private final List<HandlerMethodReturnValueHandler> returnValueHandlers = new ArrayList<HandlerMethodReturnValueHandler>();
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
// 調用selectHandler方法
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); // 使用找到的handler進行處理
}
private HandlerMethodReturnValueHandler selectHandler(Object value, MethodParameter returnType) {
boolean isAsyncValue = isAsyncReturnValue(value, returnType);
// 遍歷存在的HandlerMethodReturnValueHandler
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
continue;
}
if (handler.supportsReturnType(returnType)) { // 找到匹配的handler
return handler;
}
}
return null;
}
模板模式
跟策略模式類似,模板模式會先定義好實現的邏輯步驟,但是具體的實現方式由子類完成,跟策略模式的區別就是模板模式是有邏輯步驟的。比如要給院系裡的學生排序,並取出排名第一的學生。這裡就有2個步驟,分別是排序和取出第一名學生。
一段偽代碼:
public abstract class AbstractStudentGetter {
public final Student getStudent(List<Student> students) {
sort(students); // 第一步
if(!CollectionUtils.isEmpty(students)) {
return students.get(0); // 第二步
}
return null;
}
abstract public void sort(List<Student> students);
}
class AgeStudentGetter extends AbstractStudentGetter { // 取出年紀最大的學生
@Override
public void sort(List<Student> students) {
students.sort(new Comparator<Student>() {
@Override
public int compare(Student s1, Student s2) {
return s2.getAge() - s1.getAge();
}
});
}
}
class NameStudentGetter extends AbstractStudentGetter { // 按照名字字母排序取出第一個學生
@Override
public void sort(List<Student> students) {
students.sort(new Comparator<Student>() {
@Override
public int compare(Student s1, Student s2) {
return s2.getName().compareTo(s1.getName());
}
});
}
}
測試:
AbstractStudentGetter ageGetter = new AgeStudentGetter();
AbstractStudentGetter nameGetter = new NameStudentGetter();
List<Student> students = new ArrayList<>();
students.add(new Student("jim", 22));
students.add(new Student("format", 25));
System.out.println(ageGetter.getStudent(students)); // Student(name=format, age=25)
System.out.println(nameGetter.getStudent(students)); // Student(name=jim, age=22)
觀察者設計模式
觀察者設計模式主要的使用場景在於一個對象變化之後,依賴該對象的對象會收到通知。典型的例子就是rss的訂閱,當訂閱了博客的rss之後,當博客更新之後,訂閱者就會收到新的訂閱信息。
jdk內置提供了Observable和Observer,用來實現觀察者模式:
// 定義一個Observable
public class MetricsObserable extends Observable {
private Map<String, Long> counterMap = new HashMap<>();
public void updateCounter(String key, Long value) {
counterMap.put(key, value);
setChanged();
notifyObservers(counterMap);
}
}
// Observer
public class AdminA implements Observer {
@Override
public void update(Observable o, Object arg) {
System.out.println("adminA: " + arg);
}
}
public class AdminB implements Observer {
@Override
public void update(Observable o, Object arg) {
System.out.println("adminB: " + arg);
}
}
測試:
MetricsObserable metricsObserable = new MetricsObserable();
metricsObserable.addObserver(new AdminA());
metricsObserable.addObserver(new AdminB());
metricsObserable.updateCounter("request-count", 100l);
列印出:
adminB: {request-count=100}
adminA: {request-count=100}
享元模式
線程池中會構造幾個核心線程用於處理,這些線程會去取阻塞隊列里的任務然後進行執行。這些線程就是會被共享、且被重複使用的。因為線程的創建、銷毀、調度都是需要消耗資源的,沒有必要每次創建新的線程,而是共用一些線程。這就是享元模式的使用。類似的還有jdbc連接池,對象池等。
之前有一次面試被問到:
Integer.valueOf("1") == Integer.valueOf("1") // true還是false
當時回答的是false,後來翻了下Integer的源碼發現Integer裡面有個內部類IntegerCache,用於緩存一些共用的Integer。這個緩存的範圍可以在jvm啟動的時候進行設置。
其實後來想想也應該這麼做,我們沒有必要每次使用對象的時候都返回新的對象,可以共享這些對象,因為新對象的創建都是需要消耗內存的。
適配器模式
適配器模式比較好理解。像生活中插線口的插頭有2個口的,也有3個口的。如果電腦的電源插口只有3個口的,但是我們需要一個2個口的插口的話,這個時候就需要使用插座來外接這個3個口的插頭,插座上有2個口的插頭。
這個例子跟我們編程一樣,當用戶系統的接口跟我們系統內部的接口不一致時,我們可以使用適配器來完成接口的轉換。
使用繼承的方式實現類的適配:
public class Source {
public void method() {
System.out.println("source method");
}
}
interface Targetable {
void method();
void newMethod();
}
class Adapter extends Source implements Targetable {
@Override
public void newMethod() {
System.out.println("new method");
}
}
測試:
Targetable targetable = new Adapter();
targetable.method(); // source method
targetable.newMethod(); // new method
上述方式是用接口和繼承的方式實現適配器模式。當然我們也可以使用組合的方式實現(把Source當成屬性放到Adapter中)。
單例模式
單例模式比較好理解,Spring就是典型的例子。被Spring中的容器管理的對象都有對應的scope,配置成singleton說明這個對象就是單例,也就是在Spring容器的生命周期中,這個類只有1個實例。
java中單例模式的寫法也有好多種。比如懶漢式、餓漢式、內部類方式、枚舉方式等。
需要注意的如果使用dcl的話需要初始化過程,這篇Java內存模型之從JMM角度分析DCL文章中說明了dcl的正確用法。
Effectice java中推薦的單例方式寫法是使用枚舉類型的方式。
外觀模式
外觀模式用來包裝一組接口用於方便使用。比如系統中分10個模塊,有個功能需要組合使用所有的模塊,這個時候就需要一個包裝類包裝這10個接口,然後進行業務邏輯的調用。
歡迎在留言區留下你的觀點,一起討論提高。如果今天的文章讓你有新的啟發,學習能力的提升上有新的認識,歡迎轉發分享給更多人。
猜你還想看
阿里、騰訊、百度、華為、京東最新面試題匯集
Linux 系統 CPU 100% 異常排查實踐與總結
動畫演繹Java常用數據結構(建議收藏)
MyBatis 的9種設計模式,我猜你不一定知道!
關注「程式設計師小樂」,收看更多精彩內容
嘿,你在看嗎?