学完了创建型模式,该学结构型模式了。
适配器模式
描述:将某个类的接口转换成客户端期望的另一个接口表示。适配器模式可以消除由于接口不匹配所造成的类兼容性问题。
- 解决什么问题?
在软件开发中,系统的数据和行为都正确,但接口不符,我们应该考虑用适配器,目的是使控制范围之外的一个原有对象与某个接口匹配。适配器模式主要应用于希望复用一些现存的类,但是接口又与复用环境要求不一致的情况,比如在需要对早期代码复用一些功能等应用上很有实际价值。 - 何时使用适配器?
在你想使用一些已经存在的类,这些类的作用和复用环境里其他的类功能相似,但它们的接口不同时,考虑使用适配器模式。客户端代码可以统一调用同一接口,这样更简单、直接、紧凑。 - 注意事项:
其实使用适配器模式是无奈之举,有点“亡羊补牢”的感觉。我们不应该在设计阶段使用它,因为在初期,没有必要把功能相似的类的接口设计得不同,就算发现有设计不同的地方,也应该及时重构统一接口。当我们因不同开发人员、产品、厂家而造成功能类似而接口不同的情况,双方都不太容易修改自己的接口时,才是适配器模式大展拳脚的时候。
举个翻译器的例子,我们用两种语言说同一句话时,意思是一样的,但说出来,发出的声音肯定有很大区别。这时,我们可以把这句话的意思看作是“功能类似”,而说出的话语看作是“接口不同”,我们假设双方都不能听懂对方的语言,这时候就需要一个翻译器了,它充当适配器的角色。
用代码表示如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19//Target
interface Chinese {
void speakChinese();
}
//ConcreteTarget
class ChineseSpeaker implements Chinese {
public void speakChinese() {
System.out.println("你吃午饭了吗");
}
}
//Adaptee
class JapaneseSpeaker {
public void speakJapanese() {
System.out.println("昼食はもう食べましたか");
}
}
现在这个中国人想听懂日本人说的话,需要一个翻译器(Adapter),它有两种实现方式
类适配器
1 | //Adapter by class |
接口适配器
1 | //Adapter by interface |
他们的调用是这样子的1
2
3
4
5
6
7
8
9
10
11
12public class Adapter {
public static void main(String[] args) {
Chinese chinese = new ChineseSpeaker();
chinese.speakChinese();
Chinese chinese1 = new JapaneseToChineseTranslatorByInterface(new JapaneseSpeaker());
chinese1.speakChinese();
Chinese chinese2 = new JapaneseToChineseTranslatorByClass();
chinese2.speakChinese();
}
}
可以看到,客户端看到的都是Chinese类型的引用,都调用了统一的speakChinese()接口。
总结:如果能事先预防接口不同问题,不匹配的问题就不会发生,小接口不统一,及时重构问题不至于扩大,只有碰到无法改变原有设计和代码的情况时,才考虑用适配器,如果无视场合盲目使用,其实是本末倒置了。
桥接模式
描述:将一个抽象与实现解耦,以便两者可以独立的变化。
字面的意思解读就是通过一个中间的桥梁对两边的东西进行关联起来,但是关联的两者之间又不相互影响。
《大话设计模式》在讲到此节时提到了合成/复用原则
合成/复用原则:优先使用对象的合成或聚合,而不是类继承。
合成和聚合都是关联的特殊种类
合成:表示一种强的“拥有”关系,体现了严格的部分和整体关系,部分和整体的生命周期一样。
聚合:表示一种弱的“拥有”关系,体现的是A对象可以包含B对象,但B对象不是A对象的一部分。
如下图所示
盲目地使用继承会造成麻烦,其本质是,继承是一种强耦合结构,父类变,子类必须变。所以我们在用继承时,一定要在是“is-a”的关系再考虑使用,而不是任何时候都去使用。
用更详细的语言解释是:
对象的继承关系是在编译时就定好了,所以无法在运行时改变从父类继承的实现。子类的实现与它的父类有非常紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类的变化。当需要复用子类时,如果继承下来的实现不适合新的需求,则父类必须重写或被其他更适合的类替换。这种依赖关系限制了灵活性并最终限制了复用性
举个游戏中的例子,DNF中鬼剑士的武器是剑类。鬼剑士可以转职成4个职业:剑魂,狂战士,阿修罗,鬼泣。剑类也有巨剑,太刀,钝器,短剑之分。这时候就可以把鬼剑士和剑类当做抽象类(虽然鬼剑士不转职时也是一个具体的角色,但在这个语境中我们姑且当他是抽象类),不同的职业可以使用不同的剑,不同的剑也可以被不同的职业使用。这时候就可以使用桥接模式把角色和武器任意组合了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47//Implementor
interface Sword {
void attack();
}
//ConcreteImplementor
class GreatSword implements Sword {
public void attack() {
System.out.println("使用巨剑挥砍");
}
}
class TooKnife implements Sword {
public void attack() {
System.out.println("使用太刀突刺");
}
}
//Abstraction
abstract class Slayer {
protected Sword sword;
void setSword(Sword sword) {
this.sword = sword;
}
abstract void attack();
}
//RefinedAbstraction
class BladeMaster extends Slayer {
void attack() {
System.out.print("剑魂");
sword.attack();
}
}
class Berserker extends Slayer {
void attack() {
System.out.print("狂战士");
sword.attack();
}
}
主程序1
2
3
4
5
6
7
8
9
10
11public class Bridge {
public static void main(String[] args) {
Slayer bladeMaster = new BladeMaster();
bladeMaster.setSword(new TooKnife());
bladeMaster.attack();
Slayer berserker = new Berserker();
berserker.setSword(new GreatSword());
berserker.attack();
}
}
做个总结:对于桥接模式,通俗的理解就是:实现系统可能有多个角度分类,每一种分类都可能发生变化,那么就把这种多角度分离出来让它们独立变化,减少它们之间的耦合。
参考资料: