面向对象的设计模式系列之三:抽象工厂模式(AbstractFactory)
在上一讲我们谈到了利用工厂方法模式解决对"某个对象"的创建工作,通常这个对象是"易变的",但它们的抽象能力却相对固定(即主模块变化相对缓慢),因此我们将通过工厂的"创建"来"封装"这个变化点,使得客户端无需知道对象的具体类型。但不管是简单工厂也好,工厂方法也罢都是针对"一类"对象的创建,当面对"一系列"(我们这里可以称维度)相互依赖的的对象时,就已经力不从心了,可能工厂的数量会指数级增长,这个可能是我们目前面临的变化点,我们同样需要一种"封装机制"来隔离这个变化点,那就是所谓的抽象工厂模式。我们都知道创建型模式都是解决new的问题,即让new的对象不依赖于某个具体的类型,在讲解抽象工厂模式之前,有两点需要强调(并且我将一直强调,因为这可能是是否采用设计模式的标准):1.封装变化点:即哪里变化,封装哪里。2.相反,如果没有变化,当然不需要额外的封装。
可能一个例子会来的更实在些:假设我们有一天需要配置一台电脑,首先我们应该去电脑城并从工作人员处索取各种电脑需要的配置信息,可能包括CPU、内存、硬盘以及其他信息,我们以CPU和内存为例,可能工作人员会给我们提供Intel或AMD的CPU,并告知我们对应的插孔以及接口什么的,我们会从这些信息中总结出几套组装信息,然后选择其中一套给配货员来配置一台电脑,但我们知道有些设备之间存在兼容性,所以只能选择兼容性的一直的设备组装。例子讲到这,可能大家会问这跟我们的抽象工厂有何关系?首先我们可以把自己当成客户端,配货员当成是工厂,CPU、内存以及其他设备当成是产品对象,而电脑是由这些产品对象组合起来的产品簇。刚才也讲了,由多个产品对象经过多种组合可以构建不同的电脑构成了"多系列",而这些产品对象时相互依赖的,配货员只需要知道创建装机方案(CPU、内存等组合,可能看成了工厂集合),便可以轻而易举的组装一台电脑。有了这个感性认识后,我们来看抽象工厂的设计意图。
设计意图:提供一个接口,让该接口负责创建"一系列相互依赖的对象",而无需知道具体的实现类。
应用场景:在软件系统中,经常面临"一系列相互依赖的对象"的创建工作,同时由于需求的变化,往往存在更多系列的创建工作。如何应对这种变化?如何绕过常规的对象创建方法new,提供一种"封装机制"来避免客户端和"多系列具体对象"的紧耦合。
实际案例:我们经常在生活中出行一般靠公交车和地铁,当我们到达固定的乘车地点(可能是公交车站或地铁站),开始买票,一般都会根据你上车的车站算起(个别城市除外,以按里程计价为准)。那对我们来说,要从售票系统购买一张车票(产品簇),我们需要两个购买接口(车票和车站),同时对于公交车而言只能使用公交车票,地铁只能使用地铁票,两者不能通用。将这个场景在一张图上表示出来:
我们从图中可以看出客户端仅仅与抽象对象有关(工厂、产品),而与具体的实现类无关。我们只要选择相应的交通工具和始发车站,就能购买一张对应的车票。而构成车票的交通工具和始发车站都是彼此依赖的,但要注意的是:多系列之间是不依赖的,如公交和地铁是彼此不依赖的。 看了这张图之后,我们接下来用程序来实现。
public abstract class Ticket { public abstract string TicketNo { get; } public abstract decimal BasePrice { get; } } public class BusTicket : Ticket { public override string TicketNo { get { return "BT-001"; } } public override decimal BasePrice { get { return 1m; } } } public class SubwayTicket : Ticket { public override string TicketNo { get { return "ST-001"; } } public override decimal BasePrice { get { return 2m; } } }
public abstract class Station { public abstract string Name { get; } } public class BusStation : Station { public override string Name { get { return "高升桥"; } } } public class SubwayStation : Station { public override string Name { get { return "红牌楼"; } } }
public interface IFactory { Ticket CreateTicket(); Station CreateStation(); } public class BusFactory : IFactory { public Ticket CreateTicket() { return new BusTicket(); } public Station CreateStation() { return new BusStation(); } } public class SubwayFactory : IFactory { public Ticket CreateTicket() { return new SubwayTicket(); } public Station CreateStation() { return new SubwayStation(); } }
[TestMethod] public void TestBusTicketSystem() { //创建公交车工厂 var factory = GetFactoryByConfig("BusFormat"); var ticket = factory.CreateTicket(); Assert.AreEqual<string>("BT-001", ticket.TicketNo); Assert.AreEqual<decimal>(1m, ticket.BasePrice); var station = factory.CreateStation(); Assert.AreEqual<string>("高升桥", station.Name); } [TestMethod] public void TestSubwayTicketSystem() { //创建地铁工厂 var factory = GetFactoryByConfig("SubwayFormat"); var ticket = factory.CreateTicket(); Assert.AreEqual<string>("ST-001", ticket.TicketNo); Assert.AreEqual<decimal>(2m, ticket.BasePrice); var station = factory.CreateStation(); Assert.AreEqual<string>("红牌楼", station.Name); } private IFactory GetFactoryByConfig(string formatKey) { var format = ConfigurationManager.AppSettings[formatKey]; var appPath = AppDomain.CurrentDomain.BaseDirectory; var assembly = Assembly.LoadFrom(Path.Combine(appPath, "DesignPattern.AbstractFactoryPattern.dll")); var type = "DesignPattern.AbstractFactoryPattern." + format + "Factory"; var factory = (IFactory)assembly.CreateInstance(type); return factory; }
从以上实例我们可以看出: 如果不存在"多系列对象创建"的需求变化,则不宜采用抽象工厂,而采用简单的静态工厂即可。假设目前只有公交车这种交通出行方式,那么在这里的抽象工厂也就没有多大意义可言。其次,抽象工厂的创建任务集合中的对象时相互依赖的,如车票和车站是有对应关系的,而不同系列(如公交和地铁)是不存在依赖关系的。抽象工厂主要应对"新系列"的需求变动,如这套售票系统将来可能推广到出租车系统等,只需要添加一个出租车工厂和对应的出租车票和出租车站即可。但却不能应对"新对象"的需求变动,如现在用户除了选择车票类型和始发车站之外,还需要选择车型,车次等。这样就会去改动抽象工厂的构建方式(主模块发生了较大变动),就使得"多系列对象创建"跟客户端紧耦合了。如果要应对这种"新对象"的需求变动,可以结合抽象工厂和工厂方法模式来解决。
从以上实例我们继续探讨,可能对于用户来说(客户端),更关心的是直接能拿到票,只需要选择是公交车还是地铁即可(也就是只需要知道采用的工厂创建方案),那样就更容易和简化购票流程(至于内部到底起价多少,当时乘车的站名等等信息不太关心)。对我们而言,只需要给用户提供一种选择方案,也许才算是最好的报答。那接下来看看程序如何实现。
/// <summary> /// 定义售票窗口(客户端) /// </summary> public class Client { //定义订票需要的项 private Ticket ticket = null; private Station station = null; //售票窗口接口 public void BuyTicket(IFactory factory) { Prepare(factory); } //订票的具体准备工作 private void Prepare(IFactory factory) { //使用工厂获取对应的子项信息 this.ticket = factory.CreateTicket(); this.station = factory.CreateStation(); } public Ticket Ticket { get { return this.ticket; } } public Station Station { get { return this.station; } } }
[TestMethod] public void TestTicketSystem() { //获取售票窗口(客户端由用户操作) var client = new Client(); //获取售票工厂(公交车) var factory = GetFactoryByConfig("BusFormat"); //已经购买一张公交车票 client.BuyTicket(factory); //验证是否正确 Assert.IsNotNull(client.Ticket); Assert.AreEqual<string>("BT-001", client.Ticket.TicketNo); Assert.AreEqual<decimal>(1m, client.Ticket.BasePrice); Assert.AreEqual<string>("高升桥", client.Station.Name); }
从以上的代码我们发现客户端仅仅与抽象工厂和抽象对象有关,而与具体的实现无关,这也就达到了我们常说的"面向接口编程,而与具体实现无关"。秉承"工厂保持简洁"原则,我们不难发现BusFactory、SubwayFactory的创建工作是如此的类似,那是否可以说把他们统一起来呢?我们这里采用抽象类与反射来封装这个细节。
public abstract class VehicleFactory { public static Ticket CreateTicket() { var type = ConfigurationManager.AppSetting["vehicle"]; return (Ticket)Activator.CreateInstance(Type.GetType(type)); } public static Station CreateStation() { var type = ConfigurationManager.AppSetting["vehicle"]; return (Station)Activator.CreateInstance(Type.GetType(type)); } }