策略模式-记录日志的N种方式

285 阅读4分钟

这是我参与8月更文挑战的第21天,活动详情查看:8月更文挑战


1. 记录日志的N种方式

维护过线上系统的同学应该都清楚日志的重要性,它可以帮助我们在系统异常时快速定位到问题,并进行解决。 对于不同的环境,记录日志的方式也是不一样的,这里列举三种情况:

  1. 开发环境,控制台输出日志即可。
  2. 线上环境,日志持久化到磁盘文件,方便后期排查问题。
  3. 日志要做离线分析,需要存入数据库。

我们试着用代码来描述这个过程,类图设计如下: 在这里插入图片描述

编写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. 策略模式的优缺点

优点

  1. 策略可以自由切换,封装类不用改,符合开闭原则。
  2. 消除if分支判断,提高代码的可读性。
  3. 扩展性良好,只需派生Strategy子类就可以增加一种策略。

缺点

  1. 每个策略都需要新建一个类,容易导致类的数量膨胀。
  2. 所有的策略都需要暴露给客户端,以让客户端来选择其中一个策略。

如果你发现系统的某一处算法逻辑需要频繁的切换,或者后续可能会改变算法逻辑,就可以考虑使用策略模式。 例如:发短信功能,就可以定义两种策略:

  1. 调用第三方SDK发送短信,扣费。
  2. 仅仅输出短信到控制台,不发送。

开发环境时,为了节省费用,可以使用策略二,线上环境使用策略一。你肯定不想每次切换策略时都要修改所有调用短信的代码吧,借助Spring的依赖注入功能,开发环境和线上环境修改配置即可,一键切换策略,非常奈斯。

4. 策略枚举

策略模式还有一种扩展实现,那就是「策略枚举」。 针对上述的日志策略实现,可以优化为如下:

enum LoggerEnum implements Logger {
    CONSOLE(){
        @Override
        public void log(String s) {
                控制台输出日志
        }
    }
    ......
}

非常的简单清晰,完全的面向对象操作。

5. 总结

策略模式非常简单,应用范围也很广,它没有什么玄机,就是用到了Java的「继承」和「多态」的特性。 策略模式可以分离程序中「变与不变」的部分,将易变的算法策略抽象出来,上下文只依赖其抽象,而不关心实现,提高系统的稳定性。 策略模式最大的一个缺点就是必须将所有的策略都暴露给客户端,好让客户端可以选择一个具体的策略去使用。当策略过多时,客户端调用会非常迷惑,不知道该使用哪一个策略,可以使用工厂模式或享元模式来优化。