简单工厂模式

内容

  1. 简单工厂模式之前
  2. 简单工厂模式

简单工厂模式之前

案例分析:

某软件公司欲开发一套图表库,该图表库可以为应用系统提供各种不同外观的图表,例如柱状图、饼状图、折线图等。该公司图表库设计人员希望为应用系统开发人员提供一套灵活易用的图表库,而且可以较为方便地对图表库进行扩展,以便能够在将来增加一些新类型的图表。

该软件公司图表库设计人员提出了一个初始设计方案,将所有图表的实现代码封装在一个Chart类中,其框架代码如下所示:

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
class Chart
{
private:
string type;
public:
Chart(const string & type) : type(type)
{
if(type == "histogram")
{
//初始化柱状图
}
else if(type == "pie")
{
//初始化饼状图
}
else if(type == "line")
{
//初始化折线图
}
}
void display()
{
if(type == "histogram")
{
//显示柱状图
}
else if(type == "pie")
{
//显示饼状图
}
else if(type == "line")
{
//显示折线图
}
}
};
int main()
{
Chart("pie");
}

客户端代码通过调用Chart类的构造函数来创建图表对象,根据参数type的不同可以得到不同类型的图表,然后再调用display()方法来显示相应的图表。

Chart类是一个“巨大的”类,在该类的设计中存在如下几个问题:

  1. 在Chart类中包含很多“if…else…”代码块,整个类的代码相当冗长,代码越长,阅读难度、维护难度和测试难度也越大;而且大量条件语句的存在还将影响系统的性能,程序在执行过程中需要做大量的条件判断。
  2. Chart类的职责过重,它负责初始化和显示所有的图表对象,将各种图表对象的初始化代码和显示代码集中在一个类中实现,违反了“单一职责原则”,不利于类的重用和维护;而且将大量的对象初始化代码都写在构造函数中将导致构造函数非常庞大,对象在创建时需要进行条件判断,降低了对象创建的效率。
  3. 当需要增加新类型的图表时,必须修改Chart类的源代码,违反了“开闭原则”
  4. 客户端只能通过new关键字来直接创建Chart对象,Chart类与客户端类耦合度较高,对象的创建和使用无法分离。
  5. 客户端在创建Chart对象之前可能还需要进行大量初始化设置,例如设置柱状图的颜色、高度等,如果在Chart类的构造函数中没有提供一个默认设置,那就只能由客户端来完成初始设置,这些代码在每次创建Chart对象时都会出现,导致代码的重复。

面对一个如此巨大、职责如此重,且与客户端代码耦合度非常高的类,我们应该怎么办?简单工厂模式将在一定程度上解决上述问题。

简单工厂模式的思想

为何需要工厂[Java代码讲解]

在开始简单工厂模式前,我们先探究一个问题——创建对象可以很灵活,为什么还需要工厂?

与一个对象相关的职责通常有三类:对象本身所具有的职责、创建对象的职责和使用对象的职责。对象本身的职责比较容易理解,就是对象自身所具有的一些数据和行为,可通过一些公开的方法来实现它的职责。在本文,我们将简单讨论一下对象的创建职责和使用职责。

创建对象的方式有很多,譬如在Java语言中,有以下几种创建对象的方式:

  1. new关键字
  2. 反射机制
  3. clone()方法
  4. 通过工厂类创建对象

毫无疑问,在客户端代码中直接使用new关键字是最简单的一种创建对象的方式,但是它的灵活性较差,下面通过一个简单的示例来加以说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class LoginAction
{
private UserDAO udao;
public LoginAction()
{
udao = new JDBCUserDAO(); //创建对象
}
public void execute()
{
// ...
udao.findUserById(); //使用对象
// ...
}
}

在LoginAction类中定义了一个UserDAO类型的对象udao,在LoginAction的构造函数中创建了JDBCUserDAO类型的udao对象,并在execute()方法中调用了udao对象的findUserById()方法,这段代码看上去并没有什么问题。

下面分析以下LoginAction和UserDAO之间的关系,LoginAction类负责创建了一个UserDAO子类的对象并使用UserDAO的方法来完成相应的业务处理,也就是说LoginAction即负责udao的创建又负责udao的使用。创建对象和使用对象的职责耦合在一起,这样的设计会导致一个很严重的问题:如果在LoginAction中希望能够使用UserDAO的另一个子类如HibernateUserDAO类型的对象,必须修改LoginAction类的源代码(比如,修改构造函数,udao=new HibernateUserDAO();),违反了“开闭原则”

如何解决该问题?

最常用的一种解决方法是将udao对象的创建职责从LoginAction类中移除,在LoginAction类之外创建对象,那么谁来负责创建UserDAO对象呢?答案是工厂类。

通过引入工厂类,客户类(如LoginAction)不涉及对象的创建,对象的创建者也不需设计对象的使用。引入工厂类UserDAOFactory之后的结构如图

image-20220224235938416

好处

好处1-分离创建职责和使用职责

工厂类的引入降低了因为产品或工厂类改变所造成的维护工作量。如果UserDAO的某个子类的构造函数发生改变或者要需要添加或移除不同的子类,只要维护UserDAOFactory的代码,而不会影响到LoginAction;如果UserDAO的接口发生改变,例如添加、移除方法或改变方法名,只要修改LoginAction(譬如execute函数内部调用udao的方法),不会给UserDAOFactory带来任何影响。

工厂模式说来说去,只强调了一点:两个类A和B之间的关系应该仅仅是A创建B或者是A使用B,而不能两种关系都有。将对象的创建和使用分离。使得系统更加符合“单一职责原则”,有利于对功能的复用和系统的维护。

好处2-工厂类将对象的创建代码、配置环境封装

此外,将对象的创建和使用分离还有一个好处:防止用来实例化一个类的数据和代码在多个类中到处都是,可以将有关创建的知识搬移到一个工厂类中。这在Joshua Kerievsky的《重构与模式》一书中有专门的一节来进行介绍。因为有时候我们创建一个对象不只是简单调用其构造函数,还需要设置一些参数,可能还需要配置环境,如果将这些代码散落在每一个创建对象的客户类中,势必会出现代码重复、创建蔓延的问题,而这些客户类其实无须承担对象的创建工作,它们只需使用已创建好的对象就可以了。此时,可以引入工厂类来封装对象的创建逻辑和客户代码的实例化/配置选项

好处3-可以增加代码可读性

使用工厂类还有一个“不是特别明显的”优点,一个类可能拥有多个构造函数,而在Java、C++等语言中构造函数名字都与类名相同,客户端只能通过传入不同的参数来调用不同的构造函数创建对象,从构造函数和参数列表中也许大家根本不了解不同构造函数所构造的产品的差异。但如果将对象的创建过程封装在工厂类中,我们可以提供一系列名字完全不同的工厂方法,每一个工厂方法对应一个构造函数,客户端可以以一种更加可读、易懂的方式来创建对象,而且,从一组工厂方法中选择一个意义明确的工厂方法,比从一组名称相同参数不同的构造函数中选择一个构造函数要方便很多。

在下图,矩形工厂类RectangleFactory提供了两个工厂方法createRectangle()和createSquare(),一个用于创建长方形,一个用于创建正方形,这两个方法比直接通过构造函数来创建长方形或正方形对象意义更加明确,也在一定程度上降低了客户端调用时出错的概率。image-20220225002101931

何时不需要

那么,是否需要为设计中的每一个类都配备一个工厂类?答案是:具体情况具体分析。如果产品类很简单,而且不存在太多变数,其构造过程也很简单,此时无须为其提供工厂类,直接在使用之前实例化即可,例如Java语言中的String类,我们就无须为它专门提供一个StringFactory,这样做反而有点像杀鸡用牛刀,大材小用,而且会导致工厂泛滥,增加系统的复杂度。

简单工厂模式的开始

简单工厂模式的定义

简单工厂模式(Simple Factory Pattern):定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。在简单工厂模式中用于创建实例的方法是静态(static)方法,因此简单工厂模式又被称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。

在简单工厂模式结构图中包含如下几个角色:

  1. Factory(工厂角色)
    • 工厂角色即工厂类,它是简单工厂模式的核心,负责实现创建所有产品实例的内部逻辑
    • 工厂类可以被外界直接调用,创建所需的产品对象
    • 在工厂类中提供了静态的工厂方法factoryMethod(),它的返回类型为抽象产品类型Product
  2. Product(抽象产品角色)
    • 它是工厂类所创建的所有对象的父类,封装了各种产品对象的公有方法
    • 它的引入将提高系统的灵活性,使得在工厂类中只需定义一个通用的工厂方法,因为所有创建的具体产品对象都是其子类对象
  3. ConcreteProduct(具体产品角色)
    • 它是简单工厂模式的创建目标,所有被创建的对象都充当这个角色的某个具体类的实例
    • 每一个具体产品角色都继承了抽象产品角色,需要实现在抽象产品中声明的抽象方法。

简单工厂模式的设计步骤

在使用简单工厂模式时,首先需要对产品类进行重构,不能设计一个包罗万象的产品类,而需根据实际情况设计一个产品层次结构,将所有产品类公共的代码移至抽象产品类,并在抽象产品类中声明一些抽象方法,以供不同的具体产品类来实现。

  1. 首先将需要创建的各种不同对象(例如各种不同的Chart对象)的相关代码封装到不同的类中,这些类称为具体产品类,而将它们公共的代码进行抽象和提取后封装在一个抽象产品类中,每一个具体产品类都是抽象产品类的子类;
  2. 然后提供一个工厂类用于创建各种产品,在工厂类中提供一个创建产品的工厂方法,该方法可以根据所传入的参数不同创建不同的具体产品对象;
  3. 客户端只需调用工厂类的工厂方法并传入相应的参数即可得到一个产品对象。

典型的抽象产品类代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
class Product
{
public:
Product() {cout << "Product()" << endl;}
virtual ~Product() {cout << "~Product" << endl;}
virtual void methodSame()
{
cout << "this is Product" << endl;
//公共方法的实现
}
virtual void methodDiff() = 0;//抽象业务方法
};

在具体产品类中实现了抽象产品类中声明的抽象业务方法,不同的具体产品类可以提供不同的实现,典型的具体产品类代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ConcreteProductA : public Product
{
public:
ConcreteProductA() {cout << "ConcreteProductA()" << endl;}
~ConcreteProductA() {cout << "~ConcreteProductA" << endl;}
virtual void methodDiff()
{
cout << "this is A" << endl;
// 业务方法的覆盖实现
}
};
class ConcreteProductB : public Product
{
public:
ConcreteProductB() {cout << "ConcreteProductB()" << endl;}
~ConcreteProductB() {cout << "~ConcreteProductB" << endl;}
virtual void methodDiff()
{
cout << "this is B" << endl;
// 业务方法的覆盖实现
}
};

简单工厂模式的核心是工厂类,在没有工厂类之前,客户端一般会使用new关键字来直接创建产品对象,而在引入工厂类之后,客户端可以通过工厂类来创建产品,在简单工厂模式中,工厂类提供了一个静态工厂方法供客户端使用,根据所传入的参数不同可以创建不同的产品对象,典型的工厂类代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Factory
{
public:
Factory() {cout << "Factory()" << endl;}
~Factory() {cout << "~Factory" << endl;}
static Product* getProduct(const string & arg)
{
Product* product = nullptr;
if(arg == "A")
{
product = new ConcreteProductA();
// ... 初始化设置product
}
else if(arg == "B")
{
product = new ConcreteProductB();
// ... 初始化设置product
}
return product;
}
};

在客户端代码中,我们通过调用工厂类的工厂方法即可得到产品对象,典型代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
int main()
{
unique_ptr<Product> pA = unique_ptr<Product>(Factory::getProduct("A"));
unique_ptr<Product> pB = unique_ptr<Product>(Factory::getProduct("B"));
pA->methodSame();
pA->methodDiff();
pB->methodSame();
pB->methodDiff();

return 0;
}

示例结构

简单工厂模式的要点在于:当你需要什么,只需要传入一个正确的参数,就可以获取你所需要的对象,而无须知道其创建细节。 简单工厂模式结构比较简单,其核心是工厂类的设计image-20220225002900697

参考文献

1
[1] 刘伟. 设计模式.

设计原则

内容

  1. 面向对象的设计原则
  2. 常见七种设计原则

面向对象的设计原则:可维护性的复用

对于面向对象软件系统的设计而言,在支持可维护性的同时,提高系统的可复用性是一个至关重要的问题。
如何同时提高一个软件系统的可维护性和可复用性是面向对象设计需要解决的核心问题之一。
在面向对象设计中,可维护性的复用是以设计原则为基础的。

面向对象设计原则为支持可维护性复用而诞生,这些原则蕴含在很多设计模式中,它们是从许多设计方案中总结出的指导性原则。面向对象设计原则也是用于评价一个设计模式的重要指标之一,在设计面向对象软件系统的过程中,要多思考,“XXX模式符合XXX原则吗?”、“XXX模式违反了XXX原则吗?”。

常见设计原则 - SOLID

  1. Robert C. Martin(“Uncle Bob”)在2000年代初期提出并推广了SOLID原则,《Agile Software Development, Principles, Patterns, and Practices》一书中,对这些原则进行了详细的解释和讨论。
  2. 里氏代换原则(LSP)由计算机科学家Barbara Liskov在1987年的一篇名为《Data Abstraction and Hierarchy》的论文中首次提出。虽然Liskov并没有明确提出SOLID这个术语,但她的工作为其中的LSP原则奠定了基础。
设计原则名称 定义 使用频率
单一职责原则, Single Responsibility, SRP 一个类只负责一个功能领域中的相应职责 ★★★★☆
开闭原则, Open-Closed, OCP 软件实体应对扩展开放,而对修改关闭 ★★★★★
里氏代换原则, Liskov Substitution, LSP 如果一个子类可以替换其父类,并且程序的行为没有发生变化,那么这个子类是符合里氏代换原则的。
换句话说,子类对象必须能够替换父类对象,并且不影响程序的正确性。
★★★★★
接口隔离原则, Interface Segregation, ISP 客户端不应该被迫依赖它们不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。
具体来说,接口隔离原则提倡将臃肿的接口拆分为多个小型的、特定客户的接口,这样每个接口只包含客户端真正需要的方法。
★★☆☆☆
依赖倒转原则, Dependence Inversion, DIP (软件设计的灵魂)高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。 ★★★★★

开闭原则

一个软件实体(类、模块、函数等)应该在不修改其源代码的情况下,扩展或增强其行为。
通过在不修改现有代码的情况下增加新功能,减少对已稳定系统的影响,避免引入新的错误。这可以通过使用接口、抽象类和多态等面向对象的设计机制来实现。

示例

假设我们有一个简单的图形绘制程序,最初只支持绘制圆形。为了遵循开闭原则,我们首先定义一个图形接口,并实现一个圆形类:

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
// 图形接口
interface Shape {
void draw();
}

// 圆形类
class Circle implements Shape {
@Override
public void draw() {
System.out.println("Drawing a Circle");
}
}

// 绘制类
class Drawing {
private List<Shape> shapes;

public Drawing() {
shapes = new ArrayList<>();
}

public void addShape(Shape shape) {
shapes.add(shape);
}

public void drawShapes() {
for (Shape shape : shapes) {
shape.draw();
}
}
}

public class Main {
public static void main(String[] args) {
Drawing drawing = new Drawing();
drawing.addShape(new Circle());
drawing.drawShapes();
}
}

在这个例子中,我们的绘制程序可以绘制圆形。如果我们需要添加新的图形,例如矩形,我们可以通过扩展而不是修改现有代码来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 矩形类
class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Drawing a Rectangle");
}
}

public class Main {
public static void main(String[] args) {
Drawing drawing = new Drawing();
drawing.addShape(new Circle());
drawing.addShape(new Rectangle());
drawing.drawShapes();
}
}

通过添加一个实现了Shape接口的新类Rectangle,我们可以扩展绘制程序的功能,而不需要修改现有的Drawing类和Circle类。这种设计符合开闭原则,因为我们通过扩展(添加新类)而不是修改现有代码来增加新功能。

依赖倒转原则(DIP)

  1. 从项目管理的角度来说,如果A直接依赖B,那么开发周期就会被B模块拖累,A模块闲置,人力效率不高,从甘特图看需要开发28天。
    1. 而如果A、B在事先经过良好的协商,规范出一套接口的话,A、B就可以共同依赖这个接口,各自同时开发。假如协商需要3天,那么开发周期只需要3+14=173+14=17天。
  2. 从软件的扩展性来说,如果A直接依赖于B,没有事先协商好,那么B可能不能完全满足A的需求,到时候还是需要不断修改代码,开发效率低下。
    1. 如果都依赖一个接口,那么后期的扩展只需要在接口上增设方法即可。

示例

假设有一个简单的场景,一个Service类依赖于一个Repository类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Repository {
public void save(Object data) {
// 保存数据的实现
}
}

public class Service {
private Repository repository;

public Service() {
this.repository = new Repository();
}

public void process(Object data) {
// 处理数据
repository.save(data);
}
}

在这个例子中,Service类直接依赖于具体的Repository类。这种设计违反了依赖倒转原则。如果我们想要更改数据存储的方式,比如使用另一个数据存储实现,就必须修改Service类的代码。

按照依赖倒转原则,我们可以使用接口来实现依赖注入,将Repository的实现抽象化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface IRepository {
void save(Object data);
}

public class Repository implements IRepository {
@Override
public void save(Object data) {
// 保存数据的实现
}
}

public class Service {
private IRepository repository;

public Service(IRepository repository) {
this.repository = repository;
}

public void process(Object data) {
// 处理数据
repository.save(data);
}
}

在这个设计中,Service类依赖于IRepository接口,而不是具体的Repository类。这样,如果我们想要更改数据存储的方式,只需要提供一个实现IRepository接口的新类即可,而不需要修改Service类的代码。通过这种方式,高层模块(Service)和低层模块(Repository)都依赖于抽象(IRepository),细节(Repository的具体实现)依赖于抽象(IRepository),符合依赖倒转原则。

这种设计提高了系统的灵活性和可维护性,使得代码更容易扩展和修改。

接口隔离原则

示例

假设有一个大型接口IMultiFunctionDevice,其中包含多种功能的方法:

1
2
3
4
5
6
public interface IMultiFunctionDevice {
void print();
void scan();
void fax();
void copy();
}

如果某个客户端只需要打印功能,那么它必须实现所有的方法,即使不需要扫描、传真和复印功能。这违反了接口隔离原则。

按照接口隔离原则,我们可以将这个大接口拆分为多个小接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface IPrinter {
void print();
}

public interface IScanner {
void scan();
}

public interface IFax {
void fax();
}

public interface ICopier {
void copy();
}

然后,对于只需要打印功能的客户端,它只需要依赖IPrinter接口:

1
2
3
4
5
6
public class SimplePrinter implements IPrinter {
@Override
public void print() {
// 实现打印功能
}
}

这样,客户端只依赖于它需要的接口,实现了接口隔离原则的目标。

里氏替换原则

子类对象必须能够替换父类对象(兼容父类对象的功能),并且不影响程序的正确性。
里氏代换原则主要有以下几个要点:

  1. 子类必须完全实现父类的方法:子类不能抛出父类没有抛出的异常,子类的方法参数要与父类方法参数类型一致,返回值类型也要一致或是其子类型。
  2. 子类可以有自己的特性,但这些特性不能改变父类原有功能的含义和行为。
  3. 继承的原则:子类应该扩展父类的功能,而不是削弱父类的功能。也就是说,子类不应该删除父类中已有的功能。

比如:Old Bank的add行为结果是3,而New Bank的add虽然形式上一致,但行为却不正确。违反了里氏替换原则。猜测New Bank在重写add方法后,优化代码或更新、升级组件后造成了副作用,导致运行时行为不一致、结果不正确。

示例

考虑一个父类Bird和一个子类Penguin的例子:

1
2
3
4
5
6
7
8
9
10
11
12
public class Bird {
public void fly() {
System.out.println("Bird is flying");
}
}

public class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguins cannot fly");
}
}

在这个例子中,Penguin不能替换Bird,因为企鹅不能飞。这违反了里氏代换原则。正确的做法是重新设计类结构,使得企鹅和其他鸟类能够更好地反映现实中的行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Bird {
public void move() {
System.out.println("Bird is moving");
}
}

public class FlyingBird extends Bird {
public void fly() {
System.out.println("Bird is flying");
}
}

public class Penguin extends Bird {
@Override
public void move() {
System.out.println("Penguin is walking or swimming");
}
}

在这个设计中,FlyingBird继承了Bird,并增加了fly方法,而企鹅只是继承了Bird,并重写了move方法。这样,企鹅和其他鸟类都能正确替换Bird,符合里氏代换原则。

其他设计原则

设计原则名称 定义 使用频率
合成复用原则, Composite Reuse, CRP 尽量使用对象组合,而不是继承来达到复用的目的 ★★★★☆
迪米特法则, Law of Demeter, LoD 一个软件实体应当尽可能少地与其他实体发生相互作用,只与最近关系的实体关联。 ★★★☆☆

迪米特法则

迪米特法则由Ian Holland在1987年提出,并首次在Northeastern University(美国东北大学)的Demeter Project中得到应用,因此得名。
该原则被认为是一种促进模块化和高内聚低耦合设计的重要策略,广泛应用于面向对象的软件设计中。

核心思想是:一个对象应当对其他对象有最少的了解,具体来说:

  1. 一个对象只应与直接的朋友通信:即仅与自己直接持有的对象或在方法中创建的对象通信,而不应与被间接持有的对象通信。
  2. 避免“火车式”方法调用:不要通过一个对象链式地调用多个对象的方法,例如 a.getB().getC().doSomething()。
  3. 通过引入中介减少对象间的直接依赖:通过封装对象之间的关系,使得对象之间的通信通过中介完成,从而减少直接依赖。

示例

假设有以下类结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Engine {
public void start() {
System.out.println("Engine started");
}
}

class Car {
private Engine engine;

public Car() {
this.engine = new Engine();
}

public Engine getEngine() {
return engine;
}
}

class Driver {
public void startCar(Car car) {
car.getEngine().start(); // 违反了迪米特法则
}
}

在这个例子中,Driver类通过Car类访问Engine类的方法,违反了迪米特法则。我们可以通过引入一个中介方法来改进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Car {
private Engine engine;

public Car() {
this.engine = new Engine();
}

public void startEngine() {
engine.start();
}
}

class Driver {
public void startCar(Car car) {
car.startEngine(); // 符合迪米特法则
}
}

在改进后的设计中,Driver类不再直接访问Engine类的方法,而是通过Car类的startEngine方法来启动引擎。这符合迪米特法则,降低了对象之间的耦合性,提高了系统的可维护性。

设计原则的价值

优秀的面向对象的程序应该是有以下五个特征的:低耦合,高内聚,高复用,易维护,易扩展。

  1. 开闭原则让程序易扩展,易维护。
  2. 里氏替换原则、依赖倒置和合成复用原则可以降低耦合。
  3. 接口隔离、单一职责原则和迪米特原则可以提高内聚性。

而低耦合,高内聚的代码才能实现高复用。