在 C# 中使用部分类和嵌套类实现工厂方法模式






4.47/5 (8投票s)
本文旨在学习如何应用 C# 中的嵌套类和分部类概念来实现工厂方法模式。
引言
本文的目标是学习如何应用 C# 中的嵌套类和分部类概念来实现工厂方法模式。本文将简要介绍工厂方法模式是什么以及它解决了哪些问题,但主要侧重于如何利用嵌套类和分部类来实现更好的抽象。如果您已经熟悉工厂方法模式,可以直接跳转到“更好的抽象”部分。
背景
工厂方法模式处理对象创建的管理问题。它允许客户端使用对象,而无需担心这些对象是如何创建的。当某些客户端代码需要使用某个类的功能时,它首先需要获取该类的一个实例,然后才能使用该功能。然而,对象的创建并非总是那么简单,在大多数情况下,在创建对象之前/之后需要进行某些初始化。如果所有这些创建和初始化对象的逻辑都留给客户端代码,那么如果将来需要更改任何创建或初始化方法,可能需要修改客户端代码。
现在考虑一个更复杂的场景,您有一个类族,您的客户端需要在运行时决定使用哪个对象,因此如果您的客户端负责根据某些信息在运行时创建实际对象,那么每次向该类族添加新类型时都必须对其进行更改。如果您的类族中有很多类型并且以后还可以添加更多类型,这将变得非常复杂。让我们通过一个例子来理解这个问题
假设我们有一个应用程序,它生成并打印不同类型的文档,例如:Excel 文档、Word 文档等。客户端希望使用这些文档的“打印”功能。现在,为了在 C# 代码中创建文档家族,我们可以有一个抽象基类“Document”,如下所示:
public enum DocumentType { None, PDF, Word, Excel, PowerPoint } public abstract class Documen { public string Name { get; set; } public abstract DocumentType Type { get; } public abstract string Print(); }
以及此抽象的两个不同实现
class ExcelDocument : Document { public override DocumentType Type { get { return DocumentType.Excel; } } public override string Print() { return "Excel"; } }
并且
class WordDocument : Document { public override DocumentType Type { get { return DocumentType.Word; } } public override string Print() { return "Word"; } }
客户端代码将如下所示
static void Main(string[] args) { Document document = new NullDocument(); DocumentType documentType; string type = Console.ReadLine(); if (Enum.TryParse<DocumentType>(type, out documentType)) { switch (documentType) { case DocumentType.Excel: document = new ExcelDocument() { Name = "Excel" }; break; case DocumentType.Word: document = new WordDocument() { Name = "Word" }; break; case DocumentType.PowerPoint: document = new PowerPointDocument() { Name = "PowerPoint" }; break; case DocumentType.PDF: document = new PDFDocument() { Name = "PDF" }; break; default: document = new NullDocument(); break; } } else { Console.WriteLine("Invalid Input."); } Console.WriteLine("Printing {0} document", document.Print()); Console.ReadKey(); }
以上所有代码都非常直接,客户端代码从用户那里获取输入并创建适当的“Document”实例,然后执行所需的操作。
现在,如果需要在层次结构中添加第三种类型,称为“PowerPoint”或“PDF”,我们最终将为“PDF”和“PowerPoint”编写两个新的 Document 实现,并修改客户端以插入适当的“switch”-“case”语句来处理这两种新类型的创建。因此,客户端代码与实现类紧密耦合,这意味着我们的代码违反了“依赖反转原则”,该原则指出“高级模块(客户端代码)不应依赖于低级模块(实现各种类型 PDF、Excel 等逻辑的类),两者都应依赖于抽象(抽象“Document”)”。因此,目前来看这段代码,我们发现了一些问题:
- 客户端依赖于具体类,而不是仅仅依赖于抽象。
- 客户端正在做额外的创建对象的工作,这使得每次向层次结构添加新类型或更改对象初始化方式时都需要更改客户端代码。
所有这些问题的答案都是“工厂方法模式”,我们所需要做的就是将对象的创建分离。我们将引入另一个名为“DocumentFactory”的类,它将全权负责创建供客户端使用的对象,并且每当客户端需要特定类型的对象时,它都会要求工厂为其创建一个。让我们看看代码。
我们将引入一个“工厂”类
public class DocumentFactory { public Document CreateDocument(DocumentType documentType) { switch (documentType) { case DocumentType.Excel: return new ExcelDocument() { Name = "Excel Document" }; case DocumentType.PDF: return new PDFDocument() { Name = "PDF Document" }; case DocumentType.PowerPoint: return new PowerPointDocument() { Name = "PowerPoint Document" }; case DocumentType.Word: return new WordDocument() { Name = "Word Document" }; default: return new NullDocument() { Name = "" }; } } }
现在,根据请求的类型,此工厂将创建对象,正确初始化并返回给客户端。
请注意,“CreateDocument()”方法的返回值是“Document”,因此客户端只需知道抽象的详细信息,客户端代码将如下所示:
static void Main(string[] args) { DocumentFactory documentFactory = new DocumentFactory(); Document document = new NullDocument(); DocumentType documentType; string type = Console.ReadLine(); if (Enum.TryParse<DocumentType>(type, out documentType)) { document = documentFactory.CreateDocument(documentType); } else { Console.WriteLine("Invalid Input."); } Console.WriteLine("Printing {0} document", document.Print()); Console.ReadKey(); }
现在我们可以在层次结构中添加任意数量的类型,客户端代码永远不需要更改。这是 C# 中“工厂方法模式”的标准实现,但这种设计仍然存在某些问题,让我们再次看看并尝试找出这种设计可能还有哪些不好的地方(找到什么了吗?)
更好的抽象
客户端代码只需要知道抽象的“Document”,对吧?它不应该依赖于低级细节,而只依赖于抽象,但如果我在代码中尝试这样做会怎么样?
Document doc = new ExcelDocument();
是什么阻止我们这样做?没什么。如果明天新开发人员加入团队,或者使用该库的人尝试使用此代码并最终使用上述代码,如何确保对象在使用前已正确初始化(这是工厂的职责),以及最大的问题是如何确保只有工厂可以创建对象,而不是其他人。
另一个问题:假设我们想控制此库可以支持的文档类型数量。目前,任何人都可以扩展“Document”并提供自己的实现,如下所示:
class MyDocument : Document { public override DocumentType Type { get { return DocumentType.PDF; } } public override string Print() { throw new NotImplementedException(); } }
并且此类型将不会通过 Factory 类创建,因为 Factory 不知道此类型。
这可能会造成问题,而且至少它允许我提供比库提供的更多类型,这不是我们想要的。那么,是否可以以某种方式对其进行限制呢?幸运的是,是的,我们可以通过结合 C# 中两个强大但很少使用的功能来实现这一点:
- 嵌套类
- 分部类
实际上,这可以借助嵌套类来完成,但分部类将帮助我们保持代码整洁,我们很快就会看到。
那么,让我们回顾并重新审视我们的问题,我们希望将“对象的创建和初始化”与客户端代码分离。为此,我们决定使用一个工厂类来创建具体对象并将其提供给客户端,那么客户端需要什么呢?只需抽象,由抽象类“Document”提供,那么为什么将我的具体类型暴露给世界其他部分呢?谁需要知道实现“Document”类提供的抽象的具体类型?只有负责实际创建这些对象的类,即我们的工厂类。现在我们需要通过将具体类型隐藏起来,只让工厂类访问它们来获得更好的抽象。
嵌套类前来救援
public class DocumentFactory { public override Document CreateDocument(DocumentType documentType) { switch (documentType) { case DocumentType.Excel: return new ExcelDocument() { Name = "Excel Document" }; case DocumentType.PDF: return new PDFDocument() { Name = "PDF Document" }; case DocumentType.PowerPoint: return new PowerPointDocument() { Name = "PowerPoint Document" }; case DocumentType.Word: return new WordDocument() { Name = "Word Document" }; default: return new NullDocument() { Name = "" }; } } class ExcelDocument : Document { public override DocumentType Type { get { return DocumentType.Excel; } } public override string Print() { return "Excel"; } } }
我们可以将所有具体类型嵌套到工厂类中,这样只有工厂类才能访问并实例化这些具体类型。现在其中一个问题解决了,现在没有人可以这样做:
Document doc = new ExcelDocument();
如果您想要一个具体的“Document”类型实例,您必须向工厂请求,这是唯一的途径。
关于我们的第二个问题,任何人仍然可以从“Document”继承并提供自己的实现,这同样可以使用嵌套类来解决。嵌套类有一个非常独特的特性
“嵌套类可以访问其包含类型的私有成员。” 因此,我们可以将“Document”类的构造函数标记为私有,这将阻止任何类继承它,如下所示:
public class Document { private Document() { } public string Name { get; set; } public abstract DocumentType Type { get; } public abstract string Print(); public class DocumentFactory { public Document CreateDocument(DocumentType documentType) { switch (documentType) { case DocumentType.Excel: return new ExcelDocument() { Name = "Excel Document" }; case DocumentType.PDF: return new PDFDocument() { Name = "PDF Document" }; case DocumentType.PowerPoint: return new PowerPointDocument() { Name = "PowerPoint Document" }; case DocumentType.Word: return new WordDocument() { Name = "Word Document" }; default: return new NullDocument() { Name = "" }; } } class ExcelDocument : Document { public override DocumentType Type { get { return DocumentType.Excel; } } public override string Print() { return "Excel"; } } } }
现在我们有一个带有私有构造函数的抽象类“Document”,这意味着除了嵌套在此类中的类型之外,任何人都无法实现它。“Document”类包含一个嵌套类“DocumentFactory”,它有一个方法用于创建具体类型,并且为每个具体类型再次包含嵌套类。所有具体类型都能够从“Document”继承,即使它有一个私有构造函数,因为 C# 嵌套类特性允许嵌套类型访问其包含类型的私有成员,但现在没有其他人可以实现“Document”。这就是我们第二个问题的解决方案。
现在我们增加了一层抽象,所有不相关的细节(如具体实现、对象创建等)都完全对客户端隐藏,它只在抽象上工作。
我希望对这段代码进行的最终简化是能够将我的嵌套具体类型写在单独的文件中,因为如果我最终将所有实现都写在一个文件中的“Document”和“DocumentFactory”之下,那一个文件中的代码量会非常大,所以我们使用分部类,它将简单地允许我将代码写在单独的文件中,但仍然使用嵌套类。
Document.cs
public abstract partial class Document { private Document() { } public string Name { get; set; } public abstract DocumentType Type { get; } public abstract string Print(); public partial class DocumentFactory { } }
DocumentFactory.cs
public partial class Document { public partial class DocumentFactory { public override Document CreateDocument(DocumentType documentType) { switch (documentType) { case DocumentType.Excel: return new ExcelDocument() { Name = "Excel Document" }; case DocumentType.PDF: return new PDFDocument() { Name = "PDF Document" }; case DocumentType.PowerPoint: return new PowerPointDocument() { Name = "PowerPoint Document" }; case DocumentType.Word: return new WordDocument() { Name = "Word Document" }; default: return new NullDocument() { Name = "" }; } } } }
ExcelDocument.cs
public partial class Document { public partial class DocumentFactory { class ExcelDocument : Document { public override DocumentType Type { get { return DocumentType.Excel; } } public override string Print() { return "Excel"; } } } }
WordDocument.cs
public partial class Document { public partial class DocumentFactory { class WordDocument : Document { public override DocumentType Type { get { return DocumentType.Word; } } public override string Print() { return "Word"; } } } }
还有更多你需要的,够简洁吗?
摘要
工厂方法模式允许将对象的创建与客户端代码分离,我们可以使用 C# 的一些构造,例如分部类和嵌套类,来实现更好的抽象。
关注点
在本文中,我们学习了 C# 中嵌套类和分部类的一些实际用途。这两个是 C# 中鲜为人知/使用的特性,但如果运用得当,它们可以发挥巨大的价值,就像我们在这篇文章中做的那样。