您的位置:首页 > 游戏 > 游戏 > 百度seo点击_宣传海报设计_网络营销环境分析包括哪些内容_深圳推广公司介绍

百度seo点击_宣传海报设计_网络营销环境分析包括哪些内容_深圳推广公司介绍

2024/12/22 2:46:50 来源:https://blog.csdn.net/lyk1184919379/article/details/142731876  浏览:    关键词:百度seo点击_宣传海报设计_网络营销环境分析包括哪些内容_深圳推广公司介绍
百度seo点击_宣传海报设计_网络营销环境分析包括哪些内容_深圳推广公司介绍

目录

观察者模式

概念

代码实现

直接写

重构

其他场景

扩展-EventBus

扩展-集成Spring

总结

模板模式

概念

模板模式作用一:复用

模板模式作用二:扩展

扩展-对比回调函数Callback

回调的原理

回调和模板模式的区别

总结

策略模式

概念与初步实现

1.策略的定义

2.策略的创建

3.策略的使用

解决问题示例

问题描述

代码重构

总结


观察者模式

概念

观察者模式(Observer Design Pattern)也被称为发布订阅模式(Publish-Subscribe Design Pattern)。在GoF的《设计模式》一书中,它的定义是这样的:

Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

翻译成中文就是:在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。

观察者模式在日常开发中比较普遍,一般情况下,被依赖的对象叫作被观察者(Observable),依赖的对象叫作观察者(Observer)。不过,在实际的项目开发中,这两种对象的称呼是比较灵活的,有各种不同的叫法,比如:Subject-Observer、Publisher-Subscriber、Producer-Consumer、EventEmitter-EventListener、Dispatcher-Listener。不管怎么称呼,只要应用场景符合刚刚给出的定义,都可以看作观察者模式。

代码实现

直接写

实际上,观察者模式是一个比较抽象的模式,根据不同的应用场景和需求,有完全不同的实现方式,待会我们会详细地讲到。现在,我们先来看其中最经典的一种实现方式。这也是在讲到这种模式的时候,很多书籍或资料给出的最常见的实现方式。具体的代码如下所示:

public interface Subject {void registerObserver(Observer observer);void removeObserver(Observer observer);void notifyObservers(Message message);
}public interface Observer {void update(Message message);
}public class ConcreteSubject implements Subject {private List observers = new ArrayList();@Overridepublic void registerObserver(Observer observer) {observers.add(observer);}@Overridepublic void removeObserver(Observer observer) {observers.remove(observer);}@Overridepublic void notifyObservers(Message message) {for (Observer observer : observers) {observer.update(message);}}}public class ConcreteObserverOne implements Observer {@Overridepublic void update(Message message) {//TODO: 获取消息通知,执行自己的逻辑...System.out.println("ConcreteObserverOne is notified.");}
}public class ConcreteObserverTwo implements Observer {@Overridepublic void update(Message message) {//TODO: 获取消息通知,执行自己的逻辑...System.out.println("ConcreteObserverTwo is notified.");}
}public class Demo {public static void main(String[] args) {ConcreteSubject subject = new ConcreteSubject();subject.registerObserver(new ConcreteObserverOne());subject.registerObserver(new ConcreteObserverTwo());subject.notifyObservers(new Message());}
}

在上面的代码中,定义了接口Subject和三个操作监听对象的方法,还定义了观察者接口Observer和一个具体业务方法,然后实现他们。最后在main方法中调用。

不过我看的这个课程对实现细节有所省略,刚好我以前遇到过某个具体实现,对比起来上面的例子主要是少了构造方法的细节,所以没完全明白的话可以运行下面这个例子:

public interface Subject {void registerObserver(Observer o);void removeObserver(Observer o);void notifyObservers();
}public interface Observer {void update(float temperature, float humidity, float pressure);
}public class WeatherData implements Subject{private List<Observer> list;private float temperature;private float humidity;private float pressure;WeatherData(){list=new ArrayList<>();}@Overridepublic void registerObserver(Observer o) {list.add(o);}@Overridepublic void removeObserver(Observer o) {if(list.contains(o)){list.remove(o);}}@Overridepublic void notifyObservers() {list.forEach(e-> e.update(temperature,humidity,pressure));}public void setMeasurements(float temperature, float humidity,float pressure){this.temperature = temperature;this.humidity = humidity;this.pressure = pressure;notifyObservers();}
}public class WeatherReport implements Observer {@Overridepublic void update(float temperature, float humidity, float pressure) {System.out.println("temperature "+temperature);System.out.println("humidity "+humidity);System.out.println("pressure "+pressure);}
}public class test {public static void main(String[] args) {WeatherReport weatherReport = new WeatherReport();WeatherData weatherData = new WeatherData();weatherData.registerObserver(weatherReport);weatherData.setMeasurements(36, 58, 100);}
}

上面每当调用 setMeasurements方法填写参数后,就调用notifyObservers方法对Observer列表逐个发送通知,继承了Observer接口的实现类就会执行具体的方法。

重构

上面是直接用观察者模式实现的结果,下面再看一下重构时需要哪些步骤。

假设我们在开发一个P2P投资理财系统,用户注册成功之后,我们会给用户发放投资体验金。代码实现大致是下面这个样子的:

public class UserController {private UserService userService; // 依赖注入private PromotionService promotionService; // 依赖注入public Long register(String telephone, String password) {//省略输入参数的校验代码//省略userService.register()异常的try-catch代码long userId = userService.register(telephone, password);// 注册promotionService.issueNewUserExperienceCash(userId);// 发送体验金return userId;}
}


虽然注册接口做了两件事情,注册和发放体验金,违反单一职责原则,但是,如果没有扩展和修改的需求,现在的代码实现是可以接受的。如果非得用观察者模式,就需要引入更多的类和更加复杂的代码结构,反倒是一种过度设计。

相反,如果需求频繁变动,比如,用户注册成功之后,不再发放体验金,而是改为发放优惠券,并且还要给用户发送一封“欢迎注册成功”的站内信。这种情况下,我们就需要频繁地修改register()函数中的代码,违反开闭原则。而且,如果注册成功之后需要执行的后续操作越来越多,那register()函数的逻辑会变得越来越复杂,也就影响到代码的可读性和可维护性。

这个时候,观察者模式就能派上用场了。利用观察者模式,我对上面的代码进行了重构。重构之后的代码如下所示:

public interface RegObserver {void handleRegSuccess(long userId);
}public class RegPromotionObserver implements RegObserver {private PromotionService promotionService; // 依赖注入@Overridepublic void handleRegSuccess(long userId) {promotionService.issueNewUserExperienceCash(userId);// 发送体验金}
}public class RegNotificationObserver implements RegObserver {private NotificationService notificationService;@Overridepublic void handleRegSuccess(long userId) {notificationService.sendInboxMessage(userId, "Welcome...");// 推送用户注册信息给大数据征信系统}
}public class UserController {private UserService userService; // 依赖注入private List regObservers = new ArrayList<>();// 一次性设置好,之后也不可能动态的修改public void setRegObservers(List observers) {regObservers.addAll(observers);}public Long register(String telephone, String password) {//省略输入参数的校验代码//省略userService.register()异常的try-catch代码long userId = userService.register(telephone, password);// 注册for (RegObserver observer : regObservers) {observer.handleRegSuccess(userId);// 通知给所有观察者}return userId;}
}

当我们需要添加新的观察者的时候,比如,用户注册成功之后,推送用户注册信息给大数据征信系统,基于观察者模式的代码实现,UserController类的register()函数完全不需要修改,只需要再添加一个实现了RegObserver接口的类,并且通过setRegObservers()函数将它注册到UserController类中即可。

不过,你可能会说,当我们把发送体验金替换为发送优惠券的时候,需要修改RegPromotionObserver类中handleRegSuccess()函数的代码,这还是违反开闭原则呀?你说得没错,不过,相对于register()函数来说,handleRegSuccess()函数的逻辑要简单很多,修改更不容易出错,引入bug的风险更低。

其他场景

观察者模式的应用场景非常广泛,小到代码层面的解耦,大到架构层面的系统解耦,再或者一些产品的设计思路,都有这种模式的影子,比如,邮件订阅、RSS Feeds,本质上都是观察者模式。

不同的应用场景和需求下,这个模式也有截然不同的实现方式,开篇的时候我们也提到,有同步阻塞的实现方式,也有异步非阻塞的实现方式;有进程内的实现方式,也有跨进程的实现方式。

之前讲到的实现方式,从刚刚的分类方式上来看,它是一种同步阻塞的实现方式。观察者和被观察者代码在同一个线程内执行,被观察者一直阻塞,直到所有的观察者代码都执行完成之后,才执行后续的代码。对照上面讲到的用户注册的例子,register()函数依次调用执行每个观察者的handleRegSuccess()函数,等到都执行完成之后,才会返回结果给客户端。

如果注册接口是一个调用比较频繁的接口,对性能非常敏感,希望接口的响应时间尽可能短,那我们可以将同步阻塞的实现方式改为异步非阻塞的实现方式,以此来减少响应时间。具体来讲,当userService.register()函数执行完成之后,我们启动一个新的线程来执行观察者的handleRegSuccess()函数,这样userController.register()函数就不需要等到所有的handleRegSuccess()函数都执行完成之后才返回结果给客户端。userController.register()函数从执行3个SQL语句才返回,减少到只需要执行1个SQL语句就返回,响应时间粗略来讲减少为原来的1/3。

那如何实现一个异步非阻塞的观察者模式呢?简单一点的做法是,在每个handleRegSuccess()函数中,创建一个新的线程执行代码。不过,我们还有更加优雅的实现方式,那就是基于EventBus来实现。这点在下面扩展中会讲到。

刚刚讲到的两个场景,不管是同步阻塞实现方式还是异步非阻塞实现方式,都是进程内的实现方式。如果用户注册成功之后,我们需要发送用户信息给大数据征信系统,而大数据征信系统是一个独立的系统,跟它之间的交互是跨不同进程的,那如何实现一个跨进程的观察者模式呢?

如果大数据征信系统提供了发送用户注册信息的RPC接口,我们仍然可以沿用之前的实现思路,在handleRegSuccess()函数中调用RPC接口来发送数据。但是,我们还有更加优雅、更加常用的一种实现方式,那就是基于消息队列(Message Queue,比如ActiveMQ)来实现。

当然,这种实现方式也有弊端,那就是需要引入一个新的系统(消息队列),增加了维护成本。不过,它的好处也非常明显。在原来的实现方式中,观察者需要注册到被观察者中,被观察者需要依次遍历观察者来发送消息。而基于消息队列的实现方式,被观察者和观察者解耦更加彻底,两部分的耦合更小。被观察者完全不感知观察者,同理,观察者也完全不感知被观察者。被观察者只管发送消息到消息队列,观察者只管从消息队列中读取消息来执行相应的逻辑。

扩展-EventBus

对于异步非阻塞观察者模式,如果只是实现一个简易版本,不考虑任何通用性、复用性,实际上是非常容易的。

我们有两种实现方式。其中一种是:在每个handleRegSuccess()函数中创建一个新的线程执行代码逻辑;另一种是:在UserController的register()函数中使用线程池来执行每个观察者的handleRegSuccess()函数。两种实现方式的具体代码如下所示:

// 第一种实现方式,其他类代码不变,就没有再重复罗列
public class RegPromotionObserver implements RegObserver {private PromotionService promotionService; // 依赖注入@Overridepublic void handleRegSuccess(Long userId) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {promotionService.issueNewUserExperienceCash(userId);}});thread.start();}
}// 第二种实现方式,其他类代码不变,就没有再重复罗列
public class UserController {private UserService userService; // 依赖注入private List regObservers = new ArrayList<>();private Executor executor;public UserController(Executor executor) {this.executor = executor;}public void setRegObservers(List observers) {regObservers.addAll(observers);}public Long register(String telephone, String password) {//省略输入参数的校验代码//省略userService.register()异常的try-catch代码long userId = userService.register(telephone, password);for (RegObserver observer : regObservers) {executor.execute(new Runnable() {@Overridepublic void run() {observer.handleRegSuccess(userId);}});}return userId;}
}

对于第一种实现方式,频繁地创建和销毁线程比较耗时,并且并发线程数无法控制,创建过多的线程会导致堆栈溢出。第二种实现方式,尽管利用了线程池解决了第一种实现方式的问题,但线程池、异步执行逻辑都耦合在了register()函数中,增加了这部分业务代码的维护成本。

如果我们的需求更加极端一点,需要在同步阻塞和异步非阻塞之间灵活切换,那就要不停地修改UserController的代码。除此之外,如果在项目中,不止一个业务模块需要用到异步非阻塞观察者模式,那这样的代码实现也无法做到复用。

我们知道,框架的作用有:隐藏实现细节,降低开发难度,做到代码复用,解耦业务与非业务代码,让程序员聚焦业务开发。针对异步非阻塞观察者模式,我们也可以将它抽象成框架来达到这样的效果,而这个框架就是下面讲的EventBus。

这块没有太过细看,暂时记录如果以后用到再看。

EventBus翻译为“事件总线”,它提供了实现观察者模式的骨架代码。我们可以基于此框架,非常容易地在自己的业务场景中实现观察者模式,不需要从零开始开发。其中,Google Guava EventBus就是一个比较著名的EventBus框架,它不仅仅支持异步非阻塞模式,同时也支持同步阻塞模式
现在,我们就通过例子来看一下,Guava EventBus具有哪些功能。还是上节课那个用户注册的例子,我们用Guava EventBus重新实现一下,代码如下所示:

public class UserController {private UserService userService; // 依赖注入private EventBus eventBus;private static final int DEFAULT_EVENTBUS_THREAD_POOL_SIZE = 20;public UserController() { //eventBus = new EventBus(); // 同步阻塞模式eventBus = new AsyncEventBus(Executors.newFixedThreadPool(DEFAULT_EVENTBUS_THREAD_POOL_SIZE)); // 异步非阻塞模式 } public void setRegObservers(List observers) {for(Object observer : observers) {eventBus.register(observer);}}public Long register(String telephone, String password) {//省略输入参数的校验代码//省略userService.register()异常的try-catch代码long userId = userService.register(telephone, password);eventBus.post(userId);return userId;}
}public class RegPromotionObserver {private PromotionService promotionService; // 依赖注入@Subscribepublic void handleRegSuccess(Long userId) {promotionService.issueNewUserExperienceCash(userId);}
}public class RegNotificationObserver {private NotificationService notificationService;@Subscribepublic void handleRegSuccess(Long userId) {notificationService.sendInboxMessage(userId, "...");}
}

利用EventBus框架实现的观察者模式,跟从零开始编写的观察者模式相比,从大的流程上来说,实现思路大致一样,都需要定义Observer,并且通过register()函数注册Observer,也都需要通过调用某个函数(比如,EventBus中的post()函数)来给Observer发送消息(在EventBus中消息被称作事件event)。
但在实现细节方面,它们又有些区别。基于EventBus,我们不需要定义Observer接口,任意类型的对象都可以注册到EventBus中,通过@Subscribe注解来标明类中哪个函数可以接收被观察者发送的消息。
接下来,我们详细地讲一下,Guava EventBus的几个主要的类和函数。
•EventBus、AsyncEventBus Guava EventBus对外暴露的所有可调用接口,都封装在EventBus类中。其中,EventBus实现了同步阻塞的观察者模式,AsyncEventBus继承自EventBus,提供了异步非阻塞的观察者模式。具体使用方式如下所示:
EventBus eventBus = new EventBus(); // 同步阻塞模式 EventBus eventBus = new AsyncEventBus(Executors.newFixedThreadPool(8));// 异步阻塞模式
•register()函数 EventBus类提供了register()函数用来注册观察者。具体的函数定义如下所示。它可以接受任何类型(Object)的观察者。而在经典的观察者模式的实现中,register()函数必须接受实现了同一Observer接口的类对象。
public void register(Object object);
•unregister()函数 相对于register()函数,unregister()函数用来从EventBus中删除某个观察者。我就不多解释了,具体的函数定义如下所示:
public void unregister(Object object);
•post()函数 EventBus类提供了post()函数,用来给观察者发送消息。具体的函数定义如下所示:
public void post(Object event);
跟经典的观察者模式的不同之处在于,当我们调用post()函数发送消息的时候,并非把消息发送给所有的观察者,而是发送给可匹配的观察者。所谓可匹配指的是,能接收的消息类型是发送消息(post函数定义中的event)类型的父类。我举个例子来解释一下。
比如,AObserver能接收的消息类型是XMsg,BObserver能接收的消息类型是YMsg,CObserver能接收的消息类型是ZMsg。其中,XMsg是YMsg的父类。当我们如下发送消息的时候,相应能接收到消息的可匹配观察者如下所示:
XMsg xMsg = new XMsg(); YMsg yMsg = new YMsg(); ZMsg zMsg = new ZMsg(); post(xMsg); => AObserver接收到消息 post(yMsg); => AObserver、BObserver接收到消息 post(zMsg); => CObserver接收到消息
你可能会问,每个Observer能接收的消息类型是在哪里定义的呢?我们来看下Guava EventBus最特别的一个地方,那就是@Subscribe注解。
•@Subscribe注解 EventBus通过@Subscribe注解来标明,某个函数能接收哪种类型的消息。具体的使用代码如下所示。在DObserver类中,我们通过@Subscribe注解了两个函数f1()、f2()。
public DObserver { //...省略其他属性和方法... @Subscribe public void f1(PMsg event) { //... } @Subscribe public void f2(QMsg event) { //... } }
当通过register()函数将DObserver 类对象注册到EventBus的时候,EventBus会根据@Subscribe注解找到f1()和f2(),并且将两个函数能接收的消息类型记录下来(PMsg->f1,QMsg->f2)。当我们通过post()函数发送消息(比如QMsg消息)的时候,EventBus会通过之前的记录(QMsg->f2),调用相应的函数(f2)。

整个小框架的代码实现包括5个类:EventBus、AsyncEventBus、Subscribe、ObserverAction、ObserverRegistry。接下来,我们依次来看下这5个类。

1.Subscribe
Subscribe是一个注解,用于标明观察者中的哪个函数可以接收消息。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Beta
public @interface Subscribe {}


2.ObserverAction
ObserverAction类用来表示@Subscribe注解的方法,其中,target表示观察者类,method表示方法。它主要用在ObserverRegistry观察者注册表中。

public class ObserverAction {private Object target;private Method method;public ObserverAction(Object target, Method method) {this.target = Preconditions.checkNotNull(target);this.method = method;this.method.setAccessible(true);}public void execute(Object event) { // event是method方法的参数try {method.invoke(target, event);} catch (InvocationTargetException | IllegalAccessException e) {e.printStackTrace();}}
}


3.ObserverRegistry
ObserverRegistry类就是前面讲到的Observer注册表,是最复杂的一个类,框架中几乎所有的核心逻辑都在这个类中。这个类大量使用了Java的反射语法,不过代码整体来说都不难理解,其中,一个比较有技巧的地方是CopyOnWriteArraySet的使用。

CopyOnWriteArraySet,顾名思义,在写入数据的时候,会创建一个新的set,并且将原始数据clone到新的set中,在新的set中写入数据完成之后,再用新的set替换老的set。这样就能保证在写入数据的时候,不影响数据的读取操作,以此来解决读写并发问题。除此之外,CopyOnWriteSet还通过加锁的方式,避免了并发写冲突。具体的作用你可以去查看一下CopyOnWriteSet类的源码,一目了然。

public class ObserverRegistry {private ConcurrentMap, CopyOnWriteArraySet> registry = new ConcurrentHashMap<>();public void register(Object observer) {Map, Collection> observerActions = findAllObserverActions(observer);for (Map.Entry, Collection> entry : observerActions.entrySet()) {Class eventType = entry.getKey();Collection eventActions = entry.getValue();CopyOnWriteArraySet registeredEventActions = registry.get(eventType);if (registeredEventActions == null) {registry.putIfAbsent(eventType, new CopyOnWriteArraySet<>());registeredEventActions = registry.get(eventType);}registeredEventActions.addAll(eventActions);}}public List getMatchedObserverActions(Object event) {List matchedObservers = new ArrayList<>();Class postedEventType = event.getClass();for (Map.Entry, CopyOnWriteArraySet> entry : registry.entrySet()) {Class eventType = entry.getKey();Collection eventActions = entry.getValue();if (postedEventType.isAssignableFrom(eventType)) {matchedObservers.addAll(eventActions);}}return matchedObservers;}private Map, Collection> findAllObserverActions(Object observer) {Map, Collection> observerActions = new HashMap<>();Class clazz = observer.getClass();for (Method method : getAnnotatedMethods(clazz)) {Class[] parameterTypes = method.getParameterTypes();Class eventType = parameterTypes[0];if (!observerActions.containsKey(eventType)) {observerActions.put(eventType, new ArrayList<>());}observerActions.get(eventType).add(new ObserverAction(observer, method));}return observerActions;}private List getAnnotatedMethods(Class clazz) {List annotatedMethods = new ArrayList<>();for (Method method : clazz.getDeclaredMethods()) {if (method.isAnnotationPresent(Subscribe.class)) {Class[] parameterTypes = method.getParameterTypes();Preconditions.checkArgument(parameterTypes.length == 1,"Method %s has @Subscribe annotation but has %s parameters."+ "Subscriber methods must have exactly 1 parameter.",method, parameterTypes.length);annotatedMethods.add(method);}}return annotatedMethods;}
}

4.EventBus
EventBus实现的是阻塞同步的观察者模式。看代码你可能会有些疑问,这明明就用到了线程池Executor啊。实际上,MoreExecutors.directExecutor()是Google Guava提供的工具类,看似是多线程,实际上是单线程。之所以要这么实现,主要还是为了跟AsyncEventBus统一代码逻辑,做到代码复用。

public class EventBus {private Executor executor;private ObserverRegistry registry = new ObserverRegistry();public EventBus() {this(MoreExecutors.directExecutor());}protected EventBus(Executor executor) {this.executor = executor;}public void register(Object object) {registry.register(object);}public void post(Object event) {List observerActions = registry.getMatchedObserverActions(event);for (ObserverAction observerAction : observerActions) {executor.execute(new Runnable() {@Overridepublic void run() {observerAction.execute(event);}});}}
}

5.AsyncEventBus

有了EventBus,AsyncEventBus的实现就非常简单了。为了实现异步非阻塞的观察者模式,它就不能再继续使用MoreExecutors.directExecutor()了,而是需要在构造函数中,由调用者注入线程池。

public class AsyncEventBus extends EventBus {public AsyncEventBus(Executor executor) {super(executor);}
}

至此,我们用了不到200行代码,就实现了一个还算凑活能用的EventBus,从功能上来讲,它跟Google Guava EventBus几乎一样。不过,如果去查看Google Guava EventBus的源码,你会发现,在实现细节方面,相比我们现在的实现,它其实做了很多优化,比如优化了在注册表中查找消息可匹配函数的算法。如果有时间的话,建议你去读一下它的源码。

扩展-集成Spring

之前总结的Spring IOC中有写过,可以看Spring IOC的理解_谈谈自己对于spring ioc的理解-CSDN博客

的【4.2 事件机制】,直接粘贴过来就是

ApplicationContext提供了一套事件机制,在容器发生变动时我们可以通过ApplicationEvent的子类通知到ApplicationListener接口的实现类(或增加@EventListener注解),做对应的处理。例如ApplicationContext在启动、停止、关闭和刷新时,分别会发出ContextStartEvent、ContextStoppedEvent、ContextClosedEvent和ContextRefreshEvent事件,这些事件可以让我们有机会感知当前容器的状态。

我们也可以自己监听这些事件,只需实现ApplicationListener接口或在某个Bean方法上增加@EventListener注解即可。通过这个来自定义事件,不过该事件必须继承ApplicationEvent,而且产生事件的类需要实现ApplicationEventPublisherAware,还要从上下文中获取到ApplicationEventPublisher——可以通过上下文applicationContext.publishEvent或通过实现了ApplicationEventPublisherAware接口的类里面直接获得。

如果在监听到事件后希望再发出另一个事件,这时可以将方法返回值从void修改为对应事件的类型。

Spring的内置事件以及自定义事件其实就是设计模式的观察者模式,关于自定义事件的实现可以参考一下我之前写过的 设计模式-观察者模式-CSDN博客

另外这里补充一点,上述@EventListener默认是阻塞的,改成异步非阻塞可以同时添加

@EventListener

@Async("asyncExecutor")

或者通过配置ApplicationEventMulticaster的taskExecutor属性来实现(后者不太常见)。

总结

设计模式要干的事情就是解耦,创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦,具体到观察者模式,它将观察者和被观察者代码解耦。借助设计模式,我们利用更好的代码结构,将一大坨代码拆分成职责更单一的小类,让其满足开闭原则、高内聚低耦合等特性,以此来控制和应对代码的复杂性,提高代码的可扩展性。

观察者模式的应用场景非常广泛,小到代码层面的解耦,大到架构层面的系统解耦,再或者一些产品的设计思路,都有这种模式的影子,比如,邮件订阅、RSS Feeds,本质上都是观察者模式。不同的应用场景和需求下,这个模式也有截然不同的实现方式,有同步阻塞的实现方式,也有异步非阻塞的实现方式;有进程内的实现方式,也有跨进程的实现方式。

模板模式

概念

模板模式,全称是模板方法设计模式,英文是Template Method Design Pattern。在GoF的《设计模式》一书中,它是这么定义的:

Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.

翻译成中文就是:模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。

这里的“算法”,我们可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。

原理很简单,代码实现就更加简单,我写了一个示例代码,如下所示。templateMethod()函数定义为final,是为了避免子类重写它。method1()和method2()定义为abstract,是为了强迫子类去实现。不过,这些都不是必须的,在实际的项目开发中,模板模式的代码实现比较灵活,待会儿讲到应用场景的时候,我们会有具体的体现。

public abstract class AbstractClass {// 模板方法,里面定义了执行流程,开了两个可变函数public final void templateMethod() {//...method1();//...method2();//...}protected abstract void method1();// 可变的部分protected abstract void method2();
}public class ConcreteClass1 extends AbstractClass {@Overrideprotected void method1() {//...}@Overrideprotected void method2() {//...}
}public class ConcreteClass2 extends AbstractClass {@Overrideprotected void method1() {//...}@Overrideprotected void method2() {//...}
}AbstractClass demo = ConcreteClass1();
demo.templateMethod();

在上面的例子中,AbstractClass是模板类,里面final修饰的方法templateMethod是模板方法,程序员可以通过继承模板类,重写可变方法来自定义一些业务逻辑。

模板模式作用一:复用

开篇的时候,我们讲到模板模式有两大作用:复用和扩展。我们先来看它的第一个作用:复用。

模板模式把一个算法中不变的流程抽象到父类的模板方法templateMethod()中,将可变的部分method1()、method2()留给子类ContreteClass1和ContreteClass2来实现。所有的子类都可以复用父类中模板方法定义的流程代码。我们通过两个小例子来更直观地体会一下。

1.Java InputStream
Java IO类库中,有很多类的设计用到了模板模式,比如InputStream、OutputStream、Reader、Writer。我们拿InputStream来举例说明一下。

我把InputStream部分相关代码贴在了下面。在代码中,read()函数是一个模板方法,定义了读取数据的整个流程,并且暴露了一个可以由子类来定制的抽象方法。不过这个方法也被命名为了read(),只是参数跟模板方法不同。

public abstract class InputStream implements Closeable {//...省略其他代码...public int read(byte b[], int off, int len) throws IOException {if (b == null) {throw new NullPointerException();} else if (off < 0 || len < 0 || len > b.length - off) {throw new IndexOutOfBoundsException();} else if (len == 0) {return 0;}int c = read();if (c == -1) {return -1;}b[off] = (byte)c;int i = 1;try {for (; i < len ; i++) {c = read();if (c == -1) {break;}b[off + i] = (byte)c;}} catch (IOException ee) {}return i;}public abstract int read() throws IOException;
}public class ByteArrayInputStream extends InputStream {//...省略其他代码...@Overridepublic synchronized int read() {return (pos < count) ? (buf[pos++] & 0xff) : -1;}
}

2.Java AbstractList
在Java AbstractList类中,addAll()函数可以看作模板方法,add()是子类需要重写的方法,尽管没有声明为abstract的,但函数实现直接抛出了UnsupportedOperationException异常。前提是,如果子类不重写是不能使用的。

public boolean addAll(int index, Collection c) {rangeCheckForAdd(index);boolean modified = false;for (E e : c) {add(index++, e);modified = true;}return modified;
}public void add(int index, E element) {throw new UnsupportedOperationException();
}

模板模式作用二:扩展

模板模式可以用在框架的开发中,让框架用户可以在不修改框架源码的情况下,定制化框架的功能,比如Java Servlet、Junit TestCase:

1.Java Servlet
对于Java Web项目开发来说,常用的开发框架是SpringMVC。利用它,我们只需要关注业务代码的编写,底层的原理几乎不会涉及。但是,如果我们抛开这些高级框架来开发Web项目,必然会用到Servlet。实际上,使用比较底层的Servlet来开发Web项目也不难。我们只需要定义一个继承HttpServlet的类,并且重写其中的doGet()或doPost()方法,来分别处理get和post请求。具体的代码示例如下所示:

public class HelloServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {this.doPost(req, resp);}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.getWriter().write("Hello World.");}
}


除此之外,我们还需要在配置文件web.xml中做如下配置。Tomcat、Jetty等Servlet容器在启动的时候,会自动加载这个配置文件中的URL和Servlet之间的映射关系。

    HelloServlet
    com.xzg.cd.HelloServlet

    HelloServlet
    /hello

当我们在浏览器中输入网址(比如,http://127.0.0.1:8080/hello )的时候,Servlet容器会接收到相应的请求,并且根据URL和Servlet之间的映射关系,找到相应的Servlet(HelloServlet),然后执行它的service()方法。service()方法定义在父类HttpServlet中,它会调用doGet()或doPost()方法,然后输出数据(“Hello world”)到网页。

我们现在来看,HttpServlet的service()函数长什么样子。

public void service(ServletRequest req, ServletResponse res)throws ServletException, IOException
{HttpServletRequest  request;HttpServletResponse response;if (!(req instanceof HttpServletRequest &&res instanceof HttpServletResponse)) {throw new ServletException("non-HTTP request or response");}request = (HttpServletRequest) req;response = (HttpServletResponse) res;service(request, response);
}protected void service(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException
{String method = req.getMethod();if (method.equals(METHOD_GET)) {long lastModified = getLastModified(req);if (lastModified == -1) {// servlet doesn't support if-modified-since, no reason// to go through further expensive logicdoGet(req, resp);} else {long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);if (ifModifiedSince < lastModified) {// If the servlet mod time is later, call doGet()// Round down to the nearest second for a proper compare// A ifModifiedSince of -1 will always be lessmaybeSetLastModified(resp, lastModified);doGet(req, resp);} else {resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);}}} else if (method.equals(METHOD_HEAD)) {long lastModified = getLastModified(req);maybeSetLastModified(resp, lastModified);doHead(req, resp);} else if (method.equals(METHOD_POST)) {doPost(req, resp);} else if (method.equals(METHOD_PUT)) {doPut(req, resp);} else if (method.equals(METHOD_DELETE)) {doDelete(req, resp);} else if (method.equals(METHOD_OPTIONS)) {doOptions(req,resp);} else if (method.equals(METHOD_TRACE)) {doTrace(req,resp);} else {String errMsg = lStrings.getString("http.method_not_implemented");Object[] errArgs = new Object[1];errArgs[0] = method;errMsg = MessageFormat.format(errMsg, errArgs);resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);}
}

从上面的代码中我们可以看出,HttpServlet的service()方法就是一个模板方法,它实现了整个HTTP请求的执行流程,doGet()、doPost()是模板中可以由子类来定制的部分。实际上,这就相当于Servlet框架提供了一个扩展点(doGet()、doPost()方法),让框架用户在不用修改Servlet框架源码的情况下,将业务代码通过扩展点镶嵌到框架中执行。

2.Junit TestCase

跟Java Servlet类似,JUnit框架也通过模板模式提供了一些功能扩展点(setUp()、tearDown()等),让框架用户可以在这些扩展点上扩展功能。

在使用JUnit测试框架来编写单元测试的时候,我们编写的测试类都要继承框架提供的TestCase类。在TestCase类中,runBare()函数是模板方法,它定义了执行测试用例的整体流程:先执行setUp()做些准备工作,然后执行runTest()运行真正的测试代码,最后执行tearDown()做扫尾工作。

TestCase类的具体代码如下所示。尽管setUp()、tearDown()并不是抽象函数,还提供了默认的实现,不强制子类去重新实现,但这部分也是可以在子类中定制的,所以也符合模板模式的定义。

public abstract class TestCase extends Assert implements Test {public void runBare() throws Throwable {Throwable exception = null;setUp();try {runTest();} catch (Throwable running) {exception = running;} finally {try {tearDown();} catch (Throwable tearingDown) {if (exception == null) exception = tearingDown;}}if (exception != null) throw exception;}/*** Sets up the fixture, for example, open a network connection.* This method is called before a test is executed.*/protected void setUp() throws Exception {}/*** Tears down the fixture, for example, close a network connection.* This method is called after a test is executed.*/protected void tearDown() throws Exception {}
}

扩展-对比回调函数Callback

假设一个框架中的某个类暴露了两个模板方法,并且定义了一堆供模板方法调用的抽象方法,代码示例如下所示。在项目开发中,即便我们只用到这个类的其中一个模板方法,我们还是要在子类中把所有的抽象方法都实现一遍,这相当于无效劳动,有没有其他方式来解决这个问题呢?

public abstract class AbstractClass {public final void templateMethod1() {//...method1();//...method2();//...}public final void templateMethod2() {//...method3();//...method4();//...}protected abstract void method1();protected abstract void method2();protected abstract void method3();protected abstract void method4();
}

回调的原理

相对于普通的函数调用来说,回调是一种双向调用关系。A类事先注册某个函数F到B类,A类在调用B类的P函数的时候,B类反过来调用A类注册给它的F函数。这里的F函数就是“回调函数”。A调用B,B反过来又调用A,这种调用机制就叫作“回调”。

A类如何将回调函数传递给B类呢?不同的编程语言,有不同的实现方法。C语言可以使用函数指针,Java则需要使用包裹了回调函数的类对象,我们简称为回调对象。这里我用Java语言举例说明一下。代码如下所示:

public interface ICallback {void methodToCallback();
}public class BClass {public void process(ICallback callback) {//...callback.methodToCallback();//...}
}public class AClass {public static void main(String[] args) {BClass b = new BClass();b.process(new ICallback() { //回调对象@Overridepublic void methodToCallback() {System.out.println("Call back me.");}});}
}

上面就是Java语言中回调的典型代码实现。从代码实现中,我们可以看出,回调跟模板模式一样,也具有复用和扩展的功能。除了回调函数之外,BClass类的process()函数中的逻辑都可以复用。如果ICallback、BClass类是框架代码,AClass是使用框架的客户端代码,我们可以通过ICallback重写方法定制process()函数,也就是说,框架因此具有了扩展的能力。

实际上,回调不仅可以应用在代码设计上,在更高层次的架构设计上也比较常用。比如,通过三方支付系统来实现支付功能,用户在发起支付请求之后,一般不会一直阻塞到支付结果返回,而是注册回调接口(类似回调函数,一般是一个回调用的URL)给三方支付系统,等三方支付系统执行完成之后,将结果通过回调接口返回给用户。

回调可以分为同步回调和异步回调(或者延迟回调)。同步回调指在函数返回之前执行回调函数;异步回调指的是在函数返回之后执行回调函数。上面的代码实际上是同步回调的实现方式,在process()函数返回之前,执行完回调函数methodToCallback()。而上面支付的例子是异步回调的实现方式,发起支付之后不需要等待回调接口被调用就直接返回。从应用场景上来看,同步回调看起来更像模板模式,异步回调看起来更像观察者模式。

应用举例一:JdbcTemplate

Spring提供了很多Template类,比如,JdbcTemplate、RedisTemplate、RestTemplate。尽管都叫作xxxTemplate,但它们并非基于模板模式来实现的,而是基于回调来实现的,确切地说应该是同步回调。而同步回调从应用场景上很像模板模式,所以,在命名上,这些类使用Template(模板)这个单词作为后缀。

这些Template类的设计思路都很相近,所以,我们只拿其中的JdbcTemplate来举例分析一下。对于其他Template类,你可以阅读源码自行分析。

在前面的章节中,我们也多次提到,Java提供了JDBC类库来封装不同类型的数据库操作。不过,直接使用JDBC来编写操作数据库的代码,还是有点复杂的。比如,下面这段是使用JDBC来查询用户信息的代码。

/*** 原始代码块
*/
public class JdbcDemo {public User queryUser(long id) {Connection conn = null;Statement stmt = null;try {//1.加载驱动Class.forName("com.mysql.jdbc.Driver");conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/demo", "xzg", "xzg");//2.创建statement类对象,用来执行SQL语句stmt = conn.createStatement();//3.ResultSet类,用来存放获取的结果集String sql = "select * from user where id=" + id;ResultSet resultSet = stmt.executeQuery(sql);String eid = null, ename = null, price = null;while (resultSet.next()) {User user = new User();user.setId(resultSet.getLong("id"));user.setName(resultSet.getString("name"));user.setTelephone(resultSet.getString("telephone"));return user;}} catch (ClassNotFoundException e) {// TODO: log...} catch (SQLException e) {// TODO: log...} finally {if (conn != null)try {conn.close();} catch (SQLException e) {// TODO: log...}if (stmt != null)try {stmt.close();} catch (SQLException e) {// TODO: log...}}return null;}}

queryUser()函数包含很多流程性质的代码,跟业务无关,比如,加载驱动、创建数据库连接、创建statement、关闭连接、关闭statement、处理异常。针对不同的SQL执行请求,这些流程性质的代码是相同的、可以复用的,我们不需要每次都重新敲一遍。

针对这个问题,Spring提供了JdbcTemplate,对JDBC进一步封装,来简化数据库编程。使用JdbcTemplate查询用户信息,我们只需要编写跟这个业务有关的代码,其中包括,查询用户的SQL语句、查询结果与User对象之间的映射关系。其他流程性质的代码都封装在了JdbcTemplate类中,不需要我们每次都重新编写。我用JdbcTemplate重写了上面的例子,代码简单了很多,如下所示:

/**
* 使用jdbcTemplate
*/
public class JdbcTemplateDemo {private JdbcTemplate jdbcTemplate;public User queryUser(long id) {String sql = "select * from user where id="+id;return jdbcTemplate.query(sql, new UserRowMapper()).get(0);}class UserRowMapper implements RowMapper {public User mapRow(ResultSet rs, int rowNum) throws SQLException {User user = new User();user.setId(rs.getLong("id"));user.setName(rs.getString("name"));user.setTelephone(rs.getString("telephone"));return user;}}
}

那JdbcTemplate底层具体是如何实现的呢?我们来看一下它的源码。因为JdbcTemplate代码比较多,我只摘抄了部分相关代码,贴到了下面。其中,JdbcTemplate通过回调的机制,将不变的执行流程抽离出来,放到模板方法execute()中,将可变的部分设计成回调StatementCallback,由用户来定制。query()函数是对execute()函数的二次封装,让接口用起来更加方便。

@Override
public  List query(String sql, RowMapper rowMapper) throws DataAccessException {return query(sql, new RowMapperResultSetExtractor(rowMapper));
}@Override
public  T query(final String sql, final ResultSetExtractor rse) throws DataAccessException {Assert.notNull(sql, "SQL must not be null");Assert.notNull(rse, "ResultSetExtractor must not be null");if (logger.isDebugEnabled()) {logger.debug("Executing SQL query [" + sql + "]");}class QueryStatementCallback implements StatementCallback, SqlProvider {@Overridepublic T doInStatement(Statement stmt) throws SQLException {ResultSet rs = null;try {rs = stmt.executeQuery(sql);ResultSet rsToUse = rs;if (nativeJdbcExtractor != null) {rsToUse = nativeJdbcExtractor.getNativeResultSet(rs);}return rse.extractData(rsToUse);}finally {JdbcUtils.closeResultSet(rs);}}@Overridepublic String getSql() {return sql;}}return execute(new QueryStatementCallback());
}@Override
public  T execute(StatementCallback action) throws DataAccessException {Assert.notNull(action, "Callback object must not be null");Connection con = DataSourceUtils.getConnection(getDataSource());Statement stmt = null;try {Connection conToUse = con;if (this.nativeJdbcExtractor != null &&this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativeStatements()) {conToUse = this.nativeJdbcExtractor.getNativeConnection(con);}stmt = conToUse.createStatement();applyStatementSettings(stmt);Statement stmtToUse = stmt;if (this.nativeJdbcExtractor != null) {stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt);}T result = action.doInStatement(stmtToUse);handleWarnings(stmt);return result;}catch (SQLException ex) {// Release Connection early, to avoid potential connection pool deadlock// in the case when the exception translator hasn't been initialized yet.JdbcUtils.closeStatement(stmt);stmt = null;DataSourceUtils.releaseConnection(con, getDataSource());con = null;throw getExceptionTranslator().translate("StatementCallback", getSql(action), ex);}finally {JdbcUtils.closeStatement(stmt);DataSourceUtils.releaseConnection(con, getDataSource());}
}

应用举例二:setClickListener()

在客户端开发中,我们经常给控件注册事件监听器,比如下面这段代码,就是在Android应用开发中,给Button控件的点击事件注册监听器。

Button button = (Button)findViewById(R.id.button);
button.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {System.out.println("I am clicked.");}
});


从代码结构上来看,事件监听器很像回调,即传递一个包含回调函数(onClick())的对象给另一个函数。从应用场景上来看,它又很像观察者模式,即事先注册观察者(OnClickListener),当用户点击按钮的时候,发送点击事件给观察者,并且执行相应的onClick()函数。

我们前面讲到,回调分为同步回调和异步回调。这里的回调算是异步回调,我们往setOnClickListener()函数中注册好回调函数之后,并不需要等待回调函数执行。这也印证了我们前面讲的,异步回调比较像观察者模式。

应用举例三:addShutdownHook()

Hook可以翻译成“钩子”,那它跟Callback有什么区别呢?

网上有人认为Hook就是Callback,两者说的是一回事儿,只是表达不同而已。而有人觉得Hook是Callback的一种应用。Callback更侧重语法机制的描述,Hook更加侧重应用场景的描述。我个人比较认可后面一种说法。不过,这个也不重要,我们只需要见了代码能认识,遇到场景会用就可以了。

Hook比较经典的应用场景是Tomcat和JVM的shutdown hook。接下来,我们拿JVM来举例说明一下。JVM提供了Runtime.addShutdownHook(Thread hook)方法,可以注册一个JVM关闭的Hook。当应用程序关闭的时候,JVM会自动调用Hook代码。代码示例如下所示:

public class ShutdownHookDemo {private static class ShutdownHook extends Thread {public void run() {System.out.println("I am called during shutting down.");}}public static void main(String[] args) {Runtime.getRuntime().addShutdownHook(new ShutdownHook());}}


我们再来看addShutdownHook()的代码实现,如下所示。这里我只给出了部分相关代码。

public class Runtime {public void addShutdownHook(Thread hook) {SecurityManager sm = System.getSecurityManager();if (sm != null) {sm.checkPermission(new RuntimePermission("shutdownHooks"));}ApplicationShutdownHooks.add(hook);}
}class ApplicationShutdownHooks {/* The set of registered hooks */private static IdentityHashMap hooks;static {hooks = new IdentityHashMap<>();} catch (IllegalStateException e) {hooks = null;}}static synchronized void add(Thread hook) {if(hooks == null)throw new IllegalStateException("Shutdown in progress");if (hook.isAlive())throw new IllegalArgumentException("Hook already running");if (hooks.containsKey(hook))throw new IllegalArgumentException("Hook previously registered");hooks.put(hook, hook);}static void runHooks() {Collection threads;synchronized(ApplicationShutdownHooks.class) {threads = hooks.keySet();hooks = null;}for (Thread hook : threads) {hook.start();}for (Thread hook : threads) {while (true) {try {hook.join();break;} catch (InterruptedException ignored) {}}}}
}

从代码中我们可以发现,有关Hook的逻辑都被封装到ApplicationShutdownHooks类中了。当应用程序关闭的时候,JVM会调用这个类的runHooks()方法,创建多个线程,并发地执行多个Hook。我们在注册完Hook之后,并不需要等待Hook执行完成,所以,这也算是一种异步回调。

回调和模板模式的区别

从应用场景上来看,同步回调跟模板模式几乎一致。它们都是在一个大的算法骨架中,自由替换其中的某个步骤,起到代码复用和扩展的目的。而异步回调跟模板模式有较大差别,更像是观察者模式。
从代码实现上来看,回调和模板模式完全不同。回调基于组合关系来实现,把一个对象传递给另一个对象,是一种对象之间的关系;模板模式基于继承关系来实现,子类重写父类的抽象方法,是一种类之间的关系。
前面我们也讲到,组合优于继承。实际上,这里也不例外。在代码实现上,回调相对于模板模式会更加灵活,主要体现在下面几点。

  • 像Java这种只支持单继承的语言,基于模板模式编写的子类,已经继承了一个父类,不再具有继承的能力。
  • 回调可以使用匿名类来创建回调对象,可以不用事先定义类;而模板模式针对不同的实现都要定义不同的子类。
  • 如果某个类中定义了多个模板方法,每个方法都有对应的抽象方法,那即便我们只用到其中的一个模板方法,子类也必须实现所有的抽象方法。而回调就更加灵活,我们只需要往用到的模板方法中注入回调对象即可。

总结

模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。这里的“算法”,我们可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。

在模板模式经典的实现中,模板方法定义为final,可以避免被子类重写。需要子类重写的方法定义为abstract,可以强迫子类去实现。不过,在实际项目开发中,模板模式的实现比较灵活,以上两点都不是必须的。

模板模式有两大作用:复用和扩展。其中,复用指的是,所有的子类可以复用父类中提供的模板方法的代码。扩展指的是,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能。

回调跟模板模式具有相同的作用:代码复用和扩展。在一些框架、类库、组件等的设计中经常会用到。

相对于普通的函数调用,回调是一种双向调用关系。A类事先注册某个函数F到B类,A类在调用B类的P函数的时候,B类反过来调用A类注册给它的F函数。这里的F函数就是“回调函数”。A调用B,B反过来又调用A,这种调用机制就叫作“回调”。

回调可以细分为同步回调和异步回调。从应用场景上来看,同步回调看起来更像模板模式,异步回调看起来更像观察者模式。回调跟模板模式的区别,更多的是在代码实现上,而非应用场景上。回调基于组合关系来实现,模板模式基于继承关系来实现,回调比模板模式更加灵活。

策略模式

概念与初步实现

工厂模式是解耦对象的创建和使用,观察者模式是解耦观察者和被观察者。策略模式跟两者类似,也能起到解耦的作用,不过,它解耦的是策略的定义、创建、使用这三部分。接下来,我就详细讲讲一个完整的策略模式应该包含的这三个部分。

1.策略的定义

策略类的定义比较简单,包含一个策略接口和一组实现这个接口的策略类。因为所有的策略类都实现相同的接口,所以,客户端代码基于接口而非实现编程,可以灵活地替换不同的策略。示例代码如下所示:

public interface Strategy {void algorithmInterface();
}public class ConcreteStrategyA implements Strategy {@Overridepublic void  algorithmInterface() {//具体的算法...}
}public class ConcreteStrategyB implements Strategy {@Overridepublic void  algorithmInterface() {//具体的算法...}
}

2.策略的创建

因为策略模式会包含一组策略,在使用它们的时候,一般会通过类型(type)来判断创建哪个策略来使用。为了封装创建逻辑,我们需要对客户端代码屏蔽创建细节。我们可以把根据type创建策略的逻辑抽离出来,放到工厂类中。示例代码如下所示:

public class StrategyFactory {private static final Map strategies = new HashMap<>();static {strategies.put("A", new ConcreteStrategyA());strategies.put("B", new ConcreteStrategyB());}public static Strategy getStrategy(String type) {if (type == null || type.isEmpty()) {throw new IllegalArgumentException("type should not be empty.");}return strategies.get(type);}
}

一般来讲,如果策略类是无状态的,不包含成员变量,只是纯粹的算法实现,这样的策略对象是可以被共享使用的,不需要在每次调用getStrategy()的时候,都创建一个新的策略对象。针对这种情况,我们可以使用上面这种工厂类的实现方式,事先创建好每个策略对象,缓存到工厂类中,用的时候直接返回。

相反,如果策略类是有状态的,根据业务场景的需要,我们希望每次从工厂方法中,获得的都是新创建的策略对象,而不是缓存好可共享的策略对象,那我们就需要按照如下方式来实现策略工厂类。

public class StrategyFactory {public static Strategy getStrategy(String type) {if (type == null || type.isEmpty()) {throw new IllegalArgumentException("type should not be empty.");}if (type.equals("A")) {return new ConcreteStrategyA();} else if (type.equals("B")) {return new ConcreteStrategyB();}return null;}
}

3.策略的使用

我们知道,策略模式包含一组可选策略,客户端代码一般如何确定使用哪个策略呢?最常见的是运行时动态确定使用哪种策略,这也是策略模式最典型的应用场景。

这里的“运行时动态”指的是,我们事先并不知道会使用哪个策略,而是在程序运行期间,根据配置、用户输入、计算结果等这些不确定因素,动态决定使用哪种策略。接下来,我们通过一个例子来解释一下。

// 策略接口:EvictionStrategy
// 策略类:LruEvictionStrategy、FifoEvictionStrategy、LfuEvictionStrategy...
// 策略工厂:EvictionStrategyFactorypublic class UserCache {private Map cacheData = new HashMap<>();private EvictionStrategy eviction;public UserCache(EvictionStrategy eviction) {this.eviction = eviction;}//...
}// 运行时动态确定,根据配置文件的配置决定使用哪种策略
public class Application {public static void main(String[] args) throws Exception {EvictionStrategy evictionStrategy = null;Properties props = new Properties();props.load(new FileInputStream("./config.properties"));String type = props.getProperty("eviction_type");evictionStrategy = EvictionStrategyFactory.getEvictionStrategy(type);UserCache userCache = new UserCache(evictionStrategy);//...}
}// 非运行时动态确定,在代码中指定使用哪种策略
public class Application {public static void main(String[] args) {//...EvictionStrategy evictionStrategy = new LruEvictionStrategy();UserCache userCache = new UserCache(evictionStrategy);//...}
}

从上面的代码中,我们也可以看出,“非运行时动态确定”,也就是第二个Application中的使用方式,并不能发挥策略模式的优势。在这种应用场景下,策略模式实际上退化成了“面向对象的多态特性”或“基于接口而非实现编程原则”。这点需要注意,我们应该编写动态的策略模式。

解决问题示例

问题描述

假设有这样一个需求,希望写一个小程序,实现对一个文件进行排序的功能。文件中只包含整型数,并且,相邻的数字通过逗号来区隔。如果由你来编写这样一个小程序,你会如何来实现呢?你可以把它当作面试题,先自己思考一下,再来看我下面的讲解。

你可能会说,这不是很简单嘛,只需要将文件中的内容读取出来,并且通过逗号分割成一个一个的数字,放到内存数组中,然后编写某种排序算法(比如快排),或者直接使用编程语言提供的排序函数,对数组进行排序,最后再将数组中的数据写入文件就可以了。

但是,如果文件很大呢?比如有10GB大小,因为内存有限(比如只有8GB大小),我们没办法一次性加载文件中的所有数据到内存中,这个时候,我们就要利用外部排序算法(具体怎么做,可以参看我的另一个专栏《数据结构与算法之美-王争》中的“排序”相关章节)了。

如果文件更大,比如有100GB大小,我们为了利用CPU多核的优势,可以在外部排序的基础之上进行优化,加入多线程并发排序的功能,这就有点类似“单机版”的MapReduce。

如果文件非常大,比如有1TB大小,即便是单机多线程排序,这也算很慢了。这个时候,我们可以使用真正的MapReduce框架,利用多机的处理能力,提高排序的效率。

代码实现与分析
解决思路讲完了,不难理解。接下来,我们看一下,如何将解决思路翻译成代码实现。

我先用最简单直接的方式将它实现出来。具体代码我贴在下面了,你可以先看一下。因为我们是在讲设计模式,不是讲算法,所以,在下面的代码实现中,我只给出了跟设计模式相关的骨架代码,并没有给出每种排序算法的具体代码实现。感兴趣的话,你可以自行实现一下。

public class Sorter {private static final long GB = 1000 * 1000 * 1000;public void sortFile(String filePath) {// 省略校验逻辑File file = new File(filePath);long fileSize = file.length();if (fileSize < 6 * GB) { // [0, 6GB)quickSort(filePath);} else if (fileSize < 10 * GB) { // [6GB, 10GB)externalSort(filePath);} else if (fileSize < 100 * GB) { // [10GB, 100GB)concurrentExternalSort(filePath);} else { // [100GB, ~)mapreduceSort(filePath);}}private void quickSort(String filePath) {// 快速排序}private void externalSort(String filePath) {// 外部排序}private void concurrentExternalSort(String filePath) {// 多线程外部排序}private void mapreduceSort(String filePath) {// 利用MapReduce多机排序}
}public class SortingTool {public static void main(String[] args) {Sorter sorter = new Sorter();sorter.sortFile(args[0]);}
}

为了避免sortFile()函数过长,我们把每种排序算法从sortFile()函数中抽离出来,拆分成4个独立的排序函数。

如果只是开发一个简单的工具,那上面的代码实现就足够了。毕竟,代码不多,后续修改、扩展的需求也不多,怎么写都不会导致代码不可维护。但是,如果我们是在开发一个大型项目,排序文件只是其中的一个功能模块,那我们就要在代码设计、代码质量上下点儿功夫了。只有每个小的功能模块都写好,整个项目的代码才能不差。

在刚刚的代码中,我们并没有给出每种排序算法的代码实现。实际上,如果自己实现一下的话,你会发现,每种排序算法的实现逻辑都比较复杂,代码行数都比较多。所有排序算法的代码实现都堆在Sorter一个类中,这就会导致这个类的代码很多。而在“编码规范”那一部分中,我们也讲到,一个类的代码太多也会影响到可读性、可维护性。除此之外,所有的排序算法都设计成Sorter的私有函数,也会影响代码的可复用性。

代码重构

只要掌握了我们之前讲过的设计原则和思想,针对上面的问题,即便我们想不到该用什么设计模式来重构,也应该能知道该如何解决,那就是将Sorter类中的某些代码拆分出来,独立成职责更加单一的小类。实际上,拆分是应对类或者函数代码过多、应对代码复杂性的一个常用手段。按照这个解决思路,我们对代码进行重构。重构之后的代码如下所示:

public interface ISortAlg {void sort(String filePath);
}public class QuickSort implements ISortAlg {@Overridepublic void sort(String filePath) {//...}
}public class ExternalSort implements ISortAlg {@Overridepublic void sort(String filePath) {//...}
}public class ConcurrentExternalSort implements ISortAlg {@Overridepublic void sort(String filePath) {//...}
}public class MapReduceSort implements ISortAlg {@Overridepublic void sort(String filePath) {//...}
}public class Sorter {private static final long GB = 1000 * 1000 * 1000;public void sortFile(String filePath) {// 省略校验逻辑File file = new File(filePath);long fileSize = file.length();ISortAlg sortAlg;if (fileSize < 6 * GB) { // [0, 6GB)sortAlg = new QuickSort();} else if (fileSize < 10 * GB) { // [6GB, 10GB)sortAlg = new ExternalSort();} else if (fileSize < 100 * GB) { // [10GB, 100GB)sortAlg = new ConcurrentExternalSort();} else { // [100GB, ~)sortAlg = new MapReduceSort();}sortAlg.sort(filePath);}
}

经过拆分之后,每个类的代码都不会太多,每个类的逻辑都不会太复杂,代码的可读性、可维护性提高了。除此之外,我们将排序算法设计成独立的类,跟具体的业务逻辑(代码中的if-else那部分逻辑)解耦,也让排序算法能够复用。这一步实际上就是策略模式的第一步,也就是将策略的定义分离出来。

实际上,上面的代码还可以继续优化。每种排序类都是无状态的,我们没必要在每次使用的时候,都重新创建一个新的对象。所以,我们可以使用工厂模式对对象的创建进行封装。按照这个思路,我们对代码进行重构。重构之后的代码如下所示:

public class SortAlgFactory {private static final Map algs = new HashMap<>();static {algs.put("QuickSort", new QuickSort());algs.put("ExternalSort", new ExternalSort());algs.put("ConcurrentExternalSort", new ConcurrentExternalSort());algs.put("MapReduceSort", new MapReduceSort());}public static ISortAlg getSortAlg(String type) {if (type == null || type.isEmpty()) {throw new IllegalArgumentException("type should not be empty.");}return algs.get(type);}
}public class Sorter {private static final long GB = 1000 * 1000 * 1000;public void sortFile(String filePath) {// 省略校验逻辑File file = new File(filePath);long fileSize = file.length();ISortAlg sortAlg;if (fileSize < 6 * GB) { // [0, 6GB)sortAlg = SortAlgFactory.getSortAlg("QuickSort");} else if (fileSize < 10 * GB) { // [6GB, 10GB)sortAlg = SortAlgFactory.getSortAlg("ExternalSort");} else if (fileSize < 100 * GB) { // [10GB, 100GB)sortAlg = SortAlgFactory.getSortAlg("ConcurrentExternalSort");} else { // [100GB, ~)sortAlg = SortAlgFactory.getSortAlg("MapReduceSort");}sortAlg.sort(filePath);}
}

经过上面两次重构之后,现在的代码实际上已经符合策略模式的代码结构了。我们通过策略模式将策略的定义、创建、使用解耦,让每一部分都不至于太复杂。不过,Sorter类中的sortFile()函数还是有一堆if-else逻辑。这里的if-else逻辑分支不多、也不复杂,这样写完全没问题。

但如果你特别想将if-else分支判断移除掉,那也是有办法的。我直接给出代码,你一看就能明白。实际上,这也是基于查表法来解决的,其中的“algs”就是“表”。

public class Sorter {private static final long GB = 1000 * 1000 * 1000;private static final List algs = new ArrayList<>();static {algs.add(new AlgRange(0, 6*GB, SortAlgFactory.getSortAlg("QuickSort")));algs.add(new AlgRange(6*GB, 10*GB, SortAlgFactory.getSortAlg("ExternalSort")));algs.add(new AlgRange(10*GB, 100*GB, SortAlgFactory.getSortAlg("ConcurrentExternalSort")));algs.add(new AlgRange(100*GB, Long.MAX_VALUE, SortAlgFactory.getSortAlg("MapReduceSort")));}public void sortFile(String filePath) {// 省略校验逻辑File file = new File(filePath);long fileSize = file.length();ISortAlg sortAlg = null;for (AlgRange algRange : algs) {if (algRange.inRange(fileSize)) {// 在这里做了判断在哪个范围,这种逻辑太过定制化,一般应该不用考虑sortAlg = algRange.getAlg();break;}}sortAlg.sort(filePath);}private static class AlgRange {private long start;private long end;private ISortAlg alg;public AlgRange(long start, long end, ISortAlg alg) {this.start = start;this.end = end;this.alg = alg;}public ISortAlg getAlg() {return alg;}public boolean inRange(long size) {return size >= start && size < end;}}
}

现在的代码实现就更加优美了。我们把可变的部分隔离到了策略工厂类和Sorter类中的静态代码段中。当要添加一个新的排序算法时,我们只需要修改策略工厂类和Sort类中的静态代码段,其他代码都不需要修改,这样就将代码改动最小化、集中化了。

你可能会说,即便这样,当我们添加新的排序算法的时候,还是需要修改代码,并不完全符合开闭原则。有什么办法让我们完全满足开闭原则呢?

对于Java语言来说,我们可以通过反射来避免对策略工厂类的修改。具体是这么做的:我们通过一个配置文件或者自定义的annotation来标注都有哪些策略类;策略工厂类读取配置文件或者搜索被annotation标注的策略类,然后通过反射动态地加载这些策略类、创建策略对象;当我们新添加一个策略的时候,只需要将这个新添加的策略类添加到配置文件或者用annotation标注即可。

对于Sorter来说,我们可以通过同样的方法来避免修改。我们通过将文件大小区间和算法之间的对应关系放到配置文件中。当添加新的排序算法时,我们只需要改动配置文件即可,不需要改动代码。

总结

一提到if-else分支判断,有人就觉得它是烂代码。如果if-else分支判断不复杂、代码不多,这并没有任何问题,毕竟if-else分支判断几乎是所有编程语言都会提供的语法,存在即有理由。遵循KISS原则,怎么简单怎么来,就是最好的设计。非得用策略模式,搞出n多类,反倒是一种过度设计。

一提到策略模式,有人就觉得,它的作用是避免if-else分支判断逻辑。实际上,这种认识是很片面的。策略模式主要的作用还是解耦策略的定义、创建和使用,控制代码的复杂度,让每个部分都不至于过于复杂、代码量过多。除此之外,对于复杂代码来说,策略模式还能让其满足开闭原则,添加新策略的时候,最小化、集中化代码改动,减少引入bug的风险。

实际上,设计原则和思想比设计模式更加普适和重要。掌握了代码的设计原则和思想,我们能更清楚的了解,为什么要用某种设计模式,就能更恰到好处地应用设计模式。

上面代码重构的部分如果意犹未尽,可以再看下我以前写过的一篇例子:设计模式-策略模式_spring 策略模式与工厂模式-CSDN博客

其中【2. Spring+策略模式+工厂模式】就和上面的重构结果几乎一样;

另外上面加粗的一段,用反射来避免对策略工厂类的修改,就对应了【3. Spring+策略模式+工厂模式+自定义注解】。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com