[软构/SC]Decorator和Lab3中踩的坑

        这篇博客记录一下自己对于Decorator的认识和在写软构Lab3时踩的坑。省流就一句话,装饰器模式只能在原有基础上进行功能拓展,不能修改已有逻辑(开闭原则),也不能为类添加新方法。

问题背景

        在lab3里,提出了6种解决功能拓展的方案,前4种通过继承、组合的方式实现,仅有一两个应用或许可行,但想要实现最大的可复用性,还是方案5和方案6最合适。两个方案都是基于委派(delegation)实现的,但是使用委派的地方略有不同。

        方案5使用了CRP(Composite Reuse Principle),即组合-复用原则,这个方案下,个性化的类将需要拓展的功能委派给一个外部类,然后调用外部类的功能以实现自己的方法。关于委派的思考在另一篇文章中给出总结,一言以蔽之,委派是把功能外包给别人完成。
        方案6的使用了装饰器(decorator),属于结构型模式(Structural Pattern).下面重点说明我对装饰器的理解和该模式的使用方法。

关于装饰器的理解


我们先看看装饰器的继承树,课程ppt中的图如下。

软构PPT内关于装饰器的继承树

        为了能够组合多个装饰器的功能,需要创建一个decorator作为所有装饰器的父类。这个父类需要实现基本对象的所有功能——这样才能作为具体类被引用体现了继承树上的继承,但这些功能并不由自己实现,而是交给一个基本功能的实现类去实现——体现了继承树上的委派。
        而具体的功能是这个父类装饰器的子类,需要继承这个父类,同样需要实现基本对象的接口,然后在这个具体的装饰器中增加特性。
        但是,所有特性都可以使用装饰器实现吗?
        首先,必须理解装饰器的含义和可以做到的事。所谓“装饰”,一定是在已有功能的基础上增加“特性”,也就是说,这个特性一定是针对某个方法进行改进,而不是创造一个新的方法。
        比如对一个学生Student,有一个写报告write的方法,可以写一篇随意的报告。

interface Student{
    void write();
}

public CommonStudent implements Student{
    @Override
    public void write(){
        ..//一个普通的写报告
    }
}

        这时你想给学生增加一个特性,希望学生写报告的格式都是毕业设计的格式,这时候你就能使用“装饰器”,比如创建一个毕业设计的word模板给write方法调用,或者直接重写write方法,不再用word而是用latex,这些都是“装饰器”模式可以做到的。

public abstract StudentDecorator implements Student{
    Student student;
    @Override
    void write(){
        student.write();//将原本的功能交给“基本对象实现”
    }
}
public abstract ThesisDecorator
    extends StudentDecorator
    implements Student {

    public ThesisDecorator(Student common){
        super(common);
    }
    public 模板类型 template(){
        ..//论文格式的模板
    }
    @Override
    void write(){
        template();//使用一个模板
        ..//其他修改,比如使用latex写什么
        super.write();//完成原有的写报告的内容
    }
}

        但是如果你想让学生能检查以前所有的报告,把他们都改成毕业设计的格式,那显然需要设计一个“检查”的功能,这个功能能放在write里吗?显然是不能的,那这时候就不能使用装饰器模式,因为不是在已有功能的基础上进行特性的增加。
        根据装饰器的继承树,很容易想明白这一点。调用装饰器时,需要使用implements来实现主体的功能,同样,不论怎么包装,外部使用时都是使用接口作为引用,接口没有提供的方法,自然是不能使用的。正如上面的template方法,只能在装饰器内部调用以修改write方法,而不能在外部进行student.template——Student根本没有提供这个接口!

        下面展示了如何通过组合装饰器来组合特性,在实际使用中,可以使用多个装饰器嵌套实例化的方式,像穿衣服一样,一层一层地给原本的功能增加特性。但是不管怎么穿,都只是在原本的功能上“锦上添花”,所以装饰器内的新方法设置为“private”或许也是一样的效果。

public EnglishDecorator
    extends StudentDecorator
    implements Student{
    //把论文写成英文的
    public EnglishDecorator(Student common){
        super(common);
    }
    public void translate(){
        ..//translate sth
    }
    @Override
    public void write(){
        super.write();
        translate();// to English
        ..//做其他修改
    }
}

public void main(){
    Student student=new CommonStudent();
    Student studentWriteThesis=new ThesisDecorator(student);//ThesisDecorator是Student的子类型,所以可以实例化
    Student studen2=new ThesisDecorator(new CommonStudent());//也可以这样直接实例化
    student2.write();//写一篇论文格式的报告
    Student student3=new EnglishDecorator(new ThesisDecorator(new CommonStudent()));
    student3.write();//写一篇论文的格式的、英文的报告
}


        所以在我理解中,如果想要给类添加一个方法,就不能使用装饰器方法。装饰器对原有对象的拓展只能体现在对“原有方法”的改变,如在上一个例子中,通过重写写论文的手段(word→latex)来扩展学生的功能;装饰器内也可以增加新的方法,但是这个方法是无法被外界调用的,因为它实现的接口没有提供这个方法的接口,所以只能在“想要修改的方法”内调用这个方法,如撰写一个“毕业设计模板”方法,在write方法内调用。

lab3踩坑记录

        在lab3中,要求对是否空白、是否重叠和是否有周期进行拓展组合。实验手册中的写法让人感觉似乎每个方案都是可用的,这个地方真是一个大坑。编写这部分的时候,我并没有真正理解装饰器的“装饰”性,更可怕的是我忘了写测试用例,所以并没有发现装饰器模式无法调用检查是否空白的装饰器内部的新方法“checkBlank”,直到意识到这部分的缺失,去补充测试的时候才发现使用装饰器模式的缺陷。
        下面针对这三个功能进行分析。
        对检查空白的功能,显然检查时间轴上是否存在空白不能在插入时间段的过程中进行,所以只能作为一个新的方法,在用户输入结束时调用方法进行检查。如果要加入新的方法,就不能使用装饰器模式进行设计。由于没有认识到这一点,一开始我使用装饰器模式设计这个功能,发现无法调用,最终改成了使用委派的方式,设计为一个外部的功能。所幸在设计“装饰器”的过程中我没有轻易改变rep的暴露范围,只是调用已有方法来实现,所以更改设计模式比较容易。
        对不允许重叠的功能,如果不允许重叠,则可以在每次插入时间后进行检查,省时省力且容易实现——只要比较待插入位置前后是否有重叠即可。可见,想要拓展出这个功能,只需要修改insert方法即可,无论是通过增加一个判断是否重叠的方法在insert内调用,还是直接在insert内判断。所以这个功能拓展可以使用装饰器模式。(对MultiIntervalSet,可能还要修改remove,因为我把所有时间都统一到了一个时间轴上)对周期的分析类似,也是可以修改方法来实现,可以使用装饰器模式。

总结和教训

        踩这个坑消耗了我很长的时间,除了在思考上的纠结,还有反复修改代码结构和报告,但是也不是完全没有收获。如果没有踩这个坑,而是直接使用CRP实现三个功能,我或许不会真正认识到装饰器模式的含义。

        此外,最大的教训大概是,不要盲目听信他人的解决方案,即使是老师给出的实验手册。

        感谢实验课一起讨论这个问题的其他几位同学。撰写仓促,还有很多其他想写的点子,如有纰漏,敬请指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值