如何在 C# 中使用抽象工厂设计模式
文章《如何在 C# 中使用抽象工厂设计模式》最初发表于Gary Woodfine 的博客。
抽象工厂模式比工厂方法设计模式高一个抽象层次。抽象工厂模式定义了一个框架,该框架生成遵循通用模式的对象;在运行时,该框架可以与任何具体的工厂配对,从而生成遵循已定义模式的对象。
抽象工厂模式是四人帮(GoF)的一种创建型设计模式,在其开创性著作《设计模式:可复用面向对象软件的要素》中定义,书中他们提出了一系列针对常见设计问题的简单简洁的解决方案。
什么是抽象工厂模式
抽象工厂模式用于返回多个相关的对象类,每个对象类可以根据请求返回多个不同的对象。通常,抽象工厂模式会与其他工厂模式(例如简单工厂模式和工厂方法模式)结合使用。
理解抽象工厂模式的最佳方式是将其视为一个超级工厂,或者说是一个工厂的工厂。通常,它是一个接口,负责创建相关对象的工厂,而无需显式指定派生类。
抽象工厂模式的关键组成部分包括:
抽象工厂:一个用于创建抽象对象的抽象容器。
具体工厂:实现一个抽象容器来创建具体对象。
抽象对象:一个用待创建对象的属性定义的接口。
具体对象:通过参照相关的抽象对象来指代实际对象。
客户端:使用工厂创建相关对象族的客户端应用程序。
C语言抽象工厂示例
在我们的示例中,我们将继续使用之前的示例游戏应用程序,该应用程序会根据传入方法的一组用户需求来构建车辆。这是我们在“工厂方法设计模式”文章中开始创建的假设游戏。我们将重构此应用程序,引入抽象工厂设计模式,并为用户提供一些额外的选项。
用户可以指定车辆的轮子数量、是否配备发动机、是否用于载货以及是否用于载客。输入这些信息后,应用程序将返回最适合其用途的车辆类型。该游戏是一个简单的控制台应用程序,只需用户输入选择并给出答案即可。
为了说明一个问题,我尽量简化了代码。以下是我们整个控制台的代码:
static void Main(string[] args)
{
var requirements = new VehicleRequirements();
Console.WriteLine( "To Build a Vehicle answer the following questions");
Console.WriteLine("How many wheels do you have ");
var wheels = Console.ReadLine();
int wheelCount = 0;
if (!int.TryParse(wheels, out wheelCount))
{
wheelCount= 1;
}
requirements.NumberOfWheels = wheelCount;
Console.WriteLine("Do you have an engine ( Y/n )");
var engine = Console.ReadLine();
switch (engine)
{
case "Y":
requirements.Engine = true;
break;
case "N":
requirements.Engine = false;
break;
default:
requirements.Engine = false;
break;
}
Console.WriteLine("How many passengers will you be carrying ? (1 - 10)");
var passengers = Console.ReadLine();
var passengerCount = 0;
if (!int.TryParse(passengers, out passengerCount))
{
passengerCount = 1;
}
requirements.Passengers = passengerCount;
Console.WriteLine("Will you be carrying cargo");
var cargo = Console.ReadLine();
switch (cargo)
{
case "Y":
requirements.Engine = true;
break;
case "N":
requirements.Engine = false;
break;
default:
requirements.Engine = false;
break;
}
var vehicle = GetVehicle(requirements);
Console.WriteLine(vehicle.GetType().Name);
}
尽管本节中的代码目前相当混乱,而且很难阅读(这是有意为之,因为我们将在以后的文章中解决这个问题!),但它所做的只是向用户询问一组问题,并将答案添加到定义的数组中,VehicleRequirements然后将数组传递给方法,该方法随后会调用Abstract Factory我稍后将解释的方法。
该GetVehicle方法将返回一个实现了我们接口的类实例IVehicle。
private static IVehicle GetVehicle(VehicleRequirements requirements)
{
var factory = new VehicleFactory();
IVehicle vehicle;
if (requirements.HasEngine)
{
return factory.MotorVehicleFactory().Create(requirements);
}
return factory
.CycleFactory().Create(requirements);
}
这种方法让我们真正开始看到抽象工厂(或者说工厂的工厂)的实现。
在这个人为设计的例子中,我们的方法会检查我们想要创建的车辆类型是否带有发动机,并根据这个数据选择要创建的是机动车辆还是脚踏车辆,然后重定向到相应的工厂来创建车辆。理想情况下,你可能会把这个选择权交给抽象工厂本身,但我只是想说明,即使使用抽象工厂类,我们仍然可以决定使用哪个工厂。
抽象类中定义的两种方法都会提供一个实现了相应IVehicle接口的对象。应用程序无法控制对象的最终类型,但可以根据特定条件选择使用哪个工厂来构建对象。
我们的抽象工厂实际上是一个abstract类,它定义了两个方法,这两个方法需要实现另外两个工厂。你会注意到,这两个方法也只是 的实现IVehicleFactory。
public abstract class AbstractVehicleFactory
{
public abstract IVehicleFactory CycleFactory();
public abstract IVehicleFactory MotorVehicleFactory();
}
这个类只是一个抽象类,没有任何实现代码。如果需要,我们可以添加一些默认的实现代码,这些代码也可以被重写。但是,就我的目的而言,我只是把它们保留为占位符。
该类支持实现两个不同的工厂,每个工厂负责返回不同的车辆类型。在本例中,分别是自行车或机动车辆。
我们实际实现的抽象工厂类可能如下所示。
public class VehicleFactory : AbstractVehicleFactory
{
public override IVehicleFactory CycleFactory()
{
return new Cyclefactory();
}
public override IVehicleFactory MotorVehicleFactory()
{
return new MotorVehicleFactory();
}
}
事实上,我们实现的 Vehicle Factory 类可能被认为是一种糟糕的编码实践,只不过是传递方法而已,但我故意这样实现是为了说明一点:最终我们的方法很可能会调用另一个工厂。
传递方法是指除了调用另一个方法之外几乎不做任何其他操作的方法,而调用方法的签名与调用方法的签名相似或相同。
这通常表明类之间没有明确的划分。
如果我们看一下其中一个工厂的实现,CycleFactory你会发现我实际上选择了使用工厂方法模式来创建一个要返回的对象实例。我这样做是为了强调另一点,那就是作为开发人员,你可以交替使用这些模式,并与其他模式结合使用。
public class Cyclefactory : IVehicleFactory
{
public IVehicle Create(VehicleRequirements requirements)
{
switch (requirements.Passengers)
{
case 1:
if(requirements.NumberOfWheels == 1) return new Unicycle();
return new Bicycle();
case 2:
return new Tandem();
case 3:
return new Tricyle();
case 4:
if (requirements.HasCargo) return new GoKart();
return new FamilyBike();
default:
return new Bicycle();
}
}
}
尽管这是一个精心设计的例子,但它确实可以作为“工厂中的工厂”概念的例证和强调。
我尽量使这个示例中的逻辑尽可能简单,同时也试图说明这个工厂最终负责实例化并返回给调用客户端的对象。重点在于,客户端无需关心对象的创建过程,并且返回哪个对象的决策点是由多个层级分离的。
我们现在的工厂方法可以自由地实现某种规则引擎,根据需求创建车辆。
通过关注点分离,可以提高可测试性。即使在这个简单的例子中,您也会注意到客户端只需要关注已输入的数据,即尽可能在数据源附近验证数据,然后将数据传递给抽象工厂类,该类再将数据分发给所需的工厂来创建对象。
正是这一特性使得抽象工厂方法成为一种流行且强大的模式,它为单元测试提供了更多机会。将此模式与其他模式结合使用,可以进一步增强代码的可测试性。
重构工厂
我们的抽象工厂代码虽然能运行,但说实话,这段代码相当糟糕,并没有真正发挥出抽象工厂模式的所有优势。原因我之前已经提到过。
让我们对工厂模式进行一些重构。首先,我们可以选择要么完全移除客户端应用程序选择使用哪个工厂的选项,要么扩展这个选项的功能。
为了说明一种方案,我们来对代码进行一次彻底的重构和整理。我的第一个方案是移除那些糟糕的传递方法,它们实际上并没有提供清晰的抽象,而且我们完全可以去掉类中的两个工厂方法,只保留一个。这样一来,我们的类将更容易理解,并且消除任何出错的可能性。
在本例中,我们只为客户提供一个选择Factory。我们重构后的新抽象工厂类现在将只有一个方法。
public abstract class AbstractVehicleFactory
{
public abstract IVehicle Create();
}
完成这些之后,让我们继续完成工厂类实现的重构。
private readonly IVehicleFactory _factory;
private readonly VehicleRequirements _requirements;
public VehicleFactory(VehicleRequirements requirements)
{
_factory = requirements.HasEngine ? (IVehicleFactory) new MotorVehicleFactory() : new Cyclefactory();
_requirements = requirements;
}
public override IVehicle Create()
{
return _factory.Create(_requirements);
}
你会注意到,我们在这里使用了构造函数来传入需求,并利用这些数据来确定哪个工厂最适合创建我们选择的车辆。这将最大限度地降低客户端选择错误工厂的风险。
客户端方法的复杂性GetVehicle已大大降低,他们现在无需再费心检查数据和确定使用哪个工厂。他们只需在VehicleRequirements构造函数中传入参数并调用Create方法,一切就会自动完成。
private static IVehicle GetVehicle(VehicleRequirements requirements)
{
var factory = new VehicleFactory(requirements);
return factory.Create();
}
我们在这里取得的成就,是抽象概念的一个重要方面。
抽象是对实体的一种简化视图,它省略了不重要的细节。——约翰·奥斯特豪
特,《软件设计哲学》
我们成功地降低了复杂性,并为用户提供了易于使用的界面,从而保护了用户免受复杂性的困扰。
在开发模块时,要寻找机会让自己承受一些额外的痛苦,以减轻用户的痛苦。
结论
抽象工厂模式允许开发人员以通用方式定义相关对象族,而将这些对象的具体实现留到需要时再进行实现。
抽象工厂模式在 C# 中非常常见,.net Framework 也有一些库使用这种模式来提供扩展和自定义其标准组件的方法。
文章来源:https://dev.to/gary_woodfine/how-to-use-the-abstract-factory-design-pattern-in-c-1gpa