通过一个项目逐步学习 C# 设计模式 - 第 1 部分






4.92/5 (233投票s)
在本文中,我们将通过一个项目,一步一步学习 C# 设计模式和架构模式。
目录
第一部分我们将学习哪些模式和概念?
RIP :- 用多态替换 IF。
简单工厂 :- 将“NEW”关键字移入中心类。
延迟加载 :- 按需加载对象。
原型模式 :- 创建对象的克隆。
策略模式 :- 动态添加算法。
IOC 控制概念 :- 不必要的工作应移交给其他地方。
DI :- 依赖注入实现 IOC。
SOLID 原则 :- 涵盖了 SOLID 原则中的 SRP 和 OCP。
引言
在本文中,我们将学习如何使用 C# 语言实现设计模式和架构模式。我们不会逐个介绍模式,而是采用一个示例项目来实践这些模式。
那么,为什么这篇文章采用项目驱动的方法,而不是示例驱动的方法呢?
设计模式和架构模式是思维过程。思维过程无法通过 PPT、UML 图等方式来解释。您需要看到代码,需要感受它,并将其映射到真实的rible项目场景。
如果您查看互联网上/书籍中的大多数设计模式文章,它们要么只用 UML 图(并非所有开发人员都理解 UML)来解释,要么使用像汽车、树、人类等示例,这些示例并不能让您感觉它们是真实的。
因此,让我们采用一个示例项目需求,开始编码和设计一个应用程序,让设计模式自然而然地出现。
误区 1:- 要想实现良好的架构,就必须在项目中实现所有设计模式。
事实 :- 模式是自然产生的,并且完全是按需的。 |
在开始学习一些比较深入的内容之前,先讲个小笑话。
面试官:- 那么,请告诉我您在现实生活中使用过设计模式吗?
应聘者:- 只在面试时用过。
设计模式 VS 架构模式 VS 架构风格
误区 2:- 设计模式和架构模式是相同的。 事实 :- 设计模式至少是伪代码级别的,而架构模式是组件级别的。 |
“伪”这个词的意思是:- 大致看起来是那样。
我们看到很多人在混淆使用这些词汇。但它们的工作方式有显著区别。
所以,我们首先尝试理解“模式”这个词,然后深入探讨。
如果您查看“模式”的英文本义:- 它们是重复出现的、可预测的事件。
例如,气候变化遵循一种模式。通常(至少在印度),夏天之后是雨季,然后是寒冷。人类识别这些模式,以便更好地组织自己。
同样,在软件世界中,经常出现的问题都有特定的模式,许多开发人员已经解决了这些问题并提出了解决方案。后来,其中一些解决方案随着时间的推移被证明是有效的,并成为该问题模式的标准解决方案。
例如,如果您想进行排序,就有经过时间考验的算法,如冒泡排序、插入排序等。
设计模式是伪代码级别的解决方案,而架构模式是 30,000 英尺高空的解决方案,在组件级别定义。简单来说,如果有人说“X”是一个设计模式,那么期望代码;如果有人说“Y”是一个架构模式,那么期望某种组件级别的块状图。
架构风格是一种思维方式,一个原则,通常用一句话就能概括。例如,REST 是一种架构风格,我们重视 HTTP。
下面是它们各自的一些例子。
设计模式 | 工厂、迭代器、单例 |
架构模式 | MVC、MVP、MVVM |
架构风格 | REST、SOA、IOC |
设计模式定义
在继续深入项目之前,让我们先为设计模式下一个定义,之后再定义架构模式。
如果您查看设计模式的官方定义,如下所示:-
“针对重复出现架构问题的、经过时间考验的解决方案”。
但坦白说,随着您开始实践和阅读设计模式,这个定义可能不再适用。有时,您会发现设计模式完全是关于良好的 OOP(面向对象编程)原则。
所以,根据我的理解和经验,我将给出我的定义。我保证,当我们开始运行所有设计模式时,这个定义会更加清晰。
“针对重复出现 OOP 问题的、经过时间考验的解决方案”。
这个定义也被 GOF 团队在这次访谈中反复提及。
误区 3:- 设计模式能让你成为一名完整的架构师。
事实 :- 设计模式是架构师需要掌握的技能之一。它能让你成为更好的 OOP 开发者。 |
实际上,设计模式只是让你在 OOP 方面更出色,而成为架构师所需的技能不止于此。我们也可以说,设计模式是通过场景来理解 OOP 的一种方式。
所以,我们不要再浪费时间了,开始一个典型的软件需求吧。
酷品商店项目:- 第一阶段
“酷品商店”(CoolShop)是一家大型零售商场,在孟买和浦那城市设有连锁商场。公司管理层希望为其零售店开发一个简单的客户管理系统,具备以下功能。公司决定分阶段推出该项目。
因此,在第一阶段,他们只想捕获客户信息。以下是更详细的需求:-
- 目前,应用程序将捕获 5 个字段:客户姓名、电话号码、账单金额、账单日期和客户地址。
- 在第一阶段,收集两种类型的客户数据。一种是潜在客户(lead),另一种是客户(customer)。潜在客户是指来到酷品商店但未购买任何东西的人。他们只是咨询然后离开。客户是指来到商店并购买商品的人。客户实际上进行了财务交易。
- 当是潜在客户时,只有客户姓名和电话号码是必填项;而对于客户,所有字段都是必填项。系统应提供无缝添加新验证规则的机制,并且这些验证规则应具有灵活性和可重用性,以应用于系统。
- 系统应具备显示、添加、更新和删除客户数据的功能。
- 目前,系统将使用 SQL Server 和 ADO.NET 作为数据层技术。但在接下来的几个月里,我们将把数据层迁移到 Entity Framework。迁移应该是无缝的,并且对整个系统的更改应该不大。
- 系统应具备取消屏幕上任何修改的能力。因此,如果客户正在编辑一条记录并更改了某些值,他们应该有机会恢复到旧值。
软件架构是一个演进过程
误区 4:- 架构一开始就应该是完美的、正确的。
事实 :- 架构是演进的。从小处着手,然后逐步改进。 |
这是新架构师的一个最重要的指导方针。软件架构是不断演进的,模式会自然而然、循序渐进地出现,以形成最终产品。
许多人试图通过研究一个又一个代码,一个又一个模式来学习设计和架构模式。这种学习模式的方式非常有害,因为您只看到一些学术代码,它从未成为您自然的一部分。
学习设计模式的最佳方法是观察一个完整的演进过程,通过做一个项目,让模式自然地、逐渐地出现。
因此,与其学习模式后又学模式,不如让我们尝试架构上述项目,让模式自然地出现,并在它们出现时加以指出。
所以,让我们先从简单的 OOP 概念、类、对象开始,让模式按需出现。
什么是实体(ENTITY)?
我们需要做的第一件事是识别实体。所以,让我们理解实体在英语中的确切含义。
实体是您在现实世界中看到的事物。它们可以被唯一标识。例如,人、地点、事物等都是实体的一些例子。
在 OOP 世界中,实体的技术名称是对象。所以,在这篇文章中,我们将互换使用这两个词。
第一步:- 识别您的实体/对象
思维过程 1:- 软件代码应复制现实世界、真实的人。所以,如果您在现实世界中是一名会计,那么在您的软件代码中应该有一个名为“会计”的实体。 |
创建软件应用程序是为了自动化现实世界和真实的人。因此,如果您的软件代码复制了真实世界的对象,您就可以以更好、更受控的方式管理您的软件。
这正是面向对象编程的全部意义所在。OOP 认为您的代码应该反映您的业务领域行为和业务实体。
领域(Domain)意味着业务单元、其规则以及它的工作方式。
所以,第一步是根据上述需求识别实体/对象。因此,根据上述需求,以下是识别出的名词:-
- 孟买
- 浦那
- 客户
- 潜在客户
- 酷品商店
思维过程 2:- 名词变成实体,动词变成实体的动作。代词变成这些实体的属性和行为。 |
许多架构师遵循的一种做法是识别名词、代词和动词。然后分析这些名词并将其识别为对象/实体。但要小心这种方法,因为您可能会得到不需要的名词和动词。所以,只保留那些与最终软件系统相关的名词和动词。
如果您查看上面识别出的实体,目前“孟买”和“浦那”是城市名称,与软件本身没有直接联系。“酷品商店”作为商场的名称,也没有直接联系。
因此,在第一阶段,目前唯一有用的实体是“潜在客户”和“客户”。我们将围绕客户添加、更新、删除数据。
现在,要让这些实体在您的计算机中“活起来”,您需要代码和逻辑。所以,我们需要某种模板来编写这些代码,这就是我们称之为“类”的东西。然后,这个类将被实例化,以在您的计算机中创建对象和实体。
OOP 是一个三阶段过程:-
- 模板创建:- 创建类并在这些类中编写逻辑。
- 实例化:- 创建这些类的实体/对象,并使其在 RAM/计算机中“活起来”。
- 运行:- 与这些对象交互以实现软件功能。
现在,作为开发人员,您会在这三个阶段中遇到常见/重复出现的设计问题。设计模式为所有这三个阶段中的 OOP 问题提供了解决方案。设计模式分为三类,它们涵盖了这些阶段,如下所示:-
OOP 阶段 | 设计模式类别 |
模板/类创建问题 | 结构设计模式。 |
实例化问题 | 创建型设计模式。 |
运行时问题 | 行为设计模式。 |
所以,让我们继续创建两个类,一个名为 Lead,另一个名为 Customer。
以下是这两个类的代码。
注意:- 我注意到这里有人会提出代码重复的问题。我将很快解决它们,请继续阅读。
namespace CustomerLibrary
{
public class Lead
{
public string LeadName { get; set; }
public string PhoneNumber { get; set; }
public string PhoneNumber { get; set; }
public decimal BillAmount { get; set; }
public DateTime BillDate { get; set; }
public string Address { get; set; }
}
public class Customer
{
public string CustomerName { get; set; }
public string PhoneNumber { get; set; }
public decimal BillAmount { get; set; }
public DateTime BillDate { get; set; }
public string Address { get; set; }
}
}
第二步:- 识别实体之间的关系
现实世界中的实体会相互交互,它们之间存在关系。因此,我们的下一步是识别实体之间的关系。如果您对现实世界中存在的关系进行可视化,它们主要有两种类型:“是”(IS A)和“拥有”(HAS A)。
例如,儿子“是”父亲的孩子,儿子“拥有”父亲赠送的汽车。“是”更像是父子关系(层次结构),而“拥有”更像是使用关系(聚合、组合和关联)。
如果您阅读需求二,它清楚地表明:-
“潜在客户是具有较少验证的客户类型”。
现在,我们的类代码将变成如下所示。“Lead”是一个子类,它继承自“Customer”类。
public class Customer
{
public string CustomerName { get; set; }
public string PhoneNumber { get; set; }
public decimal BillAmount { get; set; }
public DateTime BillDate { get; set; }
public string Address { get; set; }
}
public class Lead : Customer
{
}
对于“Customer”实体,客户姓名、电话号码、账单金额和账单日期是必填项。在下面的代码中,我们创建了一个简单的“Validate”方法来检查上述所有属性。
最重要的一点是“Validate”方法被设为虚拟。这样,新类就可以重写验证逻辑。
public class Customer
{
// All properties are deleted for simplification
public virtual void Validate()
{
if (CustomerName.Length == 0)
{
throw new Exception("Customer Name is required");
}
if (PhoneNumber.Length == 0)
{
throw new Exception("Phone number is required");
}
if (BillAmount > 0)
{
throw new Exception("Bill is required");
}
if (BillDate >= DateTime.Now)
{
throw new Exception("Bill date is not proper");
}
}
}
下一步是创建一个继承自“Customer”类的“Lead”类,并重写“validate”方法。正如讨论的,一个“Lead”类限制较少,因为它只需要姓名和电话号码。以下是重写了客户类较少验证的代码。
public class Lead : Customer
{
public override void Validate()
{
if (CustomerName.Length == 0)
{
throw new Exception("Customer Name is required");
}
if (PhoneNumber.Length == 0)
{
throw new Exception("Phone number is required");
}
}}
思维过程 3:- “是”关系是父子关系,而“拥有”关系是使用关系。 |
第三步:- 从公共类派生
如果您阅读需求四,它说系统应该有能力在未来添加新的客户类型。
根据这个需求,我们的类设计看起来不太合乎逻辑。一个合乎逻辑的方法应该是有一个“半定义”的类,包含所有属性,而“Validate”方法是空的(半定义)。然后,子类可以根据它们的行为重写这个空方法。
换句话说,我们应该有一个基础类,我们可以从中派生出客户和潜在客户类。
以下是具有所有属性的客户基类,并且“Validate”方法为空,留给其子类定义。
public class CustomerBase
{
public string CustomerName { get; set; }
public string PhoneNumber { get; set; }
public decimal BillAmount { get; set; }
public DateTime BillDate { get; set; }
public string Address { get; set; }
public virtual void Validate()
{
// Let this be define by the child classes
}
}
注意:- 正在阅读本文的一些资深人士可能已经开始抱怨了,他们会说,让那个类成为“抽象”的。是的,这很快就会讲到。我想推迟一下,以便我们能真正理解抽象类的实际用途。
所以,现在如果我们想创建一个客户类,我们可以直接继承自基类 Customer 并添加验证。
public class Customer : CustomerBase
{
public override void Validate()
{
if (CustomerName.Length == 0)
{
throw new Exception("Customer Name is required");
}
if (PhoneNumber.Length == 0)
{
throw new Exception("Phone number is required");
}
if (BillAmount > 0)
{
throw new Exception("Bill is required");
}
if (BillDate >= DateTime.Now)
{
throw new Exception("Bill date is not proper");
}
}
}
如果我们想创建一个 Lead 类,它只验证姓名和电话号码,我们也可以继承自“CustomerBase”类并相应地编写验证。
public class Lead : CustomerBase
{
public override void Validate()
{
if (CustomerName.Length == 0)
{
throw new Exception("Customer Name is required");
}
if (PhoneNumber.Length == 0)
{
throw new Exception("Phone number is required");
}
}
}
技术背景
上面定义的三类存储在硬盘上,格式为“.CS”扩展名。现在需要做两件事:-
- 一些 UI 应该调用这些类,将这些实体带入 RAM。
- 第二,一旦最终用户完成操作,我们需要将这些实体保存到硬盘。
换句话说,我们的实体需要 IT 基础设施来运行。它需要 UI 基础设施(WPF、Flash、HTML)来调用,以及持久化基础设施(SQL Server、Oracle)来保存到硬盘。
如果您将这些部分的视觉图与技术背景结合起来,看起来是这样的。所以,我们可以有消费者上下文,它可以是 HTML、WPF、Windows 等形式。我们可以有持久化上下文,它可以是文件或 RDBMS,如 SQL Server、Oracle 等。
根据以上思考,我们得出三种类型的模块:-
- 消费者模块,主要包含 UI。
- 领域模块,包含您的类和业务逻辑。
- 持久化模块,就是您的 RDBMS、文件等。
思维过程 4:- 当您可视化一个类时,始终从技术上下文和业务实体上下文这两个角度来考虑它们。 |
三层架构 – 管理变更
软件架构的试金石发生在变更时。当您在一个地方进行更改时,如果您需要在很多地方都进行更改,那么这就表明架构不佳。
所以,为了避免处处都需要更改,我们需要 proper layers(层)和 compartments(模块),并将相似性质的责任放在这些层中。所以,正如在技术背景中所讨论的,我们目前至少有三个模块:UI、领域/业务层和数据层。
所以,在同一个项目中,让我们为 UI 部分添加一个简单的 Windows UI。
我们需要一个类库,一个用于“数据访问”,另一个用于客户实体。
所以,现在您的解决方案看起来如下面的图片所示。三个不同的项目层,分别处理三件不同的事情。
那么,分层如何帮助我们更好地管理变更呢?因为我们将项目分成了逻辑层,所以很容易知道哪些变更应该去哪个层。例如,如果我们想升级数据访问层,只需要更改第三层;如果我们想从一种 UI 技术迁移到另一种,需要更改 UI 层,依此类推。
上述架构被称为“三层架构”。
思维过程 5:- 始终将项目划分为逻辑层,每一层都应该有独特的责任。 |
三层 VS 三层(物理部署)
架构界一个令人困惑的术语是“层”(Layer)与“层级”(Tier)架构。“三层架构”只是逻辑分离。而在“三层级”中,这三层被部署在物理上分离的机器上。
第四步:- 创建用户界面
在 UI 层,我们放置了 UI 所需的控件。
在此 UI 层中,我们添加了对“Customer”库的引用,并在下拉列表更改时,我们创建“Lead”对象或“Customer”对象。
private void btnAdd_Click(object sender, EventArgs e)
{
CustomerBase custbase = null;
if (cmbCustomerType.SelectedIndex == 0)
{
custbase = new Lead();
}
else
{
custbase = new Customer();
}
custbase.CustomerName = txtCustomerName.Text;
custbase.Address = txtAddress.Text;
custbase.PhoneNumber = txtPhoneNumber.Text;
custbase.BillDate = Convert.ToDateTime(txtBillingDate.Text);
custbase.BillAmount = Convert.ToDecimal(txtBillingAmount.Text);
}
那么,您能猜出上述代码有什么问题吗?思考一下??
好的,我们将问题圈了出来,以帮助您理解。问题在于“变更”。正如需求 4 中所定义的,未来可以添加新的客户类型。所以,当添加新的客户类型时,我们需要更改 UI 代码,或者让我说,许多 UI 屏幕都会如此。
还记得我们说的,良好的架构是指在一个地方更改,而无需到处更改的架构吗?
SOLID 的 S (单一职责原则)
是时候讨论 SOLID 原则了。SOLID 原则是如果我们遵循它,就能使我们的 OOP 更好。
- 单一职责原则 (SRP)。
- 开闭原则 (OCP)
- 里氏替换原则 (LSP)
- 接口隔离原则 (ISP)
- 依赖倒置原则。
目前,我们不讨论所有四个 SOLID 原则,如果您现在想了解,可以阅读这篇C# SOLID 文章,它只讲 SOLID 原则。
现在,让我们只关注 SOLID 的“S”,即单一职责原则 (SRP)。
SRP 认为一个类一次只应该做一件事情,而不是不相关的事情。如果您查看 UI,它应该负责布局、视觉效果、接收输入等。但现在它正在创建“Customer”对象,这不是它的职责。
换句话说,UI 正在处理多项职责,这使得类在未来更加复杂且难以维护。
思维过程 6:- 在进行架构设计时,始终将 SOLID 原则牢记于心。 |
SRP 的同义词:关注点分离 (SOC)
SRP 的同义词之一是 SOC – 关注点分离。关注点分离规则规定,一个类应该只做它的相关事务,任何不相关的事务都应该移交给其他类。例如,在这种情况下,UI 不应该直接创建“Customer”对象。
所以,下次您看到 SOC 时,可以认为它是 SRP 的同义词,反之亦然。
注意:- 我不知道 SOC 或 SRP 哪个先出现。但它们肯定有相同的目标。
第五步:- 解耦需要抽象思维 – 创建接口
为了实现 UI 和 Customer 类型之间的解耦,UI 必须以抽象的方式看待 Customer 类型,而不是直接处理具体的类。
抽象:- 它是一个 OOP 原则,我们只向消费者展示必要的东西。
UI 应该只与纯定义交互,而不是与已实现的具体类交互。这就是接口的用武之地。接口可以帮助您创建纯定义。然后,您的 UI 将指向这些纯定义,而无需担心后端的已实现类。
所以,让我们在同一解决方案中创建一个名为“ICustomerInterface”的单独类库。我们将此接口引用到 UI 中。以下是接口“ICustomer”的样子,只有空的定义和空的方法。
public interface ICustomer
{
string CustomerName { get; set; }
string PhoneNumber { get; set; }
decimal BillAmount { get; set; }
DateTime BillDate { get; set; }
string Address { get; set; }
void Validate();
}
所以,从架构角度来看,整个解决方案现在看起来如下所示。接口现在充当 UI 和 Customer 类库之间的中介。
所以,如果我们现在看我们的客户端代码,它变成如下所示。
ICustomer icust = null;
if (cmbCustomerType.SelectedIndex == 0)
{
icust = new Lead();
}
else
{
icust = new Customer();
}
icust.CustomerName = txtCustomerName.Text;
icust.Address = txtAddress.Text;
icust.PhoneNumber = txtPhoneNumber.Text;
icust.BillDate = Convert.ToDateTime(txtBillingDate.Text);
icust.BillAmount = Convert.ToDecimal(txtBillingAmount.Text);
但实际上,情况并没有改变。如果我们添加一个新类,仍然需要创建具体已实现类的对象。
所以,我们需要更多东西,请看下一步解决方案。
思维过程 7:- 接口的主要作用是使类之间解耦。 |
第六步:- PIC 模式用于解耦 (简单工厂模式)
PIC 的缩写是“多态 + 接口 + 对象创建集中化”。
如果您仔细观察代码,您会发现尚未实现解耦的核心原因。这一切都是因为“NEW”关键字。“NEW”关键字是两个系统紧密耦合的主要原因之一。
所以,第一步是摆脱消费端的“NEW”关键字。让我们开始将“NEW”关键字移到一个中心工厂类。
所以,我们添加了一个名为“FactoryCustomer”的新类库项目,其代码如下。
public class Factory
{
public ICustomer Create(int CustomerType)
{
if (CustomerType == 0)
{
return new Lead();
}
else
{
return new Customer();
}
}
}
它有一个简单的“Factory”类,带有一个 create 方法。这个“create”方法接受一个数字值,并根据该数字值创建“Lead”对象或“Customer”对象。但这个“Create”函数特别之处在于它返回“ICustomer”接口类型。
所以,现在控制台应用程序 UI 使用“Factory”来创建对象,并且由于“Create”函数返回“ICustomer”类型,UI 无需担心后端具体的客户类。
ICustomer icust = null;
Factory obj = new Factory();
icust = obj.Create(cmbCustomerType.SelectedIndex);
您可以看到上面的代码中没有对“Lead”或“Customer”等具体类的引用,这表明 UI 未与核心客户库类解耦。
如果您想知道为什么我们将类命名为“factory”。在现实世界中,“factory”意味着一个创建(制造)事物的实体,而我们的“factory”类正是这样做的,它创建对象。所以,这个名字是相辅相成的。
思维过程 8:- “NEW”关键字是导致紧耦合的主要罪魁祸首。 |
第七步:- RIP 模式 (用多态替换 IF)
如果您查看第 6 步的代码,我们只是把责任推给了别人。原本在 UI 中的“IF”条件现在是工厂的一部分,这更好,但实际上“IF”条件仍然存在。它只是从 UI 移到了工厂。
集中对象创建的好处是,如果我们有很多客户端在很多地方使用具体类,而不需要在其他地方进行更改。
但现在,让我们开始思考如何移除“IF”条件。您一定听说过以下最佳实践声明:-
“如果存在多态,并且您看到很多 IF 条件,那么就意味着多态的好处没有被充分利用”。
移除“IF”条件是一个三步过程:-
第一步:- 创建一个“ICustomer”的集合列表。
private List<icustomer> customers = new List<icustomer>();</icustomer></icustomer>
第二步:- 在构造函数中加载客户类的类型,如 lead 和 customer。
public Factory()
{
customers.Add(new Lead());
customers.Add(new Customer());
}
第三步:- create 方法只需按索引查找列表并返回客户类型。由于多态性,具体的客户类会自动转换为泛型接口。
public ICustomer Create(int CustomerType)
{
return customers[CustomerType];
}
以下是更改后的工厂类的完整代码。
public class Factory
{
private List<icustomer> customers = new List<icustomer>();
public Factory()
{
customers.Add(new Lead());
customers.Add(new Customer());
}
public ICustomer Create(int CustomerType)
{
return customers[CustomerType];
}
}
</icustomer></icustomer>
注意:- RIP 模式最大的限制是具体的类必须在继承层次结构中,并且具有相同的签名。多态和继承是 RIP 模式存在的强制性特征。
思维过程 9:- 在多态中,IF 可以被动态多态集合替换。 |
模式 1 RIP 模式:- 如果您拥有多态的优势,并且看到很多 IF 条件,那么很可能可以用简单的集合查找替换 IF 条件。此模式属于行为类别。
IOC 是一个思想,DI 是一个实现
IOC 是一个思想,或者可以说是一个原则,即一个实体的非相关工作应该被移交给其他地方。再次阅读其全称:- 控制反转,或者更具体地说,是将不必要工作的控制权反转给其他实体。
如果您看到上述场景,我们将“Customer”对象的创建的非相关职责从 UI 转移到了“Factory”类,或者我说我们反转了它。
现在,IOC 原则可以通过多种方式实现:依赖注入、委托等。
现在,为了实现这个思维过程,我们使用了“Factory”类。如果您从工厂类的角度来看,我们实际上是将一个对象注入到 UI 中。所以,UI 需要由工厂类注入的对象。DI 是从一个单独的实体注入依赖对象以实现解耦的过程。
一脉相承:- SOC、SRP、IOC 和 DI
如果您仔细观察,SOC、SRP 和 IOC 几乎是同义词,并且这些原则可以通过使用 DI、Events、Delegates、Constructor injection、service locator 等来实现。所以,对我来说,SOC、SRP 和 IOC 都看起来是同义词。
我鼓励您观看此视频,其中实际讲解 IOC 和 DI。
第八步:- 提高工厂类的性能
在上述架构中,如果明天我们有很多具体的对象,工厂类的性能会非常差。
并且如果我们一遍又一遍地创建工厂实例,它会导致大量内存消耗。
所以,如果我们只有一个工厂类的实例,并且所有具体的对象都加载一次,那将极大地提高性能。这个实例可以用来服务所有需要实例的客户端。
为了拥有单例副本,我们需要做以下几点:-
- 将类声明为静态。
- 将存储类型的列表声明为静态。
- 最后,Create 函数也应该被定义为静态,这样它就可以访问静态变量。
public static class Factory
{
private static List<icustomer> customers = new List<icustomer>();
static Factory()
{
customers.Add(new Lead());
customers.Add(new Customer());
}
public static ICustomer Create(int CustomerType)
{
return customers[CustomerType];
}
}
</icustomer></icustomer>
在客户端,工厂调用代码现在变得简单多了。
icust = Factory.Create(cmbCustomerType.SelectedIndex);
模式 2 简单工厂模式:- 通过集中对象创建并返回通用接口引用,有助于在应用程序发生更改时最大限度地减少更改。这属于创建类别。
此模式不应与 GoF 的工厂模式混淆。工厂模式的基础是简单工厂模式。
第九步:- 延迟加载工厂
另外,我们可以对工厂做的另一件很棒的事情是按需加载对象。如果您看到,目前对象是加载的,无论您是否需要它们。如果我们只加载“即时”(Just-in-time),换句话说,当我们想要对象时才加载它们,怎么样?
所以,将上述工厂转换为延迟加载是一个两步过程:-
第一步:- 将对象集合类型设为 null。不要加载它们。
private static List<icustomer> customers = null;</icustomer>
第二步:- create 函数现在将首先检查对象是否为 NULL,如果是则加载它,否则只是在集合中查找。
public static ICustomer Create(int CustomerType)
{
if (customers == null)
{
LoadCustomers();
}
return customers[CustomerType];
}
以下是具有延迟加载的完整代码。
public static class Factory
{
private static List<icustomer> customers = null;
private static void LoadCustomers()
{
customers = new List<icustomer>();
customers.Add(new Lead());
customers.Add(new Customer());
}
public static ICustomer Create(int CustomerType)
{
if (customers == null)
{
LoadCustomers();
}
return customers[CustomerType];
}
}
</icustomer></icustomer>
模式 3 延迟加载:- 这是一种创建型设计模式,我们只在需要时才加载对象。延迟加载的对立面是贪婪加载。
作业:- 使用 Lazy 关键字自动化延迟加载
延迟设计模式可以通过 C# Lazy 关键字自动化并简化。我将把这个留给你们作为作业。观看下面的 YouTube 视频以了解 C# Lazy 加载概念,然后尝试用 C# Lazy 关键字替换您的自定义代码。
以下是使用 C# Lazy 关键字的代码。
public static class Factory
{
private static Lazy<list<icustomer>> customers = null;
public Factory()
{
customers = new Lazy<list<icustomer>>(() => LoadCustomers());
}
private List<icustomer> LoadCustomers()
{
List<icustomer> custs = new List<icustomer>();
custs.Add(new Lead());
custs.Add(new Customer());
return custs;
}
public static ICustomer Create(int CustomerType)
{
return customers.Value[CustomerType];
}
}
</icustomer></icustomer></icustomer></list<icustomer></list<icustomer>
第十步:- 实现克隆 (原型模式)
现在,上述工厂模式类有一个缺陷,您能猜到是什么吗?
icust = Factory.Create(0);
现在它返回相同的实例,因为工厂模式指向的是同一个集合实例。这太糟糕了,因为工厂的整个目的是创建新实例,而不是返回相同的实例。
所以,我们需要某种机制,而不是返回相同的对象,而是返回对象的 CLONE,就像 BY VAL 副本一样。这就是原型模式发挥作用的地方。
所以,第一步是在“ICustomer”接口中定义一个“Clone”方法。
public interface ICustomer
{
string CustomerName { get; set; }
string PhoneNumber { get; set; }
decimal BillAmount { get; set; }
DateTime BillDate { get; set; }
string Address { get; set; }
void Validate();
ICustomer Clone(); // Added an extra method clone
}
为了创建一个 .NET 对象的“Clone”,我们已经有了现成的“MemberwiseClone”函数。在客户基类中,我们实现了它。通过这种方法,任何其他继承的客户类也将能够克隆对象。
public class CustomerBase : ICustomer
{
// Other codes removed for readability
public ICustomer Clone()
{
return (ICustomer) this.MemberwiseClone();
}
}
现在,工厂的“Create”函数将在集合查找后调用 clone 方法。因此,不会发送相同的对象引用,而是一个全新的对象副本。
public static class Factory
{
// Other codes are removed for readability purpose
public static ICustomer Create(int CustomerType)
{
return customers.Value[CustomerType].Clone();
}
}
模式 4 原型模式:- 这是一种创建型设计模式,我们创建一个全新的克隆/实例对象。
第十一步:- 使用 Unity 自动化简单工厂
一个好的开发者总是会思考如何通过现成的框架自动化设计模式。例如,上述简单工厂类很棒,但现在请思考一下,如果我们还需要支持其他对象类型,如订单、日志记录器等,该怎么办?所以,为每个对象编写工厂,创建多态集合、查找,并在其之上测试所有这些东西本身就是一项艰巨的任务。
整个简单工厂和多态集合查找可以通过使用一些 DI 框架(如 unity、ninject、MEF 等)来自动化/替换。
我能理解有些人会大喊“什么是 DI?”。屏住呼吸,我们很快就会讨论。现在,让我们专注于如何使用 DI 框架来自动化简单工厂。现在,我们将选择 unity application block。
所以,第一步是在工厂类中使用 NUGET 获取 unity application block。如果您是 NUGET 新手,可以观看此视频,其中讲解 NUGET 基础知识。
所以,第一步是获取 unity application block 的命名空间。
using Microsoft.Practices.Unity;
在 unity 或任何 DI 框架中,我们都有容器的概念。这些容器只不过是集合。“RegisterType”和“ResolveType”方法分别帮助将对象添加到容器集合中并从中获取对象。
static IUnityContainer cont = null;
static Factory()
{
cont = new UnityContainer();
cont.RegisterType<icustomer, lead="">("0");
cont.RegisterType<icustomer, customer="">("1");
}
public static ICustomer Create(int CustomerType)
{
return cont.Resolve<icustomer>(CustomerType.ToString());
}
</icustomer></icustomer,></icustomer,>
下图展示了手动工厂模式代码如何映射到自动化的 Unity 容器代码。
第十二步:- 抽象类 – 半成品
如果您还记得“CustomerBase”类,它是一个半定义类。它定义了所有属性,但 validate 方法由具体的子类稍后定义。现在,请想一下,如果有人创建了这个半定义类的对象,会产生什么后果?
如果调用下面的空“Validate”方法会发生什么?
是的,您猜对了,混乱。
public class CustomerBase : ICustomer
{
public string CustomerName { get; set; }
public string PhoneNumber { get; set; }
public decimal BillAmount { get; set; }
public DateTime BillDate { get; set; }
public string Address { get; set; }
public void Validate()
{
// To be defined by the child classes
}
}
所以,解决方案是通过不允许客户端创建半定义类对象来避免混乱,即创建“抽象类”。
public abstract class CustomerBase : ICustomer
{
public string CustomerName { get; set; }
public string PhoneNumber { get; set; }
public decimal BillAmount { get; set; }
public DateTime BillDate { get; set; }
public string Address { get; set; }
public abstract void Validate();
}
现在,如果客户端尝试创建抽象类的对象,它将收到以下错误,这有助于我们避免使用半定义类的混乱。
第十三步:- 泛型工厂
如果您看到工厂类,它目前绑定到“Customer”类型。换句话说,如果我们想用“Supplier”类型替换它,我们需要另一个“Create”方法,如下面的代码所示。所以,如果我们有很多这样的业务对象,我们将最终有很多“Create”方法。
public static class Factory
{
public static ICustomer Create(int CustomerType)
{
return cont.Resolve<icustomer>(CustomerType.ToString());
}
public static Supplier Create(int Supplier)
{
return cont.Resolve<isupplier>(Supplier.ToString());
}
}
</isupplier></icustomer>
所以,与其将“Factory”绑定到单个类型,不如使其成为“泛型”类。
如果您对“泛型”(Generics)不熟悉,我建议您观看这个YouTube C# 泛型视频,其中详细解释了“泛型”的概念。而且,如果您属于那种认为“泛型”和“泛型集合”是同义词的学派,您绝对应该观看上面的视频,以消除这种误解。
泛型可以帮助您将逻辑与数据类型解耦。所以,这里的逻辑是“对象创建”,但仅将其与“Customer”绑定会使架构变得僵化。所以,为什么不让它成为一个泛型类型“AnyType”,如下面的代码所示。
public static class Factory<anytype>
{
static IUnityContainer container = null;
public static AnyType Create(string Type)
{
if (container == null)
{
container = new UnityContainer();
container.RegisterType<icustomer, lead="">("Lead");
container.RegisterType<icustomer, customer="">("Customer");
}
return container.Resolve<anytype>(Type.ToString());
}
}
</anytype></icustomer,></icustomer,></anytype>
注意:- 我已将键从数字“0”和“1”更改为“Lead”和“Customer”,以便更易于阅读。
所以,现在当客户端想要创建“Customer”对象时,他需要像下面这样调用。
ICustomer Icust = Factory<icustomer>.Create(“Customer”);</icustomer>
第十四步:- 使用策略模式进行验证
如果您阅读需求三,它提到“Customer”和“Lead”的验证是不同的。对于“Customer”,所有字段都是必填项,而对于“Lead”,只需要姓名和电话号码就足够了。
因此,为了实现这一点,我们创建了一个虚拟的“Validate”方法,并在新的类中为每个类单独重写了该方法以实现新规则。
public class Customer : CustomerBase
{
public override void Validate()
{
if (CustomerName.Length == 0)
{
throw new Exception("Customer Name is required");
}
if (PhoneNumber.Length == 0)
{
throw new Exception("Phone number is required");
}
if (BillAmount == 0)
{
throw new Exception("Bill Amount is required");
}
if (BillDate >= DateTime.Now)
{
throw new Exception("Bill date is not proper");
}
}
}
public class Lead : CustomerBase
{
public override void Validate()
{
if (CustomerName.Length == 0)
{
throw new Exception("Customer Name is required");
}
if (PhoneNumber.Length == 0)
{
throw new Exception("Phone number is required");
}
}
}
但是,如果您再次阅读需求,它会进一步指出,未来有可能添加新的验证,并且我们期望系统能够灵活,或者我更愿意说“动态”地实现这一点。
但是,当我们使用继承时,它比动态更静态。其次,“Customer”类和“Lead”类与验证算法/策略紧密耦合。所以,如果我们想实现动态灵活性,我们需要从实体中移除这个验证逻辑,并将其移到别处。
此时,实体类与验证算法绑定。简而言之:-
- 我们没有遵循 SRP。
- 我们没有做到 SOC,因此
- 我们需要实现 IOC,这意味着我们需要将算法逻辑从实体类移到其他类。
现在,为了实现“实体”和“验证逻辑”之间的解耦,我们需要确保这两个方通过通用接口进行通信,而不是直接与具体类通信。
我们在同一个地方已经有了一个“Customer”的通用接口,同样,让我们为验证算法创建一个通用接口,我们将其命名为“IValidationStratergy”。同时请注意,我们将接口设计为泛型,以便将来可以将其用于其他类型,如“Supplier”、“Accounts”等。
public interface IValidationStratergy<anytype>
{
void Validate(AnyType obj);
}
</anytype>
我们现在可以进一步实现上述接口并创建不同的验证逻辑。例如,下面的验证检查
public class CustomerAllValidation : IValidationStratergy<icustomer>
{
public void Validate(ICustomer obj)
{
if (obj.CustomerName.Length == 0)
{
throw new Exception("Customer Name is required");
}
if (obj.PhoneNumber.Length == 0)
{
throw new Exception("Phone number is required");
}
if (obj.BillAmount == 0)
{
throw new Exception("Bill Amount is required");
}
if (obj.BillDate >= DateTime.Now)
{
throw new Exception("Bill date is not proper");
}
}
}
</icustomer>
public class LeadValidation : IValidationStratergy<icustomer>
{
public void Validate(ICustomer obj)
{
if (obj.CustomerName.Length == 0)
{
throw new Exception("Customer Name is required");
}
if (obj.PhoneNumber.Length == 0)
{
throw new Exception("Phone number is required");
}
}
}
</icustomer>
现在,基类将内部指向通用的验证接口。现在,“Customer”和“Lead”类对它将执行哪种验证策略一无所知。
public abstract class CustomerBase : BoBase, ICustomer
{
// Code removed for simplification
private IValidationStratergy<icustomer> _ValidationType = null;
public CustomerBase(IValidationStratergy<icustomer> _Validate)
{
_ValidationType = _Validate;
}
public IValidationStratergy<icustomer> ValidationType
{
get
{
return _ValidationType;
}
set
{
_ValidationType = value;
}
}
}
}
</icustomer></icustomer></icustomer>
现在,在工厂类中,我们可以创建任何实体并将任何验证注入其中。您可以看到,我们创建了一个客户类,并将所有验证类对象注入其中。同样,我们创建了一个潜在客户类,并将潜在客户验证对象注入其中。
container.RegisterType<icustomer, customer="">("Customer", new InjectionConstructor(newCustomerAllValidation()));
container.RegisterType<icustomer, lead="">("Lead", new InjectionConstructor(new LeadValidation()));
</icustomer,></icustomer,>
以下是 Factory 的完整代码。
public static class Factory<anytype>
{
static IUnityContainer container = null;
public static AnyType Create(string Type)
{
if (container == null)
{
container = new UnityContainer();
container.RegisterType<icustomer, customer="">("Customer",
new InjectionConstructor(newCustomerAllValidation()));
container.RegisterType<icustomer, lead="">("Lead",
new InjectionConstructor(new LeadValidation()));
}
return container.Resolve<anytype>(Type.ToString());
}
}
</anytype></icustomer,></icustomer,></anytype>
模式 5 策略模式:- 这是一种行为设计模式,有助于在运行时选择算法。
消费并投入使用
如果您还记得我们已经有一个 UI,所以我只是在我的 UI 中应用了更新。
以下是按钮 validate 事件的代码。
// Create customer or lead type depending on the value of combo box
icust = Factory<icustomer>.Create(cmbCustomerType.Text);
// Set all values
icust.CustomerName = txtCustomerName.Text;
icust.Address = txtAddress.Text;
icust.PhoneNumber = txtPhoneNumber.Text;
icust.BillDate = Convert.ToDateTime(txtBillingDate.Text);
icust.BillAmount = Convert.ToDecimal(txtBillingAmount.Text);
// Call validate method
icust.Validate();
</icustomer>
下一部分将包含什么?
在下一部分,我们将涵盖以下五个模式:-
- 使用 Repository 模式、UOW 和 Adapter 模式创建 ADO.NET DAL 层并进行解耦。
- 在 ADO.NET 代码中使用模板模式来重用命令和连接对象。
- 使用 Façade 模式简化 UI 代码。
进一步阅读,请观看下面的面试准备视频和分步视频系列。