缘起
在软件系统中,经常面临着“某个对象”的创建工作;由于需求的变化,这个对象的具体实现(类)经常面临着剧烈的变化,但是它却拥有比较稳定的接口。
如何应对这种变化?如何提供一种“封装机制”来隔离出“这个易变对象”的变化,从而保持系统中“其他依赖该对象的对象”不随着需求变化而变化?
我们可以使用工厂方法模式来应对这种变化。下面我们通过软件系统的需求演化过程,来看下工厂模式在其中的应用。
一、第一阶段
1、需求
网站报500类错误时,管理员和开发人员并不能实时知道,等查看日志时或用户打电话过来返回问题时,有可能已经造成了极大的不良影响。所以需要开发一个实时通知功能,将网站的报错信息通过 Email 发送给管理员。
2、实现
写一个异常处理器,配置到系统中进行监听,渲染时走 Email 类发出去。具体代码如下。
package com.taoxi.designpattern.factorymethod.v1; import java.util.ArrayList; import java.util.List; //Email类 public class Email { private String hostName; private int port; private String userName; private String password; private String from; private List<String> toAddress = new ArrayList<>(); private String subject; private String message; //发送 public void send(){ System.out.println("send email!"); } public String getHostName() { return hostName; } public void setHostName(String hostName) { this.hostName = hostName; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getFrom() { return from; } public void setFrom(String from) { this.from = from; } public List<String> getToAddress() { return toAddress; } public void setToAddress(List<String> toAddress) { this.toAddress = toAddress; } public String getSubject() { return subject; } public void setSubject(String subject) { this.subject = subject; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
package com.taoxi.designpattern.factorymethod.v1; import java.util.ArrayList; import java.util.List; //错误异常处理类 public class ErrorHandler { //渲染异常和错误 public void renderException() { String hostName = "SMTP.163.com"; int port = 586; String userName = "taoxi@163.com"; String password = "******";//授权码 String from = "taoxi@163.com"; List<String> toAddress = new ArrayList<String>(); toAddress.add("tx@jd.com"); toAddress.add("lj@jd.com"); String subject = "报错了!"; String message = "具体的报错内容"; Email email = new Email(); email.setHostName(hostName); email.setPort(port); email.setUserName(userName); email.setPassword(password); email.setFrom(from); email.getToAddress().addAll(toAddress); email.setSubject(subject); email.setMessage(message); email.send(); } }
3、问题
ErrorHandler
作为客户端(调用方),想发送邮件出去,却参与了邮件发送对象的初始化工作,不符合单一职责原则
。
二、第二阶段
1、需求
需要让客服跟进一下异常订单的情况,有人退订单了或下单了长时间没有付款的客户,邮件通知到客服。
2、实现
再写一个订单监听器,配置到系统中进行监听,需要的时候走 EMail 类发出去。
package com.taoxi.designpattern.factorymethod.v2; import java.util.ArrayList; import java.util.List; //订单监听器 public class OrderHandler { //通知处理 public void notifyHandler() { String hostName = "SMTP.163.com"; int port = 586; String userName = "taoxi@163.com"; String password = "******";//授权码 String from = "taoxi@163.com"; List<String> toAddress = new ArrayList<String>(); toAddress.add("tx@jd.com"); toAddress.add("lj@jd.com"); String subject = "订单异常!"; String message = "具体的异常内容"; Email email = new Email(); email.setHostName(hostName); email.setPort(port); email.setUserName(userName); email.setPassword(password); email.setFrom(from); email.getToAddress().addAll(toAddress); email.setSubject(subject); email.setMessage(message); email.send(); } }
3、问题
可复用性太差了,更换个邮件配置还得改多处,需要优化一下了。
4、改进
把邮件处理封装到Email类里。ErrorHandler
、OrderHandler
作为客户端,不再关心邮件对象的创建过程,直接拿来就用,符合了单一职责原则
。
package com.taoxi.designpattern.factorymethod.v2_1; import java.util.ArrayList; import java.util.List; //Email类 public class Email { private String hostName; private int port; private String userName; private String password; private String from; private List<String> toAddress = new ArrayList<>(); private String subject; private String message; //获取对象 public static Email getInstance() { String hostName = "SMTP.163.com"; int port = 586; String userName = "taoxi@163.com"; String password = "******";//授权码 String from = "taoxi@163.com"; List<String> toAddress = new ArrayList<String>(); toAddress.add("tx@jd.com"); toAddress.add("lj@jd.com"); Email email = new Email(); email.setHostName(hostName); email.setPort(port); email.setUserName(userName); email.setPassword(password); email.setFrom(from); email.getToAddress().addAll(toAddress); return email; } //设置消息体 public void initMessage(String subject, String message) { this.subject = subject; this.message = message; } //发送消息 public void send(){ System.out.println("send email!"); } public String getHostName() { return hostName; } public void setHostName(String hostName) { this.hostName = hostName; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getFrom() { return from; } public void setFrom(String from) { this.from = from; } public List<String> getToAddress() { return toAddress; } public void setToAddress(List<String> toAddress) { this.toAddress = toAddress; } public String getSubject() { return subject; } public void setSubject(String subject) { this.subject = subject; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
package com.taoxi.designpattern.factorymethod.v2_1; //错误异常处理 public class ErrorHandler { //渲染异常和错误 public void renderException() { String subject = "报错了!"; String message = "具体的报错内容"; Email email = Email.getInstance(); email.initMessage(subject, message); email.send(); } }
package com.taoxi.designpattern.factorymethod.v2_1; //订单监听器 public class OrderHandler { //监听处理 public void notifyHandler() { String subject = "订单异常!"; String message = "具体的异常内容"; Email email = Email.getInstance(); email.initMessage(subject, message); email.send(); } }
三、第三阶段
1、需求
邮件通知只通知了相应几个管理员,当有人员变化是还需要改收信人配置,最主要的是邮件提醒也不及时,有的人还懒的刷邮件。最近公司启用了钉钉,直接走钉钉群自定义消息,人员变动直接屏蔽在外部,增删群成员就行,消息收取方便了,谁看了过了也能知道,报错信息也从共有知识变成了公共知识。
因此需要增加发送通道,通过钉钉群消息发送。
2、实现
新增钉钉通知类,以及修改错误异常处理器类。
package com.taoxi.designpattern.factorymethod.v3; //钉钉通知类 public class DingDing { private String url; private String token; private String subject; private String message; //获取对象 public static DingDing getInstance() { String url = "https://oapi.dingtalk.com/robot/send?access_token="; String token = "123456abcdefg"; DingDing dingDing = new DingDing(); dingDing.setUrl(url); dingDing.setToken(token); return dingDing; } //设置消息体 public void initMessage(String subject, String message) { this.subject = subject; this.message = message; } //发送消息 public void send(){ System.out.println("send DingDing!"); } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getToken() { return token; } public void setToken(String token) { this.token = token; } public String getSubject() { return subject; } public void setSubject(String subject) { this.subject = subject; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
package com.taoxi.designpattern.factorymethod.v3; /** * 需求:切换发送通道,通过钉钉群消息发送,关闭原来的 Email 发送通道。 */ public class ErrorHandler { public void renderException() { String subject = "报错了!"; String message = "具体的报错内容"; // 通过配置获取使用的消息通道 String type = "dingding"; if (type.equals("dingding")) { DingDing dingDing = DingDing.getInstance(); dingDing.initMessage(subject, message); dingDing.send(); } else if (type.equals("email")) { Email email = Email.getInstance(); email.initMessage(subject, message); email.send(); } } }
package com.taoxi.designpattern.factorymethod.v3; /** * 需求:有人退订单了或下单了长时间没有付款的客户,邮件通知到客服。 */ public class OrderHandler { public void notifly() { String subject = "订单异常!"; String message = "具体的异常内容"; Email email = Email.getInstance(); email.initMessage(subject, message); email.send(); } }
3、问题
《从0到1》告诉我们,0到1很难,1到n却很简单,需求也是这样。有2个通知类型很快就会有多个通知类,到时候 renderException()
将会很臃肿。
而且,身为 高层组件 的 ErrorHandler
异常处理器类直接依赖的 底层组件 的 Email
邮件通知类 和 DingDing
钉钉通知类,也违背了 依赖倒置原则
。(高层组件,是由其他低层组件定义其行为的类。例如,ErrorHandler是个高层组件,因为它的行为是由消息通知定义的,ErrorHandler创建所有不同的消息通知对象,进行消息的通知。)
每次新增、修改通知类时都需要修改 ErrorHandler
类,也不符合 开放-封闭原则
。
4、改进
通过 依赖倒置原则
我们将 依赖细节(类、对象)改为 依赖抽象(抽象类、接口),对消息通知类进行抽象出 消息通知接口。
高层模块的 ErrorHandler
异常处理器依赖了抽象的 INotify
接口,符合了 依赖倒置原则
。
当有新的的消息通知需求时直接实现 INotify
接口,并通过 初始化参数
传入即可,不用再修改 ErrorHandler
类,也符合了 开发-封闭原则
。
package com.taoxi.designpattern.factorymethod.v3_1; //通知接口 public interface INotify { //准备消息体 public void initMessage(String subject, String message); //发送消息 public void send(); }
package com.taoxi.designpattern.factorymethod.v3_1; //钉钉类 public class DingDing implements INotify{ private String url; private String token; private String subject; private String message; public static DingDing getInstance() { String url = "https://oapi.dingtalk.com/robot/send?access_token="; String token = "123456abcdefg"; DingDing dingDing = new DingDing(); dingDing.setUrl(url); dingDing.setToken(token); return dingDing; } @Override public void initMessage(String subject, String message) { this.subject = subject; this.message = message; } @Override public void send(){ System.out.println("send DingDing!"); } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getToken() { return token; } public void setToken(String token) { this.token = token; } public String getSubject() { return subject; } public void setSubject(String subject) { this.subject = subject; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
package com.taoxi.designpattern.factorymethod.v3_1; import org.apache.commons.mail.HtmlEmail; import java.util.ArrayList; import java.util.List; //Email类 public class Email implements INotify{ private String hostName; private int port; private String userName; private String password; private String from; private List<String> toAddress = new ArrayList<>(); private String subject; private String message; public static Email getInstance() { String hostName = "SMTP.163.com"; int port = 586; String userName = "taoxi@163.com"; String password = "******";//授权码 String from = "taoxi@163.com"; List<String> toAddress = new ArrayList<String>(); toAddress.add("tx@jd.com"); toAddress.add("lj@jd.com"); Email email = new Email(); email.setHostName(hostName); email.setPort(port); email.setUserName(userName); email.setPassword(password); email.setFrom(from); email.getToAddress().addAll(toAddress); return email; } @Override public void initMessage(String subject, String message) { this.subject = subject; this.message = message; } @Override public void send(){ System.out.println("send email!"); HtmlEmail email = new HtmlEmail(); } public String getHostName() { return hostName; } public void setHostName(String hostName) { this.hostName = hostName; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getFrom() { return from; } public void setFrom(String from) { this.from = from; } public List<String> getToAddress() { return toAddress; } public void setToAddress(List<String> toAddress) { this.toAddress = toAddress; } public String getSubject() { return subject; } public void setSubject(String subject) { this.subject = subject; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
package com.taoxi.designpattern.factorymethod.v3_1; /** * 需求:切换发送通道,通过钉钉群消息发送,关闭原来的 Email 发送通道。 */ public class ErrorHandler { private INotify notify; ErrorHandler(INotify notifyObject) { this.notify = notifyObject; } public void renderException() { String subject = "报错了!"; String message = "具体的报错内容"; //初始化消息体 this.notify.initMessage(subject, message); //发送消息 this.notify.send(); } }
package com.taoxi.designpattern.factorymethod.v3_1; /** * 需求:有人退订单了或下单了长时间没有付款的客户,邮件通知到客服。 */ public class OrderHandler { private INotify notify; OrderHandler(INotify notifyObject) { this.notify = notifyObject; } public void notifly() { String subject = "订单异常!"; String message = "具体的异常内容"; //初始化消息体 this.notify.initMessage(subject, message); //发送消息 this.notify.send(); } }
package com.taoxi.designpattern.factorymethod.v3_1; public class ErrorHandlerMain { public static void main(String[] args) { // 通过配置获取使用的消息通道 String channelError = "dingding"; INotify messageNotify ; if (channelError.equals("dingding")) { messageNotify = DingDing.getInstance(); } else if (channelError.equals("email")) { messageNotify = Email.getInstance(); } else { return; } ErrorHandler errorHandler = new ErrorHandler(messageNotify); errorHandler.renderException(); } }
package com.taoxi.designpattern.factorymethod.v3_1; public class OrderHandlerMain { public static void main(String[] args) { // 通过配置获取使用的消息通道 String channelError = "email"; INotify messageNotify ; if (channelError.equals("dingding")) { messageNotify = DingDing.getInstance(); } else if (channelError.equals("email")) { messageNotify = Email.getInstance(); } else { return; } OrderHandler orderHandler = new OrderHandler(messageNotify); orderHandler.notifly(); } }
5、问题
具体创建哪个消息通知对象的处理出现了重复,需要整合到一处,方便修改和复用。
6、改进
增加通知消息工厂类。变成简单工厂模式。
package com.taoxi.designpattern.factorymethod.v3_2; // 通知消息工厂 public class NotifyFactory { // 创建通知消息对象 public static INotify create(String channel) { INotify messageNotify = null; if ("dingding".equals(channel)) { messageNotify = DingDing.getInstance(); }else if ("email".equals(channel)) { messageNotify = Email.getInstance(); } return messageNotify; } }
package com.taoxi.designpattern.factorymethod.v3_2; public class ErrorHandlerMain { public static void main(String[] args) { // 通过配置获取使用的消息通道 String channelError = "dingding"; INotify messageNotify = NotifyFactory.create(channelError); ErrorHandler errorHandler = new ErrorHandler(messageNotify); errorHandler.renderException(); } }
package com.taoxi.designpattern.factorymethod.v3_2; public class OrderHandlerMain { public static void main(String[] args) { // 通过配置获取使用的消息通道 String channelError = "email"; INotify messageNotify =NotifyFactory.create(channelError); OrderHandler orderHandler = new OrderHandler(messageNotify); orderHandler.notifly(); } }
7、问题
每次新增消息通知类时都需要修改 NotifyFactory
消息通知工厂的代码,往里添加 case
判断,不符合 开发-封闭原则
。
8、改进
进一步抽象,将对代码的修改调整为对类的增删上。变成工厂方法模式。当新增消息通知类
时,已不需要修改任何已有代码,只需要新增一个 通知工厂类
和 一个消息通知类
即可。
此时启用消息通知类的变更被限定在配置文件或数据库数据配置变化上,切换消息通知通道并不需要修改程序代码。
package com.taoxi.designpattern.factorymethod.v3_3; //通知消息工厂接口 public interface INotifyFactory { public INotify create(); }
package com.taoxi.designpattern.factorymethod.v3_3; //钉钉工厂类 public class DingDingFactory implements INotifyFactory{ @Override public INotify create() { INotify messageNotify = DingDing.getInstance(); return messageNotify; } }
package com.taoxi.designpattern.factorymethod.v3_3; //Email工厂类 public class EmailFactory implements INotifyFactory{ @Override public INotify create() { INotify messageNotify = Email.getInstance(); return messageNotify; } }
package com.taoxi.designpattern.factorymethod.v3_3; public class ErrorHandlerMain { public static void main(String[] args) { // 通过配置获取消息通知工厂类名 INotifyFactory iNotifyFactory = new DingDingFactory(); INotify messageNotify = iNotifyFactory.create(); ErrorHandler errorHandler = new ErrorHandler(messageNotify); errorHandler.renderException(); } }
package com.taoxi.designpattern.factorymethod.v3_3; public class OrderHandlerMain { public static void main(String[] args) { // 通过配置获取消息通知工厂类名 INotifyFactory iNotifyFactory = new EmailFactory(); INotify messageNotify = iNotifyFactory.create(); OrderHandler orderHandler = new OrderHandler(messageNotify); orderHandler.notifly(); } }
四、第四阶段
1、需求
有了即时通知,但想后期查询或统计怎么办,记录一下日志吧。异常订单比较重要,记录到 MySQL
中,查询异常报错字段比较多,记录的 Elasticsearch
中。
因此需要发送通知时记录一下日志,方便日后查询与统计。
2、实现
日志类和消息通知类很像,直接实现成工厂方法模式。
package com.taoxi.designpattern.factorymethod.v4; //日志接口 public interface ILog { //写日志 public void write(); }
package com.taoxi.designpattern.factorymethod.v4; public class ElasticSearchLog implements ILog { public static ElasticSearchLog getInstance() { ElasticSearchLog elasticSearchLog = new ElasticSearchLog(); return elasticSearchLog; } @Override public void write() { System.out.println("write elasticSearch log!"); } }
package com.taoxi.designpattern.factorymethod.v4; public class MysqlLog implements ILog { public static MysqlLog getInstance() { MysqlLog mysqlLog = new MysqlLog(); return mysqlLog; } @Override public void write() { System.out.println("write mysql log!"); } }
package com.taoxi.designpattern.factorymethod.v4; //日志工厂接口 public interface ILogFactory { //创建日志记录对象 public ILog create(); }
package com.taoxi.designpattern.factorymethod.v4; //ElasticSearch日志工厂类 public class ElasticSearchFactory implements ILogFactory { @Override public ILog create() { ElasticSearchLog elasticSearchLog = ElasticSearchLog.getInstance(); return elasticSearchLog; } }
package com.taoxi.designpattern.factorymethod.v4; //Mysql日志工厂类 public class MysqlLogFactory implements ILogFactory { @Override public ILog create() { MysqlLog mysqlLog = MysqlLog.getInstance(); return mysqlLog; } }
package com.taoxi.designpattern.factorymethod.v4; public class ErrorHandlerMain { public static void main(String[] args) { // 通过配置获取消息通知工厂类名 INotifyFactory iNotifyFactory = new DingDingFactory(); INotify messageNotify = iNotifyFactory.create(); // 通过配置获取日志记录工厂类名 ILogFactory iLogFactory = new ElasticSearchFactory(); ILog log = iLogFactory.create(); ErrorHandler errorHandler = new ErrorHandler(messageNotify, log); errorHandler.renderException(); } }
package com.taoxi.designpattern.factorymethod.v4; public class OrderHandlerMain { public static void main(String[] args) { //通过配置获取消息通知工程类 INotifyFactory iNotifyFactory = new EmailFactory(); INotify messageNotify = iNotifyFactory.create(); //通过配置获取日志记录工程类 ILogFactory iLogFactory = new MysqlLogFactory(); ILog log = iLogFactory.create(); OrderHandler orderHandler = new OrderHandler(messageNotify, log); orderHandler.notifly(); } }
3、问题
新增一个工厂模式并非这么简单就能解决问题的。 目前需求是 消息通知 并 记录日志 ,两者已经是 组合
关系,将这种组合关系下放到客户进行创建那么就不符合 单一职责原则
。
客户端还知道有2个工厂,2个工厂生产的东西必须搭配在一起才能实现消息通知并记录日志的功能,不符合 迪米特原则
。
类似买苹果笔记本,有不同的配置,简单那2个配置举例子,cpu 分为 i7 和 i5, 屏幕分为 13 寸 和 15 寸。
普通消费者买笔记本会说:“我要个玩游戏爽的笔记本”,店员应该直接给出 i7 + 15 寸配置的机型。
如果一个人也是玩游戏,过来直接说:“要个 i7 + 15 寸配置的机型”,那这个人一听就是程序员(知道的太多了!不符合单一职责原则
和 迪米特原则
)。
4、改进
使用抽象工厂模式。
package com.taoxi.designpattern.factorymethod.v4_1; //异常处理工厂接口 public interface IFactory { //创建通知消息对象 public INotify createNotify(); //创建日志记录对象 public ILog createLog(); }
package com.taoxi.designpattern.factorymethod.v4_1; //异常报错工厂类 public class ErrorFactory implements IFactory { @Override public INotify createNotify() { return DingDing.getInstance(); } @Override public ILog createLog() { return ElasticSearchLog.getInstance(); } }
package com.taoxi.designpattern.factorymethod.v4_1; //异常订单工厂类 public class OrderFactory implements IFactory { @Override public INotify createNotify() { return Email.getInstance(); } @Override public ILog createLog() { return MysqlLog.getInstance(); } }
package com.taoxi.designpattern.factorymethod.v4_1; public class ErrorHandlerMain { public static void main(String[] args) { // 通过配置获取异常错误工厂类名 IFactory iFactory = new ErrorFactory(); INotify messageNotify = iFactory.createNotify(); ILog log = iFactory.createLog(); ErrorHandler errorHandler = new ErrorHandler(messageNotify, log); errorHandler.renderException(); } }
package com.taoxi.designpattern.factorymethod.v4_1; public class OrderHandlerMain { public static void main(String[] args) { // 通过配置获取异常订单工厂类名 IFactory iFactory = new OrderFactory(); INotify messageNotify = iFactory.createNotify(); ILog log = iFactory.createLog(); OrderHandler orderHandler = new OrderHandler(messageNotify, log); orderHandler.notifly(); } }