阅读 835

为什么总是用不好设计模式?

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

先点赞再看,养成好习惯

前言

经常看到一些设计模式的文章,写了很多内容,也举了一些很“生动形象”的例子。 ​

但是可能和《Head First 设计模式》会有一样的问题:看完了,我会了,但是好像用不上?或者是硬套设计模式。 ​

举几个我见过的极端的例子:

  1. 俩字段,也要来个 Builder
  2. 3个 if,提个策略模式
  3. 5行代码还很简单的初始化,也要弄个 Factory
  4. ……

至于为什么会出现这种问题……我聊聊我的看法

原因

大多数的研发人员,做的工作都是业务功能开发,也就是常说的 CRUD。只是不同的业务场景,CRUD的复杂度不同而已。 ​

可是对于业务代码来说,很多情况下不太好套用设计模式,或者说没法很好的应用设计模式。 ​

平时看到的最多的是策略模式的文章吧,为什么呢? ​

我猜是因为这个最好写,应用在业务代码里比较简单;随便一个稍微复杂点的场景,就可以套用一下策略模式,把多个 if 拆分到多个类里。 ​

的确,业务代码里适当的使用策略模式可以降低复杂度;但就算用也得住一个度,不要把各种业务里的 if 都换成策略模式了,不然代码会炸的…… ​

之前见过一个项目,虽然是内部xx系统,但那个研发小哥可能是走火入魔了。抽了 80 多个策略类出来,这八十多个类里又分为十几个组,每组七八个策略类,但每个类里的代码,也不过十几二十行,而且还有重复代码。 ​

我当时问了他一句:你在养蛊吗? ​

像这个研发小哥就是一个反例,滥用设计模式,过分的将各种分支代码全部套进设计模式了。不过我猜想他可能是为了学习吧,学以致用…… ​

其他的委托代理状态之类的模式,想应用在业务代码里,就比较费劲了,因为没有那么多合适的场景。但策略模式则不同,有 if 的地方都可以尝试套一下…… ​

可是设计模式是用来解决问题,降低/转移复杂度的,而不是增加复杂度。

非业务代码里的设计模式

跳出业务代码来,甚至说跳出纯业务代码来之后,想应用设计模式就比较简单了,甚至不需要你硬套,遇到问题时就自然的会想到用设计模式来解决。

举个栗子

系统里一般需要一个 traceId/requestId 来将整个链路串起来,配合日志打印或者集中式的APM抽取。 ​

就拿单体应用来说,一般用日志框架的 MDC 来绑定这个 traceId。在 Filter 或者 一些 AOP 里,给 MDC 一个 traceID,那么整个调用链路都可以用这一个 ID,打印日志时就可以根据 traceId 区分不同请求了,就像这样:

2021-06-10 18:31:44.227 [ThreadName] [000] INFO loggerName - 请求第0步
2021-06-10 18:31:44.227 [ThreadName] [000] INFO loggerName - 请求第1步
2021-06-10 18:31:44.227 [ThreadName] [000] INFO loggerName - 请求第2步

2021-06-10 18:31:44.227 [ThreadName] [111] INFO loggerName - 请求第0步
2021-06-10 18:31:44.227 [ThreadName] [111] INFO loggerName - 请求第1步
2021-06-10 18:31:44.227 [ThreadName] [111] INFO loggerName - 请求第2步

...
复制代码

通过 000/111 这个 traceId 就可以区分是哪个请求。 ​

可 MDC 是通过 ThreadLocal 进行存储数据的,ThreadLocal 毕竟是和线程绑定的。如果链路中使用了线程池处理,那可怎么办?线程池里子线程打印日志的时候,MDC 可获取不到主线程的 traceId,但对于这个请求来说,主子线程都是一个链路…… ​

还记得这句话吗?

计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”

这里借助委托模式,来增加一个中间层,问题就很好解决了。 ​

既然是主子线程的数据传递问题,那么只需要在创建子线程的时候,从主线程里将 MDC 里的 traceId 拿出来,传递给新建的子线程就可以了,就像这样:

public class MDCDelegateRunnable implements Runnable{

    private Runnable target;

    private String traceId;

    public MDCDelegateRunnable(Runnable target, String traceId) {
        this.target = target;
        this.traceId = traceId;
    }

    @Override
    public void run() {
        MDC.put("traceId",traceId);
        target.run();
        MDC.remove("traceId");
    }
}
复制代码

然后再来一个委托模式的线程池,将 execute方法重写。把线程池中原本的 Runnable 对象包装为刚才的 MDCDelegateRunnable,在创建时,将 traceId 通过构造参数传递

public class MDCDelegateExecutorService extends AbstractExecutorService {

    public MDCDelegateExecutorService(AbstractExecutorService target) {
        this.target = target;
    }

    private AbstractExecutorService target;

    @Override
    public void shutdown() {
        target.shutdown();
    }

    //...

    @Override
    public void execute(@NotNull Runnable command) {
        target.execute(new MDCDelegateRunnable(command, MDC.get("traceId")));
    }

}
复制代码

搞定,来测试一下:

public static void main(String[] args) throws IOException, ExecutionException, InterruptedException {
    MDC.put("traceId","111");
    new MDCDelegateExecutorService((AbstractExecutorService) Executors.newFixedThreadPool(5)).execute(new Runnable() {
        @Override
        public void run() {
            System.out.println("runnable: "+MDC.get("traceId"));
        }
    });
    Future<String> future = new MDCDelegateExecutorService((AbstractExecutorService) Executors.newFixedThreadPool(5)).submit(new Callable<String>() {
        @Override
        public String call() throws Exception {
            return MDC.get("traceId");
        }
    });

    System.out.println("callable: "+future.get());

    System.in.read();
}

//output
runnable: 111
callable: 111
复制代码

完美,本来麻烦的 traceId 传递问题,现在通过一个简单的委托模式就解决了。不用修改调用方代码,也没有破坏线程池的代码。

JDK 里的委托模式

还记得Executors#newSingleThreadExecutor这个单线程线程池的创建方法吧,那什么情况下需要单线程的线程池呢? ​

比如我只是需要一个异步并且获取返回的操作,直接 new 线程 start 的话,获取返回值又不太方便,如果通过线程池的 Callable/Runnable + Future 就方便了:

ExecutorService executorService = Executors.newSingleThreadExecutor();

Future<String> future = executorService.submit(new Callable<String>() {
    @Override
    public String call() throws Exception {
        // do sth...
        return data;
    }
});

String data = future.get();

executorService.shutdown();
复制代码

对于单线程异步的场景来说,甚至都不需要维护一个单例的线程池,每次 new/shutdown 也可以。可是我都单线程了,每次还要 shutdown 是不是有点不太方便,万一哪里忘了 shutdown 了,那可不完蛋了…… ​

JDK 的设计者也想到了这个问题,而且他们也已经解决了这个问题。和上面的例子类似,利用一个简单的委托模式,就可以完美解决这个问题:

public static ExecutorService newSingleThreadExecutor() {
    //创建 FinalizableDelegatedExecutorService 委托类
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

// 委托类里,在 finalize 被委托的线程池对象的 shutdown方法,自动关闭线程池
static class FinalizableDelegatedExecutorService
    extends DelegatedExecutorService {
    FinalizableDelegatedExecutorService(ExecutorService executor) {
        super(executor);
    }
    protected void finalize() {
        super.shutdown();
    }
}

// 公共的抽象委托线程池……
static class DelegatedExecutorService extends AbstractExecutorService {
    private final ExecutorService e;
    
    DelegatedExecutorService(ExecutorService executor) { e = executor; }
    
    public void execute(Runnable command) { e.execute(command); }
    
    public void shutdown() { e.shutdown(); }
    //...
}
复制代码

这样一来,在使用 newSingleThreadExecutor的时候,甚至都不需要显示 shutdown 了…… ​

注意:虽然JDK 帮我们关了……但还是建议手动 shutdown,把 JDK 的这个机制当做一个防呆设计,万一忘了 JDK 还能自动关闭,避免泄露的问题

总结

结合上面两个例子来看,一旦跳出业务代码的范围,应用设计模式是不是变得很简单?甚至都不需要硬往设计模式上套,遇到问题你自然会想到用设计模式来解决问题,而不是用设计模式在代码里养蛊…… ​

在纯业务代码中,适当的拆分,保持代码整洁可读性强带来的收益,远比套一堆设计模式要强 ​

重复一遍:设计模式是用来解决问题,降低/转移复杂度的,而不是增加复杂度

以上仅个人看法,如有不同意见欢迎评论区留言

原创不易,禁止未授权的转载。如果我的文章对您有帮助,就请点赞/收藏/关注鼓励支持一下吧❤❤❤❤❤❤

文章分类
后端
文章标签