在学习可复用和可维护的设计模式时,对各种设计模式理解不是很透彻,现总结如下。内容参考《设计模式:可复用面向对象软件的基础》(这也是23种设计模式的出处,MIT课件中也有部分原文),书中采用C++/Smalltalk描述,这里将描述语言改为Java,并对书中示例进行了简化处理,以便更好地说明思想。若有理解上的偏差还请指出。
创建型模式
创建型设计模式抽象了实例化过程。它们帮助一个系统独立于如何创建、组合和表示它的那些对象。一个类创建型模式使用继承改变被实例化的类,而一个对象创建型模式将实例化委托给另一个对象。
Factory Method(工厂方法)
首先考虑一个应用场景(这个示例取自原书,但进行了简化)。我们需要实现一个应用框架——Application类,它可以新建并打开文件(接口MyFile,以区别于java的File类)、删除文件等。Application的子类型可以有图片应用、文本应用,分别打开图片文件(如JPG格式)和文本文件(如TXT格式)。同时假定Application有一个文件列表,用来保存所有已经打开的文件。Application和MyFile大致的框架如下:
class Application {
private final List<MyFile> Files = new ArrayList<MyFile>();
/**
* 创建并打开文件,并将其存入列表中
*/
void newFile();
//...
}
interface MyFile {
//打开文件
void open();
//...
}
我们可以发现newFile方法的行为是相当固定的:创建文件并打开文件,将其加入列表。而且其它更具体的Application类应当继承Application,可以对其他不同的方法进行重写。因此我们考虑将Application变为抽象类,只实现newFile方法,其它方法留待子类自己实现。修改后的Application类如下:
abstract class Application {
private final List<MyFile> Files = new ArrayList<MyFile>();
/**
* 创建、打开文件,并将其加入应用的文件列表
* 问题:在父类中需要将文件实例化,但又不知道是哪种文件
*/
void newFile() {
//实现上述功能
}
}
这里有一个问题:由于需要打开文件(调用它的open方法),又需要把它加入列表中,我们难以避免地需要将文件实例化。但是在父类Application中我们无法得知继承它的子类到底是谁,是要打开JPG还是TXT,也就无法直接实例化。我们可能会想放弃采用抽象类来实现公用方法newFile,这就需要我们在每个子类中都重写一次newFile。但工厂方法模式可以解决这个问题,代码如下:
abstract class Application {
private final List<MyFile> Files = new ArrayList<MyFile>();
/**
* 创建、打开文件,并将其加入应用的文件列表
* 问题:在父类中需要将文件实例化,但又不知道是哪种文件
*/
void newFile() {
MyFile file = createFile();
file.open();
Files.add(file);
}
/**
* 工厂方法,将“产品”的创建留待子类实现
* @return 新的文件
*/
abstract MyFile createFile();
}
工厂方法模式引入工厂方法createFile().可以看到它是一个返回MyFile类的抽象方法,也就是让子类来实现这个方法。在newFile中直接调用该方法来获取一个新的对象,在子类的createFile方法被实现后,它的newFile方法中调用的createFile就会变为它想创建的具体“产品”(JPG/TXT)。如:
abstract class Application {
private final List<MyFile> Files = new ArrayList<MyFile>();
/*
* 创建、打开文件,并将其加入应用的文件列表
*/
void newFile() {
MyFile file = createFile();
file.open();
Files.add(file);
}
abstract MyFile createFile();
}
interface MyFile {
void open();
//...
}
class JPG implements MyFile {
public void open() {
System.out.println("Open JPG!");
}
}
class PictureApplication extends Application {
public MyFile createFile() {
return new JPG();
}
}
public class FactoryMethod {
static public void main(String[] args) {
Application app = new PictureApplication();
app.newFile();
}
}
这样我们无需在父类中指定具体的产品类(文件),就可以方便地定义共性的newFile方法,这是通过在子类PictureApplication中重写工厂方法createFile实现的。
工厂方法模式更一般的结构如下:
Creator对应的就是上面的Application抽象类;ConcreteCreator对应Application的子类(如图片应用);FactoryMethod即工厂方法,AnOperation对应我们要用工厂方法实现的共性操作。Product为产品接口(例子中的文件),ConcreteProduct是Product的实现类。
事实上我们已经在Lab2中见过这种设计模式了(虽然当时并不是我们自己写的)。回想一下测试文件GraphInstanceTest.java:
public abstract class GraphInstanceTest {
/**
* Overridden by implementation-specific test classes.
*
* @return a new empty graph of the particular implementation being tested
*/
public abstract Graph<String> emptyInstance();
@Test
public void testAdd() {
//...
}
当时我们需要测试两个类:ConcreteEdgesGraph和ConcreteVerticesGraph。由于二者测试有相似性,所以我们把二者的共同点提取到抽象类GraphInstanceTest中实现。我们此时遭遇了同样的问题:在GraphInstanceTest中,我们不知道继承它的子类到底想实例化哪种Graph(ConcreteEdgesGraph/ConcreteVerticesGraph).因此采用工厂方法模式,引入工厂方法emptyInstance,留待两个子测试类实现(在这里,“产品”为两种图):
public class ConcreteEdgesGraphTest extends GraphInstanceTest {
/*
* Provide a ConcreteEdgesGraph for tests in GraphInstanceTest.
*/
@Override public Graph<String> emptyInstance() {
return new ConcreteEdgesGraph<>();
}
//...
}
适用性
在以下场景下可以考虑使用工厂方法模式:
1.当一个类不知道它所必须创建的对象的类时;
2.当一个类希望由它的子类来指定它所创建的对象的时候;
3.当类将创建对象的职责委托给多个帮助子类中的某一个,并且你希望将哪一个帮助子类是代理者这一信息局部化的时候。(原文:Delegates responsibility to one of multiple helper subclasses, and you need to localize the knowledge of which helper is the delegate.这句话比较难懂,个人理解与2.的目的类似)
优点
工厂方法不再将与特定应用有关的类绑定到你的代码中。代码仅处理Product接口,因此它可以与用户定义的任何ConcreteProduct类一起使用。
(原文:Eliminates the need to bind application-specific classes to your code.Code deals only with the Product interface, so it can work with any user-defined ConcreteProduct.个人觉得中文翻译版有的地方会导致迷惑,故附上原文)
个人理解这段话的意思是,工厂方法模式提供了一种避免直接操作产品类(依赖转置原则)的方法。
缺点
工厂方法的潜在缺点在于,客户可能仅仅为了创建一个特定的ConcreteProduct对象就不得不创建Creator的子类(如,为了创建一个JPG就不得不创建一个PictureApplication)。
当Creator子类不是必须的时,客户现在必然要处理类演化的其他方面。但是当客户无论如何必须创建Creator的子类时,创建子类也是可行的。
(原文:This would be acceptable if the client has to subclass the Creator anyway, but if not then the client has to deal with another point of evolution.这句话的译文尤其迷惑,个人理解为:由于客户不需要Creator,就必然要操作具体的产品类,违反了依赖转置原则——面向接口编程)
结构型模式
结构型模式涉及如何组合类和对象以获得更大的结构。
Adapter(适配器)
同样以一个例子引入。假如我们要实现一个描述平面上几何图形的类,其接口Shape如下:
interface Shape {
/**
* 把图形移动到指定位置
* @param x 目的横坐标
* @param y 目的纵坐标
*/
void moveTo(double x, double y);
/**
* 计算图形面积
* @return 图形的面积
*/
double calculateArea();
/**
* 计算图形周长
* @return 图形的周长
*/
double calculatePerimeter();
//...
}
我们计划实现圆、正方形、椭圆等图形(在学习里氏替换原则LSP时,我们们知道正方形不应该继承长方形,同理椭圆也不应当是圆的子类)。其中圆和正方形已经实现如下:
class Circle implements Shape {
private double x, y;
private double radius;
public Circle(double radius) {
this.radius = radius;}
@Override
public void moveTo(double x, double y) {
this.x = x;
this.y = y;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;}
@Override
public double calculatePerimeter() {
return 2 * Math.PI * radius;}
}
class Square implements Shape {
private double x, y;
private double side;
//...重写方法
}
现在我们还要实现椭圆类。我们知道椭圆没有初等的精确周长计算公式(需要积分),所以想偷个懒用别人实现好的库来实现我们自己的类。但是我们不一定直接能拿到该类的源码,别人实现好的类也基本不可能和我们的类的方法完全一致。但是我们复用代码一定能获取到它的方法列表。比如它的方法签名长这个样子:
//OtherOval.java
/**
* 修改位置
* @param x 目的横坐标
* @param y 目的纵坐标
*/
public void setPosition(double x, double y);
/**
* 计算椭圆面积
* @return 椭圆面积
*/
public double getArea();
/**
* 计算椭圆周长
* @return 椭圆周长
*/
public double getPerimeter();
这时需要采用适配器模式,对其进行适配。我们可以定义一个自己的椭圆类,采用委派&