目录
前言
通过C#的类、接口、委托等知识,实现抽象工厂模型。
一、功能说明
使用 C# 编码,实现对周黑鸭工厂的产品生产统一管理,假设周黑鸭主要产品包括鸭脖和鸭翅。武汉工厂能生生产鸭脖和鸭翅,南京工厂只能生产鸭翅,长沙工厂只能生产鸭脖。
基本框架如下:
- 定义接口 IProductionFactory,包含生产鸭脖和鸭翅的方法。
- 定义类WuhanFactory、NanjingFactory、ChangshaFactory分别实现接口 IProductionFactory,用于具体的生产工厂。
- 使用委托 ProductionDelegate 定义生产委托。
- 在 Main 函数中,创建不同工厂的实例,并通过生产委托进行生产。
下面将会有一段较长篇幅的基础知识介绍,想直接看代码可以从目录跳转到“三、代码实现”,如果想看代码的进一步拓展,可以从目录跳转到“五、功能拓展”。
二、基础知识准备
本节将会对C#中类、接口和委托的知识进行简单的介绍。这里只是个人认为有助于理解抽象工厂模型的知识,若想详细、准确地了解,请翻阅更专业的书籍。如有错误,敬请雅正。
1、类
类(Class)是一种用户自定义的引用类型,用于创建对象。类可以为具有相同属性和方法的对象定义一个模板。这些对象根据类的定义被创建,并称为类的实例。
声明格式:访问修饰字 class 类名{变量声明,方法声明}
C#所有的代码都是在某一个类中,因此不可能在类之外的全局区域定义变量和方法。
类中的成员包含常量、字段、类型、方法、属性、事件、索引器、运算符、实例构造函数、析构函数、静态构造函数等。
下面,是一个类的例子。
这个例子是用类写一个打招呼功能,输入名字和年龄,输出完整的问好语句。
先写出类的框架,定义一个Person类。后面的代码均写在这个框架中。
public class Person
{
}
为该类定义两个字段(即该类的变量),一个是string类型的名字Name,另一个是int类型的年龄Age。
public string Name;
public int Age;
并为这个两个字段定义属性(用于定义一些命名特性),通过属性来读取和写入。
属性主要用于描述和维护类对象的状态。从客户端看,对属性的访问就好像直接访问public字段成员,但在类内部是通过类方法访问的。
将上述代码修改为以下代码:
public string Name { get; set; }
public int Age { get; set; }
类的实例构造函数(构造函数规定在初始化该类的实例时需要做些什么)。
在这个例子中,主函数中可以创建该类的一个对象,并用这个对象调用这个构造函数,调用时传递的string类型的值和int类型的值,用来初始化这个对象的Name和Age(后续调用时会进一步解释)。定义代码如下:
public Person(string name, int age)
{
Name = name;
Age = age;
}
类的方法(方法成员的本质就是在类中声明的函数,描述类能够“做什么”)。在这个例子中,若想要完成“问好”的功能,则可以用Console.WriteLine();来实现特定的输出。输出时,{Name}和{Age}会自动填充上面实例构造函数初始化之后对应的值(后续调用时会进一步介绍)。代码如下:
public void HelloFun()
{
Console.WriteLine($"Hello, my name is {Name} and I am {Age} years old.");
}
至此,该类的声明已经完成,下面是完整代码:
public class Person
{
// 定义类的字段及其属性
public string Name { get; set; }
public int Age { get; set; }
// 定义类的构造函数
public Person(string name, int age)
{
Name = name;
Age = age;
}
// 定义类的方法
public void HelloFun()
{
Console.WriteLine($"Hello, my name is {Name} and I am {Age} years old.");
}
}
接下来是类的使用。
类声明后,可以创建类的实例,即对象。创建类的实例需要使用new关键字。
类的实例相当于一个变量,创建类实例的格式:类名 对象名=new 构造函数(参数类表);
创建对象代码如下:
Person person = new Person("Alice", 30);
创建对象person之后,可以直接用person.字段名/函数名等来调用类的属性和方法等成员。
例如,用WriteLine函数输出person.Name,会直接显示这个对象初始化的值。
也可以直接用person.HelloFun()来调用类中的HelloFun函数。
调用属性和实例代码如下:
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
person.HelloFun();
完整代码如下:
class Program
{
static void Main(string[] args)
{
// 创建Person类的一个实例
Person person = new Person("Alice", 30);
// 访问实例的属性和方法
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
person.HelloFun();
}
}
运行结果如图:
2、接口
在C#中,接口(Interface)是一种引用类型,它定义了一组方法的契约,这些方法必须由实现接口的类来提供具体的实现。
接口是一种抽象类型,它不能包含字段、属性或事件的实现细节,只能声明方法、属性、事件和索引器的签名。接口的主要目的是实现多态性,允许不同的类以统一的方式工作。
接口的语法结构:
[访问修饰符] interface 接口标识符 [:基接口列表]
{
接口体;
}
要点:
- 1.接口成员访问权限为public,但不能加访问修饰符。
- 2.接口成员不能有定义。
- 3.接口的成员必须是方法,属性,事件或索引器,不能包含常数、字段、运算符、实例构造函数、析构函数或类型。
为了说明接口的应用,下面列举了三个方面的例子:具有多个接口实现的类、接口作为参数和接口作为返回值。
(1)具有多个接口实现的类
这里列举的是,用两个接口分别实现读和写的功能。
首先定义两个接口,一个用来读,一个用来写。接口成员只能声明不能定义。代码如下:
//定义一个读的接口
public interface IReadable
{
void Read();
}
// 定义第一个写的接口
public interface IWritable
{
void Write();
}
定义一个类,实现这两个接口,即给接口成员进行定义。
// 一个类实现了两个接口
public class File : IReadable, IWritable
{
public void Read()
{
Console.WriteLine("Reading from the file.");
}
public void Write()
{
Console.WriteLine("Writing to the file.");
}
}
接口由类实现后,接口成员可以通过对象实例访问,就像类的成员一样。
格式:接口标识符 接口对象名 = new 类名();
接口对象可以直接访问接口成员。
代码如下:
class Program
{
static void Main(string[] args)
{
// 使用IReadable接口调用方法
IReadable readable = new File();
readable.Read();
// 使用IWritable接口调用方法
IWritable writable = new File();
writable.Write();
}
}
完整代码如下:
using System;
// 定义第一个接口
public interface IReadable
{
void Read();
}
// 定义第二个接口
public interface IWritable
{
void Write();
}
// 一个类实现了两个接口
public class File : IReadable, IWritable
{
public void Read()
{
Console.WriteLine("Reading from the file.");
}
public void Write()
{
Console.WriteLine("Writing to the file.");
}
}
class Program
{
static void Main(string[] args)
{
// 使用IReadable接口调用方法
IReadable readable = new File();
readable.Read();
// 使用IWritable接口调用方法
IWritable writable = new File();
writable.Write();
}
}
运行结果如图:
(2)接口作为参数
这里举的例子中,定义了一个处理接口,定义了一个图片类和一个文档类,这两个类都实现处理接口。在主函数内创建接口的实例,使用接口的实例作为参数,调用Process方法。
定义接口
public interface IProcessor
{
void Process();
}
定义图像类实现接口
public class ImageProcessor : IProcessor
{
public void Process()
{
Console.WriteLine("Processing an image.");
}
}
定义文本类实现接口
public class TextProcessor : IProcessor
{
public void Process()
{
Console.WriteLine("Processing text.");
}
}
主函数实现功能
class Program
{
static void ProcessItem(IProcessor processor)
{
processor.Process();
}
static void Main(string[] args)
{
// 创建实现接口的类实例
IProcessor imageProcessor = new ImageProcessor();
IProcessor textProcessor = new TextProcessor();
// 调用Process方法,不需要关心具体是哪个类实现的
ProcessItem(imageProcessor);
ProcessItem(textProcessor);
}
}
完整代码如下:
using System;
// 定义接口
public interface IProcessor
{
void Process();
}
// 实现接口的类
public class ImageProcessor : IProcessor
{
public void Process()
{
Console.WriteLine("Processing an image.");
}
}
public class TextProcessor : IProcessor
{
public void Process()
{
Console.WriteLine("Processing text.");
}
}
class Program
{
static void Main(string[] args)
{
// 创建实现接口的类实例
IProcessor imageProcessor = new ImageProcessor();
IProcessor textProcessor = new TextProcessor();
// 调用Process方法,不需要关心具体是哪个类实现的
ProcessItem(imageProcessor);
ProcessItem(textProcessor);
}
static void ProcessItem(IProcessor processor)
{
processor.Process();
}
}
运行结果如图:
(3)接口作为返回值
这里定义一个接口,定义一个Dog类来实现这个接口,再定义一个AnimalFactory类,包含返回接口的方法。在主函数中,用AnimalFactory类创建一个接口的实例,再调用接口的方法。
AnimalFactory类中的CreateAnimal方法返回一个IAnimal接口类型的对象。在Main方法中,调用了这个方法,并将返回的接口引用赋值给IAnimal类型的变量animal。随后,调用animal的Speak和Move方法,这些方法的具体实现Dog类中的定义。
完整代码如下:
using System;
// 定义接口
public interface IAnimal
{
void Speak();
void Move();
}
// 实现接口的类
public class Dog : IAnimal
{
public void Speak()
{
Console.WriteLine("The dog barks.");
}
public void Move()
{
Console.WriteLine("The dog runs.");
}
}
// 另一个类,包含返回接口的方法
public class AnimalFactory
{
// 返回一个实现了IAnimal接口的对象
public IAnimal CreateAnimal()
{
// 返回一个Dog对象
return new Dog();
}
}
// 主程序
class Program
{
static void Main(string[] args)
{
// 获取一个IAnimal接口的实例
IAnimal animal = new AnimalFactory();
// 调用接口的方法,不关心具体是哪个类实现的
animal.Speak();
animal.Move();
}
}
运行结果如图:
3、委托
在C#中,委托(Delegate)是一种可以指向方法的引用,可以理解为一种函数指针,是类型安全的。委托安全地封装方法的签名和定义,并允许将方法作为参数传递或赋值给变量。
委托类似于C++中的函数指针,通过对于方法特征和返回值类型的声明,封装了具有相同特征和返回类型的方法。
使用委托需要三个步骤:
- 1.声明委托类型
- 2.创建委托实例
- 3.向委托实例注册方法
下面,将举例说明委托的声明和使用
该示例的功能是实现两位数的加减法
委托声明:
public delegate int OperationFun(int x, int y);
delegate是委托标识符,相当于类声明中的class
该委托接收两个int类型的参数,返回一个int类型的参数
委托的使用:
首先声明一个类Calculator,定义方法,一个用来做加法,一个用来做减法。
public class Calculator
{
public static int Add(int a, int b)
{
return a + b;
}
public static int Subtract(int a, int b)
{
return a - b;
}
}
在主函数中创建委托实例,并关联方法。
OperationFun addDelegate = new OperationFun(Calculator.Add);
OperationFun subtractDelegate = new OperationFun(Calculator.Subtract);
这个时候,就可以直接使用委托实例,直接传递参数即可。
int resultAdd = addDelegate(5, 3);
int resultSubtract = subtractDelegate(5, 3);
接下来就可以直接输出查看结果了。
完整代码如下:
class Program
{
static void Main(string[] args)
{
// 创建委托实例并关联方法
OperationFun addDelegate = new OperationFun(Calculator.Add);
OperationFun subtractDelegate = new OperationFun(Calculator.Subtract);
// 调用委托实例
int resultAdd = addDelegate(5, 3);
int resultSubtract = subtractDelegate(5, 3);
// 输出结果
Console.WriteLine("Add: " + resultAdd);
Console.WriteLine("Subtract: " + resultSubtract);
}
}
运行结果如图:
委托还可以实现链式调用,即引用多个方法,并且这些方法可以被连续调用。
三、代码实现
1.代码分步讲解
1.1定义生产鸭脖和鸭翅的接口 :
定义一个公共的接口IProductionFactory,并用IProductionFactory接口定义两个方法:ProduceDuckNeck和ProduceDuckWing。若有类或结构想要生产鸭脖和鸭翅,则需要实现这个接口,并为这两个方法定义具体的实例。
public interface IProductionFactory
{
void ProduceDuckNeck();
void ProduceDuckWing();
}
1.2定义武汉工厂类,实现 IProductionFactory 接口 :
WuhanFactory类实现了IProductionFactory接口,为ProduceDuckNeck和ProduceDuckWing方法提供了具体的实现,这两个方法分别在控制台输出表示武汉工厂生产了鸭脖和鸭翅的消息。
这样,任何需要用到IProductionFactory接口的地方,都可以使用WuhanFactory类的实例,调用其方法来模拟该工厂生产鸭脖和鸭翅。
public class WuhanFactory : IProductionFactory
{
public void ProduceDuckNeck()
{
Console.WriteLine("武汉工厂生产了鸭脖!");
}
public void ProduceDuckWing()
{
Console.WriteLine("武汉工厂生产了鸭翅!");
}
}
1.3定义南京工厂类,实现 IProductionFactory 接口 :
定义方式与武汉工厂相似,但南京工厂不支持生产鸭脖。
public class NanjingFactory : IProductionFactory
{
public void ProduceDuckNeck()
{
throw new NotSupportedException("南京工厂不生产鸭脖!");
}
public void ProduceDuckWing()
{
Console.WriteLine("南京工厂生产了鸭翅!");
}
}
1.4定义长沙工厂类,实现 IProductionFactory 接口 :
定义方式与武汉工厂相似,但长沙工厂不支持生产鸭翅。
public class ChangshaFactory : IProductionFactory
{
public void ProduceDuckNeck()
{
Console.WriteLine("长沙工厂生产了鸭脖!");
}
public void ProduceDuckWing()
{
throw new NotSupportedException("长沙工厂不生产鸭翅!");
}
}
1.5定义生产委托
public delegate void ProductionDelegate();
1.6主函数内创建对象
为每个工厂类都创建一个对象,用来进行调用等操作。
IProductionFactory wuhanFactory = new WuhanFactory();
IProductionFactory nanjingFactory = new NanjingFactory();
IProductionFactory changshaFactory = new ChangshaFactory();
1.7定义生产委托实例
这里列举了两种方案:武汉工厂生产鸭脖和鸭翅。
ProductionDelegate produceDuckNeck = wuhanFactory.ProduceDuckNeck;
ProductionDelegate produceDuckWing = nanjingFactory.ProduceDuckWing;
1.8调用委托进行生产
由于武汉工厂能生产鸭脖,也能生产鸭翅,输出结果应该为"武汉工厂生产了鸭脖!" 和 "武汉工厂生产了鸭翅!" 。南京工厂生产鸭翅和长沙工厂生产鸭脖方法相同。
produceDuckNeck();
produceDuckWing();
1.9尝试使用长沙工厂生产鸭翅
在实际工作中,有可能会出现错误的指派,比如让南京工厂生产鸭脖、让长沙工厂生产鸭翅,因此代码中需要避免这种异常。
使用try和catch语句能够用来处理这种错误。try中放尝试语句,catch中放各种错误类型的处理异常的代码块和相关输出,如果try中的尝试语句出错,那么则会跳转到错误类型对应的catch中去。
try
{
produceDuckWing = changshaFactory.ProduceDuckWing;
produceDuckWing(); // 这行会抛出异常,因为长沙工厂不支持生产鸭翅
}
catch (NotSupportedException ex)
{
Console.WriteLine(ex.Message);
}
2.完整代码
using System;
// 定义生产鸭脖和鸭翅的接口
public interface IProductionFactory
{
void ProduceDuckNeck();
void ProduceDuckWing();
}
// 武汉工厂类,实现 IProductionFactory 接口
public class WuhanFactory : IProductionFactory
{
public void ProduceDuckNeck()
{
Console.WriteLine("武汉工厂生产了鸭脖!");
}
public void ProduceDuckWing()
{
Console.WriteLine("武汉工厂生产了鸭翅!");
}
}
// 南京工厂类,实现 IProductionFactory 接口
public class NanjingFactory : IProductionFactory
{
public void ProduceDuckNeck()
{
throw new NotSupportedException("南京工厂不生产鸭脖!");
}
public void ProduceDuckWing()
{
Console.WriteLine("南京工厂生产了鸭翅!");
}
}
// 长沙工厂类,实现 IProductionFactory 接口
public class ChangshaFactory : IProductionFactory
{
public void ProduceDuckNeck()
{
Console.WriteLine("长沙工厂生产了鸭脖!");
}
public void ProduceDuckWing()
{
throw new NotSupportedException("长沙工厂不生产鸭翅!");
}
}
// 定义生产委托
public delegate void ProductionDelegate();
class Program
{
static void Main(string[] args)
{
// 创建工厂实例
IProductionFactory wuhanFactory = new WuhanFactory();
IProductionFactory nanjingFactory = new NanjingFactory();
IProductionFactory changshaFactory = new ChangshaFactory();
// 定义武汉工厂生产委托实例
ProductionDelegate produceDuckNeck = wuhanFactory.ProduceDuckNeck;
ProductionDelegate produceDuckWing = wuhanFactory.ProduceDuckWing;
// 调用委托进行生产
produceDuckNeck();
produceDuckWing();
//定义南京工厂生产委托实例
produceDuckWing = nanjingFactory.ProduceDuckWing;
produceDuckWing();
//定义长沙工厂生产委托实例
produceDuckNeck = changshaFactory.ProduceDuckNeck;
produceDuckNeck();
// 尝试使用长沙工厂生产鸭翅
try
{
produceDuckWing = changshaFactory.ProduceDuckWing;
produceDuckWing(); // 这行会抛出异常,因为长沙工厂不支持生产鸭翅
}
catch (NotSupportedException ex)
{
Console.WriteLine(ex.Message);
}
// 等待用户输入
Console.ReadLine();
}
}
3.运行结果
四、代码分析
1.代码思路
1.1 定义接口
首先,定义了一个IProductionFactory接口,该接口包含了两个方法ProduceDuckNeck和ProduceDuckWing,分别用于生产鸭脖和鸭翅。这个接口是工厂模式的核心,它定义了工厂类应该具备的基本功能。
1.2 创建工厂类
接着,根据不同的生产需求,创建了多个实现了IProductionFactory接口的工厂类。
这些工厂类提供了具体的生产逻辑,通过实现接口中的方法来实现不同产品的生产。每个工厂类都有自己的特色,有的能够生产多种产品,有的只能生产特定产品。
WuhanFactory:能够生产鸭脖和鸭翅。
NanjingFactory:只能生产鸭翅,如果尝试生产鸭脖会抛出异常。
ChangshaFactory:只能生产鸭脖,如果尝试生产鸭翅会抛出异常。
1.3 使用委托进行生产
在Main函数中,首先创建了各个工厂类的实例。然后,定义了一个ProductionDelegate委托,用于封装工厂类中的生产方法。通过将该委托指向不同的工厂方法,可以方便地调用不同的生产逻辑。
代码中使用了try-catch块来处理可能发生的NotSupportedException异常。这是因为某些工厂类可能不支持生产某些产品,当尝试调用这些不支持的方法时,会抛出异常。通过catch并处理这些异常,可以确保程序的稳定运行。
1.4 总结
在这段代码中,主要定义了以下组件:
- 产品接口(IProductionFactory):定义了一个生产产品所需的操作集合,这里是生产鸭脖(ProduceDuckNeck)和鸭翅(ProduceDuckWing)的接口。
- 具体工厂类(如WuhanFactory、NanjingFactory等):实现了产品接口,并具体实现了生产产品的操作。不同的工厂类可能生产不同的产品组合,或者对于相同产品的生产有不同的实现方式。
- 生产委托(ProductionDelegate):用于在运行时将具体的生产操作与调用者分离,使得调用者无需关心具体是哪个工厂类在执行生产操作。
2.难点分析
- 接口定义与实现:在设计工厂模式时,需要明确产品接口的定义,确保它能涵盖所有产品共有的生产需求。同时,具体工厂类需要正确实现这些接口,以满足不同的生产需求。
- 异常处理:当某个工厂不支持生产某个产品时,需要妥善处理异常。在代码中,如果调用了一个工厂不支持的方法,会抛出
NotSupportedException
异常,调用者需要捕获并处理这些异常。 - 委托的使用:委托提供了一种将方法作为参数传递的机制,这使得可以在运行时动态地决定执行哪个工厂的方法。这个过程需要理解委托的定义、创建、调用以及它们与事件、返回值函数之间的关系。
3.工厂模型的特点
3.1 关键点
- 封装性:工厂方法模式封装了对象的创建过程,客户端只需要通过工厂接口来请求所需的对象,而无需了解对象的具体创建细节。
- 开闭原则:工厂方法模式符合开闭原则,即对扩展开放,对修改封闭。当需要添加新的产品时,只需要添加新的具体产品类和相应的具体工厂类,而无需修改已有的代码。
- 多态性:通过工厂方法返回的是抽象产品类的引用,客户端代码可以统一地处理所有产品对象,体现了多态性的优点。
3.2 优点
- 解耦:工厂模式将对象的创建与使用解耦,使得代码更加清晰和模块化。客户端只需要关注如何使用产品对象,而不需要关心对象的创建细节。
- 降低代码复杂度:通过工厂方法或抽象工厂,可以隐藏具体类的实现细节,客户端只需要与抽象接口打交道,降低了代码的复杂度和耦合度。
- 易于扩展:当需要增加新的产品类时,只需要添加相应的具体产品类和工厂类,而不需要修改已有代码,符合开闭原则。
- 创建方式灵活:工厂模式提供了创建对象的灵活方式,可以根据不同的条件或配置来创建不同的对象实例。
- 统一接口,使用简单:工厂模式能够提供一个统一的接口来创建对象,客户端只需要通过这个接口就可以获取到所需的对象,无需关心具体的实现细节
3.3 缺点
- 增加系统复杂度:引入工厂类增加了系统的类数量,可能导致系统结构变得相对复杂,特别是当工厂类和产品类数量较多时。
- 过度使用会导致难以维护:如果过度使用工厂模式,可能会导致滥用,使得代码变得难以理解和维护。每个类和对象都使用工厂模式创建可能并不总是必要的。
- 增加了额外的开销:工厂模式的引入可能带来一些额外的开销,因为需要通过工厂类来创建对象,而不是直接实例化。这可能会稍微降低性能,特别是在大量创建对象的情况下。
- 隐藏了类之间的依赖关系:工厂模式隐藏了类之间的依赖关系,可能导致依赖关系变得不够直观和易于理解。在大型项目中,这可能会增加理解和维护的难度。
五、功能拓展
通过添加新的工厂类,可以轻松地扩展生产系统的功能。只需要实现IProductionFactory
接口,并定义相应的生产逻辑,就可以创建新的工厂类来满足新的生产需求。这种扩展性使得工厂模式非常灵活,能够适应不断变化的需求。
例如,可以增加一个新的生产鸭脖的工厂:
public class DuckNeckOnlyFactory : IProductionFactory
{
public void ProduceDuckNeck()
{
Console.WriteLine("新的鸭脖工厂生产了鸭脖!");
}
public void ProduceDuckWing()
{
throw new NotSupportedException("新的鸭脖工厂不生产鸭翅!");
}
}
在主函数中创建新的鸭脖工厂的实例
IProductionFactory duckNeckOnlyFactory = new DuckNeckOnlyFactory();
生产鸭脖,正常运行
produceDelegate = duckNeckOnlyFactory.ProduceDuckNeck;
produceDelegate();
尝试生产鸭翅,使用try-catch语句来避免错误
try
{
produceDelegate = duckNeckOnlyFactory.ProduceDuckWing;
produceDelegate();
}
catch (NotSupportedException ex)
{
Console.WriteLine(ex.Message);
}
添加上述代码即可完成新的工厂的添加及调用。