Head First设计模式学习笔记四

工厂模式

Posted by Emo on June 27, 2022

设计模式

练习实例仓库

EMO’s Blog

一句话总结设计模式

  • 策略模式:变与不变的分离,通过接口与组合构建更灵活更安全的项目结构。尽量避免修改原有的代码。
  • 观察者模式:构建解耦的类与类之间的一对多关系
  • 装饰者模式:弹性设计,构建可运行时扩展的类结构

工厂模式和生成器模式能帮助解决装饰者模式在实例化组件时的代码复杂度过高的问题。

  • 工厂模式:将对象的实例化部分封装起来。当一个抽象产品的子类实现种类特别多的时候,就需要用到工厂模式来封装管理这些子类的实例化。

提出问题

虽然我们针对接口编程,可以在之后用任何新类实现该接口。但是我们在实例化具体类的时候却始终还是在针对实现编程了。既然针对实现编程了,那么我们在扩展系统的时候也就免不了要修改原有代码。比如以下例子

Duck duck
if (picnic) {
	duck = new MallardDuck();
} else if (hunting) {
	duck = new DecoyDuck();
}

在这个例子里面我们针对不同的行为去实例化了不同的具体鸭子类型。如果我们要新增一种可能的话,比如说新增了RubberDuck这个类型,除了针对接口构建一个新的RubberDuck以外,我们还需要对原有代码进行改变

if (picnic) {
	duck = new MallardDuck();
} else if (hunting) {
	duck = new DecoyDuck();
} else if (inBAthTub) {
	duck = new RubberDuck();
}

这看上去违背了下面这个OO设计原则

开放-关闭原则: 类应该对扩展开放,对修改关闭。

解决问题

其实new或者说实例化对象并不是什么问题,关键的问题还是回到了我们最开始提出的一个OO原则

变与不变原则:封装变化

上诉的问题的例子当中if板块是涉及到需要变化的代码块,因为根据需求的变更,我们需要修改这部分的代码,所以根据上述原则我们需要将他封装起来。所以如果我们将创建对象的代码封装起来,是否就可以解决这个问题了呢?在这里我们将加深对封装变化的理解。

简单工厂模式

假设我们有一个Pizza订单系统,代码如下

Pizza orderPizza(String type) {
	Pizza pizza;
	/* 实例化板块
	根据type实例化不同类型的pizza
	*/
	pizza.prepare();
	pizza.bake();
	pizza.cut();
	pizza.box();
	return pizza;
}

在这个例子当中,我们可识别出,实列化板块是需要变化的板块,所以根据OO原则我们应当将其封装起来。而其它部分代码则是不需要变化的板块。我们可以创建一个新对象SimplePizzaFactory,然后将这部分需要变化的代码搬运到这个对象当中去封装起来。这个新对象就叫做工厂

public class SimplePizzaFactory {
	public Pizza createPizza(String type) {
		Pizza pizza = null;
		if (type.equals("cheese")) {
			pizza  = new CheesePizza();
		} else if (type.equals("pepperoni")) {
			pizza = new PepperoniCheese();
		}
		return pizza
	}
}

我们将Pizza的创建移到这个工厂里面,以后不只是我们orderPizza,其他可能会需要用到创建Pizza的方法都可以直接调用这个工厂里的方法来实现。这也是封装后的好处,只需要针对工厂进行修改后,其他所有的方法都会相应的接收到修改。这叫做简单工厂模式,很多时候这个其实只是一种编程习惯,不能叫做一种模式

实现接口很多时候并不是指实现interface类的一个具体类,而是泛指实现某一个超类型(类或者接口)的某个方法

工厂方法模式

abstract Product factoryMethod(String type)

  • 工厂方法模式定义了一个创建对象的接口,但由子类来决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。
  • 工厂方法将客户(也就是超类中的方法)与实例化具体产品的代码分离开来。
  • 当产品涉及到的子类很多的时候,如何封装管理这些子类的实例化,就需要用到工厂模式。
  • 将产品的“实现”从“使用”中解耦,同时又方便扩展各种各样的产品实现方法。

工厂方法模式则不需要构建工厂类,而是在超类中将实例化板块构建成抽象的工厂方法,而后在子类中去进行具体的实现(当然该方法也可以不是抽象的,我们也可以提供一种默认的方式,这个时候其实就跟简单工厂模式比较类似了)。比如还是Pizza例子,我们有有多家店不同的Pizza店,各个Pizza店提供的Pizza种类都不一样,这时候他们使用同样的Pizza工厂类就不合适了,因为他们都有自己独特的实例化方法。这个时候我们就可以构建一个超类PizzaShop,然后设立抽象的createPizza方法,随后在继承于PizzaShop的子类PizzaShopA和PizzaShopB中实现具体的createPizza接口。

public abstract class PizzaShop {
	public abstract Pizza createPizza(String type);
	public Pizza orderPizza(String type) {
		Pizza pizza;
		pizza = createPizza(type);
		if (pizza != null) {
			pizza.prepare();
			pizza.bake();
			pizza.cut();
			pizza.box();
		}
		return pizza;
	}
}

public class PizzaShopA extends PizzaShop {
	public Pizza createPizza(String type) {
		Pizza pizza = null;
		if (type.equals("a1")) {
			pizza = ...
		} else if (type.equals("b1")) {
			pizza = ...
		}
		return pizza
	}
}

public class PizzaShopB extends PizzaShop {
	public Pizza createPizza(String type) {
		Pizza pizza = null;
		if (type.equals("a2")) {
			pizza = ...
		} else if (type.equals("b2")) {
			pizza = ...
		}
		return pizza
	}
}

这种方式构建了两个平行的类层级,一个是产品类一个是创建者类,它们都具有一样的结构,即一个抽象的超类下实现很多具体实现的子类。我们也可以把所有的产品具体子类看作知识,而具体的创建者子类就是在管理这些知识。

创建者类需要实现所有操控产品子类的方法,但不需要实现工厂方法。

简单工厂模式和工厂方法模式的区别

简单工厂模式是创建一个可供使用的对象,而工厂方法模式则是子类通过继承超类并实现一个具体的工厂方法来进行的。或者说简单工厂模式提供的实现方式比较单一,缺乏一种规范的扩展方式。

新的OO设计原则

依赖倒置原则:要依赖抽象,不要依赖具体类。

这个原则说的是不能让高层组件依赖于底层组件。在上面的例子中,PizzaShop就是高层组件,Pizza就是低层组件。如果让PizzaShop直接依赖所有的Pizza具体实现,那么就会混乱。应用工厂模式之前,PizzaShop和Pizza实现的以来模式是 PizzaShop —–> 所有Pizza子类 使用工厂模式后是 PizzaShop —–> Pizza <—– 所有的Pizza子类 依赖倒置指的是,Pizza的所有子类现在反而在依赖高层的Pizza抽象类,更高层的PizzaShop也依赖于Pizza抽象类。这个时候Pizza的所有子类与PizzaShop依赖于同一个抽象类。

遵循此原则的指导方针:

  • 变量不可以持有具体类的应用(如果使用new,考虑用工厂模式解决)
  • 不要让类派生自具体类
  • 不要覆盖基类当中已实现的方法

抽象工厂模式

其实本质上就是将简单工厂模式中的具体类转变成抽象的接口类,然后根据这个接口类去实现具体的工厂子类。比如Pizza工厂要实现一个原料工厂来控制原料对象的实例化,不同的地区需要使用到不同的原料实例。所以不同地区需要有自己特定的原料工厂来控制原料的实例化。这个时候我们就可以构建一个PizzaIngredientFactory接口类,而后根据地区特色去实现具体的子类。

工厂模式 + 反射机制

在单纯的简单工厂模式当中,一旦我们需要新增新的子类,那么在工厂内依然需要进行修改。如果出现了大量的子类,那么就会很麻烦。这个时候我们可以使用反射机制来构建工厂类,这样就可以在添加新类的同时又不需要修改代码。

public interface Duck {
	......
}

public class DuckA implements Duck {
	......
}

public class DuckB implements Duck {
	......
}

public class DuckFactory {
	public Duck getInstance(String duckName) {
		Duck d = null;
		try {
			d = (Duck) Class.forName(duckName).newInstance();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return d;
	}
}

利用上面的方式直接将类名反射到具体的类去调用其构造方法,这样就不需要在新增子类的时候去修改代码了。


{ % if page.mermaid % } { % endif % }