灵动的适配器模式
文/ 杨加康,CFUG 社区成员,《Flutter 开发之旅从南到北》作者,小米工程师
设计模式系列的前两篇,分别向大家介绍了一种 创建性型模式(单例模式) 和一种 行为型设计模式(观察者模式),今天我们再来介绍一种结构型设计模式 —— 适配器模式。
适配器模式 (Adapter Design Pattern),顾名思义,这个模式就是用来做适配的,像一个「粘合剂」一样。
适配器模式可以将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类黏合在一起,最终使他们可以一起工作。
和 观察者模式 中的观察者与被观察者类似,适配器模式中担任主要角色是 适配器 (Adapter) 和 被适配者 (Adaptee)。一个比较典型的例子是,插座转接头可以被认为是一种适配器,可以把本身不兼容的接口,通过转接变得可以一起工作。
在代码世界中,也有很多接口不适配的场景,如我们引入了一个第三方库后,发现它其中的类实现与我们现有代码并不兼容,需要一个 Adapter 类做一层转换才行。另外,相较于直接接触原始的代码实现,这种模式下,客户端仅仅依赖适配器类,对于代码复用和维护性也多了一层保障。
类适配器与对象适配器
#适配器模式有两种实现方式:类适配器 和 对象适配器。其中,类适配器使用继承关系来实现,而对象适配器使用组合关系来实现。具体的代码实现如下所示。
/// 被适配者
class Adaptee {
String concreteOperator() {
return 'Adaptee';
}
}
abstract class ITarget {
String operator();
}
/// 对象适配器
class ObjectAdapter implements ITarget {
var adaptee = Adaptee();
String operator() {
return adaptee.concreteOperator();
}
}
/// 类适配器
class ClassAdapter extends Adaptee {
String operator() {
return super.concreteOperator();
}
}
ITarget
表示要转化成的接口,是一个规范化的接口定义。
Dart 本身不支持关键词 interface
,因此我们可以创建一个没有默认实现的抽象类代替。
需要被适配的 Adaptee
表示一组不兼容 ITarget
接口定义的类或接口,ObjectAdapter
和 ClassAdapter
两种适配器分别用不同的方式将 Adaptee
转化成了符合 ITarget
接口定义的接口。而在客户端使用时只需要依赖 ITarget
即可完成对 Adaptee
的适配。
class Client {
Client(this.adapter);
final ITarget adapter;
operator() {
var result = adapter.operator();
assert(result == 'Adaptee');
}
}
关于类适配器与对象适配器:
- 如果希望你一个适配器可以同时适配多个不同的类,则单继承机制的 Dart 语言无法使用 类适配器 实现这种一对多的适配器。
- 如果
Adaptee
接口很多,而且Adaptee
和ITarget
接口定义大部分都相同,那我们推荐使用类适配器,因为可以充分将继承的代码复用作用利用起来。 - 大部分场景下,我们推荐使用 对象适配器 的方式实现适配器模式,因为 继承 在很多情况下容易被 滥用 并造成 层级过多 的现象,而 组合 更加灵活。
实现
#在代码应用中,适配器模式典型的例子是 Android 中的 ListView,在 Android 中,ListView 作为一个展示列表的 UI 组件,它的主要作用是将用户交给它的 Item View 以列表形式展示出来,然而描述 View 的形式却多种多样,可以是 Android 中 XML 布局,也可以是以 Java 代码中自定义 View 的形式提供,甚至可以是自定义的一套规则,实现自己的 UI 描述语言。
本身,XML 或者其他描述语言对于 ListView 是不可知的,所以,在 ListView 和它们之前介入一个适配器就可以有效的解决这个问题,适配器的作用就是将这些形式转换成 Item View 以适应 Listview。
在 Flutter 中,这种形式的模式很容易实现,例如,我们想自定义一个可以展示蔬果列表的组件 VeggieList
:
class VeggieList extends StatefulWidget {
@override
_VeggieListState createState() => _VeggieListState();
}
class _VeggieListState extends State<VeggieList> {
final List<Veggie> veggies = [];
@override
Widget build(BuildContext context) {
return veggies.isEmpty
? Text('无水果')
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
for (var veggie in veggies)
ListTile(title: Text('${veggie.name}')))
],
);
}
}
这个组件主要关心的是 veggies
,一个提供一组 Veggie
对象的数组。
veggies
数据源并不统一,可能来自多个不同的接口,可能在云端,也可能是本地的假数据,并且不同的接口提供的数据格式也可能不相同,可能是 xml 或者是 json。
/// 返回 json 数据格式的接口
class JsonVeggiesApi {
final String _veggiesJson = '''
{
"veggies": [
{
"name": "apple (JSON)",
},
{
"name": "banana (JSON)",
},
]
}
''';
String getVeggiesJson() {
return _veggiesJson;
}
}
/// 返回 xml 数据格式的接口
class XmlVeggiesApi {
final String _contactsXml = '''
<?xml version="1.0"?>
<veggies>
<veggie>
<name>apple (XML)</name>
</veggie>
<veggie>
<name>banana (XML)</name>
</veggie>
</veggies>
''';
String getVeggiesXml() {
return _contactsXml;
}
}
这些接口显然不能直接应用在 VeggieList
中展示,因此,需要做一些适配工作,适配的目的就是将这些数据转换成 Veggie
对象的数组,因此我们可以定义如下这个接口:
abstract class IVeggiesAdapter {
List<Veggie> getVeggies();
}
其中的 getVeggies
方法返回的就是 VeggieList
组件需要的 Veggie
对象数组。
创建适配器时,只需要实现这个接口,然后组合目标需要被适配的类做接口转换即可,例如下面的 JsonnVeggiesAdapter
,专门负责将 JsonVeggiesApi
转换为兼容 VeggieList
的适配器:
class JsonnVeggiesAdapter implements IVeggiesAdapter {
final JsonVeggiesApi _api = JsonVeggiesApi();
@override
List<Veggie> getVeggies() {
final veggiesJson = _api.getVeggiesJson();
final veggiesList = _parseContactsJson(veggiesJson);
return veggiesList;
}
List<Veggie> _parseContactsJson(String contactsJson) {
final contactsMap = json.decode(contactsJson) as Map<String, dynamic>;
final contactsJsonList = contactsMap['contacts'] as List;
final contactsList = contactsJsonList.map((json) {
final contactJson = json as Map<String, dynamic>;
return Veggie(
name: contactJson['name'] as String,
);
}).toList();
return contactsList;
}
}
最终,在使用到 VeggieList
时,注入 JsonnVeggiesAdapter
这个适配器就可以将原本不兼容的 JsonVeggiesApi
中的数据展示出来了:
class AdapterExample extends StatelessWidget {
const AdapterExample();
@override
Widget build(BuildContext context) {
return ScrollConfiguration(
behavior: const ScrollBehavior(),
child: SingleChildScrollView(
child: VeggieList(
adapter: JsonnVeggiesAdapter(),
),
),
);
}
}
/// 最终的 VeggieList
class VeggieList extends StatefulWidget {
final IVeggiesAdapter adapter;
const VeggieList({
@required this.adapter,
});
@override
_VeggieListState createState() => _VeggieListState();
}
class _VeggieListState extends State<VeggieList> {
final List<Veggie> veggies = [];
void _getVeggies() {
setState(() {
veggies.addAll(widget.adapter.getVeggies());
});
}
@override
Widget build(BuildContext context) {
return veggies.isEmpty
? Text('无水果',)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
for (var veggie in veggies)
ListTile(title: Text('${veggie.name}')))
],
);
}
}
同理,不同接口来源的数据都可以通过适配器实现 IVeggiesAdapter
接口与 VeggieList
做兼容。
Flutter
#在应用中,我们经常会使用到 CustomScrollView
创建拥有自定义滚动效果的组件,而 CustomScrollView
只允许包含 sliver
系列组件 (SliverAppBar
、SliverList
、SliverPersistentHeader
等) ,如果想包含普通的组件,必然需要使用 SliverToBoxAdapter
:
return MaterialApp(
home: CustomScrollView(
controller: scrollController,
slivers: <Widget>[
SliverAppBar(),
SliverToBoxAdapter(
child: Container(
height: 100.0,
),
),
],
),
);
这里,将 Container
放入 SliverToBoxAdapter
中便可以在 CustomScrollView
展示出来了。
我们认为普通的 widget 是不兼容 CustomScrollView
的,SliverToBoxAdapter
在其中就扮演了适配器的角色。它使用 类适配器 的方式,将 SingleChildRenderObjectWidget
中 createRenderObject
接口重写转换成可以包含 RenderBox
(对应一般 widget 的 RenderObject
) 的 RenderSliver
(对应 sliver 系列 widget 的 RenderObject
),即这里的 RenderSliverToBoxAdapter
:
class SliverToBoxAdapter extends SingleChildRenderObjectWidget {
/// Creates a sliver that contains a single box widget.
const SliverToBoxAdapter({
Key? key,
Widget? child,
}) : super(key: key, child: child);
@override
RenderSliverToBoxAdapter createRenderObject(BuildContext context) => RenderSliverToBoxAdapter();
}
拓展阅读
#- 适配器模式:https://refactoringguru.cn/design-patterns/adapter
- 组合优于继承:https://time.geekbang.org/column/article/169593
- Flutter Sliver:https://juejin.cn/post/6844903901720739848
关于本系列文章
#Flutter / Dart 设计模式从南到北 (简称 Flutter 设计模式) 系列内容由 CFUG 社区成员、《Flutter 开发之旅从南到北》作者、小米工程师杨加康撰写并发布在 Flutter 社区公众号和 flutter.cn 网站的社区教程栏目。
本系列内容旨在推进 Flutter / Dart 语言特性的普及,帮助开发者更高效地开发出高质量、可维护的 Flutter 应用。如果你对本文还有任何疑问或者文章的建议,欢迎向中文社区官方 GitHub 仓库 (cfug/flutter.cn) 提交 Issue 或者直接与我联系 (yangjiakay@gmail.com)。
除非另有说明,本文档之所提及适用于 Flutter 的最新稳定版本,本页面最后更新时间: 2024-05-10。 查看文档源码 或者 为本页面内容提出建议。