使用Builder模式改进多参数方法


title: 使用Builder模式改进多参数方法
date: 2016-10-16 11:58:39
tags:

  • 设计模式
  • Java
    category: 设计模式

概述

记一次工作当中对多参数方法重构。

  1. 使用对象封装对多参数,简化方法调用
  2. 使用Builder(创建者)模式简化多属性对象的创建

问题

业务系统中统一的邮件发送服务接口在改造前大概长着个样子:

    /**
     * 不帶附件的邮件发送
     * 未使用建造者模式的原始方法(不良代码)
     * @param template                      模板
     * @param subjects                      主题
     * @param contents                      内容
     * @param toPersons                     收件人
     * @param ccPersons                     抄送人
     * @param bccPersons                    暗送人
     */
    public static void sendEmail(String template, List<String> subjects, List<String> contents, List<String> toPersons,
                                  List<String> ccPersons, List<String> bccPersons){
        logger.info("send email");
    }


    /**
     * 带附件的邮件发送
     * 未使用建造者模式的原始方法(不良)
     * @param template                      模板
     * @param subjects                      主题
     * @param contents                      内容
     * @param toPersons                     收件人
     * @param ccPersons                     抄送人
     * @param bccPersons                    暗送人
     * @param docName                       文档名称
     * @param fileName                      文件名称(单个文件)
     */
    public static void sendEmail(String template, List<String> subjects, List<String> contents, List<String> toPersons,
                                  List<String> ccPersons, List<String> bccPersons, String docName, String fileName){
        logger.info("send email");
    }

    /**
     * 带附件的邮件发送
     * 未使用建造者模式的原始方法(不良代码)
     * @param template                      模板
     * @param subjects                      主题
     * @param contents                      内容
     * @param toPersons                     收件人
     * @param ccPersons                     抄送人
     * @param bccPersons                    暗送人
     * @param docName                       文档名称
     * @param fileNames                     文件名称(多个文件,文件名称列表)
     */
    public static void sendEmail(String template, List<String> subjects, List<String> contents, List<String> toPersons,
                                  List<String> ccPersons, List<String> bccPersons, String docName, List<String> fileNames){
        logger.info("send email");
    }

接口有个重要的内容注释中并没有说明:参数中“模板”和“收件人”是必填的,而其他参数是非必填的。

接口的可配置程度还是不错的,但是调用的过程就比较痛苦了。

一大堆String和List接口暴露出来,同时又使用了不同的参数个数来进行重载。

调用的时候光是创建这些参数就够麻烦的了,还要考虑哪些参数是必填的以及参数的正确位置。更糟糕的是参数传错位置你会发现很有可能并没有显式的暴露出问题,邮件还是发送了只是发送到错误的相关人员那里。

其他人看到方法调用也无法清晰知道这个到底是要发什么邮件,给哪些人。

这应该就是坏代码的味道吧。

改进

简单的改进思路就是把参数做成一个modle封装起来,以后传递model给方法。就像这样:

    /**
     * 邮件发送通用接口
     * @param email 邮件发送参数对象
     */
    public static void sendEmail(EmailSendMain email){
        logger.info("send email:" + email);
    }

但是问题还是没有根本解决,对象的构造还是需要多个参数的构造方法,可能还需要重载。

比较容易想到的改进是使用Java Bean模式。简化构造方法字段,构造方法只传入必要的字段,使用setter方法设置其他值。我就是这么肤浅的想到这个地步了。

乍一看问题是解决了,其实不然。

  1. 在对象创建过程中Java Bean可能处于不一致状态
  2. 使用Java Bean就将不能创建不可变对象

读了《Effective Java》只是第二章就有了更好的解决思路——Builder模式。

先看改进后的代码:

/**
 * 复杂类型构建接口
 *
 * 建造者模式中的抽象构建者
 * Created by ZhangHao on 2016/10/15.
 */
public interface Builder<T> {
    T build();
}

/**
 * 邮件发送参数对象
 * 包含多个字段的复杂类型,使用内部类实现Builder接口创建对象
 *
 * 建造者模式中的产品类
 * Created by ZhangHao on 2016/10/15.
 */
public final class EmailSendMain {
    private final String template;  // 模板名称
    private final List<String> subjects;  // 主题参数列表
    private final List<String> contents;  // 内容参数列表
    private final List<String> toPersons;  // 收件人列表
    private final List<String> ccPersons;  // 抄送人列表
    private final List<String> bccPersons;  // 暗送人列表
    private final String docName;  // 文档名称
    private final List<String> fileNames;  // 文件名称列表

    private EmailSendMain(Builder builder) {
        this.template = builder.template;
        this.subjects = builder.subjects;
        this.contents = builder.contents;
        this.toPersons = builder.toPersons;
        this.ccPersons = builder.ccPersons;
        this.bccPersons = builder.bccPersons;
        this.docName = builder.docName;
        this.fileNames = builder.fileNames;
    }

    /**
     * 实现Builder接口的构建类,用于创建EmailSendMain
     *
     * 建造者模式中的建造类
     */
    public static class Builder implements tk.zhangh.pattern.create.builder.demo1.Builder<EmailSendMain> {
        private String template;  // 模板名称
        private List<String> subjects;  // 主题参数列表
        private List<String> contents;  // 内容参数列表
        private List<String> toPersons;  // 收件人列表
        private List<String> ccPersons;  // 抄送人列表
        private List<String> bccPersons;  // 暗送人列表
        private String docName;  // 文档名称
        private List<String> fileNames;  // 文件名称列表

        public Builder(String template, List<String> toPersons) {
            this.template = template;
            this.toPersons = toPersons;
        }

        @Override
        public EmailSendMain build() {
            return new EmailSendMain(this);
        }

        public Builder subjects(List<String> subjects) {
            this.subjects = subjects;
            return this;
        }

        public Builder contents(List<String> contents) {
            this.contents = contents;
            return this;
        }

        public Builder ccPersons(List<String> ccPersons) {
            this.ccPersons = ccPersons;
            return this;
        }

        public Builder bccPersons(List<String> bccPersons) {
            this.bccPersons = bccPersons;
            return this;
        }

        public Builder docName(String docName) {
            this.docName = docName;
            return this;
        }

        public Builder fileNames(List<String> fileNames) {
            this.fileNames = fileNames;
            return this;
        }
    }

    // getter,toString方法省略
}

重写做的接口方法封装:

    /**
     * 邮件发送通用接口
     * @param email 邮件发送参数对象
     */
    public static void sendEmail(EmailSendMain email){
        logger.info("send email:" + email);
        if ((email.getDocName() == null || email.getDocName().equals("")) ||
                (email.getFileNames() == null || email.getFileNames().size() == 0)) {
            sendEmail(email.getTemplate(), email.getSubjects(), email.getContents(), email.getToPersons(),
                    email.getCcPersons(), email.getBccPersons());
        }else {
            sendEmail(email.getTemplate(), email.getSubjects(), email.getContents(), email.getToPersons(),
                    email.getCcPersons(), email.getBccPersons(), email.getDocName(), email.getFileNames());
        }
    }

客户端调用:

    @Test
    public void testSendEmail() throws Exception {
        EmailSendMain email =
                new EmailSendMain.Builder("邮件模版名",toPersons).
                        subjects(subjects).
                        contents(contents).
                        ccPersons(ccPersons).
                        bccPersons(bccPersons).build();
        SendEmailUtil.sendEmail(email);
    }

问题圆满解决,支持可选参数的链式结构调用,创建过程也确保了一致性,使用不可变类也没问题。

如果说缺点,其实不难看出EmailSendMain的字段和它的内部类Builder字段完全重复了。为了创建EmailSendMain的对象将必须先创建Builder也会带来轻微的性能问题。创建的调用过程虽然看起来更清晰,但也更加冗长。

但是Builder模式还是创建多参数类的不错选择,尤其是大多数参数是可选。

扩展

这篇文章的思路是根据《Effective Java》得来的,文章只提到书中建议的第二条,实际关于上面使用到的内部类,泛型,在书中的建议都让我有了更多的认识。我就不赘述了,连上8天班我要去偷懒了。

写这篇文章的时候看到有个系列专门讲Java方法参数太多的问题

传送门:https://dzone.com/articles/too-many-parameters-java

以及翻译:http://www.importnew.com/6518.html

代码我放在了学习设计模式的项目下:

传送门:https://github.com/zhanghTK/HelloDesignPattern

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,110评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,443评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,474评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,881评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,902评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,698评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,418评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,332评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,796评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,968评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,110评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,792评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,455评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,003评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,130评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,348评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,047评论 2 355

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,664评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,639评论 18 399
  • ——“你家学校定了吗” ——“还没消息呢,说是这两天能定下来。你怎么样?” ——“说是没问题了,可是没到报到那一天...
    牛小牛GO阅读 141评论 0 0
  • 产品经理是一个岗位名称,而不是岗位级别。 产品经理,百度百科是这样定义的:是企业中专门负责产品管理的职位,产品经...
    Himor阅读 319评论 0 2
  • 29日与天津大学MBA举办的一场别开生面的会议,欢声笑语,有歌声,有小品,还有别具一格的对口相声! 一场会议下来,...
    踏雪无痕Sunny阅读 553评论 0 0