我草草地估算了一下,基本上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),在不改变一个现有对象结构的情况下,为其动态地增加一些额外的职责。
装饰器模式的优点在于:
- 可动态地为现有对象增加额外的职责,无需改动原来的代码,具备更好的灵活性和可扩展性,且符合开闭原则。
- 每种额外的职责都被实现为一个单独且通用的装饰器,符合单一职责,解决了“过长的方法”、“重复的代码”和“发散式变化”等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端课表展示
展示课表
展示对应的课前预习
展示对应的课后作业
展示对应的课件
展示对应的学生评分
数学课表展示
展示课表
展示对应的课前预习
展示对应的课后作业
展示对应的课件
展示对应的老师标签
绘画课表展示
展示课表
展示对应的课前预习
展示对应的课后作业
展示对应的课件
展示对应的老师标签
展示对应的完课奖品
至此,展示课表业务场景的代码改造完毕。
有的同学可能会问,为什么不通过继承的方式进行实现呢?
其原因在于,继承的方式不如这种动态组合的方式灵活,也很难实现这种细粒度的代码复用。
举个例子:如果数学和绘画课程又新增了需求,需要额外展示对应的辅修资料,但英语课程则不需要展示这类信息,那按照继承的方式应该如何实现呢?