再读GOF设计模式-结构型模式

接上一篇《再读GOF设计模式-创建型模式》

结构型系列模式是指通过组合对象的方式来降低系统的耦合性,下面具体来说一下每个设计模式的理解。

结构型模式

下面所有模式,都需要对目标对象进行接口抽象,这是模式的前提,没有抽象是无法解耦的。其次,这些模式在实现上非常相近甚至边界模糊,认识这些模式最重要的是体会用途的差异,因此为每个模式讲一个故事是一个不错的解读方式。

适配器模式

比如玩塞车游戏,对于自动挡的汽车来说,我们只需要踩油门即可前进,但是手动挡的车是需要挂档的,对于玩家来说只想踩踩油门握一握方向盘而已,这就要求我们把手动挡的车改造成像自动挡一样的操作体验。

方法就是我们提供给玩家一个抽象类,包含踩油门方法和变更方向方法。为了让自动挡车变得和手动挡车一样无需关心挂档,我们可以重新定义一个新的手动挡车类继承抽象类并实现踩油门方法,在该方法中调用老的手动挡车对象的挂档和踩油门方法从而根据车速自动的换挡。在这里,新的手动挡车类就是适配者,而老的手动挡车是被适配者,这样无论哪种车都可以踩踩油门就开走了。

当然,虽然自动挡车本来就无需挂档,但是为了接口统一的抽象目的,自动挡车也要继承抽象,并把请求直接转发给老的自动挡车即可。

适配器目的很简单,多个类接口或者功能上有细微差异,又想统一到一个抽象接口给用户使用,那么就可以来适配多个类到一个抽象类,实现接口统一的目的。

实现适配器模式,除了必须要公有继承抽象类这个前提外,可以将被适配的对象通过构造参数传给适配者对象,也可以让适配者类私有继承被适配类。

桥接模式

也就是很常见的impl方式,举例来说就是:奥迪生产汽车产品,包含奥迪轿车和奥迪跑车,它们都有启动,熄火,拐弯接口,但是实现不同。

桥接模式希望给用户提供一个统一汽车类,它内部实现到底是奥迪轿车还是奥迪跑车是可以灵活替换的,这要求奥迪轿车和奥迪跑车继承自同一个接口,之后统一汽车类通过impl成员保存指向,提供给用户的接口通过impl的方法来实现。

另外,impl实现可以用于跨对象共享数据的目的,因为核心逻辑实现都在impl中,而impl可以提供给多个对象共同使用。

我们定义类通常除了public方法还有很多private方法,private方法用于public方法的实现,但是我们知道类定义是暴露给用户的,即便我们只修改了private方法的定义(非实现),也会影响到用户,在编译类语言中需要重新编译。

另外,有一些场景我们也不希望把private方法暴露给用户看到,这在实现一些开放的收费的SDK的时候尤为重要。

组合模式

比如快递包裹,一个盒子可以发出去,一个盒子套几个盒子也可以整体发出去,那么盒子套盒子就是一种组合。一个盒子和盒子套盒子对于快递员来说,并没有差别,都可以发送签收。

组合模式这个特点要求抽象类支持add和remove接口,从而实现对抽象对象的组合,每个实现类都有一致的接口,因此可以任意组合和递归。

没有孩子的对象也称为叶子节点,而有孩子的对象叫做组合(非叶子)节点,但它们对于用户而言是看不出差别的。

装饰器模式

这个模式很好理解,就是说冰激凌可以撒上巧克力粉,再涂上草莓酱,它还是一个冰激凌,只是外面裹上了几层外衣。

在实现上来说,冰激凌是继承自抽象类的原物(待装饰的对象),之后需要继承抽象类实现一个装饰器抽象类,它的构造函数接受一个抽象类对象(也就是待修饰的对象),而所有接口实现都是默认将请求转发给待修饰对象。接下来,巧克力粉继承装饰器类,我们在创建这个巧克力粉对象时传给它冰激凌对象,这样巧克力粉对象的接口可以先调用装饰器父类接口(装饰器父类的默认行为就是调用待装饰对象的接口)得到一个冰激凌,之后再撒上巧克力粉就实现了巧克力冰激凌。

装饰器模式通过一个对象装饰前一个对象的方式,链式的增加原始对象的外衣,实现对个别方法的选择性修饰。

外观模式

这个模式也很直接,一个公司Boss如果直接管理各个部门,肯定是很辛苦了。那么为了解决这个问题呢,可以给BOSS封装一个类,这个类的实现中将各个部门的业绩指标拿回来展示给BOSS看,这个类相当于BOSS的秘书。

这样的好处也是解耦,本来BOSS要和各个部门的leader谈话,现在BOSS只需要认识秘书,秘书再和各个部门leader沟通,这样的好处就是BOSS的工作简化了,下面部门变动也不会影响到BOSS。

实现上来说,就是原本系统里有很多很多子系统以及类,现在写一个很大很全的超级类,它把用户需要用到的各种系统的功能整合到一起,实现一系列方法内部调用各个子系统和类来提供给用户整合后的功能,这样可以一定程度降低各模块之间的耦合,大家都只需要认识超级类就可以了。

享元模式

这个模式是为了共享对象而存在的,目的就是节省内存,避免用重复的内存表达一样的东西,它在实现上一般是定义抽象类,然后所有子类继承它,另外定义一个独立的工厂方法来生产抽象对象。

工厂方法维护一个hashmap,key是子类的名称,value是子类的共享对象,这样用户只需要和工厂方法通讯来获取一个抽象对象,传入的参数是类的名字,如果对象已存在则直接返回,否则创建并保存到hashmap。工厂方法返回的是抽象的对象,这样即可以享受到工厂方法的解耦能力,又可以实现多处共享对象的目的。

大家共享对象,自然要求这些对象是通用的,那么又怎么体现同类对象间的差异呢?这个只能由用户在自己的环境下自己记录了,享元模式提供的仅仅是共享对象的能力而已。

代理模式

代理模式和装饰器模式在某方面类似,都试图伪装的和原始类一致,提供相同的抽象接口。但它们2个和桥接模式则不同,桥接模式不试图伪装的和原始类一致,而仅仅是分离接口和实现。

代理模式和装饰器模式在某方面不同,装饰器模式是可选的修饰已有抽象对象的个别接口,并且可以链式装饰。而代理模式则侧重于隐藏被代理的对象,而不是修改被代理对象的原有行为,通常来说代理类可以延迟创建被代理的对象,也就是说代理类对象创建的时候不需要传入被代理的对象,这和装饰器存在本质的差别,另外代理类通常用来做一些额外的控制(类似权限)。

总得来说,代理和被代理类都继承自抽象类,代理类在定义中保存指向抽象对象的引用,并在类实现种分配具体的被代理对象,用户感知不到被代理对象的类型是否变化。另外,代理模式强调代理和被代理类的接口1:1映射,而桥接模式并没有这个限制(仅仅是用impl来实现另外一套接口),因此桥接模式也不强调代理类的抽象。

这些模式非常相似,重要的是从功能用途区分它们,并且它们并不都是互斥关系,几个模式可以并存从而解决各种真实场景。

同时,这些模式对于C++这种编译型语言尤为重要,因为类定义(头文件)的稳定性对于用户来说很重要,尽量的隐藏变化,同时提供抽象,才能让用户做到不变,系统耦合性才更低。

但是,再好的抽象在代码里也至少在某处分配了某个具体类对象,我们要考虑的是这个类对象传递给其他系统后,以及在后续更多的系统中传递这个对象对于系统会造成什么样耦合性,照着这个目标去设计。

发表评论

电子邮件地址不会被公开。