软件构造:设计模式总结

在学习可复用和可维护的设计模式时,对各种设计模式理解不是很透彻,现总结如下。内容参考《设计模式:可复用面向对象软件的基础》(这也是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();

这时需要采用适配器模式,对其进行适配。我们可以定义一个自己的椭圆类,采用委派&

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值