65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (233投票s)

2015 年 7 月 14 日

CPOL

31分钟阅读

viewsIcon

455197

downloadIcon

11627

在本文中,我们将通过一个项目,一步一步学习 C# 设计模式和架构模式。

目录

第一部分我们将学习哪些模式和概念?

引言

设计模式 VS 架构模式 VS 架构风格

设计模式定义

酷品商店项目:- 第一阶段

软件架构是一个演进过程

什么是实体(ENTITY)?

第一步:- 识别您的实体/对象

第二步:- 识别实体之间的关系

第三步:- 从公共类派生

技术背景

三层架构 – 管理变更

三层 VS 三层(物理部署)

第四步:- 创建用户界面

SOLID 的 S (单一职责原则)

SRP 的同义词:关注点分离 (SOC)

第五步:- 解耦需要抽象思维 – 创建接口

第六步:- PIC 模式用于解耦 (简单工厂模式)

第七步:- RIP 模式 (用多态替换 IF)

IOC 是一个思想,DI 是一个实现

一脉相承:- SOC、SRP、IOC 和 DI

第八步:- 提高工厂类的性能

第九步:- 延迟加载工厂

作业:- 使用 Lazy 关键字自动化延迟加载

第十步:- 实现克隆 (原型模式)

第十一步:- 使用 Unity 自动化简单工厂

第十二步:- 抽象类 – 半成品

第十三步:- 泛型工厂

第十四步:- 使用策略模式进行验证

消费并投入使用

下一部分将包含什么?

第一部分我们将学习哪些模式和概念?

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)是一家大型零售商场,在孟买和浦那城市设有连锁商场。公司管理层希望为其零售店开发一个简单的客户管理系统,具备以下功能。公司决定分阶段推出该项目。

因此,在第一阶段,他们只想捕获客户信息。以下是更详细的需求:-

  1. 目前,应用程序将捕获 5 个字段:客户姓名、电话号码、账单金额、账单日期和客户地址。
  2. 在第一阶段,收集两种类型的客户数据。一种是潜在客户(lead),另一种是客户(customer)。潜在客户是指来到酷品商店但未购买任何东西的人。他们只是咨询然后离开。客户是指来到商店并购买商品的人。客户实际上进行了财务交易。
  3. 当是潜在客户时,只有客户姓名和电话号码是必填项;而对于客户,所有字段都是必填项。系统应提供无缝添加新验证规则的机制,并且这些验证规则应具有灵活性和可重用性,以应用于系统。
  4. 系统应具备显示、添加、更新和删除客户数据的功能。
  5. 目前,系统将使用 SQL Server 和 ADO.NET 作为数据层技术。但在接下来的几个月里,我们将把数据层迁移到 Entity Framework。迁移应该是无缝的,并且对整个系统的更改应该不大。
  6. 系统应具备取消屏幕上任何修改的能力。因此,如果客户正在编辑一条记录并更改了某些值,他们应该有机会恢复到旧值。

软件架构是一个演进过程

误区 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”扩展名。现在需要做两件事:-

  1. 一些 UI 应该调用这些类,将这些实体带入 RAM。
  2. 第二,一旦最终用户完成操作,我们需要将这些实体保存到硬盘。

换句话说,我们的实体需要 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 代码。

进一步阅读,请观看下面的面试准备视频和分步视频系列。

© . All rights reserved.