面试官:我不想听单例、工厂了,跟我说说装饰器模式吧!

开发 前端
如果数学和绘画课程又新增了需求,需要额外展示对应的辅修资料,但英语课程则不需要展示这类信息,那按照继承的方式应该如何实现呢?

我草草地估算了一下,基本上80% Java 候选人的简历上,在专业技能栏上都会写上这么一条:

熟悉常用的GOF设计模式,可在实际业务场景中进行合理运用;

如果面试官恰好看到了这条专业技能,问道:“那你说一下,都熟悉并使用过哪些设计模式呢?”

然后,绝大多数候选人都会回答说:“嗯,熟悉单例模式和工厂模式。”

面试官接着问道:“还有其他的吗?”

候选人一般会说:“嗯,还有代理模式、策略模式这些吧,其实平时用到的也不是很多。”

此时,若面试官继续追问:“装饰器模式有没有了解过?”

候选人往往会发愣一下,然后说:“嗯,这个设计模式也听过,但没太深入了解。”

嗯,本文我们以真实场景带入的方式来讲解一下,有用且有趣的“装饰器”模式。

接下来话不多说,Show me the case。

业务背景

某大型在线教育学习平台,其学生端最重要的功能就是展示学生的课程列表,学生可点击课程列表中的某个课程进教室上课,还可以查看这节课对应的课件、课前预习和课后作业等。

如下图所示:

图片

当然,真实的业务场景还是要复杂很多的,比如:英语课程的 PC 端按照上图中的展示方式即可,而英语课程 iPad 端的产品经理,则希望在已经上过的课程中,加上老师给学生的打分。

数学课程与英语课程也有不同的地方,课程卡片上需要展示教师的标签,如:名师、活跃、名校毕业、教龄长、好评多等;

最近新推出的绘画课程,不但需要在课程卡片上展示教师的标签,而且为了鼓励学生更多地上课学习,会在课程卡片上展示一个完课奖品。

当然,我仅仅是举个例子,实际的课表展示逻辑会复杂很多。

代码质量问题

说说目前这块的代码实现情况。

最开始的时候,公司只有英语课程,且 PC 端和 iPad 端的课表展示逻辑是一样的。

代码demo如下:

public class Curriculum {


    public void query(int studentID) {
    
        System.out.println("展示课表");
        System.out.println("展示对应的课前预习");
        System.out.println("展示对应的课后作业");
        System.out.println("展示对应的课件");
    }
}

后来,英语课程的 PC 端和 iPad 端的课表展示逻辑不一样了,iPad 端的课表展示需要加上老师给学生的打分,代码实现如下:

public class Curriculum {


    public void query(int studentID, int origin) {


        System.out.println("展示课表");
        System.out.println("展示对应的课前预习");
        System.out.println("展示对应的课后作业");
        System.out.println("展示对应的课件");


        //英语课程iPad端
        if(origin == 1){
            System.out.println("展示对应的学生评分");
        }
    }
}

再后来,又增加了需要展示老师标签的数学课程,以及增加老师标签和完课奖品的绘画课程,都在一个类中以 if else 分支判断的方式来实现,代码的可读性和可维护性就太差了。

于是,负责维护这块业务代码的工程师干脆一刀切,直接写成了四套代码。

英语课程PC端:

public class EnglishPCCurriculum {


    public List query(int studentID) {
      
        System.out.println("展示课表");
        System.out.println("展示对应的课前预习");
        System.out.println("展示对应的课后作业");
        System.out.println("展示对应的课件");
    }
}

英语课程iPad端:

public class EnglishIPadCurriculum {


    public void query(int studentID) {
    
        System.out.println("展示课表");
        System.out.println("展示对应的课前预习");
        System.out.println("展示对应的课后作业");
        System.out.println("展示对应的课件");
        
        System.out.println("展示对应的学生评分");
    }
}

数学课程:

public class MathCurriculum {


    public void query(int studentID) {
    
        System.out.println("展示课表");
        System.out.println("展示对应的课前预习");
        System.out.println("展示对应的课后作业");
        System.out.println("展示对应的课件");
        
        System.out.println("展示对应的老师标签");
    }
}

绘画课程:

public class DrawCurriculum {


     public void query(int studentID) {
     
        System.out.println("展示课表");
        System.out.println("展示对应的课前预习");
        System.out.println("展示对应的课后作业");
        System.out.println("展示对应的课件");
        
        System.out.println("展示对应的老师标签");
        System.out.println("展示对应的完课奖品");
      }
}

划重点,代码按照上述方式实现,有何问题?

在《重构—改善既有代码的设计》一书中,有两种非常常见的Bad Smell(糟糕的代码),叫做 “过长的方法” 和 “重复的代码” 。

其实问题还是挺大的,我们上面的代码只是实现了一个demo而已,如果是真实的代码,这个查询课表的query()方法实现了太多的业务逻辑,一定命中了“过长的方法”这个Bad Semll。

而且,上面这四个类中的query()方法,在实现展示课表、作业、预习、课件业务无逻辑的时候,也命中了“重复的代码”这个Bad Semll。

除此之外,这段代码还命中了一种叫做 “发散式变化” 的 Bad Smell。

发散式变化的定义是,一个类被锚定了多个变化,当这些变化中的任意一个发生时,就必须对类进行修改。这说明该类承担的职责过多,不符合单一职责的设计原则。

而上面这四个类的query()方法中,从头到尾实现了整个课表展示的逻辑,只要课表、作业、预习、课件等任意逻辑发生变化都需要对这个类进行修改,确实承担的职责过多了。

接下来,我们看看如何这块代码进行重构,使其实现方式更具可维护性和可扩展性。

装饰器模式

装饰器模式(Decorator Pattern),在不改变一个现有对象结构的情况下,为其动态地增加一些额外的职责。

装饰器模式的优点在于:

  1. 可动态地为现有对象增加额外的职责,无需改动原来的代码,具备更好的灵活性和可扩展性,且符合开闭原则。
  2. 每种额外的职责都被实现为一个单独且通用的装饰器,符合单一职责,解决了“过长的方法”、“重复的代码”和“发散式变化”等Bad Smell。

其类结构图如下:

图片图片

Component:定义了被装饰对象和装饰器都需要实现的接口。

ConcreteComponent:被装饰对象,需要提供业务逻辑的核心功能。

Decorator:抽象装饰器,可通过其子类进行额外功能职责的扩展。

ConcreteDecorator:具体装饰类,对被装饰对象进行额外功能职责的扩展。

代码重构优化

接下来我们通过装饰器模式将代码进行重构优化。

Curriculum接口:

public interface Curriculum {


    public void query(int studentID);
}

Curriculum具体实现:

public class ConcreteCurriculum implements Curriculum {


    public void query(int studentID) {


        System.out.println("展示课表");
        System.out.println("展示对应的课前预习");
        System.out.println("展示对应的课后作业");
        System.out.println("展示对应的课件");
    }
}

Curriculum的抽象装饰器:

public abstract class CurriculumDecorator implements Curriculum {
    protected Curriculum curriculum;


    public CurriculumDecorator(Curriculum curriculum){
        this.curriculum = curriculum;
    }


    public void query(int studentID){
        curriculum.query(studentID);
    }
}

Curriculum的评分装饰器:

public class ScoreDecorator extends CurriculumDecorator{


    public ScoreDecorator(Curriculum curriculum) {
        super(curriculum);
    }


    @Override
    public void query(int studentID) {
        curriculum.query(studentID);
        System.out.println("展示对应的学生评分");


    }
}

Curriculum的老师标签装饰器:

public class LabelDecorator extends CurriculumDecorator{


    public LabelDecorator(Curriculum curriculum) {
        super(curriculum);
    }


    @Override
    public void query(int studentID) {
        curriculum.query(studentID);
        System.out.println("展示对应的老师标签");


    }
}

Curriculum的奖品装饰器:

public class GiftDecorator extends CurriculumDecorator{


    public GiftDecorator(Curriculum curriculum) {
        super(curriculum);
    }


    @Override
    public void query(int studentID) {
        curriculum.query(studentID);
        System.out.println("展示对应的完课奖品");


    }
}

Demo:

public class Demo {
    public static void main(String[] args) {
        Curriculum curriculum = new ConcreteCurriculum();
        CurriculumDecorator scoreDecorator = new ScoreDecorator(new ConcreteCurriculum());
        CurriculumDecorator labelDecorator = new LabelDecorator(new ConcreteCurriculum());
        CurriculumDecorator giftLabelDecorator = new GiftDecorator(labelDecorator);




        System.out.println("英语PC端课表展示");
        curriculum.query(123);
        System.out.println();


        System.out.println("英语iPad端课表展示");
        scoreDecorator.query(123);
        System.out.println();


        System.out.println("数学课表展示");
        labelDecorator.query(123);
        System.out.println();


        System.out.println("绘画课表展示");
        giftLabelDecorator.query(123);


    }
}

执行结果:

英语PC端课表展示
展示课表
展示对应的课前预习
展示对应的课后作业
展示对应的课件


英语iPad端课表展示
展示课表
展示对应的课前预习
展示对应的课后作业
展示对应的课件
展示对应的学生评分


数学课表展示
展示课表
展示对应的课前预习
展示对应的课后作业
展示对应的课件
展示对应的老师标签


绘画课表展示
展示课表
展示对应的课前预习
展示对应的课后作业
展示对应的课件
展示对应的老师标签
展示对应的完课奖品

至此,展示课表业务场景的代码改造完毕。

有的同学可能会问,为什么不通过继承的方式进行实现呢?

其原因在于,继承的方式不如这种动态组合的方式灵活,也很难实现这种细粒度的代码复用。

举个例子:如果数学和绘画课程又新增了需求,需要额外展示对应的辅修资料,但英语课程则不需要展示这类信息,那按照继承的方式应该如何实现呢?


责任编辑:武晓燕 来源: 托尼学长
相关推荐

2021-11-02 22:04:58

模式

2020-08-03 07:38:12

单例模式

2021-11-03 14:10:28

工厂模式场景

2020-07-20 07:48:53

单例模式

2020-07-02 07:52:11

RedisHash映射

2021-02-16 10:53:19

单例模式面试

2021-09-10 06:50:03

TypeScript装饰器应用

2021-05-28 11:18:50

MySQLbin logredo log

2024-03-06 13:19:19

工厂模式Python函数

2024-08-22 10:39:50

@Async注解代理

2024-03-05 10:33:39

AOPSpring编程

2024-05-30 08:04:20

Netty核心组件架构

2024-02-29 16:49:20

volatileJava并发编程

2024-08-29 16:30:27

2024-08-12 17:36:54

2023-12-27 18:16:39

MVCC隔离级别幻读

2024-11-19 15:13:02

2019-12-20 08:52:01

算法单链表存储

2021-11-09 14:08:45

DockerDockerfileJava

2021-11-05 07:47:56

代理模式对象
点赞
收藏

51CTO技术栈公众号