不同类设计模式对比

Published on 2016 - 09 - 07

创建类模式描述如何创建对象,行为类模式关注如何管理对象的行为,结构类模式则着重于如何建立一个软件结构,虽然三种模式的着重点不同,但是在实际应用中还是有重叠的,会出现一种模式适用、另外一种模式也适用的情况,我们到底该选用哪一个设计模式呢?本文就带领读者进入不同类设计模式PK的世界中,让你清晰地认识到各个模式的不同点以及它们的特长。

策略模式VS桥梁模式

这对冤家终于碰头了,策略模式与桥梁模式是如此相似,简直就是孪生兄弟,要把它们两个分开可不太容易。我们来看看两者的通用类图,如图1所示。

两者之间确实很相似。如果把策略模式的环境角色变更为一个抽象类加一个实现类,或者桥梁模式的抽象角色未实现,只有修正抽象化角色,想想看,这两个类图有什么地方不一样?完全一样!正是由于类似场景的存在才导致了两者在实际应用中经常混淆的情况发生,我们来举例说明两者有何差别。

大家都知道邮件有两种格式:文本邮件(Text Mail)和超文本邮件(HTML MaiL),在文本邮件中只能有简单的文字信息,而在超文本邮件中可以有复杂文字(带有颜色、字体等属性)、图片、视频等,如果你使用Foxmail邮件客户端的话就应该有深刻体验,看到一份邮件,怎么没内容?原来是你忘记点击那个“HTML邮件”标签了。下面我们就来讲解如何发送这两种不同格式的邮件,研究一下这两种模式如何处理这样的场景。

策略模式实现邮件发送

使用策略模式发送邮件,我们认为这两种邮件是两种不同的封装格式,给定了发件人、收件人、标题、内容的一封邮件,按照两种不同的格式分别进行封装,然后发送之。按照这样的分析,我们发现邮件的两种不同封装格式就是两种不同的算法,具体到策略模式就是两种不同策略,这样看已经很简单了,我们可以直接套用策略模式来实现。先看类图,如图2所示。

我们定义了一个邮件模板,它有两个实现类:TextMail(文本邮件)和HtmlMail(超文本邮件),分别实现两种不同格式的邮件封装。MailServer是一个环境角色,它接收一个MailTemplate对象,然后通过sendMail方法发送出去。我们来看具体的代码,先看抽象邮件,如代码清单1所示。

public abstract class MailTemplate {
     //邮件发件人
     private String from;
     //收件人
     private String to;
     //邮件标题
     private String subject;
     //邮件内容
     private String context;
     //通过构造函数传递邮件信息
     public MailTemplate(String _from,String _to,String _subject,String _context){
             this.from = _from;
             this.to = _to;
             this.subject = _subject;
             this.context = _context;
     }
     public String getFrom() {
             return from;
     }
     public void setFrom(String from) {
             this.from = from;
     }
     public String getTo() {
             return to;
     }
     public void setTo(String to) {
             this.to = to;
     }
     public String getSubject() {
             return subject;
     }
     public void setSubject(String subject) {
             this.subject = subject;
     }
     public void setContext(String context){
             this.context = context;
     }
    //邮件都有内容
     public String getContext(){
             return context;
     }
}

很奇怪,是吗?抽象类没有抽象的方法,设置为抽象类还有什么意义呢?有意义,在这里我们定义了一个这样的抽象类:它具有邮件的所有属性,但不是一个具体可以被实例化的对象。例如,你对邮件服务器说“给我制造一封邮件”,邮件服务器肯定拒绝,为什么?你要产生什么邮件?什么格式的?邮件对邮件服务器来说是一个抽象表示,是一个可描述但不可形象化的事物。你可以这样说:“我要一封标题为XX,发件人是XXX的文本格式的邮件”,这就是一个可实例化的对象,因此我们的设计就产生了两个子类以具体化邮件,而且每种邮件格式对邮件的内容都有不同的处理。我们首先看文本邮件,如代码清单2所示。

public class TextMail extends MailTemplate {
     public TextMail(String _from, String _to, String _subject, String _context) {
             super(_from, _to, _subject, _context);
     }
     public String getContext() {
             //文本类型设置邮件的格式为:text/plain
             String context = "\nContent-Type: text/plain;charset=GB2312\n" +super.getContext();
             //同时对邮件进行base64编码处理,这里用一句话代替
             context = context + "\n邮件格式为:文本格式";
             return context;
     }
}

我们覆写了getContext方法,因为要把一封邮件设置为文本邮件必须加上一个特殊的标志:text/plain,用于告诉解析这份邮件的客户端:“我是一封文本格式的邮件,别解析错了”。同样,超文本格式的邮件也有类似的设置,如代码清单3所示。

public class HtmlMail extends MailTemplate {
     public HtmlMail(String _from, String _to, String _subject, String _context) {
             super(_from, _to, _subject, _context);
     }
     public String getContext(){
             //超文本类型设置邮件的格式为:multipart/mixed
             String context = "\nContent-Type: multipart/mixed; charset= GB2312\n" +super.getContext();
             //同时对邮件进行HTML检查,是否有类似未关闭的标签
             context = context + "\n邮件格式为:超文本格式";
             return context;
     }
}

优秀一点的邮件客户端会对邮件的格式进行检查,比如编写一封超文本格式的邮件,在内容中加上了标签,但是遗忘了结尾标签,邮件的产生者(也就是邮件的客户端)会提示进行修正,我们这里用了“邮件格式为:超文本格式”来代表该逻辑。

两个实现类实现了不同的算法,给定相同的发件人、收件人、标题和内容可以产生不同的邮件信息。我们看看邮件是如何发送出去的,如代码清单4所示。

public class MailServer {
     //发送的是哪封邮件
     private MailTemplate m;
     public MailServer(MailTemplate _m){
             this.m  = _m;
     }
     //发送邮件
     public void sendMail(){
             System.out.println("====正在发送的邮件信息====");
             //发件人
             System.out.println("发件人:" + m.getFrom());
             //收件人
             System.out.println("收件人:" + m.getTo());
             //标题
             System.out.println("邮件标题:" + m.getSubject());
             //邮件内容
             System.out.println("邮件内容:" + m.getContext());
     }
}

很简单,邮件服务器接收了一封邮件,然后调用自己的发送程序进行发送。可能读者要问了,为什么不把sendMail方法移植到邮件模板类中呢?这也是邮件模板类的一个行为,邮件可以被发送。是的,这确实是邮件的一个行为,完全可以这样做,两者没有什么区别,只是从不同的角度看待该方法而已。我们继续看场景类,如代码清单5所示。

public class Client {
     public static void main(String[] args) {
             //创建一封TEXT格式的邮件
             MailTemplate m = new HtmlMail("a@a.com","b@b.com","外星人攻击地球了","结局是外星人被地球人打败了!");
             //创建一个Mail发送程序
             MailServer mail = new MailServer(m);
             //发送邮件
             mail.sendMail();
     }
}

运行结果如下所示:

====正在发送的邮件信息====
发件人:a@a.com
收件人:b@b.com
邮件标题:外星人攻击地球了
邮件内容:
Content-Type: multipart/mixed;charset=GB2312
结局是外星人被地球人打败了!
邮件格式为:超文本格式

当然,如果想产生一封文本格式的邮件,只要稍稍修改一下场景类就可以了:new HtmlMail修改为new TextMail,读者可以自行实现,非常简单。在该场景中,我们使用策略模式实现两种算法的自由切换,它提供了这样的保证:封装邮件的两种行为是可选择的,至于选择哪个算法是由上层模块决定的。策略模式要完成的任务就是提供两种可以替换的算法。

桥梁模式实现邮件发送

桥梁模式关注的是抽象和实现的分离,它是结构型模式,结构型模式研究的是如何建立一个软件架构,下面我们就来看看桥梁模式是如何构件一套发送邮件的架构的,如图3所示。

类图中我们增加了SendMail和Postfix两个邮件服务器来实现类,在邮件模板中允许增加发送者标记,其他与策略模式都相同。我们在这里已经完成了一个独立的架构,邮件有了,发送邮件的服务器也具备了,是一个完整的邮件发送程序。需要读者注意的是,SendMail类不是一个动词行为(发送邮件),它指的是一款开源邮件服务器产品,一般*nix系统的默认邮件服务器就是SendMail;Postfix也是一款开源的邮件服务器产品,其性能、稳定性都在逐步赶超SendMail。

我们来看代码实现,邮件模板仅仅增加了一个add方法,如代码清单6所示。

public abstract class MailTemplate {
     /*
     *该部分代码不变,请参考代码清单1
     */     
     //允许增加邮件发送标志
     public void add(String sendInfo){
             context = sendInfo + context;
     }
}

文本邮件、超文本邮件都没有任何改变,如代码清单2、3所示,这里不再赘述。

我们来看邮件服务器,也就是桥梁模式的抽象化角色,如代码清单7所示。

public abstract class MailServer {
     //发送的是哪封邮件
     protected final MailTemplate m;
     public MailServer(MailTemplate _m){
             this.m  = _m;
     }
     //发送邮件
     public void sendMail(){
             System.out.println("====正在发送的邮件信息====");
             //发件人
             System.out.println("发件人:" + m.getFrom());
             //收件人
             System.out.println("收件人:" + m.getTo());
             //标题
             System.out.println("邮件标题:" + m.getSubject());
             //邮件内容
             System.out.println("邮件内容:" + m.getContext());
     }
}

该类相对于策略模式的环境角色有两个改变:

  • 修改为抽象类。为什么要修改成抽象类?因为我们在设计一个架构,邮件服务器是一个具体的、可实例化的对象吗?“给我一台邮件服务器”能实现吗?不能,只能说“给我一台Postfix邮件服务器”,这才能实现,必须有一个明确的可指向对象。
  • 变量m修改为Protected访问权限,方便子类调用。

我们再来看看Postfix邮件服务器的实现,如代码清单8所示。

public class Postfix extends MailServer {
     public Postfix(MailTemplate _m) {
             super(_m);
     }
     //修正邮件发送程序
     public void sendMail(){
                     //增加邮件服务器信息
             String context ="Received: from XXXX (unknown [xxx.xxx.xxx.xxx]) by aaa.aaa.com (Postfix) with ESMTP id 8DBCD172B8\n" ;
             super.m.add(context);
             super.sendMail();
     }
}

为什么要覆写sendMail程序呢?这是因为每个邮件服务器在发送邮件时都会在邮件内容上留下自己的标志,一是广告作用,二是为了互联网上统计需要,三是方便同质软件的共振。我们再来看SendMail邮件服务器的实现,如代码清单9所示。

public class SendMail extends MailServer {
     //传递一封邮件
     public SendMail(MailTemplate _m) {
             super(_m);
     }
     //修正邮件发送程序
     @Override
     public void sendMail(){
             //增加邮件服务器信息
             super.m.add("Received: (sendmail); 7 Nov 2009 04:14:44 +0100");
             super.sendMail();
     }
}

邮件和邮件服务器都有了,我们来看怎么发送邮件,如代码清单10所示。

public class Client {
     public static void main(String[] args) {
             //创建一封TEXT格式的邮件
             MailTemplate m = new HtmlMail("a@a.com","b@b.com","外星人攻击地球了"," 结局是外星人被地球人打败了!");
             //使用Postfix发送邮件
             MailServer mail = new Postfix(m);
             //发送邮件
             mail.sendMail();
     }
}

运行结果如下所示:

====正在发送的邮件信息====
发件人:a@a.com
收件人:b@b.com
邮件标题:外星人攻击地球了
邮件内容:
Content-Type: multipart/mixed;charset=GB2312
Received: from XXXX (unknown [xxx.xxx.xxx.xxx]) by aaa.aaa.com (Postfix) with ESMTP id 8DBCD172B8
结局是外星人被地球人打败了!
邮件格式为:超文本格式

当然了,还有其他三种发送邮件的方式:Postfix发送文本邮件以及SendMail发送文本邮件和超文本邮件。修改量很小,读者可以自行修改实现,体会一下桥梁模式的特点。

最佳实践

策略模式和桥梁模式是如此相似,我们只能从它们的意图上来分析。策略模式是一个行为模式,旨在封装一系列的行为,在例子中我们认为把邮件的必要信息(发件人、收件人、标题、内容)封装成一个对象就是一个行为,封装的格式(算法)不同,行为也就不同。而桥梁模式则是解决在不破坏封装的情况下如何抽取出它的抽象部分和实现部分,它的前提是不破坏封装,让抽象部分和实现部分都可以独立地变化,在例子中,我们的邮件服务器和邮件模板是不是都可以独立地变化?不管是邮件服务器还是邮件模板,只要继承了抽象类就可以继续扩展,它的主旨是建立一个不破坏封装性的可扩展架构。

简单来说,策略模式是使用继承和多态建立一套可以自由切换算法的模式,桥梁模式是在不破坏封装的前提下解决抽象和实现都可以独立扩展的模式。桥梁模式必然有两个“桥墩”——抽象化角色和实现化角色,只要桥墩搭建好,桥就有了,而策略模式只有一个抽象角色,可以没有实现,也可以有很多实现。

还是很难区分,是吧?多想想两者的意图,就可以理解为什么要建立两个相似的模式了。我们在做系统设计时,可以不考虑到底使用的是策略模式还是桥梁模式,只要好用,能够解决问题就成,“不管黑猫白猫,抓住老鼠的就是好猫”。

门面模式VS中介者模式

门面模式为复杂的子系统提供一个统一的访问界面,它定义的是一个高层接口,该接口使得子系统更加容易使用,避免外部模块深入到子系统内部而产生与子系统内部细节耦合的问题。中介者模式使用一个中介对象来封装一系列同事对象的交互行为,它使各对象之间不再显式地引用,从而使其耦合松散,建立一个可扩展的应用架构。

中介者模式实现工资计算

大家工作会得到工资,那么工资与哪些因素有关呢?这里假设工资与职位、税收有关,职位提升工资就会增加,同时税收也增加,职位下降了工资也同步降低,当然税收也降低。而如果税收比率增加了呢?工资自然就减少了!这三者之间两两都有关系,很适合中介者模式的场景,类图如图4所示。

类图中的方法比较简单,我们主要分析的是三者之间的关系,通过类图可以发现三者之间已经没有耦合,原本在需求分析时我们发现三者有直接的交互,采用中介者模式后,三个对象之间已经相互独立了,全部委托中介者完成。我们在类图中还定义了一个抽象同事类,它是一个标志性接口,其子类都是同事类,都可以被中介者接收,如代码清单11所示。

public abstract class AbsColleague {
     //每个同事类都对中介者非常了解
     protected AbsMediator mediator;
     public AbsColleague(AbsMediator _mediator){
             this.mediator = _mediator;
     }
}

在抽象同事类中定义了每个同事类对中介者都非常了解,如此才能把请求委托给中介者完成。三个同事类都具有相同的设计,即定义一个业务接口以及每个对象必须实现的职责,同时既然是同事类就都继承AbsColleague。抽象同事类只是一个标志性父类,并没有限制子类的业务逻辑,因此每一个同事类并没有违背单一职责原则。首先来看职位接口,如代码清单12所示。

public interface IPosition {
     //升职
     public void promote();
     //降职
     public void demote();
}

职位会有升有降,职位变化如代码清单13所示。

public class Position extends AbsColleague implements IPosition {
     public Position(AbsMediator _mediator){
             super(_mediator);
     }
     public void demote() {
             super.mediator.down(this);
     }
     public void promote() {          
             super.mediator.up(this);
     }
}

每一个职位的升降动作都委托给中介者执行,具体一个职位升降影响到谁这里没有定义,完全由中介者完成,简单而且扩展性非常好。下面我们来看工资接口,如代码清单14所示。

public interface ISalary {
     //加薪
     public void increaseSalary();
     //降薪
     public void decreaseSalary();
}

工资也会有升有降,如代码清单15所示。

public class Salary extends AbsColleague implements ISalary {
     public Salary(AbsMediator _mediator){
             super(_mediator);
     }
     public void decreaseSalary() {
             super.mediator.down(this);
     }
     public void increaseSalary() {
             super.mediator.up(this);
     }
}

交税是公民的义务,税收接口如代码清单16所示。

public interface ITax {
     //税收上升
     public void raise();
     //税收下降
     public void drop();
} 

税收的变化对我们的工资当然有影响,如代码清单17所示。

public class Tax extends AbsColleague implements ITax {
     //注入中介者
     public Tax(AbsMediator _mediator){
             super(_mediator);
     }
     public void drop() {
             super.mediator.down(this);
     }
     public void raise() {
             super.mediator.up(this);
     }
}

以上同事类的业务都委托给了中介者,其本类已经没有任何的逻辑了,非常简单,现在的问题是中介者类非常复杂,因为它要处理三者之间的关系。我们首先来看抽象中介者,如代码清单18所示。

public abstract class AbsMediator {
     //工资
     protected final ISalary salary;
     //职位
     protected final IPosition position;
     //税收
     protected final ITax tax;
     public AbsMediator(){
             salary = new Salary(this);
             position = new Position(this);
             tax = new Tax(this);
     }
     //工资增加了
     public abstract void up(ISalary _salary);
     //职位提升了
     public abstract void up(IPosition _position);
     //税收增加了
     public abstract void up(ITax _tax);
     //工资降低了
     public abstract void down(ISalary _salary);
     //职位降低了
     public abstract void down(IPosition _position);
     //税收降低了
     public abstract void down(ITax _tax);     
}

在抽象中介者中我们定义了6个方法,分别处理职位升降、工资升降以及税收升降的业务逻辑,采用Java多态机制来实现,我们来看实现类,如代码清单19所示。

public class Mediator extends AbsMediator{
     //工资增加了
     public void up(ISalary _salary) {
             upSalary();
             upTax();
     }
     //职位提升了
     public void up(IPosition position) {
             upPosition();
             upSalary();
             upTax();
     }
    //税收增加了
     public void up(ITax tax) {
             upTax();
             downSalary();
     }
     /*
     *工资、职位、税收降低的处理方法相同,不再赘述
     */
     //工资增加
     private void upSalary(){
             System.out.println("工资翻倍,乐翻天");
     }
     private void upTax(){
             System.out.println("税收上升,为国家做贡献");
     }
     private void upPosition(){
             System.out.println("职位上升一级,狂喜");
     }
     private void downSalary(){
             System.out.println("经济不景气,降低工资");
     }
     private void downTax(){
             System.out.println("税收减低,国家收入减少");          
     }
     private void downPostion(){
             System.out.println("官降三级,比自杀还痛苦");
     }
}

该类的方法较多,但是还是非常简单的,它的12个方法分为两大类型:一类是每个业务的独立流程,比如增加工资,仅仅实现单独增加工资的职能,而不关心职位、税收是如何变化的,该类型的方法是private私有类型,只能提供本类内访问;另一类是实现抽象中介者定义的方法,完成具体的每一个逻辑,比如职位上升,同时也引起了工资增加、税收增加。我们编写一个场景类,看看运行结果,如代码清单20所示。

public class Client {
     public static void main(String[] args) {
             //定义中介者
             Mediator mediator = new Mediator();
             //定义各个同事类
             IPosition position = new Position(mediator);
             ISalary salary = new Salary(mediator);
             ITax tax = new Tax(mediator);
             //职位提升了
             System.out.println("===职位提升===");
             position.promote();
     }
}

运行结果如下所示:

===职位提升===
职位上升一级,狂喜
工资翻倍,乐翻天
税收上升,为国家做贡献

我们回过头来分析一下设计,在接收到需求后我们发现职位、工资、税收之间有着紧密的耦合关系,如果不采用中介者模式,则每个对象都要与其他两个对象进行通信,这势必会增加系统的复杂性,同时也使系统处于僵化状态,很难实现拥抱变化的理想。通过增加一个中介者,每个同事类的职位、工资、税收都只与中介者通信,中介者封装了各个同事类之间的逻辑关系,方便系统的扩展和维护。

门面模式实现工资计算

工资计算是一件非常复杂的事情,简单来说,它是对基本工资、月奖金、岗位津贴、绩效、考勤、税收、福利等因素综合运算后的一个数字。即使设计一个HR(人力资源)系统,员工工资计算也是非常复杂的模块,但是对于外界,比如高管层,最希望看到的结果是张三拿了多少钱,李四拿了多少钱,而不是看中间的计算过程,怎么计算那是人事部门的事情。换句话说,对外界的访问者来说,它只要传递进去一个人员名称和月份即可获得工资数,而不用关心其中的计算有多么复杂,这就用得上门面模式了。

门面模式对子系统起封装作用,它可以提供一个统一的对外服务接口,如图5所示。

该类图主要实现了工资计算,通过HRFacade门面可以查询用户的工资以及出勤天数等,而不用关心这个工资或者出勤天数是怎么计算出来的,从而屏蔽了外系统对工资计算模块的内部细节依赖。我们先看子系统内部的各个实现,考勤情况如代码清单21所示。

public class Attendance {
     //得到出勤天数
     public int getWorkDays(){
             return (new Random()).nextInt(30);
     }
}

非常简单,只用一个方法获得一个员工的出勤天数。我们再来看奖金计算,如代码清单22所示。

public class Bonus {
     //考勤情况
     private Attendance atte = new Attendance();
     //奖金
     public int getBonus(){
             //获得出勤情况
             int workDays = atte.getWorkDays();
             //奖金计算模型
             int bonus = workDays * 1800 / 30;
             return bonus;
     }
}

我们在这里实现了一个示意方法,实际的奖金计算是非常复杂的,与考勤、绩效、基本工资、岗位都有关系,单单一个奖金计算就可以设计出一个门面。我们再来看基本工资,这个基本上是按照职位而定的,比较固定,如代码清单23所示。

public class BasicSalary {
     //获得一个人的基本工资
     public int getBasicSalary(){
             return 2000;
     }
}

我们定义了员工的基本工资都为2000元,没有任何浮动的余地。再来看绩效,如代码清单24所示。

public class Performance {
     //基本工资
     private BasicSalary salary = new BasicSalary();
     //绩效奖励
     public int getPerformanceValue(){
             //随机绩效
             int perf = (new Random()).nextInt(100);
             return salary.getBasicSalary() * perf /100;
     }
}

绩效按照一个非常简单的算法,即基本工资乘以一个随机的百分比。我们再来看税收,如代码清单25所示。

public class Tax {
     //收取多少税金
     public int getTax(){
             //交纳一个随机数量的税金
             return (new Random()).nextInt(300);
     }
}

一个计算员工薪酬的所有子元素都已经具备了,剩下的就是编写组合逻辑类,总工资的计算如代码清单26所示。

public class SalaryProvider {
     //基本工资
     private BasicSalary basicSalary = new BasicSalary();
     //奖金
     private  Bonus bonus = new Bonus();
     //绩效
     private  Performance perf = new Performance();
     //税收
     private  Tax tax = new Tax();
     //获得用户的总收入
     public int totalSalary(){
             return basicSalary.getBasicSalary() + bonus.getBonus() + perf.getPerformanceValue() - tax.getTax();
     }
}

这里只是对前面的元素值做了一个加减法计算,这是对实际HR系统的简化处理,如果把这个类暴露给外系统,那么被修改的风险是非常大的,因为它的方法totalSalary是一个具体的业务逻辑。我们采用门面模式的目的是要求门面是无逻辑的,与业务无关,只是一个子系统的访问入口。门面模式只是一个技术层次上的实现,全部业务还是在子系统内实现。我们来看HR门面,如代码清单27所示。

public class HRFacade {
     //总工资情况
     private  SalaryProvider salaryProvider = new SalaryProvider();
     //考勤情况
     private  Attendance attendance = new Attendance();
     //查询一个人的总收入
     public int querySalary(String name,Date date){
             return salaryProvider.totalSalary();          
     }
     //查询一个员工一个月工作了多少天
     public int queryWorkDays(String name){
             return attendance.getWorkDays();
     }
}

所有的行为都是委托行为,由具体的子系统实现,门面只是提供了一个统一访问的基础而已,不做任何的校验、判断、异常等处理。我们编写一个场景类查看运行结果,如代码清单28所示。

public class Client {
     public static void main(String[] args) {
             //定义门面
             HRFacade facade = new HRFacade();
             System.out.println("===外系统查询总收入===");
             int salary = facade.querySalary("张三",new Date(System.
                 currentTimeMillis()));
             System.out.println( "张三 11月 总收入为:" +salary);
             //再查询出勤天数
             System.out.println("\n===外系统查询出勤天数===");
             int workDays = facade.queryWorkDays("李四");
             System.out.println("李四 本月出勤:" +workDays);
     }
}

运行结果如下所示:

===外系统查询总收入===
张三 11月 总收入为:4133
===外系统查询出勤天数===
李四 本月出勤:22

在该例中,我们使用了门面模式对薪水计算子系统进行封装,避免子系统内部复杂逻辑外泄,确保子系统的业务逻辑的单纯性,即使业务流程需要变更,影响的也是子系统内部功能,比如奖金需要与基本工资挂钩,这样的修改对外系统来说是透明的,只需要子系统内部变更即可。

最佳实践

门面模式和中介者模式之间的区别还是比较明显的,门面模式是以封装和隔离为主要任务,而中介者模式则是以调和同事类之间的关系为主,因为要调和,所以具有了部分的业务逻辑控制。两者的主要区别如下:

  • 功能区别

门面模式只是增加了一个门面,它对子系统来说没有增加任何的功能,子系统若脱离门面模式是完全可以独立运行的。而中介者模式则增加了业务功能,它把各个同事类中的原有耦合关系移植到了中介者,同事类不可能脱离中介者而独立存在,除非是想增加系统的复杂性和降低扩展性。

  • 知晓状态不同

对门面模式来说,子系统不知道有门面存在,而对中介者来说,每个同事类都知道中介者存在,因为要依靠中介者调和同事之间的关系,它们对中介者非常了解。

  • 封装程度不同

门面模式是一种简单的封装,所有的请求处理都委托给子系统完成,而中介者模式则需要有一个中心,由中心协调同事类完成,并且中心本身也完成部分业务,它属于更进一步的业务功能封装。

包装模式群PK

我们讲了这么多的设计模式,大家有没有发觉在很多的模式中有些角色是不干活的?它们只是充当黔首作用,你有问题,找我,但我不处理,我让其他人处理。最典型的就是代理模式了,代理角色接收请求然后传递到被代理角色处理。门面模式也是一样,门面角色的任务就是把请求转发到子系统。类似这种结构的模式还有很多,我们先给这种类型的模式定义一个名字,叫做包装模式(wrapping pattern)。注意,包装模式是一组模式而不是一个。包装模式包括哪些设计模式呢?包装模式包括:装饰模式、适配器模式、门面模式、代理模式、桥梁模式。下面我们通过一组例子来说明这五个包装模式的区别。

代理模式

现在很多明星都有经纪人,一般有什么事他们都会说:“你找我的经纪人谈好了”,下面我们就看看这一过程怎么模拟。假设有一个追星族想找明星签字,我们看看采用代理模式怎么实现。代理模式是包装模式中的最一般的实现,类图如图6所示。

类图很简单,就是一个简单的代理模式,我们来看明星的定义,明星接口如代码清单29所示。

public interface IStar {
     //明星都会签名
     public void sign();
}

明星只有一个行为:签字。我们来看明星的实现,如代码清单30所示。

public class Singer implements IStar {
     public void sign() {
             System.out.println("明星签字:我是XXX大明星");
     }
}

经纪人与明星应该有相同的行为,比如说签名,虽然经纪人不签名,但是他把你要签名的笔记本、衣服、CD等传递过去让真正的明星签字,经纪人如代码清单31所示。

public class Agent implements IStar {
     //定义是谁的经纪人
     private IStar star;
     //构造函数传递明星
     public Agent(IStar _star){
             this.star = _star;
     }
     //经纪人是不会签字的,签字了歌迷也不认
     public void sign() {
             star.sign();
     }
}

应该非常明确地指出一个经纪人是谁的代理,因此要在构造函数中接收一个明星对象,确定是要做这个明星的代理。我们再来看看追星族是怎么找明星签字的,如代码清单32所示。

public class Idolater {
     public static void main(String[] args) {
             //崇拜的明星是谁
             IStar star = new Singer();
             //找到明星的经纪人
             IStar agent = new Agent(star);
             System.out.println("追星族:我是你的崇拜者,请签名!");
             //签字
             agent.sign();
     }
}

很简单,找到明星的代理,然后明星就签字了。运行结果如下所示:

追星族:我是你的崇拜者,请签名!
明星签字:我是XXX大明星

看看我们的程序逻辑,我们是找明星的经纪人签字,真实签字的是明星,经纪人只是把这个请求传递给明星处理而已,这是普通的代理模式的典型应用。

装饰模式

明星也都是一步一步地奋斗出来的,谁都不是一步就成为大明星的。甚至一些演员通过粉饰自己给观众一个好的印象,现在我们就来看怎么粉饰一个演员,如图7所示。

下面我们就来看看这些过程如何实现,先看明星接口,如代码清单33所示。

public interface IStar {
     //演戏
     public void act();
}

我们来看看我们的主角,如代码清单34所示。

public class FreakStar implements IStar {
     public void act() {
             System.out.println("演中:演技很拙劣");
     }
}

我们看看这个明星是怎么粉饰的,先定义一个抽象装饰类,如代码清单35所示。

public abstract class Decorator implements IStar {
     //粉饰的是谁
     private IStar star;
     public Decorator(IStar _star){
             this.star = _star;
     }
     public void act() {
             this.star.act();
     }
}

前后两次修饰,开演前毫无忌惮地吹嘘,如代码清单36所示。

public class HotAir extends Decorator {
     public HotAir(IStar _star){
             super(_star);
     }
     public void act(){
             System.out.println("演前:夸夸其谈,没有自己不能演的角色");
             super.act();
     }
}

大家发现这个明星演技不好的时候,他拼命找借口,说是那天天气不好、心情不好等,如代码清单37所示。

public class Deny extends Decorator {
     public Deny(IStar _star){
             super(_star);
     }
     public void act(){
             super.act();
             System.out.println("演后:百般抵赖,死不承认");
     }
}

我们建立一个场景把这种情况展示一下,如代码清单38所示。

public class Client {
     public static void main(String[] args) {
             //定义出所谓的明星
             IStar freakStar = new FreakStar();
             //看看他是怎么粉饰自己的
             //演前吹嘘自己无所不能
             freakStar = new HotAir(freakStar);
             //演完后,死不承认自己演的不好
             freakStar = new Deny(freakStar);
             System.out.println("====看看一些虚假明星的形象====");
             freakStar.act();
     }
}

运行结果如下所示:

====看看一些虚假明星的形象====
演前:夸夸其谈,没有自己不能演的角色
演中:演技很拙劣
演后:百般抵赖,死不承认

适配器模式

我们知道在演艺圈中还存在一种情况:替身,替身也是演员,只是普通的演员而已,在一段戏中,前十五分钟是明星本人,后十五分钟也是明星本人,就中间的五分钟是替身,那这个场景该怎么描述呢?注意中间那五分钟,这个时候一个普通演员被导演认为是明星演员,我们来看类图,如图8所示。

导演找了一个普通演员作为明星的替身,不过观众看到的还是明星的身份。我们来看代码,首先看明星接口,如代码清单39所示。

public interface IStar {
     //明星都要演戏
     public void act(String context);
}

再来看一个具体的电影明星,他的主要职责就是演戏,如代码清单40所示。

public class FilmStar implements IStar {
     public void act(String context) {
             System.out.println("明星演戏:" + context);
     }
}

我们再来看普通演员,明星就那么多,但是普通演员非常多,我们看其接口,如代码清单41所示。

public interface IActor {
     //普通演员演戏
     public void playact(String contet);
}

普通演员也是演员,是要演戏的,我们来看一个普通演员的实现,如代码清单42所示。

public class UnknownActor implements IActor {
     //普通演员演戏
     public void playact(String context) {
             System.out.println("普通演员:"+context);
     }
}

我们来看替身该怎么编写,如代码清单43所示。

public class Standin implements IStar {
     private IActor actor;
     //替身是谁
     public Standin(IActor _actor){
             this.actor = _actor;
     }
     public void act(String context) {
             actor.playact(context);
     }
}

这是一个通用的替身,哪个普通演员能担任哪个明星的替身是由导演决定的,导演想让谁当就让谁当,我们来看导演,如代码清单44所示。

public class direcotr {
     public static void main(String[] args) {
             System.out.println("=======演戏过程模拟==========");
            //定义一个大明星
             IStar star = new FilmStar();
             star.act("前十五分钟,明星本人演戏");     
             //导演把一个普通演员当做明星演员来用
             IActor actor = new UnknownActor();
             IStar standin= new Standin(actor);
             standin.act("中间五分钟,替身在演戏");
             star.act("后十五分钟,明星本人演戏");     
     }
}

运行结果如下所示:

=======演戏过程模拟==========
明星演戏:前十五分钟,明星本人演戏
普通演员:中间五分钟,替身在演戏
明星演戏:后十五分钟,明星本人演戏

这里使用了适配器模式,把一个普通的演员转换为一个明星演员。

桥梁模式

我们继续说明星圈的事情,现在明星类型太多了,比如电影明星、电视明星、歌星、体育明星、网络明星等,每个类型的明星都有明确的职责,电影明星的主要工作就是演电影,电视明星的主要工作就是演电视剧或者主持电视节目。再看看现在的明星,单一发展的基本没有,主持人出专辑、体育明星演电影、歌星拍戏等太平常了,我们就用程序来表现一下多元化情形,如图9所示。

图9中定义了一个抽象明星AbsStar,然后产生出各个具体类型的明星,比如电影明星FilmStar、歌星Singer,当然还可以继续扩展下去。这里还定义了一个抽象的行为AbsAction,描述明星所具有的活动,比如演电影、唱歌等,在这种设计下,明星可以扩展,明星的活动也可以扩展,非常灵活。我们先来看明星的活动,抽象活动如代码清单45所示。

public abstract class AbsAction {
     //每个活动都有描述
     public abstract void desc();
}

很简单,只有一个活动的描述,由子类来实现。我们来看演电影和唱歌两个活动,分别如代码清单46、47所示。

public class ActFilm extends AbsAction {
     public void desc() {
             System.out.println("演出精彩绝伦的电影");
     }
}
public class Sing extends AbsAction {
     public void desc() {
             System.out.println("唱出优美的歌曲");
     }
}

各种精彩的活动都有了,我们再来看抽象明星,它是所有明星的代表,如代码清单48所示。

public abstract class AbsStar {
     //一个明星参加哪些活动
     protected final AbsAction action;
     //通过构造函数传递具体活动
     public AbsStar(AbstAction _action){
             this.action = _action;
     }
     //每个明星都有自己的主要工作
     public void doJob(){
             action.desc();
     }
}

明星都有自己的主要活动(或者是主要工作),我们在抽象明星中只是定义明星有活动,具体有什么活动由各个子类实现。我们再来看电影明星,如代码清单49所示。

public class FilmStar extends AbsStar {
     //默认的电影明星的主要工作是拍电影
     public FilmStar(){
             super(new ActFilm());
     }
     //也可以重新设置一个新职业
     public FilmStar(AbsAction _action){
             super(_action);
     }
     //细化电影明星的职责
     public void doJob(){
             System.out.println("\n======影星的工作=====");
             super.doJob();
     }
}

电影明星的本职工作就应该是演电影,因此就有了一个无参构造函数来定义电影明星的默认工作,如果明星要客串一下去唱歌也可以,有参构造解决了该问题。歌星的实现与此相同,如代码清单50所示。

public class Singer extends AbsStar {
     //歌星的默认活动是唱歌
     public Singer(){
             super(new Sing());
     }
     //也可以重新设置一个新职业
     public Singer(AbsAction _action){
             super(_action);
     }
     //细化歌星的职责
     public void doJob(){
             System.out.println("\n======歌星的工作=====");
             super.doJob();
     }
}

我们使用电影明星和歌星来作为代表,这两类明星也是我们经常听到或看到的,下面建立一个场景类来模拟一下明星的事迹,如代码清单51所示。

public class Client {
     public static void main(String[] args) {
             //声明一个电影明星
             AbsStar zhangSan = new FilmStar();
             //声明一个歌星
             AbsStar liSi = new Singer();
             //展示一下各个明星的主要工作
             zhangSan.doJob();
             liSi.doJob();
             //当然,也有部分明星不务正业,比如歌星演戏
             liSi = new Singer(new ActFilm());
             liSi.doJob();
     }
}

运行结果如下所示:

======影星的工作=====
演出精彩绝伦的电影
======歌星的工作=====
唱出优美的歌曲
======歌星的工作=====
演出精彩绝伦的电影

好了,各类明星都有自己的本职工作,但是偶尔客串一个其他类型的活动也是允许的,如此设计后,明星就可以不用固定在自己的本职工作上,而是向其他方向发展,比如影视歌三栖明星。

最佳实践

5个包装模式是大家在系统设计中经常会用到的模式,它们具有相似的特征:都是通过委托的方式对一个对象或一系列对象(例如门面模式)施行包装,有了包装,设计的系统才更加灵活、稳定,并且极具扩展性。从实现的角度来看,它们都是代理的一种具体表现形式,我们来看看它们在使用场景上有什么区别。

代理模式主要用在不希望展示一个对象内部细节的场景中,比如一个远程服务不需要把远程连接的所有细节都暴露给外部模块,通过增加一个代理类,可以很轻松地实现被代理类的功能封装。此外,代理模式还可以用在一个对象的访问需要限制的场景中,比如AOP。

装饰模式是一种特殊的代理模式,它倡导的是在不改变接口的前提下为对象增强功能,或者动态添加额外职责。就扩展性而言,它比子类更加灵活,例如在一个已经运行的项目中,可以很轻松地通过增加装饰类来扩展系统的功能。

适配器模式的主要意图是接口转换,把一个对象的接口转换成系统希望的另外一个接口,这里的系统指的不仅仅是一个应用,也可能是某个环境,比如通过接口转换可以屏蔽外界接口,以免外界接口深入系统内部,从而提高系统的稳定性和可靠性。

桥梁模式是在抽象层产生耦合,解决的是自行扩展的问题,它可以使两个有耦合关系的对象互不影响地扩展,比如对于使用笔画图这样的需求,可以采用桥梁模式设计成用什么笔(铅笔、毛笔)画什么图(圆形、方形)的方案,至于以后需求的变更,如增加笔的类型,增加图形等,对该设计来说是小菜一碟。

门面模式是一个粗粒度的封装,它提供一个方便访问子系统的接口,不具有任何的业务逻辑,仅仅是一个访问复杂系统的快速通道,没有它,子系统照样运行,有了它,只是更方便访问而已。

参考文档