设计模式(design pattern)是对软件设计中普遍存在的各种问题,所提出的解决方案。本文以面试题作为切入点,介绍了设计模式的常见问题。我们需要掌握各种设计模式的原理、实现、设计意图和应用场景,搞清楚能解决什么问题。本文是设计模式第四讲:行为型设计模式
创建型设计模式主要解决“对象的创建”问题,结构型设计模式主要解决“类或对象的组合”问题,那行为型设计模式主要解决的就是“类或对象之间的交互”问题。行为型模式比较多,有 11 种,它们分别是:观察者模式,模板方法模式,策略模式,迭代器模式,责任链模式,状态模式,命令模式(不常用),备忘录模式(不常用),访问者模式(不常用),中介者模式(不常用),解释器模式(不常用)
观察者模式的实现方式:
1、同步阻塞的实现方式:编码层次的解耦
2、观察者模式异步非阻塞实现方式:EventBus
3、观察者模式跨进程的实现方式(不同的两系统):
观察者模式的优缺点:
优点:
缺点:
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<Observer> observers = new ArrayList<Observer>();
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers(Message message) {
for (Observer observer : observers) {
// 会一直阻塞,直到所有观察者代码都执行完成
observer.update(message);
}
}
}
// 观察者1
public class ConcreteObserverOne implements Observer {
@Override
public void update(Message message) {
//TODO: 获取消息通知,执行自己的逻辑...
System.out.println("ConcreteObserverOne is notified.");
}
}
// 观察者2
public class ConcreteObserverOne implements Observer {
@Override
public void update(Message message) {
//TODO: 获取消息通知,执行自己的逻辑...
System.out.println("ConcreteObserverOne is notified.");
}
}
public class Demo {
public static void main(String[] args) {
ConcreteSubject subject = new ConcreteSubject();
// 接受实现了同一 Observer 接口的类对象
subject.registerObserver(new ConcreteObserverOne());
subject.registerObserver(new ConcreteObserverTwo());
subject.notifyObservers(new Message());
}
}
如果被观察者的接口调用频繁,对性能非常敏感,我们可以将同步阻塞的实现方式改为异步非阻塞的方式,示例代码如下
案例:用户在注册后需要通知给业务方A处理,也需要发送给业务方B处理,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_SIZE);// 异步非阻塞模式
}
public void setRegObservers(List<Object> 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;
}
}
// 观察者1,任意类型的对象都可以注册到 eventBus 中
public class RegPromotionObserver {
private PromotionService promotionService; // 依赖注入
// eventBus 会根据该注解找到方法,将方法能接收到的消息类型记录下来
@Subscribe
public void handleRegSuccess(long userId) {
promotionService.issueNewUserExperienceCash(userId);
}
}
// 观察者2
public class RegNotificationObserver {
private NotificationService notificationService;
@Subscribe
public void handleRegSuccess(long userId) {
notificationService.sendInboxMessage(userId, "...");
}
}
优点:EventBus作为一个总线,还考虑了递归传送事件的问题,可以选择广度优先传播和深度优先传播,遇到事件死循环的时候还会报错。Guava项目对这个模块的封装非常值得我们去阅读,复杂的逻辑都封装在里头,提供的对外接口极为易用。
(1)使用 EventBus 注意事项:
① EventBus 实现了同步阻塞的观察者模式,AsyncEventBus 继承自 EventBus,提供了异步非阻塞的观察者模式。在 AsyncEventBus 中,可以在构造器中使用独立线程池,防止某个事件很忙导致其余事件产生饥饿的情况;
② 观察者通过 @Subscribe 注解定义能接收的消息类型,调用 post() 函数发送消息的时候,能接收观察者消息类型是发送消息(post 函数定义中的 event)类型的父类;
③ EventBus 没有采用单例模式,如果想在进程范围内使用唯一的事件总线,可以将其声明为全局单例。
(2)原理分析:
核心原理:EventBus 中两个核心函数 register() 和 post() 的实现原理。
源码中最关键的数据结构是 Observer 注册表,记录了消息类型和可接收消息函数的对应关系。当调用 register() 函数注册观察者的时候,EventBus 通过解析@Subscribe 注解,生成 Observer 注册表。当调用 post() 函数发送消息的时候,EventBus 通过注册表找到相应的可接收消息的函数,然后通过 Java 的反射语法来动态地创建对象、执行函数。对于同步阻塞模式,EventBus 在一个线程内依次执行相应的函数。对于异步非阻塞模式,EventBus 通过一个线程池来执行相应的函数。
EventBus作为一个总线,还考虑了递归传送事件的问题,可以选择广度优先传播和深度优先传播,遇到事件死循环的时候还会报错。Guava的项目对这个模块的封装非常值得我们去阅读,复杂的逻辑都在里头,外面极为易用。
EventBus在商品中心的使用
缺点:
①EventBus 没有持久化机制,没有重试机制;
②EventBus 的异步处理,是直接丢在同一个线程池处理,存在某个事件很忙导致其余事件饥饿的情况,因此给每个任务都需要自定义线程池;
③ event 需要加 @AllowConcurrentEvents 标识其线程安全时,否则在执行方法的过程是加了 synchronized 关键字控制的,锁的粒度太大;
④当事件监听者过多或者项目中监听者过多时,由于没有平台能查看其依赖关系,因此程序调试困难
由于 EventBus 的上述缺点,它的使用场景局限在耗时且不重要的业务 例如记录日志,消息通知,数据统计等
如果是需要保持数据一致性,需要接入 MQ 来保证业务的准确性。
Action:
异步的观察者模式,如果获取观察者的结果?
模板方法模式的定义:在一个方法中定义一个算法(业务逻辑)骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
两大作用:复用和扩展。
复用:子类可以复用父类中提供的模板方法,相同的代码放在抽象的父类中;
扩展:框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能。
类比生活中的场景:每日工作:上班打卡----工作—下班打卡。每个人工作的内容不一样,后端开发、前端开发、测试、产品每个人的工作内容不一。
应用场景: 在开发通用框架时,提供功能扩展点
templateMethod() 函数定义为 final,是为了避免子类重写它。method1() 和 method2() 定义为 abstract,为了强迫子类去实现。
public abstract class AbstractClass {
// 定义为final,避免子类重写,实际开发中并不是必须的
public final void templateMethod() {
//...
method1();
//...
method2();
//...
}
// 拓展点1
protected abstract void method1();
// 拓展点2
protected abstract void method2();
}
public class ConcreteClass1 extends AbstractClass {
@Override
protected void method1() {
//...
}
@Override
protected void method2() {
//...
}
}
public class ConcreteClass2 extends AbstractClass {
@Override
protected void method1() {
//...
}
@Override
protected void method2() {
//...
}
}
// 使用
AbstractClass demo = ConcreteClass1();
demo.templateMethod();
在 Java AbstractList 类中,addAll() 函数可以看作模板方法,add() 是子类需要重写的方法,尽管没有声明为 abstract 的,但函数实现直接抛出了UnsupportedOperationException 异常,让子类重写该方法。
public boolean addAll(int index, Collection<? extends E> 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();
}
在使用底层的 Servlet 来开发 Web应用时,我们只需要定义一个继承 HttpServlet 的类,并且重写其中的 doGet() 或 doPost() 方法,来分别处理 get 和 post 请求即可。
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Exception {
this.doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throw Exception {
resp.getWriter().write("Hello World.");
}
}
Servlet 容器接收到相应的请求,并且根据 URL 和 Servlet 之间的映射关系,找到相应的Servlet(HelloServlet),然后执行 service() 方法(父类中),它会调动 doGet() 和 doPost() 方法,然后输出结果。
下面的代码可以看出,HttpServlet 的 service() 方法就是一个模板方法,它实现了整个 HTTP 请求的执行流程,doGet()、doPost() 是模板中可以由子类来定制的部分。
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 logic
doGet(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 less
maybeSetLastModified(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);
}
}
Action:如果模板模式中需要重写的子类太多,怎么处理?
1、什么是回调:回调是一种双向调用关系
定义:A 类事先注册某个函数 F 到 B 类,A类在调用 B 类的 P 函数的时候,B 类反过来调用 A 类注册给它的 F 函数。这里的 F 函数就是“回调函数”。A 调用 B,B 反过来又调用 A,这种调用机制就叫作“回调”。
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();
// 在 process 返回之前执行了回调函数,属同步回调
// 如果是不需要等待回调函数被调用就返回,属于异步回调
b.process(new ICallback() { //回调对象
@Override
public void methodToCallback() {
System.out.println("Call back me.");
}
});
}
}
2、回调的应用场景
Spring 提供了很多 Template 类,比如,JdbcTemplate、RedisTemplate、RestTemplate ,并非使用了模板模式,而是同步回调的方式。Spring 提供了 JdbcTemplate,对 JDBC 进一步封装,来简化数据库编程。使用 JdbcTemplate 查询用户信息,我们只需要编写跟这个业务有关的代码,其中包括,查询用户的 SQL 语句、查询结果与 User 对象之间的映射关系。其他流程性质的代码都封装在了 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<User> {
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 源码实现,通过回调机制,将不变的执行流程抽离出来,放到模板方法 execute() 中,可变的部分设计成回调 StatementCallback,由用户来定制。
public class JdbcTemplate extends JdbcAccessor implements JdbcOperations {
public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
return (List)result(this.query((String)sql, (ResultSetExtractor)(new RowMapperResultSetExtractor(rowMapper))));
}
@Nullable
public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
Assert.notNull(sql, "SQL must not be null");
Assert.notNull(rse, "ResultSetExtractor must not be null");
if (this.logger.isDebugEnabled()) {
this.logger.debug("Executing SQL query [" + sql + "]");
}
// 可变部分放到了 StatementCallback
class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
QueryStatementCallback() {
}
@Nullable
public T doInStatement(Statement stmt) throws SQLException {
ResultSet rs = null;
Object var3;
try {
rs = stmt.executeQuery(sql);
var3 = rse.extractData(rs);
} finally {
JdbcUtils.closeResultSet(rs);
}
return var3;
}
public String getSql() {
return sql;
}
}
// 不变部分放到了 execute() 中
return this.execute((StatementCallback)(new QueryStatementCallback()));
}
@Nullable
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");
Connection con = DataSourceUtils.getConnection(this.obtainDataSource());
Statement stmt = null;
Object var11;
try {
stmt = con.createStatement();
this.applyStatementSettings(stmt);
T result = action.doInStatement(stmt);
this.handleWarnings(stmt);
var11 = result;
} catch (SQLException var9) {
String sql = getSql(action);
JdbcUtils.closeStatement(stmt);
stmt = null;
DataSourceUtils.releaseConnection(con, this.getDataSource());
con = null;
throw this.translateException("StatementCallback", sql, var9);
} finally {
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con, this.getDataSource());
}
return var11;
}
}
钩子和回调的区别:Callback 更侧重语法机制的描述,Hook 更加侧重应用场景的描述
钩子的应用场景: Tomcat 和 JVM 的 shutdown hook
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());
}
}
public class Runtime {
public void addShutdownHook(Thread hook) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("shutdownHooks"));
}
ApplicationShutdownHooks.add(hook);
}
}
// 当应用程序关闭的时候,JVM 会调用这个类的 runHooks() 方法,创建多个线程,并发地执行多个 Hook
class ApplicationShutdownHooks {
/* The set of registered hooks */
private static IdentityHashMap<Thread, Thread> hooks;
static {
try {
Shutdown.add(1 /* shutdown hook invocation order */,
false /* not registered if shutdown in progress */,
new Runnable() {
public void run() {
runHooks();
}
}
);
hooks = new IdentityHashMap<>();
} catch (IllegalStateException e) {
// application shutdown hooks cannot be added if
// shutdown is in progress.
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<Thread> 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) {
}
}
}
}
}
3、模板模式与回调的区别?
①应用场景:同步回调与模板模式几乎一致,异步回调类似观察者模式
②代码实现:回调基于组合关系来实现,把一个对象传递给另一个对象,是一种对象间的关系;模板模式基于继承关系来实现,子类重写父类抽象方法,是一种类间的关系。
结论:回调基于组合关系来实现,模板模式基于继承关系来实现。回调比模板模式更加灵活。
定义:定义一组策略类,将每个策略分别封装起来,让它们可以互相替换
应用场景:①将冗长的 if-else 或 switch 分支判断抽出来 ②为框架提供扩展点
策略模式的决定权在用户,系统本身提供不同算法的实现,新增或者删除算法,对各种算法做封装。因此,策略模式多用在算法决策系统中,外部用户只需要决定用哪个算法即可。
生活中的应用场景:我们去北京的交通方式(策略)很多,比如说坐飞机、坐高铁、自己开车等方式。每一种方式就可以理解为每一种策略
目前做的需求中,将业务逻辑梳理后抽离出来,借助Spring IOC依赖注入特性,使用到了策略模式 + 工厂模式,向外提供统一的调用方式,有效减少了 if/else 的业务代码,使得代码更易维护、拓展。
业务场景:微服务A中协议状态的变更,会影响微服务B中商品状态的变更。我们的期望目标是,根据不同协议状态,我们能够快速找到对应的策略实现类去执行对商品的操作。
如果不使用策略模式 将策略的定义、创建、使用直接耦合在一起
public class ProtocolChangeService {
public double change(Order order) {
ChangeType type = ChangeType.getType();
if (type.equals(itemShelf)) {
// 商品上架
} else if (type.equals(itemUnShelf)) {
// 商品下架
} else if (type.equals(itemUnfreeze)) {
// 商品解冻
}
// 处理业务逻辑
}
}
如何来移除掉分支判断逻辑呢?策略模式就能使用上了
策略模式分为以下三部分:
1)策略定义:包含一个策略接口和一组实现这个接口的策略类。
public interface ProtocolChangeStrategy {
/**
* @param items 商品
* @param changeType 变更类型
*/
void associateItemChange(List<Item> items, Byte changeType);
}
实现类
@Component
public class ItemShelfStrategy implements ProtocolChangeStrategy {
@Override
public void associateItemChange(List<Item> agItems, Byte changeType) {
// todo 业务逻辑
}
}
@Component
public class ItemUnShelfStrategy implements ProtocolChangeStrategy {
@Override
public void associateItemChange(List<Item> agItems, Byte changeType) {
// todo 业务逻辑
}
}
2)策略创建:一般通过type创建策略的逻辑抽离出来,放到工厂类中
借助Spring 强大的依赖注入
// 消除if/else的关键代码,定义了一个StrategyHolder 来当做工厂类
@Component
public class StrategyHolder {
// 关键功能 Spring 会自动将 EntStrategy 接口的类注入到这个Map中
@Autowired
private Map<String, ProtocolChangeStrategy> strategyMap;
public ProtocolChangeStrategy getBy(String entNum) {
return strategyMap.get(entNum);
}
}
这个Map的key值就是你的 bean id,你可以用@Component(“value”)的方式设置,像我上面直接用默认的方式的话,就是首字母小写。value值则为对应的策略实现类。
3)策略的使用:运行时动态确定使用哪种策略
public static void main(String[] args) {
// 在代码中指定使用哪种策略
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Application.class);
context.getBean(StrategyHolder.class).getBy("itemShelfStrategy").associateItemChange("","");
}
别名转换
启动类里面的“itemShelfStrategy”填的实际上是bean id的值。那在实际业务中肯定是不会这样的,怎么可能把一个策略编号定义的这么奇怪呢?
我们可以利用 SpringBoot 的特性,通过配置文件的方式来修改建立 name 与 bean id 间的映射关系。
@Component
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "ent")
public class EntAlias {
private HashMap<String, String> aliasMap;
public static final String DEFAULT_STATEGY_NAME = "defaultStrategy";
public HashMap<String, String> getAliasMap() {
return aliasMap;
}
public void setAliasMap(HashMap<String, String > aliasMap) {
this.aliasMap = aliasMap;
}
String of(String entNum) {
return aliasMap.get(entNum);
}
}
在对应配置文件application.yml中配置:
ent:
aliasMap:
entA: entAStrategy
entB: entBStrategy
改写一下 ProtocolChangeStrategy 类
@Component
public class ProtocolChangeStrategy {
@Autowired
private EntAlias entAlias;
// 关键功能 Spring 会自动将 EntStrategy 接口的类注入到这个Map中
@Autowired
private Map<String, ProtocolChangeStrategy> strategyMap;
// 找不到对应的策略类,使用默认的
public ProtocolChangeStrategy getBy(String entNum) {
String name = entAlias.of(entNum);
if (name == null) {
return strategyMap.get(EntAlias.DEFAULT_STATEGY_NAME);
}
ProtocolChangeStrategy strategy = strategyMap.get(name);
if (strategy == null) {
return strategyMap.get(EntAlias.DEFAULT_STATEGY_NAME);
}
return strategy;
}
}
JDK 中Arrays的 Comparator 使用了策略模式
① 关键:分析项目中的变化部分和不变部分
② 核心思想:多用组合,少用继承
③ 每添加一个策略就要增加一个类,当策略过多时,会导致类数量庞大
使用场景:
/**
* 商品下架策略
* 只有一个方法的接口,可被定义为函数接口
*/
@FunctionalInterface
private interface UnderStrategy {
void underItem(List<AgSearchItem> agItems , String brandName , String showReason);
}
/**
* 策略1:下架并且撤回在途审核
*/
private UnderStrategy UNDER_AND_REJECT = (agItems , brandName , showReason) -> {
if (CollectionUtils.isEmpty(agItems)) {
return;
}
toUnderItem(agItems, true,brandName , showReason);
};
/**
* 策略2:仅下架无在途审核
*/
private UnderStrategy UNDER_NO_AUDIT = (agItems , brandName , showReason) -> {
if (CollectionUtils.isEmpty(agItems)) {
return;
}
List<AgSearchItem> noAuditItems = agItems.stream()
.filter(agItem -> Objects.equals(agItem.getAuditStatus(), ImGoodsMapperAuditStateEnum.NORMAL.getCode()))
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(noAuditItems)) {
return;
}
toUnderItem(noAuditItems, false,brandName , showReason);
};
定义:使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系, 将这个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理他为止。
职责链模式的涉及初衷:是为了解耦代码,应对代码的复杂性,让代码满足开闭原则,提高代码的可扩展性。
职责链模式的应用场景
职责链模式的优缺点
优点:
缺点:
生活中的案列:在公司内部发起一个OA审批流程,项目经理审批、部门经理审批。老板审批、人力审批。这就是生活中的责任链模式,每个角色的责任是不同。
对于支持 UGC(User Generated Content,用户生成内容)的应用(比如论坛)来说,用户生成的内容(比如,在论坛中发表的帖子)可能会包含一些敏感词(比如涉黄、广告、反动等词汇)。针对这个应用场景,我们就可以利用职责链模式来过滤这些敏感词。对于包含敏感词的场景,我们直接禁止发布。
public interface SensitiveWordFilter {
boolean doFilter(Content content);
}
public class SexyWordFilter implements SensitiveWordFilter {
@Override
public boolean doFilter(Content content) {
boolean legal = true;
//...
return legal;
}
}
// PoliticalWordFilter、AdsWordFilter类代码结构与SexyWordFilter类似
public class SensitiveWordFilterChain {
private List<SensitiveWordFilter> filters = new ArrayList<>();
public void addFilter(SensitiveWordFilter filter) {
this.filters.add(filter);
}
//return true if content doesn't contain sensitive words.
public boolean filter(Content content) {
for(SensitiveWordFilter filter : filters) {
if(!filter.doFilter(content)){
return false;
}
}
return true;
}
}
public class ApplicationDemo {
public static void main(String[] args) {
SensitiveWordFilterChain filterChain = new SensitiveWordFilterChain();
filterChain.addFilter(new AdsWordFilter());
filterChain.addFilter(new SexyWordFilter());
filterChain.addFilter(new PoliticalWordFilter());
boolean legal = filterChain.filter(new Content());
if(!legal) {
//不发表
} else {
// 发表
}
}
}
在Spring初始化时,被注册了10来个规则,放在规则引擎集合里面
在发商品和更新商品/spu时调用
// 设计的不友好,破坏了规则引擎,既要保证性能又要满足审核时去掉校验规则,所以只能硬编码处理
// 只有申请变更才会走handleInboundAuditData逻辑,审核单独走自己的校验规则
// 校验数据,一旦数据不合法, 会抛出异常
//* 重要: 可以根据需求而定是否抛出异常,
//* 例如可能的需求有:
//* 1.忽略或者过滤用户提交的无效数据
//* 2. 可以处理用户提交的数据, 比如修正或者添加更多的信息
if(Objects.nonNull(auditState) && Objects.equals(GoodsAuditStateEnum.IN_UPDATE.getCode(),auditState)){
ruleEngine.handleInboundAuditData(newFullItem, null);
}else {
ruleEngine.handleInboundData(newFullItem, null);
}
在用户查询商品/spu, 或者进入编辑商品/spu的界面
//* 规则引擎在处理数据输出时会调用这个方法 (例如用户查询商品/spu, 或者进入编辑商品/spu的界面)
//* 处理数据, 如果数据不合法, 不会抛出异常, 而是根据规则做相应的修正
//* 目前的策略是, 根据规则本身来修正或者过滤, 或者添加信息, 也可以根据需要抛出异常
public <T extends BaseInput> void handleOutboundData(T input, BaseOutput output) {
for (GeneralRuleExecutor ruleExecutor : ruleExecutorPipeline.getRuleExecutors()) {
ruleExecutor.handleOutboundData(input, output, propertyBusinessTagCode);
}
}
1、servlet中的filter:定义一个Chain链,里面包含了Filter列表和servlet,达到在调用真正servlet之前进行各种filter逻辑
提供规范,其实现依赖于web容器,例如tomcat,ApplicationFilterChain 类就是 Tomcat 提供的 FilterChain 的实现类。
2、dubbo中的filter :把Filter封装成 Invoker的匿名类,通过链表这样的数据结构来完成责任链,调用的时候我们只知道第一个节点,每个节点包含了下一个调用的节点信息
3、亮点:mybatis中的plugin(插件):与过滤器类似,例如,我们在项目里面用到了mybatis的分页插件pagehelper,相当于执行Sql语句的时候做一些操作。
4、netty中的channelhandler和pipeline:共同构成了责任链模式
6、Spring中的Interceptor
可以讨论的问题?
Spring AOP 是基于代理模式来实现的。在实际的项目开发中,我们可以利用 AOP 来实现访问控制功能,比如鉴权、限流、日志等。而Servlet Filter、Spring Interceptor 也可以用来实现访问控制。那在项目开发中,类似权限这样的访问控制功能,我们该选择三者(AOP、Servlet Filter、Spring Interceptor)中的哪个来实现呢?有什么参考标准吗?
Filter 可以拿到原始的http请求,但是拿不到你请求的控制器和请求控制器中的方法的信息; Interceptor 可以拿到你请求的控制器和方法,却拿不到请求方法的参数; Aop 可以拿到方法的参数,但是却拿不到http请求和响应的对象。
要区分三者的特点,Spring AOP的使用粒度是类,是对类的一个包装;servlet filter 和 spring interceptor主要是对httpRequest、httpResponse做处理,servlet filterChain的实现依赖于具体的Web容器,而spring interceptor和spring AOP都依赖于spring框架,servlet filter在一个函数里拦截请求和响应,而spring interceptor将请求、响应的拦截分成了两个函数;其次,针对特定的应用场景,选择适合的。
demo?
定义:
类图
1)环境角色(Context)
客户程序需要的接口,并且维护一个具体状态的实例,这个实例决定当前状态。
2)状态角色(State)
定义一个接口以封装与使用环境角色的一个特定状态的相关行为。
定义一个接口用以封装对象的一个特定状态所对应的行为。
3)具体状态角色(ConcreteState)
实现状态角色定义的接口,结构十分简单与策略模式相似。
一个具体的状态的实现类实现了Context对象的一个状态所对应的行为
《重构:改善既有代码的设计》中第一个例子
使用场景1:
使用场景2:
使用场景3:
优点:
缺点:
1) 状态模式的使用必然会增加系统类和对象的个数。
2) 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
状态模式面试题:
金融借贷平台项目:借贷平台的订单,有审核-发布-抢单 等等 步骤,随着操作的不同,会改变订单的状态, 项目中的这个模块实现就会使用到状态模式,请你使用状态模式进行设计,并完成实际代码
状态模式的适用性
java.util.Iterator
迭代器模式的角色组成
代码示例(错误):
public class Order {
public static void main(String [] args) {
// 关于订单状态的事情,大家有没有这样判断过
String stateName = getStateName(1);
System.out.println(stateName);
}
/**
* @method getStateName
* @description 获取订单状态的方法
* 订单状态1、已下单2、已付款3、已发货4、送货中5、确认收货
* @date: 2018/12/19 22:04
* @author: Ni Shichao
* @param status 状态
* @return
*/
public static String getStateName (int status) {
String stateName = "" ;
if (status == 1) {
stateName = "已下单";
} else if (status == 2) {
stateName = "已付款";
} else if (status == 3) {
stateName = "已发货";
} else if (status == 4) {
stateName = "送货中";
} else if (status == 5) {
stateName = "确认收货";
}
return stateName;
}
}
代码示例(状态模式的使用):
// 1、定义state接口
public interface State {
void handle();
}
// 2、定义一个环境类来维护State接口
public class Context {
private State state;
// 无参构造
public Context() {}
// 有参构造
public Context(State state) {
this.state = state;
}
public void setState(State state) {
System.out.println("订单信息已更新!");
this.state = state;
this.state.handle();
}
}
// 3、具体状态角色 ConcreteState
public class Booked implements State {
@Override
public void handle() {
System.out.println("您已下单!");
}
}
//4、测试类
public class Client {
public static void main(String [] args) {
Context context = new Context();
context.setState(new Booked());
context.setState(new Payed());
context.setState(new Sended());
context.setState(new InWay());
context.setState(new Recieved());
}
}
测试结果
订单信息已更新!
您已下单!
订单信息已更新!
您已付款!
订单信息已更新!
已发货!
订单信息已更新!
送货中。。。
订单信息已更新!
已确认收货!
// 状态机的定义
public class ItemFSM {
// 事件
private enum Trigger {
APPLY_ONSHELF, // 申请上架
APPLY_UNFREEZE, // 申请解冻
UNFREEZE, // 解冻
CANCEL_AUDIT, // 撤销审核
FREEZE // 撤销审核
}
private final StateMachine<ItemStatus, Trigger> stateMachine;
public ItemFSM(ItemStatus status) throws Exception {
this.stateMachine = new StateMachine<>(status);
/** * 【冻结】---(申请解冻)---> 【解冻审核中】*/
stateMachine.Configure(ItemStatus.FROZEN)
.Permit(Trigger.APPLY_UNFREEZE, ItemStatus.WAIT_UNFREEZE);
/*** 【解冻审核中】---(解冻)---> 【下架】*/
stateMachine.Configure(ItemStatus.WAIT_UNFREEZE)
.Permit(Trigger.UNFREEZE, ItemStatus.UNDERSHELF);
/*** 【待审核】---(取消审核)---> 【下架】*/
stateMachine.Configure(ItemStatus.AUDITWAIT)
.Permit(Trigger.CANCEL_AUDIT, ItemStatus.UNDERSHELF);
/*** 【上架】---(冻结)---> 【冻结中】*/
stateMachine.Configure(ItemStatus.ONSHELF)
.Permit(Trigger.FREEZE, ItemStatus.FROZEN);
/*** 【下架】---(申请上架)---> 【审核中】*/
stateMachine.Configure(ItemStatus.UNDERSHELF)
.Permit(Trigger.APPLY_ONSHELF, ItemStatus.AUDITWAIT);
/** * 【审核失败】---(申请上架)---> 【审核中】*/
stateMachine.Configure(ItemStatus.AUDITREJECT)
.Permit(Trigger.APPLY_ONSHELF, ItemStatus.AUDITWAIT);
}
/*** 获取状态机当前状态*/
public ItemStatus getCurrentState() {
return stateMachine.getState();
}
/** * 申请上架*/
public void applyOnshelf() throws StateMachineException {
try {
stateMachine.Fire(Trigger.APPLY_ONSHELF);
} catch (Exception e) {
log.warn("item audit failed", e);
throw new StateMachineException("item audit statemachine trigger apply unfreeze.");
}
}
...
}
// 状态机的使用
ItemStatus originalStatus = ItemStatus.from(blockItem.getStatus());
ItemFSM fsm = new ItemFSM(originalStatus);
fsm.applyOnshelf();
ItemStatus newStatus = fsm.getCurrentState();
...
状态模式的主要优点在于封装了转换规则,并枚举可能的状态,它将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为,还可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数;其缺点在于使用状态模式会增加系统类和对象的个数,且状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱,对于可以切换状态的状态模式不满足“开闭原则”的要求。
问题:
1、在 Java 中,如果在使用迭代器的同时删除容器中的元素,会导致迭代器报错,这是为什
么呢?如何来解决这个问题呢?
2、除了编程语言中基础类库提供的针对集合对象的迭代器之外,实际上,迭代器还有其他的应用场景,比如MySQL ResultSet 类提供的 first()、last()、previous() 等方法,也可以看作一种迭代器,你能分析一下它的代码实现吗?
定义:允许一个或者多个操作应用到一组对象上,解耦操作和对象本身
使用场景:
dubbo,服务中间件 zookeeper
实现方式:
a)角色抽象类(提供对观察者的添加,删除和通知功能)。
b)角色具体类,实现a,维护一个c的集合(对角色抽象类的实现)。
c)观察者抽象类(被角色通知后实现的方法)。
d)观察者实现类,实现c(多个)。
定义:要在不违背封装原则的前提下,进行对象的备份和恢复
使用场景:主要是用来防丢失、撤销、恢复等。
定义:控制命令的执行
使用场景:
定义:根据语法规则,定义一个解释器用来处理这个语法
使用场景:
面试题:
画出解释器设计模式的UML类图,分析设计模式中的各个角色是什么?
请说明Spring的框架中,哪里使用到了解释器设计模式,并做源码级别的分析。
定义:引入中间层,将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。
todo
todo
二十多岁刚进入职场时,总要工作到头昏眼花,才有做了工作的感觉,着实白费了相当多的时间。虽说年轻时体力充沛,像那样的工作方式,也算有助于心情愉快。然而说穿了,其实那也不过是自我陶醉而已,所谓的收获可能也只是了解到自己体力的极限,只有在确实产生出有意义的成果(输出)之后才能获得成长。若能持续有价值的工作,并保持其质量,就算“偷工减料”也完全不成问题。如果是问人就可以解决的事,那么问人就好;如果有更简单的完成工作的方法,就该换个方式处理。 --《麦肯锡教我的思考武器:从逻辑思考到真正解决问题》