适配器模式与外观模式

  • 随遇而安

文章说明

  • 该文章为《Head First 设计模式》的学习笔记
  • 非常推荐大家去买一本《Head First 设计模式》去看看,节奏轻松愉悦,讲得通俗易懂,非常适合想要学习、了解、应用设计模式以及OO设计原则的小白。

1. 适配器模式

1.1 定义适配器模式

适配器模式将一个类的接口,转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作无间。

1.2 对象和类适配器的类图

实际上有两种适配模式,"对象"适配器"类"适配器,在Java中类适配器不能实现,因为需要多重继承的支持。

  • "对象"适配器
«Interface» Target request() Adapter Adaptee request() Adaptee specificRequest() Cilent has-a
  • "类"适配器
Target request() Adapter request() Adaptee specificRequest() Cilent

"对象"适配器是通过使用对象的组合实现的接口转换,"类"适配器则是同时继承被适配者和目标类实现的。

1.3 实现对象适配器

先来个简单的实现,现在我有一个鸭子类,它的子类都可以呱呱叫,我想让一个咯咯叫的火鸡也拥有鸭子的行为。我们可以使用对象适配器伪装火鸡让它看起来像鸭子。


鸭子行为类:Duck接口

public interface Duck {
	void quack(); // 呱呱叫
	void fly();
}

绿头鸭是鸭子的子类:MallardDuck

// 绿头鸭
public class MallardDuck implements Duck {
	public void quack() { // 呱呱叫
		System.out.println("Quack...");
	}
	public void fly() {
		System.out.println("I'm flying 5 meters");
	}
}

火鸡行为类:Turkey接口

public interface Turkey {
	void gobble(); // 咯咯叫
	void fly();
}

野火鸡是火鸡的子类:WildTurkey

public class WildTurkey implements Turkey {
	public void gobble() { // 咯咯叫
		System.out.println("Gobble...");
	}
	public void fly() {
		System.out.println("I'm flying a 1 meter");
	}
}

接下来我们需要让火鸡伪装成鸭子,写一个适配器,让它适配火鸡

火鸡适配器类:TurkeyAdapter

public class TurkeyAdapter implements Duck {
	Turkey turkey; // 组合
	// 取得要适配的对象引用
	public TurkeyAdapter(Turkey turkey) {
		this.turkey = turkey;
	}
	// 接口装换
	public void quack() { // 简单的转换
		turkey.gobble();
	}
	public void fly() { // 稍微难一点的转换
		for(int i = 0; i < 5; i++)
			turkey.fly();
	}
}

测试适配器:DuckTestDriver

public class DuckTestDriver {
	public static void main(String[] args) {
		// 绿头鸭子
		Duck duck = new MallardDuck(); 
		// 野火鸡
		Turkey turkey = new WildTurkey();
		// 野火鸡使用适配器伪装成鸭子
		TurkeyAdapter turkeyAdapter = 
			new TurkeyAdapter(turkey);
		
		System.out.println("The Turkey says...");
		turkey.gobble();
		turkey.fly();
		
		System.out.println("\nThe Duck says...");
		TestDuck(duck);
		
		System.out.println("\nThe TurkeyAdapter says...");
		TestDuck(turkeyAdapter);
	}
	static void TestDuck(Duck duck) {
		duck.quack();
		duck.fly();
	}
}

测试结果

The Turkey says...
Gobble...
I'm flying a 1 meter

The Duck says...
Quack...
I'm flying 5 meters

The TurkeyAdapter says...
Gobble...
I'm flying a 1 meter
I'm flying a 1 meter
I'm flying a 1 meter
I'm flying a 1 meter
I'm flying a 1 meter

可见,火鸡在适配器的帮助下成功的被认为是鸭子,但是它的本质不变它还是火鸡,只能咯咯叫。


1.4 真实世界的适配器

旧世界的迭代器新世界的迭代器

  • 在Java早期的集合(Collection)类型(例如:VectorStackHashTable这些类现在都被遗弃了)都实现了一个名为elements()方法。它会返回一个Enumeration举,跟我们现在的Iterator迭代器作用差不多,就是遍历集合,但是枚举是"只读"的,意味着它不能像迭代器那样删除元素

面对以前的遗留代码,这些代码只暴露出枚举器的接口,但是我们希望用使用迭代器,那么我们需要构造一个适配器让枚举器看起来像迭代器,进而使用它。


根据上例所了解的对象适配器我们先设计出类图,像这样:

«Interface» Iterator hasNext() next() remove() «Interface» EnumerationIterator hasNext() next() remove() Enumeration hasMoreElements() nextElement() Cilent has-a

编写一个EnumerationIterator适配器:

//使用了Iterator伪装的一个Enumeration
public class EnumerationIterator implements Iterator<Object> {
	Enumeration<?> enumeration;
	public EnumerationIterator(Enumeration<?> enumeration) {
		this.enumeration = enumeration;
	}
	public boolean hasNext() {
		return enumeration.hasMoreElements();
	}
	public Object next() {
		return enumeration.nextElement();
	}
	public void remove() { // 未获支持的操作
		throw new UnsupportedOperationException();
	}
}

可以看见适配器适配得并不完美,但这没有办法,毕竟Enumeration不是一个Iterator,只能选择在remove()方法直接抛出了一个未获支持操作异常UnsupportedOperationException,做出一个文档说明,让客户在使用的时候小心就没有太大问题。

测试一下:EnumerationIteratorTestDriver

public class EnumerationIteratorTestDriver {
	public static void main(String[] args) {
		Vector<Integer> v = new Vector<Integer>(
			Arrays.asList(1, 2, 3, 4, 5));
		// 使用了Iterator伪装的一个Enumeration
		Iterator<?> iterator = new EnumerationIterator(v.elements());
		while(iterator.hasNext())
			System.out.print(iterator.next());
	}
}

测试结果:

12345

1.5 与外观和装饰器混淆

简而言之:
适配器模式职责是 转换接口
外观模式职责是 简化接口
装饰器模式职责是 扩展对象的行为与责任

1.6 适配器的扩展应用

双向适配器,支持两边的接口,想要创建这样的适配器,必须实现涉及的两个接口。


1.3的例子中,如果来了一个嘎嘎叫的天鹅,我想让火鸡也伪装成它,可以实现一个双向的适配器去适配火鸡,让火鸡可以被看作是鸭子和鹅,像这样:

public interface Goose {
	gaggle(); // 嘎嘎叫
	gFly();
}
public class TwoWayAdapter implements Duck, Goose {
	Turkey turkey; // 组合
	Random rand;
	// 取得要适配的对象引用
	public TwoWayAdapter(Turkey turkey) {
		this.turkey = turkey;
		rand = new Random();
	}
	// 接口装换
	public void quack() { 
		turkey.gobble();
	}
	public void gaggle() {
		turkey.gobble();
	}
	public void fly() { // 模拟鸭子的飞行距离
		for(int i = 0; i < 5; i++)
			turkey.fly();
	}
	public void gFly() { // 鹅的飞行距离比火鸡短	
		for(rand.nextInt(5) == 0) // 五分之一
			turkey.fly();
	}
}

2. 外观模式

2.1 定义外观模式

外观模式提供了一个统一的接口,用来访问子系统中的一群接口,外观定义了一个高层接口,让子系统更容易使用。

2.2 外观模式类图

Cilent Facade ComplacatedSubsystem

Cilent: 有了外观,客户只需要跟外观打交道,工作变得简单了,客户与子系统解耦了。
Facade:外观统一了接口
ComplacatedSubsystem:复杂的子系统

现在还是有点迷糊,通过接下来的例子更深一步认识外观模式

2.3 实现外观类

我组装了一个家庭游戏空间的系统,内含Xbox游戏机,超大屏电视,环绕立体声,空调,灯光。

看看这些组件的类图:

Light on() off() dim() bright() Xbox on() off() setGame() AirConditioner on() off() temperatureUp() temperatureDown() TV StereoSpeaker speaker on() off() setVolume() getStereoSpeaker() StereoSpeaker setVolume() setSurround() toString() has-a

当我需要启动我的游戏空间系统时,我会这么做,先开灯,然后打开电视,然后。。。

light.on(); // 开灯
light.dim(10); // 亮度降低10%
airConditioner.on();// 开空调,默认20度
tv.on(); // 开电视	
tv.setVolume(11); // 设成最大音量11
StereoSpeaker speaker = tv.getStereoSpeaker();
speaker.setSurround(); // 设为环绕音
xbox.on(); // 打开Xbox游戏主机
xbox.setGame(game); // 选择游戏

用完了我还得需要反向地执行关闭动作,虽然该系统功能性很强但是使用非常麻烦,难道不能一键开启关闭吗?能!

我们可以实现一个提供更合理的接口的外观类,将这个复杂的系统变得容易使用

下面是实现了外观类的家庭游戏空间的系统:

Light on() off() dim() bright() Xbox on() off() setGame() AirConditioner on() off() temperatureUp() temperatureDown() TV StereoSpeaker speaker on() off() setVolume() getStereoSpeaker() StereoSpeaker setVolume() setSurround() toString() FamilyPlayPlaceFacade Light light AirConditioner airConditioner TV tv Xbox xbox playGames() endGames() has-a

外观类FamilyPlayPlaceFacade 只暴露出几个简单的方法,它将家庭游戏空间多个组件视为一个子系统Subsystem,通过调用这个子系统来实现playGames()方法,而且并未将子系统的类阻隔起来,我还是随时可以使用原来的子系统。

构造一个家庭游戏空间的外观类:FamilyPlayPlaceFacade

// 使用外观封装特定的一组行为,但不阻隔子系统
public class FamilyPlayPlaceFacade {
	// 使用对象组合
	Light light;
	AirConditioner airConditioner;
	TV tv;
	Xbox xbox;
	// 在构造器初始化对象
	public FamilyPlayPlaceFacade(
		Light light,
		AirConditioner airConditioner,
		TV tv, 
		Xbox xbox) {
		this.light = light;
		this.airConditioner = airConditioner;
		this.tv = tv;
		this.xbox = xbox;
	}
	// 设定特定一系列的操作
	public void playGames(String game) {
		System.out.println("Get ready to play a game...");
		light.on();
		light.dim(10); // 亮度降低10%
		airConditioner.on(); // 默认20度
		tv.on(); // 先开电视	
		tv.setVolume(11); // 设成最大音量11
		StereoSpeaker speaker = tv.getStereoSpeaker();
		speaker.setSurround(); // 设为环绕音
		xbox.on();
		xbox.setGame(game); // 选择游戏
		System.out.println();
	}
	// 负责关闭一切
	public void endGames() {
		System.out.println("Shutting game down...");
		xbox.off();
		tv.off();
		airConditioner.off();
		light.off();
	}
}

当然每个任务的细节都委托相应的组件处理,毕竟我只是个外观而已。

现在我终于可以一步到位了,来肝一把《怪物猎人:世界》,测试一下:

public class FamilyPlayPlaceDrive {
	public static void main(String[] args) {
		String location = "Living Room";
		// 实例化组件
		Light light = new Light(location);
		AirConditioner airConditioner = new AirConditioner(location);
		StereoSpeaker stereoSpeaker = new StereoSpeaker(location);
		TV tv = new TV(location, stereoSpeaker);
		Xbox xbox = new Xbox(location);
		// 实例化外观
		FamilyPlayPlaceFacade gameFacade = 
			new FamilyPlayPlaceFacade(light, airConditioner, tv, xbox);
		gameFacade.playGames("Monster Hunter:World");
		gameFacade.endGames();
	}
}

3. 最少知识原则

3.1原则定义

莫忒耳法则(Law of Demeter)指的也是最少知识原则

最少知识原则:只和你的密友谈话

不要让太多的类耦合到一起,免得修改系统的一部分,会影响其他部分。如果许多类之间相互依赖,那么这个系统就会变成一个易碎的系统,它需要花许多成本维护,也会因为太复杂而容易被他人了解。

3.2指导方针

在该对象的方法内,我们只调用属于以下范围的方法:

  • 该对象本身
  • 被当做方法的参数而传递进来的对象
  • 此方法所创建或实例化的任何对象
  • 对象的任何组件

当遵守该原则对你的程序百利而无一害时,那就尽量去遵守它。

下面是四条指导方针的应用

public class Car {
	Engine engine; // 本类组件engine
	// 其他组件
	public Car() {
		// 初始化发动机
	}
	// 被当做参数传进来的对象key
	public void start(Key key) {
		// 方法内创建的对象doors
		Doors doors = new Doors();
		// 被当做参数传进来的对象key的方法
		boolean authorized = key.turns();
		if(authorized) {
			// 本类组件engine的方法
			engine.start();
			// 该对象本身Car的方法
			updateDashboardDisplay();
			// 方法内创建的对象door的方法
			doors.lock();
		}
	}
	public void updateDashboardDisplay() {
		// 显示更新
	}
}

3.3 缺点

虽然减少了对象之间的依赖减少了软件的维护成本

但是也会导致过多的"包装"类被制造出来,以处理和其他组件的沟通,这可能会导致复杂度开发时间的增加,并降低运行时的性能

4. 要点

  • 当需要使用一个现有的类而其他接口不符合你的需求时,就使用适配器
  • 当需要简化接口并统一一个很大的接口或者一群复杂的接口时,使用外观
  • 适配器改变接口以符合客户的期望
  • 外观将客户从一个复杂的子系统中解耦
  • 实现一个适配器的工作量根据目标接口的大小与复杂度规定
  • 实现一个外观,需要将子系统组合进外观,然后将工作委托给子系统去执行
  • 适配器模式的两种形式:对象适配器 和 类适配器。类适配器需要用到多重继承
  • 可以为一个子系统实现一个以上的外观
  • 适配器将一个对象包装起来以改变接口;装饰者将一个对象包装起来以增加新的行为和责任;而外观将一群对象"包装"起来以简化接口
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
适配器模式(Adapter Pattern)是一种结构型设计模式,它允许你将现有的类(即不兼容的类)转换为其他接口所期望的接口。适配器模式旨在使原本由于接口不兼容而不能一起工作的类可以一起工作[^1]。适配器模式通常用于系统的接口设计中,对于现有的系统,由于接口不兼容而不能满足现有的需求,但是系统的功能又不能改变,此时可以考虑采用适配器模式外观模式(Facade Pattern)是一种结构型设计模式,它提供了一个简化接口,使得客户端能够更方便地访问复杂系统。外观模式可以将系统的复杂性与客户端的代码分开,降低客户端的复杂度,并提高系统的可复用性[^2]。外观模式通常用在大型系统的开发中,可以将系统的各个模块进行组织,提供简化的接口给客户端使用。 下面是适配器模式外观模式的代码范例: 适配器模式: ```python class Adaptee: def specific_request(self): return 42 class Adapter: def __init__(self, adaptee): self.adaptee = adaptee def request(self): return self.adaptee.specific_request() if __name__ == "__main__": adaptee = Adaptee() adapter = Adapter(adaptee) assert adapter.request() == 42 ``` 外观模式: ```python class SubSystem1: def method1(self): return "SubSystem1 method1" def method2(self): return "SubSystem1 method2" class SubSystem2: def method1(self): return "SubSystem2 method1" def method2(self): return "SubSystem2 method2" class Facade: def __init__(self): self.subsystem1 = SubSystem1() self.subsystem2 = SubSystem2() def operation(self): results = [] results.append("Facade initializes subsystems:") results.append(self.subsystem1.method1()) results.append(self.subsystem2.method1()) results.append("Facade orders subsystems to perform the action:") results.append(self.subsystem1.method2()) results.append(self.subsystem2.method2()) return "\n".join(results) if __name__ == "__main__": facade = Facade() print(facade.operation()) ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值