JAVA设计模式第四讲:行为型设计模式

设计模式(design pattern)是对软件设计中普遍存在的各种问题,所提出的解决方案。本文以面试题作为切入点,介绍了设计模式的常见问题。我们需要掌握各种设计模式的原理、实现、设计意图和应用场景,搞清楚能解决什么问题。本文是设计模式第四讲:行为型设计模式

10、行为型设计模式

创建型设计模式主要解决“对象的创建”问题,结构型设计模式主要解决“类或对象的组合”问题那行为型设计模式主要解决的就是“类或对象之间的交互”问题。行为型模式比较多,有 11 种,它们分别是:观察者模式,模板方法模式,策略模式,迭代器模式,责任链模式,状态模式,命令模式(不常用),备忘录模式(不常用),访问者模式(不常用),中介者模式(不常用),解释器模式(不常用)

10.1、观察者模式(也称发布订阅模式,在开发常用)

10.1.1、观察者模式概念
  • 在对象之间定义一个一对多的依赖当一个对象状态改变的时候,所有依赖的对象都会自动收到通知被依赖的对象叫作被观察者(Observable),依赖的对象叫作观察者(Observer),也可以被称为发布-订阅模式。
10.1.2、应用场景
  • 非常广泛,小到代码层面的解耦,大到架构层面的系统解耦,再或者一些产品的设计思路,都用到了这种模式,比如,邮件订阅、EventBus、MQ,本质上都是观察者模式。

观察者模式的实现方式:
1、同步阻塞的实现方式:编码层次的解耦
2、观察者模式异步非阻塞实现方式:EventBus

  • 在商品中心大量使用

3、观察者模式跨进程的实现方式(不同的两系统):

  • 基于消息队列来实现

观察者模式的优缺点:
优点:

  • 观察者模式可以实现表示层和数据逻辑层的分离,并定义了稳定的消息更新传递机制,抽象了更新接口,使得可以有各种各样不同的表示层作为具体观察者角色;
  • 观察者模式在观察目标和观察者之间建立一个抽象的耦合;
  • 观察者模式支持广播通信;
  • 观察者模式符合开闭原则(对拓展开放,对修改关闭)的要求

缺点:

  • 如果一个观察目标对象有很多直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间;
  • 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃;
  • 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知
    道观察目标发生了变化
Demo1 观察者模式样例代码(同步阻塞方式)
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项目对这个模块的封装非常值得我们去阅读,复杂的逻辑都封装在里头,提供的对外接口极为易用。

10.1.3、使用 EventBus 的注意事项并分析其原理

(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
异步的观察者模式,如果获取观察者的结果?

  • 做法1:使用 redis 保存数据,然后前端调用

10.2、模板方法模式

模板方法模式的定义:在一个方法中定义一个算法(业务逻辑)骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。

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

类比生活中的场景:每日工作:上班打卡----工作—下班打卡。每个人工作的内容不一样,后端开发、前端开发、测试、产品每个人的工作内容不一。

应用场景: 在开发通用框架时,提供功能扩展点

Demo1 模板模式典型使用方法

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();
Demo2 模板模式在Java AbstractList 中的使用

在 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();
}
Demo3、模板模式在 Servlet 中的使用

在使用底层的 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);
	}
}
10.2.2、模板模式在商品中心的使用
  • 开放平台发布商品

Action:如果模板模式中需要重写的子类太多,怎么处理?

  • 提供适配器类,将所有抽象方法默认实现一下,子类继承这个 adapter 即可
10.2.3、模板模式与回调对比

1、什么是回调:回调是一种双向调用关系
定义:A 类事先注册某个函数 F 到 B 类,A类在调用 B 类的 P 函数的时候,B 类反过来调用 A 类注册给它的 F 函数。这里的 F 函数就是“回调函数”。A 调用 B,B 反过来又调用 A,这种调用机制就叫作“回调”

  • 如何将回调函数传递给B类? 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();
		// 在 process 返回之前执行了回调函数,属同步回调
		// 如果是不需要等待回调函数被调用就返回,属于异步回调
		b.process(new ICallback() { //回调对象
			@Override
			public void methodToCallback() {
				System.out.println("Call back me.");
			}
		});
	}
}

2、回调的应用场景

  • 类似于模板模式,在开发通用框架时,提供功能扩展点
Demo2:JdbcTemplate

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;
    }
}
Demo3 addShutdownHook()

钩子和回调的区别: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、模板模式与回调的区别?
应用场景:同步回调与模板模式几乎一致,异步回调类似观察者模式
代码实现:回调基于组合关系来实现,把一个对象传递给另一个对象,是一种对象间的关系;模板模式基于继承关系来实现,子类重写父类抽象方法,是一种类间的关系。

结论:回调基于组合关系来实现,模板模式基于继承关系来实现。回调比模板模式更加灵活


10.3、策略模式

定义:定义一组策略类,将每个策略分别封装起来,让它们可以互相替换
应用场景:①将冗长的 if-else 或 switch 分支判断抽出来 ②为框架提供扩展点

策略模式的决定权在用户,系统本身提供不同算法的实现,新增或者删除算法,对各种算法做封装。因此,策略模式多用在算法决策系统中,外部用户只需要决定用哪个算法即可。

生活中的应用场景:我们去北京的交通方式(策略)很多,比如说坐飞机、坐高铁、自己开车等方式。每一种方式就可以理解为每一种策略

Demo1 使用 Spring 原生注解,快速实现策略模式 + 工厂模式?

目前做的需求中,将业务逻辑梳理后抽离出来,借助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;
    }
}
Demo2 策略模式在 JDK Arrays 源码中的应用

JDK 中Arrays的 Comparator 使用了策略模式



10.3.2、策略模式在使用时的注意事项

① 关键:分析项目中的变化部分和不变部分
② 核心思想:多用组合,少用继承
③ 每添加一个策略就要增加一个类,当策略过多时,会导致类数量庞大

Demo3 使用函数接口实现策略模式

使用场景:

  • 如果接口只有一个方法,可以使用这种方案,更简单
 /**
  * 商品下架策略
  * 只有一个方法的接口,可被定义为函数接口
  */
@FunctionalInterface
private interface UnderStrategy {
  	void underItem(List<AgSearchItem> agItems , String brandName , String showReason);
}
  • 策略1:仅下架无在途审核 --》协议商品ids为空,return --》先过滤出审核状态正常的商品,如果商品为空,return,否则 --》调用toUnderItem(无需撤回审核)
  • 策略2:下架并且撤回在途审核 --》协议商品ids为空,return --》调用toUnderItem(需撤回审核)
 /**
  * 策略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);
};

10.4、职责链模式

定义:使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系, 将这个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理他为止。

职责链模式的涉及初衷:是为了解耦代码,应对代码的复杂性,让代码满足开闭原则,提高代码的可扩展性。

职责链模式的应用场景

  • 最常用来开发框架的过滤器(SpringMVC)和拦截器,Mybatis中的插件机制

职责链模式的优缺点
优点:

  • 解耦了请求与处理;
  • 请求处理者(节点对象)只需关注自己感兴趣的请求进行处理即可,对于不感兴趣的请求,直接转发给下一级节点对象;
  • 具备链式传递处理请求功能,请求发送者无需知晓链路结构,只需等待请求处理结果;
  • 链路结构灵活,可以通过改变链路结构动态地新增或删减责任;
  • 易于扩展新的请求处理类(节点),符合开闭原则

缺点:

  • 责任链路过长时,可能对请求传递处理效率有影响;
  • 如果节点对象存在循环引用时,会造成死循环,导致系统崩溃。

生活中的案列:在公司内部发起一个OA审批流程,项目经理审批、部门经理审批。老板审批、人力审批。这就是生活中的责任链模式,每个角色的责任是不同。

Demo1 敏感词校验

对于支持 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 {
			// 发表
		}
	}
}
Demo2:职责链在商品中心的应用
// 设计的不友好,破坏了规则引擎,既要保证性能又要满足审核时去掉校验规则,所以只能硬编码处理
// 只有申请变更才会走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);
    }
}
10.4.2、职责链模式在Spring中的使用
  • 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?


10.5、状态模式

10.5.1、什么是状态模式?

定义

  • 状态模式是状态机的一种实现方式即可
  • 在软件开发过程中,应用程序可能会根据不同的情况作出不同的处理。最直接的解决方案是将这些所有可能发生的情况全都考虑到。然后使用if… ellse语句来做状态判断来进行不同情况的处理。但是对复杂状态的判断就显得“力不从心了”。随着增加新的状态或者修改一个状体(if else(或switch case)语句的增多或者修改)可能会引起很大的修改,而程序的可读性,扩展性也会变得很弱。维护也会很麻烦。这时就要考虑只修改自身状态的模式。

类图
在这里插入图片描述
1)环境角色(Context)
客户程序需要的接口,并且维护一个具体状态的实例,这个实例决定当前状态。
2)状态角色(State)
定义一个接口以封装与使用环境角色的一个特定状态的相关行为。
定义一个接口用以封装对象的一个特定状态所对应的行为。
3)具体状态角色(ConcreteState)
实现状态角色定义的接口,结构十分简单与策略模式相似。
一个具体的状态的实现类实现了Context对象的一个状态所对应的行为

10.5.2、使用场景

《重构:改善既有代码的设计》中第一个例子
使用场景1:

  • 在日志系统中可以用起来(对于各条业务线:都有上架、下架、冻结、开启、关闭、解冻、删除等状态)。

使用场景2:

  • 我们在购物网站进行购物时,订单会产生几种状况:已下单、已付款、送货中、确定收货等状态。所以系统会判断该订单的状态,不管是哪种状态都应给出对应的操作,这就是状态

使用场景3:

  • 商品状态(上架、下架、冻结、解冻)
10.5.3、状态模式优缺点

优点:

  1. 每个状态都是一个子类,只要增加状态就要增加子类,修改状态,只修改一个子类即可。
  2. 结构清晰,避免了过多的switch…case或者if…else语句的使用,避免了程序的复杂性,提高可维护性。
  3. State对象可被共享 如果State对象没有实例变量—即它们表示的状态完全以它们的类型来编码—那么各Context对象可以共享一个State对象。当状态以这种方式被共享时, 它们必然是没有内部状态, 只有行为的轻量级对象。

缺点:
1) 状态模式的使用必然会增加系统类和对象的个数。
2) 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。

状态模式面试题
金融借贷平台项目:借贷平台的订单,有审核-发布-抢单 等等 步骤,随着操作的不同,会改变订单的状态, 项目中的这个模块实现就会使用到状态模式,请你使用状态模式进行设计,并完成实际代码

10.5.5、状态模式和策略模式比较
  • 在状态模式中,状态的变迁是由对象的内部条件决定,外界只需关心其接口,不必关心其状态对象的创建和转换。
  • 而策略模式里,采取何种策略由外部条件(C)决定。Strategy模式与State模式的结构形式完全一样。但它们的应用场景(目的)却不一样,State模式重在强调对象的内部状态的变化改变对象的行为,Strategy模式重在外部对策略的选择,策略的选择由外部条件决定,也就是说算法动态的切换。但由它们的结构是如此的相似。我们可以认为状态模式是完全封装且自修改的策略模式。
  • 所以说策略和状态模式是孪生兄弟。

状态模式的适用性

  1. 一个对象的行为取决于它的状态, 并且它必须在运行时刻根据状态改变它的行为。
  2. 代码中包含大量与对象状态有关的条件语句:一个操作中含有庞大的多分支的条件(if else(或switch case)语句,且这些分支依赖于该对象的状态。这个状态通常用一个或多个枚举常量表示。通常 , 有多个操作包含这一相同的条件结构。 State模式将每一个条件分支放入一个独立的类中。这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化。
Demo1 JDK中用到的状态模式

java.util.Iterator
迭代器模式的角色组成

  1. 迭代器角色(Iterator):迭代器角色负责定义访问和遍历元素的接口。
  2. 具体迭代器角色(Concrete Iterator):具体迭代器角色要实现迭代器接口,并要记录遍历中的当前位置。
  3. 容器角色(Container):容器角色负责提供创建具体迭代器角色的接口。
  4. 具体容器角色(Concrete Container):具体容器角色实现创建具体迭代器角色的接口—这个具体迭代器角色与该容器的结构相关。

代码示例(错误):

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());
    }
} 

测试结果

订单信息已更新!
您已下单!
订单信息已更新!
您已付款!
订单信息已更新!
已发货!
订单信息已更新!
送货中。。。
订单信息已更新!
已确认收货!
Demo2 状态模式在商品审核中的应用
// 状态机的定义
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();
...
10.5.6、总结

状态模式的主要优点在于封装了转换规则,并枚举可能的状态,它将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为,还可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数;其缺点在于使用状态模式会增加系统类和对象的个数,且状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱,对于可以切换状态的状态模式不满足“开闭原则”的要求


10.6、迭代器模式

10.6.1、迭代器模式定义
  • 将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,专用来遍历集合对象
  • 使用场景:
    • 使用场景:编辑集合

问题
1、在 Java 中,如果在使用迭代器的同时删除容器中的元素,会导致迭代器报错,这是为什
么呢?如何来解决这个问题呢?

  • 使用 foreach 或者 iterator 进行迭代删除 remove 时,容易导致 next() 检测的 modCount 不等于 expectedModCount 从而引发 ConcurrentModificationException。
  • 在通过迭代器来遍历集合元素的同时,增加或者删除集合中的元素,有可能会导致某个元素被重复遍历或遍历不到。不过,并不是所有情况下都会遍历出错,有的时候也可以正常遍历,所以,这种行为称为结果不可预期行为或者未决行为。

2、除了编程语言中基础类库提供的针对集合对象的迭代器之外,实际上,迭代器还有其他的应用场景,比如MySQL ResultSet 类提供的 first()、last()、previous() 等方法,也可以看作一种迭代器,你能分析一下它的代码实现吗?

  • ResultSet 内部通过维护一个类型为 ResultSetRows 的 rowData 变量来实现迭代,而 rowData 的迭代方法本质就是实现了标准库的 Iterator 接口。
demo1:


10.7、访问者模式(不常用)

定义:允许一个或者多个操作应用到一组对象上,解耦操作和对象本身
使用场景

dubbo,服务中间件 zookeeper
实现方式:
a)角色抽象类(提供对观察者的添加,删除和通知功能)。
b)角色具体类,实现a,维护一个c的集合(对角色抽象类的实现)。
c)观察者抽象类(被角色通知后实现的方法)。
d)观察者实现类,实现c(多个)。


10.9、备忘录模式(不常用)

定义:要在不违背封装原则的前提下,进行对象的备份和恢复
使用场景:主要是用来防丢失、撤销、恢复等。


10.8、命令模式

定义:控制命令的执行
使用场景


10.9、解释器模式(不常用)

定义:根据语法规则,定义一个解释器用来处理这个语法
使用场景:

面试题:
画出解释器设计模式的UML类图,分析设计模式中的各个角色是什么?
在这里插入图片描述
请说明Spring的框架中,哪里使用到了解释器设计模式,并做源码级别的分析。

  • Spring框架中 SpelExpressionParser就使用到解释器模式。

10.10 中介模式(不常用)

定义:引入中间层,将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。

行为型设计模式总结

todo


11、在实际项目开发中,如何避免过度设计?

todo

二十多岁刚进入职场时,总要工作到头昏眼花,才有做了工作的感觉,着实白费了相当多的时间。虽说年轻时体力充沛,像那样的工作方式,也算有助于心情愉快。然而说穿了,其实那也不过是自我陶醉而已,所谓的收获可能也只是了解到自己体力的极限,只有在确实产生出有意义的成果(输出)之后才能获得成长若能持续有价值的工作,并保持其质量,就算“偷工减料”也完全不成问题如果是问人就可以解决的事,那么问人就好;如果有更简单的完成工作的方法,就该换个方式处理。 --《麦肯锡教我的思考武器:从逻辑思考到真正解决问题》

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员 jet_qi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值