这是我参与8月更文挑战的第21天,活动详情查看:8月更文挑战
1. 记录日志的N种方式
维护过线上系统的同学应该都清楚日志的重要性,它可以帮助我们在系统异常时快速定位到问题,并进行解决。 对于不同的环境,记录日志的方式也是不一样的,这里列举三种情况:
- 开发环境,控制台输出日志即可。
- 线上环境,日志持久化到磁盘文件,方便后期排查问题。
- 日志要做离线分析,需要存入数据库。
我们试着用代码来描述这个过程,类图设计如下:
编写Logger接口,定义日志具有的功能:
interface Logger {
void log(String s);
}
编写实现类,有三种记录日志的方式:
// 单纯的控制台输出日志
class ConsoleLogger implements Logger {
@Override
public void log(String s) {
System.out.println(s);
}
}
// 磁盘文件记录日志
class FileLogger implements Logger {
@Override
public void log(String s) {
// 日志追加到文件
FileUtil.appendToFile(s, File);
}
}
// 数据库记录日志
class DbLogger implements Logger {
@Override
public void log(String s) {
// 写入到数据库
DBUtil.insert(...);
}
}
定义LogContext日志上下文对象:
class LogContext {
Logger console = new ConsoleLogger();
Logger file = new FileLogger();
Logger db = new DbLogger();
public void doSomething(int type, String arg) {
if (type == 1) {
console.log(arg);
} else if (type == 2) {
file.log(arg);
} else if (type == 3) {
db.log(arg);
}
}
}
OK,功能完成,现在我们重新审视一下代码,有没有什么问题?
LogContext严重依赖于各种日志实现类,而且存在大量的if分支,如果以后要将日志存入ElasticSearch,就要修改LogContext的代码,分支也要增加,代码可读性降低,违反了「开闭原则」。
LogContext应该只依赖Logger抽象,只负责记录日志,但是日志以何种方式记录,它并不关心。
LogContext修改如下:
class LogContext {
private Logger logger;
// 省略set方法
public void doSomething(String arg) {
logger.log(arg);
}
}
客户端这样调用:
main(String[] args) {
LogContext logContext = new LogContext();
logContext.setLogger(new ConsoleLogger());
logContext.doSomething("console");
}
LogContext变的非常简单,消除了if分支,只依赖Logger抽象,不关心日志实现,哪怕后续再新增日志记录策略,它也无需修改。
这就是策略模式!
2. 策略模式的定义
定义一组算法,将每个算法都封装起来,并且使它们之间可以互换。
- Context:封装的上下文角色,避免高层模块对算法策略的直接访问。
- Strategy:策略抽象,定义所有策略具有的功能。
- ConcreteStrategy:具体策略,真正的逻辑实现者。
策略模式的定义非常简单清晰,定义一组算法:三种日志的不同实现逻辑就是一组算法。使它们能够互换:当然可以互换,Context只依赖策略抽象,具体策略可以随时更换,Context并不关心。
3. 策略模式的优缺点
优点
- 策略可以自由切换,封装类不用改,符合开闭原则。
- 消除if分支判断,提高代码的可读性。
- 扩展性良好,只需派生
Strategy子类就可以增加一种策略。
缺点
- 每个策略都需要新建一个类,容易导致类的数量膨胀。
- 所有的策略都需要暴露给客户端,以让客户端来选择其中一个策略。
如果你发现系统的某一处算法逻辑需要频繁的切换,或者后续可能会改变算法逻辑,就可以考虑使用策略模式。 例如:发短信功能,就可以定义两种策略:
- 调用第三方SDK发送短信,扣费。
- 仅仅输出短信到控制台,不发送。
开发环境时,为了节省费用,可以使用策略二,线上环境使用策略一。你肯定不想每次切换策略时都要修改所有调用短信的代码吧,借助Spring的依赖注入功能,开发环境和线上环境修改配置即可,一键切换策略,非常奈斯。
4. 策略枚举
策略模式还有一种扩展实现,那就是「策略枚举」。 针对上述的日志策略实现,可以优化为如下:
enum LoggerEnum implements Logger {
CONSOLE(){
@Override
public void log(String s) {
控制台输出日志
}
}
......
}
非常的简单清晰,完全的面向对象操作。
5. 总结
策略模式非常简单,应用范围也很广,它没有什么玄机,就是用到了Java的「继承」和「多态」的特性。 策略模式可以分离程序中「变与不变」的部分,将易变的算法策略抽象出来,上下文只依赖其抽象,而不关心实现,提高系统的稳定性。 策略模式最大的一个缺点就是必须将所有的策略都暴露给客户端,好让客户端可以选择一个具体的策略去使用。当策略过多时,客户端调用会非常迷惑,不知道该使用哪一个策略,可以使用工厂模式或享元模式来优化。