1. 设计模式概述
1.1 软件设计模式的产生背景
"设计模式"最初并不是出现在软件设计中,而是被用于建筑领域的设计中。
1977年美国著名建筑大师、加利福尼亚大学伯克利分校环境结构中心主任 克里斯托夫·亚历山大(Christopher Alexander) 在他的著作《建筑模式语言:城镇、建筑、构造》中描述了一些常见的建筑设计问题,并提出了 253
种关于对城镇、邻里、住宅、花园和房间等进行设计的基本模式。
1990年软件工程界开始研讨设计模式的话题,后来召开了多次关于设计模式的研讨会。直到1995 年,艾瑞克·伽马(ErichGamma)、理査德·海尔姆(Richard Helm)、拉尔夫·约翰森(Ralph Johnson)、约翰·威利斯迪斯(John Vlissides) 等 4
位作者合作出版了《设计模式:可复用面向对象软件的基础》一书,在此书中收录了 23
个设计模式,这是设计模式领域里程碑的事件,导致了软件设计模式的突破。这 4
位作者在软件开发领域里也以他们的 “四人组”(Gang of Four,GoF) 著称。
1.2 软件设计模式的概念
软件设计模式(Software Design Pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。也就是说,它是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。
1.3 学习设计模式的必要性
设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。
正确使用设计模式具有以下优点。
- 可以提高程序员的思维能力、编程能力和设计能力。
- 使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。
- 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。
1.4 设计模式分类
-
创建型模式
用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。GoF(四人组)书中提供了单例、原型、工厂方法、抽象工厂、建造者等 5 种创建型模式。
-
结构型模式
用于描述如何将类或对象按某种布局组成更大的结构,GoF(四人组)书中提供了代理、适配器、桥接、装饰、外观、享元、组合等 7 种结构型模式。
-
行为型模式
用于描述类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责。GoF(四人组)书中提供了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等 11 种行为型模式。
2. UML图
统一建模语言(Unified Modeling Language,UML) 是用来设计软件的可视化建模语言。它的特点是简单、统一、图形化、能表达软件设计中的动态与静态信息。
UML 从目标系统的不同角度出发,定义了用例图、类图、对象图、状态图、活动图、时序图、协作图、构件图、部署图等 9 种图。
2.1 类图概述
类图(Class diagram) 是显示了模型的静态结构,特别是模型中存在的类、类的内部结构以及它们与其他类的关系等。类图不显示暂时性的信息。类图是面向对象建模的主要组成部分。
2.2 类图的作用
- 在软件工程中,类图是一种静态的结构图,描述了系统的类的集合,类的属性和类之间的关系,可以简化了人们对系统的理解。
- 类图是系统分析和设计阶段的重要产物,是系统编码和测试的重要模型。
2.3 类图表示法
2.3.1 类的表示方式
在 UML
类图中,类使用包含类名、属性(field) 和方法(method) 且带有分割线的矩形来表示,比如下图表示一个 Employee
类,它包含 name
, age
和 address
这3个属性,以及work()
方法。
2.3.1.1 静态方法的表示
静态方法(类方法)在UML类图中通过在方法名下面加下划线来表示。
2.3.1.2 可见性
属性/方法名称前加的加号和减号表示了这个属性/方法的可见性,UML
类图中表示可见性的符号有三种:
+
:表示public-
:表示private#
:表示protected
属性的完整表示方式是: 可见性 名称 : 类型 [= 缺省值]
方法的完整表示方式是: 可见性 名称(参数列表) [: 返回类型]
2.3.2 类与类之间关系的表示方式
2.3.2.1 关联关系
关联关系是对象之间的一种引用关系,用于表示一类对象与另一类对象之间的联系,如老师和学生、师傅和徒弟、丈夫和妻子等。关联关系是类与类之间最常用的一种关系,分为一般关联关系、聚合关系和组合关系,一般关联关系又可以分为单向关联,双向关联,自关联。
2.3.2.1.1 一般关联关系
-
单向关联
在 UML
类图中单向关联用一个带箭头的实线表示。上图表示每个顾客都有一个地址,这通过让 Customer
类持有一个类型为 Address
的成员变量类实现。
- 双向关联
从上图中我们很容易看出,所谓的双向关联就是双方各自持有对方类型的成员变量。
在UML
类图中,双向关联用一个不带箭头的直线表示。上图中在 Customer
类中维护一个 List<Product>
,表示一个顾客可以购买多个商品;在 Product
类中维护一个 Customer
类型的成员变量表示这个产品被哪个顾客所购买。
- 自关联
自关联在 UML
类图中用一个带有箭头且指向自身的线表示。上图的意思就是Node类包含类型为 Node
的成员变量,也就是“自己包含自己”。
2.3.2.1.2 聚合关系
聚合关系是关联关系的一种,是强关联关系,是整体和部分之间的关系。
聚合关系也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立存在。例如,学校与老师的关系,学校包含老师,但如果学校停办了,老师依然存在。
在 UML
类图中,聚合关系可以用带空心菱形的实线来表示,菱形指向整体。下图所示是大学和教师的关系图:
2.3.2.1.3 组合关系
组合表示类之间的整体与部分的关系,但它是一种更强烈的聚合关系。
在组合关系中,整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在,部分对象不能脱离整体对象而存在。例如,头和嘴的关系,没有了头,嘴也就不存在了。
在 UML
类图中,组合关系用带实心菱形的实线来表示,菱形指向整体。下图所示是头和嘴的关系图:
2.3.2.2 依赖关系
依赖关系是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。在代码中,某个类的方法通过局部变量、方法的参数或者对静态方法的调用来访问另一个类(被依赖类)中的某些方法来完成一些职责。
在 UML
类图中,依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖的类。下图所示是司机和汽车的关系图,司机驾驶汽车:
2.3.2.3 继承关系
继承关系是对象之间耦合度最大的一种关系,表示一般与特殊的关系,是父类与子类之间的关系,是一种继承关系。
在 UML
类图中,泛化关系用带空心三角箭头的实线来表示,箭头从子类指向父类。在代码实现时,使用面向对象的继承机制来实现泛化关系。例如,Student 类和 Teacher 类都是 Person 类的子类,其类图如下图所示:
2.3.2.4 实现关系
实现关系是接口与实现类之间的关系。在这种关系中,类实现了接口,类中的操作实现了接口中所声明的所有的抽象操作。
在 UML
类图中,实现关系使用带空心三角箭头的虚线来表示,箭头从实现类指向接口。例如,汽车和船实现了交通工具,其类图如下图所示:
软件设计原则是在软件开发过程中,帮助设计出可维护、可扩展和可重用的系统的指导方针。以下是一些常见的软件设计原则及其权威定义和例子:
3. 软件设计原则
在设计模式中,软件设计原则是指导我们构建可维护、可扩展和可重用的软件系统的基本准则。以下是一些常见的软件设计原则及其权威定义和例子:
3.1 单一职责原则(SRP)
单一职责原则(Single Responsibility Principle,SRP) 是软件设计中的一个重要原则,旨在帮助开发者创建更清晰、可维护和可重用的代码。通过将类的职责分离,可以减少代码之间的耦合,提高系统的灵活性和可理解性。在软件开发中,遵循这一原则可以有效降低复杂性,减少后期修改时的风险。以下是对单一职责原则的详细解释和示例。
3.1.1 定义
单一职责原则:一个类应该只有一个理由去改变,也就是说,一个类应该仅有一个职责或功能。这个原则强调每个模块或类应该专注于它的单一任务,这样可以降低系统的复杂性。
3.1.2 理论背景
- 维护性:当一个类的职责过多时,如果需要修改某个功能,可能会影响到其他功能,导致系统的不稳定和复杂性增加。
- 可理解性:职责明确的类更容易理解,开发者可以快速掌握它的功能和目的。
- 可重用性:当一个类专注于特定职责时,它更容易被重用在不同的上下文中。
3.1.3 例子
假设我们有一个 User
类,负责处理用户信息和发送用户的邮件。这个类违反了单一职责原则,因为它承担了两个不同的职责。
3.1.3.1 违反 SRP 的代码示例
class User {
private String name;
private String email;
public User(String name, String email) {
this.name = name;
this.email = email;
}
public void sendEmail(String message) {
// 发送邮件的逻辑
System.out.println("Sending email to " + email + ": " + message);
}
public void displayUserInfo() {
System.out.println("User: " + name + ", Email: " + email);
}
}
在这个例子中,User
类既负责用户信息的管理(如 displayUserInfo
方法),又负责发送邮件(如 sendEmail
方法)。这导致了两个职责的耦合,违反了单一职责原则。
3.1.3.2 遵循 SRP 的代码示例
我们可以将职责分开,创建一个 User
类来管理用户信息,另一个 EmailService
类来处理邮件发送。
class User {
private String name;
private String email;
public User(String name, String email) {
this.name = name;
this.email = email;
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
public void displayUserInfo() {
System.out.println("User: " + name + ", Email: " + email);
}
}
class EmailService {
public void sendEmail(User user, String message) {
// 发送邮件的逻辑
System.out.println("Sending email to " + user.getEmail() + ": " + message);
}
}
在这个遵循 SRP 的实现中:
-
User
类只负责用户信息的管理。 -
EmailService
类只负责发送邮件。
3.2 开放-关闭原则(OCP)
开放-关闭原则(Open-Closed Principle,OCP) 是软件设计中的一个重要原则,强调通过抽象和接口实现代码的扩展性。通过遵循 OCP,可以在不修改现有代码的情况下,轻松添加新功能,从而提高系统的灵活性、可维护性和可重用性。这一原则在软件开发中广泛应用,特别是在设计大型系统和框架时。以下是对开放-关闭原则的详细解释和示例。
3.2.1 定义
开放-关闭原则:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着我们应该能够在不修改现有代码的情况下,扩展系统的功能。
3.2.2 理论背景
- 扩展性:通过遵循 OCP,系统可以在不影响现有功能的情况下,轻松添加新功能。
- 维护性:减少对现有代码的修改可以降低引入错误的风险,提高代码的稳定性。
- 可重用性:通过抽象和接口,能够实现更高的代码重用率。
3.2.3 例子
假设我们有一个简单的图形绘制程序,它支持绘制圆形和方形。如果我们通过修改现有的代码来添加新形状(如三角形),这将违反开放-关闭原则。
3.2.3.1 违反 OCP 的代码示例
class GraphicEditor {
public void drawShape(Shape shape) {
if (shape instanceof Circle) {
// 绘制圆形的逻辑
} else if (shape instanceof Rectangle) {
// 绘制矩形的逻辑
}
}
}
class Circle {
// 圆形的属性和方法
}
class Rectangle {
// 矩形的属性和方法
}
在这个例子中,GraphicEditor
类需要根据不同的形状类型进行判断,添加新形状时需要修改 drawShape
方法,这违反了开放-关闭原则。
3.2.3.2 遵循 OCP 的代码示例
我们可以通过引入一个 Shape
接口来遵循开放-关闭原则。每种形状实现该接口,GraphicEditor
类只需要调用接口的方法,而不需要了解具体的实现。
interface Shape {
void draw();
}
class Circle implements Shape {
public void draw() {
// 绘制圆形的逻辑
}
}
class Rectangle implements Shape {
public void draw() {
// 绘制矩形的逻辑
}
}
class GraphicEditor {
public void drawShape(Shape shape) {
// 调用形状的 draw 方法
shape.draw();
}
}
在这个遵循 OCP 的实现中:
-
Shape
接口定义了一个draw
方法。 - 每种形状(如
Circle
和Rectangle
)实现该接口,提供各自的绘制逻辑。 -
GraphicEditor
类只依赖于Shape
接口,可以在不修改现有代码的情况下,添加新形状(如Triangle
),只需实现Shape
接口即可。
3.3 里氏替换原则(LSP)
里氏替换原则(Liskov Substitution Principle,LSP) 是软件设计中的一个重要原则,强调子类应该能够替代父类而不影响程序的正确性。遵循这一原则有助于提高代码的可维护性和可扩展性,确保系统的灵活性。通过合理的设计,比如使用接口或抽象类,可以更好地实现这一原则,使代码结构更加清晰和稳健。以下是对里氏替换原则的详细解释和示例。
3.3.1 定义
里氏替换原则:如果 S
是 T
的子类型,那么在程序中可以用 S
替代 T
,而不影响程序的正确性。这意味着子类应该能够扩展父类的功能,而不改变父类的期望行为。
3.3.2 理论背景
- 可替代性:遵循 LSP,可以确保子类在逻辑上能够替代父类,增强了代码的灵活性和可扩展性。
- 正确性:程序在使用父类的地方,无论是使用父类还是子类,都应保持相同的行为,确保不引入错误。
3.3.3 例子
假设我们有一个 Bird
类和一个子类 Penguin
。根据 LSP,Penguin
不能是 Bird
的子类,因为企鹅不能飞。
3.3.3.1 违反 LSP 的代码示例
class Bird {
public void fly() {
// 飞行的逻辑
}
}
class Sparrow extends Bird {
// 麻雀会飞,继承 Bird 的飞行行为
}
class Penguin extends Bird {
@Override
public void fly() {
// 企鹅不会飞,不符合预期行为
throw new UnsupportedOperationException("Penguins can't fly!");
}
}
在这个例子中,Penguin
类重写了 fly
方法,抛出了异常,这违反了里氏替换原则,因为在使用 Bird
的地方,无法用 Penguin
替代 Bird
。
3.3.3.2 遵循 LSP 的代码示例
为了遵循里氏替换原则,可以将飞行和非飞行的鸟类分开,使用接口来表示飞行行为。
interface Flyable {
void fly();
}
class Bird {
// 一些共同的鸟类属性和方法
}
class Sparrow extends Bird implements Flyable {
public void fly() {
// 实现飞行逻辑
}
}
class Penguin extends Bird {
// 不实现 Flyable 接口
}
在这个遵循 LSP 的实现中:
-
Bird
类包含所有鸟类的共享逻辑。 -
Flyable
接口定义了fly
方法,只有会飞的鸟(如Sparrow
)实现该接口。 -
Penguin
类不实现Flyable
接口,这样就不会有不符合预期的行为。
3.4 依赖倒置原则(DIP)
依赖倒置原则(Dependency Inversion Principle,DIP) 是软件设计中的一个关键原则,强调高层模块和低层模块都应依赖于抽象,而不是具体实现。通过遵循这一原则,可以降低系统的耦合度,提高灵活性和可维护性,使代码更容易进行修改和扩展。此外,依赖于抽象还增强了可测试性,便于进行单元测试和模拟。以下是对依赖倒置原则的详细解释和示例。
3.4.1 定义
依赖倒置原则:高层模块不应依赖低层模块,二者都应依赖抽象;抽象不应依赖细节,细节应依赖抽象。这意味着我们应该依赖于接口或抽象类,而不是具体的实现。
3.4.2 理论背景
- 降低耦合:通过依赖抽象而不是具体实现,可以减少模块之间的耦合,提高代码的可重用性。
- 提高灵活性:系统更容易进行扩展和修改,因为改变具体实现不会影响到依赖于它的高层模块。
- 增强可测试性:依赖于抽象使得单元测试更容易,因为可以用模拟对象替代具体实现。
3.4.3 例子
假设我们有一个 EmailService
类依赖于具体的 SmtpClient
类。如果我们想要更改发送邮件的方式,必须修改 EmailService
类,违反了依赖倒置原则。
3.4.3.1 违反 DIP 的代码示例
class SmtpClient {
public void sendEmail(String message) {
// 发送邮件的逻辑
}
}
class EmailService {
private SmtpClient smtpClient;
public EmailService() {
// 依赖于具体实现
this.smtpClient = new SmtpClient();
}
public void send(String message) {
smtpClient.sendEmail(message);
}
}
在这个例子中,EmailService
类直接依赖于 SmtpClient
类,这使得它无法灵活地更改邮件发送的方式。
3.4.3.2 遵循 DIP 的代码示例
可以通过引入一个 EmailClient
接口来遵循依赖倒置原则。
interface EmailClient {
void sendEmail(String message);
}
class SmtpClient implements EmailClient {
public void sendEmail(String message) {
// 发送邮件的逻辑
}
}
class EmailService {
private EmailClient emailClient;
public EmailService(EmailClient emailClient) {
// 依赖于抽象
this.emailClient = emailClient;
}
public void send(String message) {
emailClient.sendEmail(message);
}
}
在这个遵循 DIP 的实现中:
-
EmailClient
接口定义了发送邮件的方法。 -
SmtpClient
类实现了EmailClient
接口。 -
EmailService
类依赖于EmailClient
接口,而不是具体的SmtpClient
实现。
3.5 接口隔离原则(ISP)
接口隔离原则(Interface Segregation Principle,ISP) 是软件设计中的一个重要原则,强调应避免不必要的依赖,确保类只依赖于它实际需要的接口。通过遵循这一原则,可以提高系统的灵活性和可维护性,减少类之间的耦合,增强代码的可读性和可理解性。这一原则在设计复杂系统时尤为重要,能够有效减少潜在的错误和复杂性。以下是对接口隔离原则的详细解释和示例。
3.5.1 定义
接口隔离原则:不应强迫一个类依赖于它不需要的接口。换句话说,一个接口应该只包含客户端所需要的方法,避免不必要的复杂性。
3.5.2 理论背景
- 减少耦合:通过将大型接口拆分成小型接口,可以减少类之间的依赖关系,使得系统更加灵活。
- 提高可读性:客户端只需要关心它所使用的接口,从而提高代码的可读性和理解性。
- 增强可维护性:当接口过于庞大时,修改接口可能会影响多个实现,导致代码不易维护。隔离接口可以减少这种影响。
3.5.3 例子
假设我们有一个 Animal
接口,包含所有动物的行为,包括 fly
和 swim
方法。如果一个 Dog
类实现这个接口,那么它将需要实现 swim
方法,尽管狗并不会游泳,这违反了接口隔离原则。
3.5.3.1 违反 ISP 的代码示例
interface Animal {
void fly();
void swim();
void walk();
}
class Dog implements Animal {
@Override
public void fly() {
throw new UnsupportedOperationException("Dogs can't fly!");
}
@Override
public void swim() {
// 实现游泳逻辑
}
@Override
public void walk() {
// 实现行走逻辑
}
}
在这个例子中,Dog
类被迫实现 Animal
接口的所有方法,包括不适用的方法。
3.5.3.2 遵循 ISP 的代码示例
可以将接口拆分为更小的接口,使每个接口只包含相关的方法。
interface Walkable {
void walk();
}
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
class Dog implements Walkable {
@Override
public void walk() {
// 实现行走逻辑
}
}
class Bird implements Walkable, Flyable {
@Override
public void walk() {
// 实现行走逻辑
}
@Override
public void fly() {
// 实现飞行逻辑
}
}
在这个遵循 ISP 的实现中:
-
Walkable
、Flyable
和Swimmable
接口被拆分,包含各自相关的方法。 -
Dog
类只实现Walkable
接口,而Bird
类实现Walkable
和Flyable
接口。
3.6 迪米特法则(LoD)
迪米特法则(Law of Demeter,LoD),也称为“最少知识原则”,是软件设计中的一个重要原则,强调对象之间的交互应尽量简化,减少对陌生对象的依赖。通过遵循这一原则,可以降低系统的耦合度,提高代码的可维护性和可理解性。这一原则特别适用于复杂系统的设计,有助于实现模块的独立性和灵活性。以下是对迪米特法则的详细解释和示例。
3.6.1 定义
迪米特法则:一个对象应该对其他对象有最少的了解,即一个对象只应该调用直接的朋友(直接依赖的对象)的方法,而不应该调用陌生人的方法或朋友的朋友的方法。
3.6.2 理论背景
- 降低耦合:通过限制对象之间的交互,可以降低系统的耦合度,增强模块的独立性。
- 提高可维护性:减少对象之间的依赖关系,使得系统更易于维护和修改,降低了引入错误的风险。
- 增强可理解性:对象之间的交互变得更加清晰,便于理解和调试。
3.6.3 例子
假设有一个 Order
类,它需要获取 Customer
的地址来进行配送。如果 Order
类直接访问 Customer
的 Address
对象并进一步调用方法,这就违反了迪米特法则。
3.6.3.1 违反 LoD 的代码示例
class Address {
public String getStreet() {
return "123 Main St";
}
}
class Customer {
private Address address;
public Address getAddress() {
return address;
}
}
class Order {
private Customer customer;
public void printShippingAddress() {
// 违反迪米特法则
String street = customer.getAddress().getStreet();
System.out.println("Shipping to: " + street);
}
}
在这个例子中,Order
类直接依赖于 Customer
和 Address
的实现,导致耦合度过高。
3.6.3.2 遵循 LoD 的代码示例
可以通过引入一个方法,让 Customer
类直接返回地址信息,减少 Order
类对内部实现的了解。
class Address {
public String getStreet() {
return "123 Main St";
}
}
class Customer {
private Address address;
public String getShippingAddress() {
// 提供直接访问方法
return address.getStreet();
}
}
class Order {
private Customer customer;
public void printShippingAddress() {
// 遵循迪米特法则
String street = customer.getShippingAddress();
System.out.println("Shipping to: " + street);
}
}
在这个遵循 LoD 的实现中:
-
Customer
类提供了getShippingAddress
方法,直接返回地址信息。 -
Order
类只与Customer
交互,而不直接访问Address
,从而减少了对其内部实现的了解。
3.7 合成复用原则(Composition over Inheritance)
合成复用原则(Composition over Inheritance) 是一种软件设计原则,强调通过聚合或者组合等关联关系来实现代码复用,而不是依赖于继承。这一原则旨在提高系统的灵活性、可维护性和可扩展性。以下是对合成复用原则的详细解释和示例。
3.7.1 定义
合成复用原则:优先使用对象组合来实现代码复用,而不是使用类继承。通过组合多个对象,创建新的功能,而不是通过继承单个类来扩展功能。
3.7.2 理论背景
- 降低耦合:通过组合,可以减少类之间的耦合,增强系统的灵活性和可维护性。
- 提高灵活性:组合允许动态地选择和改变行为,而继承是在编译时确定的,较少灵活。
- 避免继承的缺陷:继承会引入复杂的类层次结构,可能导致“菱形继承”等问题,使得代码难以理解和维护。
3.7.3 例子
假设我们有一个 Bird
类,它有一个 fly
方法。如果我们使用继承来创建 FlyingBird
和 NonFlyingBird
类,这可能会导致代码的复杂性。
3.7.3.1 使用继承的代码示例
class Bird {
public void eat() {
System.out.println("Eating...");
}
}
class FlyingBird extends Bird {
public void fly() {
System.out.println("Flying...");
}
}
class NonFlyingBird extends Bird {
// 不会飞的鸟
}
在这个例子中,FlyingBird
和 NonFlyingBird
是通过继承 Bird
类来实现的。如果我们需要添加其他特性(如游泳),将会使得类的层次结构变得复杂。
3.7.3.2 遵循合成复用原则的代码示例
我们可以通过组合不同的行为来遵循合成复用原则。
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
class Bird {
public void eat() {
System.out.println("Eating...");
}
}
class FlyingBird extends Bird implements Flyable {
public void fly() {
System.out.println("Flying...");
}
}
class SwimmingBird extends Bird implements Swimmable {
public void swim() {
System.out.println("Swimming...");
}
}
在这个遵循合成复用原则的实现中:
-
Flyable
和Swimmable
接口定义了不同的行为。 -
Bird
类依然提供基本功能。 -
FlyingBird
和SwimmingBird
类通过实现接口组合了不同的能力。