使用设计模式和原则构建 C# 应用程序
我写这篇文章是为了演示如何从头开始使用 SOLID 原则和常见设计模式构建应用程序
介绍
本文将作为一系列文章的第一部分,介绍如何使用常见的企业架构原则和设计模式在 C# 中构建软件应用程序。文章将首先提供一种快速、粗糙但并非理想的编码示例,然后解释如何重构代码以使其更符合通用的编码指南。我将假设读者熟悉 C# 和基本面向对象原则。我将引用 SOLID 原则和某些设计模式,而不会在此定义它们。市面上有很多文章可以做到这一点。
那么,我们想要完成什么呢?让我想一个类比。您想建造一栋坚固的房子。您怎么做到这一点?希望建筑师和建造者知道他们在做什么:打好稳定的地基,以正确的方式进行框架搭建,电气工程符合规范等等。那么,这就是我们在编码时试图完成的目标。我们希望我们的代码是 SOLID 的(查找该缩写以获取更多解释)。我会在过程中解释它的含义。我们如何实现这一点?希望建筑师和开发人员知道他们在做什么并使用设计模式。您使用设计模式作为创建稳健代码的一种技术。SOLID 是目标,设计模式是实现该目标的一种方式。
开始
让我们从典型的电子商务场景开始。我们需要一个对象来表示订单(Order)、订单项(Order Item)和客户(Customer)。给定一个 Order
对象,该对象有一个方法可以计算总价(通过对订单项成本的总和应用税费),那么您能想到的最简单但最糟糕的编程方法是什么?当然是将税费例程逻辑放在 Order 类中。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace solid
{
public class Order
{
List<OrderItem> _orderItems = new List<OrderItem>();
public decimal CalculateTotal(Customer customer)
{
decimal total = _orderItems.Sum((item)=>{
return item.Cost * item.Quantity;
});
decimal tax;
if (customer.StateCode == "TX")
tax = total * .08m;
else if (customer.StateCode == "FL")
tax = total * .09m;
else
tax = .03m;
total = total + tax;
return total;
}
}
public class OrderItem
{
public int Quantity;
public string Code;
public decimal Cost;
}
public class Customer
{
public string StateCode;
public string ZipCode;
public string County;
}
}
这样做有什么问题?从学术上讲,它违反了 SOLID 编程原则的第一条规则:单一职责。换句话说,Order 对象应该只负责处理与 Order 紧密相关的事务,例如计算订单项的总成本。它不应该还要负责按州管理税费例程。您甚至可以说它不应该负责计算订单项的总数,而它的唯一职责是协调计算订单项总数、添加税费以及可能添加运费或应用折扣的工作流程。但为了简单起见,我们暂时保持现状。
那么,还有什么问题呢?这段代码违反了另一项 SOLID 原则:开闭原则。换句话说,一旦定义了一个类,它就不应该改变(它是封闭的),而只能通过继承或运行时变异来扩展(对扩展开放)。这可能看起来奇怪或不切实际,但理想情况下,如果从业务规则收集、设计到编码的每一步都做得正确,一旦类经过测试并获准用于生产,您就不应该再更改其源代码。然而,在这里我们可以看到事实并非如此。任何时候添加或更改州税例程,Order 类的代码都必须更改。那么该怎么办?让我们稍微修改一下代码并开始使用一种模式。
策略模式
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace solid
{
public class Order
{
List<OrderItem> _orderItems = new List<OrderItem>();
public decimal CalculateTotal(Customer customer)
{
decimal total = _orderItems.Sum((item) =>
{
return item.Cost * item.Quantity;
});
total = total + new Tax().CalculateTax(customer,total);
return total;
}
public class Tax
{
public decimal CalculateTax(Customer customer, decimal total)
{
decimal tax;
if (customer.StateCode == "TX")
tax = total * .08m;
else if (customer.StateCode == "FL")
tax = total * .09m;
else
tax = .03m;
return tax;
}
}
}
}
我们所做的只是创建了一个名为 Tax 的新类,并将税费例程逻辑放在其中。现在 Order 对象不再负责税费逻辑,可以将这项工作委托给 tax 对象来处理。这是策略模式的一个简单示例。简单定义一下,它就是将所有 独立的 逻辑放在自己的类中。很好,但现在有什么问题呢?我们违反了另一项 SOLID 原则:依赖倒置。它说类应该依赖于抽象而不是具体实现。在 C# 中,抽象由接口或抽象类表示。在我们上面的代码中,我们通过
new Tax();
更不用说 Tax
对象仍然违反开闭原则,因为税费例程仍然可以添加或更改。我们就是赢不了。让我们继续重构。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace solid
{
public class Order
{
List<OrderItem> _orderItems = new List<OrderItem>();
public decimal CalculateTotal(Customer customer)
{
decimal total = _orderItems.Sum((item) =>
{
return item.Cost * item.Quantity;
});
ITax tax = new Tax();
total = total + tax.CalculateTax(customer, total);
return total;
}
public interface ITax
{
decimal CalculateTax(Customer customer, decimal total);
}
public class Tax : ITax
{
public decimal CalculateTax(Customer customer, decimal total)
{
decimal tax;
if (customer.StateCode == "TX")
tax = total * .08m;
else if (customer.StateCode == "FL")
tax = total * .09m;
else
tax = .03m;
return tax;
}
}
}
}
所以,我们现在所做的是创建了一个税费接口来抽象实现。这现在好了一点,但我们仍然在实例化一个具体的类。Order 类不应该为此负责。它不应该关心它正在处理哪种 ITax
对象,只关心它是一个具有 CalculateTax
方法的 ITax
实例。在上面的代码中,它知道它正在创建 Tax 对象。这不好。我们需要其他东西来负责选择创建哪种 ITax
对象。
工厂模式
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace solid
{
public class Order
{
List<OrderItem> _orderItems = new List<OrderItem>();
public decimal CalculateTotal(Customer customer)
{
decimal total = _orderItems.Sum((item) =>
{
return item.Cost * item.Quantity;
});
ITax tax = new TaxFactory().GetTaxObject();
total = total + tax.CalculateTax(customer, total);
return total;
}
public class TaxFactory
{
public ITax GetTaxObject()
{
return new Tax();
}
}
public interface ITax
{
decimal CalculateTax(Customer customer, decimal total);
}
public class Tax : ITax
{
public decimal CalculateTax(Customer customer, decimal total)
{
decimal tax;
if (customer.StateCode == "TX")
tax = total * .08m;
else if (customer.StateCode == "FL")
tax = total * .09m;
else
tax = .03m;
return tax;
}
}
}
}
简单定义一下,这种模式说对于任何给定的场景,应该有一个类,其唯一 职责 是根据一些变化的标准来创建其他类。使用工厂模式,我们创建一个负责为我们创建 tax 对象的类。现在 Order 对象完全不知道我们如何创建 tax 对象,也不知道它正在处理哪个 ITax 的具体实现。它只关心它有一个具有 CalculateTax
方法的 ITax
。这时您可能想知道这样做有什么意义,它只是更多的代码,而 TaxFactory 只返回一种税费对象,不太有用。让我们引入一个新问题:如果引入了一个新的业务规则,规定如果存在县,则使用特定的税费例程,否则就使用州税费例程。现在我们需要更改代码。让我们从糟糕的方式开始编码。我们可能会这样做:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace solid
{
public class Order
{
List<OrderItem> _orderItems = new List<OrderItem>();
public decimal CalculateTotal(Customer customer)
{
decimal total = _orderItems.Sum((item) =>
{
return item.Cost * item.Quantity;
});
ITax tax = new TaxFactory().GetTaxObject();
total = total + tax.CalculateTax(customer, total);
return total;
}
public interface ITax
{
decimal CalculateTax(Customer customer, decimal total);
}
public class Tax : ITax
{
public decimal CalculateTax(Customer customer, decimal total)
{
decimal tax;
if (!string.IsNullOrEmpty(customer.County))
{
if (customer.County == "Travis")
tax = total * .085m;
else if (customer.County == "Hays")
tax = total * .095m;
else
tax = .03m;
}
else
{
if (customer.StateCode == "TX")
tax = total * .08m;
else if (customer.StateCode == "FL")
tax = total * .09m;
else
tax = .03m;
}
return tax;
}
}
public class TaxFactory
{
public ITax GetTaxObject()
{
return new Tax();
}
}
}
}
我们在 tax 对象中加入了条件语句来检查县。嗯,我们打破了更多规则。我们更改了一个不应该改变的类(开闭原则)。现在 Tax 对象负责决定是使用县税还是州税例程的逻辑(单一职责)。 如果业务规则再次更新,该类将不得不再次更改。听起来我们需要添加另一个类。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace solid
{
public class Order
{
List<OrderItem> _orderItems = new List<OrderItem>();
public decimal CalculateTotal(Customer customer)
{
decimal total = _orderItems.Sum((item) =>
{
return item.Cost * item.Quantity;
});
ITax tax = new TaxFactory().GetTaxObject();
total = total + tax.CalculateTax(customer, total);
return total;
}
public interface ITax
{
decimal CalculateTax(Customer customer, decimal total);
}
public class Tax : ITax
{
public decimal CalculateTax(Customer customer, decimal total)
{
decimal tax;
if (customer.StateCode == "TX")
tax = total * .08m;
else if (customer.StateCode == "FL")
tax = total * .09m;
else
tax = .03m;
return tax;
}
}
public class TaxByCounty : ITax
{
public decimal CalculateTax(Customer customer, decimal total)
{
decimal tax;
if (customer.County == "Travis")
tax = total * .08m;
else if (customer.County == "Hays")
tax = total * .09m;
else
tax = .03m;
return tax;
}
}
public class TaxFactory
{
public ITax GetTaxObject()
{
return new Tax();
}
}
}
}
在这里,我们添加了一个新类来处理基于县的税费例程,即 TaxCounty
。太好了,现在我们有了一个类来处理所有这些,但是需要有人决定是使用 TaxCounty
对象还是普通的 Tax
对象,谁来决定?TaxFactory
似乎是一个不错的选择。工厂对象是为了做出决定并确定返回哪个类。但是等等,现在我们必须更改 TaxFactory
,因为它没有客户的引用来做出决定。 让我们回去做一些我们一开始就应该做的事情。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace solid
{
public class Order
{
List<OrderItem> _orderItems = new List<OrderItem>();
public decimal CalculateTotal(Customer customer)
{
decimal total = _orderItems.Sum((item) =>
{
return item.Cost * item.Quantity;
});
ITax tax = new TaxFactory().GetTaxObject();
total = total + tax.CalculateTax(customer, total);
return total;
}
public interface ITax
{
decimal CalculateTax(Customer customer, decimal total);
}
public class Tax : ITax
{
public decimal CalculateTax(Customer customer, decimal total)
{
decimal tax;
if (customer.StateCode == "TX")
tax = total * .08m;
else if (customer.StateCode == "FL")
tax = total * .09m;
else
tax = .03m;
return tax;
}
}
public class TaxByCounty : ITax
{
public decimal CalculateTax(Customer customer, decimal total)
{
decimal tax;
if (customer.County == "Travis")
tax = total * .08m;
else if (customer.County == "Hays")
tax = total * .09m;
else
tax = .03m;
return tax;
}
}
public interface ITaxFactory
{
ITax GetTaxObject();
}
public class TaxFactory:ITaxFactory
{
public ITax GetTaxObject()
{
return new Tax();
}
}
public class CustomerBasedTaxFactory : ITaxFactory
{
Customer _customer;
public CustomerBasedTaxFactory(Customer customer)
{
_customer = customer;
}
public ITax GetTaxObject()
{
if (!string.IsNullOrEmpty(_customer.County))
return new TaxByCounty();
else
return new Tax();
}
}
}
}
您可能已经注意到 Order 类中仍然存在一个问题。找到了吗?是的,我们仍然有一个对 TaxFactory
具体实现的引用。让我们为 Factory 类创建一个新的抽象,即 ITaxFactory
。现在我们可以创建一个新的工厂,它在构造函数中接受一个 Customer 对象,即 CustomerBasedTaxFactory
。现在怎么办?我们想使用 CustomerBasedTaxFactory
,但我们不允许在 Order 类中 实例化 它的具体实例,那么该怎么办。这似乎需要另一个工厂来提供工厂,然后这些工厂又需要一个工厂。我们陷入了无限循环。有什么办法可以解决?
依赖注入
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace solid
{
public class Order
{
ITaxFactory _taxFactory;
public Order(ITaxFactory taxFactory)
{
_taxFactory = taxFactory;
}
List<OrderItem> _orderItems = new List<OrderItem>();
public decimal CalculateTotal(Customer customer)
{
decimal total = _orderItems.Sum((item) =>
{
return item.Cost * item.Quantity;
});
ITax tax = _taxFactory.GetTaxObject();
total = total + tax.CalculateTax(customer, total);
return total;
}
public interface ITax
{
decimal CalculateTax(Customer customer, decimal total);
}
public class Tax : ITax
{
public decimal CalculateTax(Customer customer, decimal total)
{
decimal tax;
if (customer.StateCode == "TX")
tax = total * .08m;
else if (customer.StateCode == "FL")
tax = total * .09m;
else
tax = .03m;
return tax;
}
}
public class TaxByCounty : ITax
{
public decimal CalculateTax(Customer customer, decimal total)
{
decimal tax;
if (customer.County == "Travis")
tax = total * .08m;
else if (customer.County == "Hays")
tax = total * .09m;
else
tax = .03m;
return tax;
}
}
public interface ITaxFactory
{
ITax GetTaxObject();
}
public class TaxFactory:ITaxFactory
{
public ITax GetTaxObject()
{
return new Tax();
}
}
public class CustomerBasedTaxFactory : ITaxFactory
{
Customer _customer;
public CustomerBasedTaxFactory(Customer customer)
{
_customer = customer;
}
public ITax GetTaxObject()
{
if (!string.IsNullOrEmpty(_customer.County))
return new TaxByCounty();
else
return new Tax();
}
}
}
}
依赖注入允许一个类告诉类的使用者它为了正常运行而需要什么。在 Order
类的情况下,它需要一个 tax 工厂来工作,或者需要一个 ITaxFactory
。DI 有 3 种方式,我更喜欢构造函数方法,因为它明确地告诉类的使用者它需要什么。您可以看到在上面的代码中,我们添加了一个接受 ITaxFactory
类型并存储其引用的构造函数。 在 CalculateTotal
方法中,它只是使用其对 ITaxFactory
的引用来调用 GetTaxObject
。它完全不知道它正在使用哪个 Tax Factory 或哪个 Tax 对象。无知是福。但谁负责决定使用哪个 TaxFactory
呢? 让我们实现一种技术来实现这一点,如下所示。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace solid
{
public class Order
{
ITaxFactory _taxFactory;
public Order()
: this(new TaxFactory())
{
}
public Order(Customer c)
: this(new CustomerBasedTaxFactory(c))
{
}
public Order(ITaxFactory taxFactory)
{
_taxFactory = taxFactory;
}
List<OrderItem> _orderItems = new List<OrderItem>();
public decimal CalculateTotal(Customer customer)
{
decimal total = _orderItems.Sum((item) =>
{
return item.Cost * item.Quantity;
});
ITax tax = _taxFactory.GetTaxObject();
total = total + tax.CalculateTax(customer, total);
return total;
}
public interface ITax
{
decimal CalculateTax(Customer customer, decimal total);
}
public class Tax : ITax
{
public decimal CalculateTax(Customer customer, decimal total)
{
decimal tax;
if (customer.StateCode == "TX")
tax = total * .08m;
else if (customer.StateCode == "FL")
tax = total * .09m;
else
tax = .03m;
return tax;
}
}
public class TaxByCounty : ITax
{
public decimal CalculateTax(Customer customer, decimal total)
{
decimal tax;
if (customer.County == "Travis")
tax = total * .08m;
else if (customer.County == "Hays")
tax = total * .09m;
else
tax = .03m;
return tax;
}
}
public interface ITaxFactory
{
ITax GetTaxObject();
}
public class TaxFactory:ITaxFactory
{
public ITax GetTaxObject()
{
return new Tax();
}
}
public class CustomerBasedTaxFactory : ITaxFactory
{
Customer _customer;
public CustomerBasedTaxFactory(Customer customer)
{
_customer = customer;
}
public ITax GetTaxObject()
{
if (!string.IsNullOrEmpty(_customer.County))
return new TaxByCounty();
else
return new Tax();
}
}
}
}
有时被称为“简陋的依赖注入”的方法使用构造函数来硬编码一个默认工厂,如果没有指定则使用它。还有一个称为 IOC 容器的东西,它会在 运行时 自动注入所需的依赖项。这可以根据运行时的某些标准进行配置。因此,让我们引入另一个业务规则:也许大多数时候您想使用普通的税费逻辑,但在某个特殊日子,公司为德克萨斯州的人们承担所有税费
可空对象
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace solid
{
public class Order
{
ITaxFactory _taxFactory;
public Order()
: this(new TaxFactory())
{
}
public Order(Customer c)
: this(new DateBasedTaxFactory(c,new CustomerBasedTaxFactory(c)))
{
}
public Order(ITaxFactory taxFactory)
{
_taxFactory = taxFactory;
}
List<OrderItem> _orderItems = new List<OrderItem>();
public decimal CalculateTotal(Customer customer)
{
decimal total = _orderItems.Sum((item) =>
{
return item.Cost * item.Quantity;
});
total = total + _taxFactory.GetTaxObject().CalculateTax(customer, total);
return total;
}
public interface ITax
{
decimal CalculateTax(Customer customer, decimal total);
}
public class Tax : ITax
{
public decimal CalculateTax(Customer customer, decimal total)
{
decimal tax;
if (customer.StateCode == "TX")
tax = total * .08m;
else if (customer.StateCode == "FL")
tax = total * .09m;
else
tax = .03m;
return tax;
}
}
public class TaxByCounty : ITax
{
public decimal CalculateTax(Customer customer, decimal total)
{
decimal tax;
if (customer.County == "Travis")
tax = total * .08m;
else if (customer.County == "Hays")
tax = total * .09m;
else
tax = .03m;
return tax;
}
}
public class NoTax : ITax
{
public decimal CalculateTax(Customer customer, decimal total)
{
return 0.0m;
}
}
public interface ITaxFactory
{
ITax GetTaxObject();
}
public class TaxFactory : ITaxFactory
{
public ITax GetTaxObject()
{
return new Tax();
}
}
public class CustomerBasedTaxFactory : ITaxFactory
{
Customer _customer;
public CustomerBasedTaxFactory(Customer customer)
{
_customer = customer;
}
public ITax GetTaxObject()
{
if (!string.IsNullOrEmpty(_customer.County))
return new TaxByCounty();
else
return new Tax();
}
}
public class DateBasedTaxFactory : ITaxFactory
{
Customer _customer;
ITaxFactory _taxFactory;
public DateBasedTaxFactory(Customer c, ITaxFactory cb)
{
_customer = c;
_taxFactory = cb;
}
public ITax GetTaxObject()
{
if (_customer.StateCode == "TX" &&
DateTime.Now.Month == 4 && DateTime.Now.Day == 4)
{
return new NoTax();
}
else
return _taxFactory.GetTaxObject();
}
}
}
}
在上面的代码中,您可以看到我们使用了可空对象模式来创建一个新的 ITax
对象,称为 NoTax
,它始终返回 0。 我们还使用了装饰器模式来更改税费工厂的行为。我们创建了一个名为 DateBasedTaxFactory
的新工厂,它接受一个 ITaxFactory
实例作为默认值以及一个客户对象。DateBasedTaxFactory
负责检查日期并决定是否应使用 NoTax
对象。我们将此工厂注入 Order 的构造函数。现在它将自动处理决定是否应使用 NoTax
对象,或者只是让 CustomerBasedTaxFactory
来决定使用什么。
事情看起来好多了,所有逻辑都封装在自己的类中,但我们可以做得更进一步。看看 Tax 对象,有没有发现什么问题?每次添加新州时,您都必须添加更多的 if 语句,如果逻辑发生变化,您必须更新 Tax 类。您不应该 ever 更改它!那么该怎么办?
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace solid
{
public class Order
{
ITaxFactory _taxFactory;
public Order(Customer c)
: this(new DateBasedTaxFactory(c, new CustomerBasedTaxFactory(c)))
{
}
public Order(ITaxFactory taxFactory)
{
_taxFactory = taxFactory;
}
List<OrderItem> _orderItems = new List<OrderItem>();
public decimal CalculateTotal(Customer customer)
{
decimal total = _orderItems.Sum((item) =>
{
return item.Cost * item.Quantity;
});
total = total + _taxFactory.GetTaxObject().CalculateTax(customer, total);
return total;
}
public interface ITax
{
decimal CalculateTax(Customer customer, decimal total);
}
public class TXTax:ITax
{
public decimal CalculateTax(Customer customer, decimal total)
{
return total * .08m;
}
}
public class FLTax : ITax
{
public decimal CalculateTax(Customer customer, decimal total)
{
return total * .09m;
}
}
public class TaxByCounty : ITax
{
public decimal CalculateTax(Customer customer, decimal total)
{
decimal tax;
if (customer.County == "Travis")
tax = total * .08m;
else if (customer.County == "Hays")
tax = total * .09m;
else
tax = .03m;
return tax;
}
}
public class NoTax : ITax
{
public decimal CalculateTax(Customer customer, decimal total)
{
return 0.0m;
}
}
public interface ITaxFactory
{
ITax GetTaxObject();
}
public class CustomerBasedTaxFactory : ITaxFactory
{
Customer _customer;
static Dictionary<string, ITax> stateTaxObjects = new Dictionary<string, ITax>();
public CustomerBasedTaxFactory(Customer customer)
{
_customer = customer;
}
public ITax GetTaxObject()
{
ITax tax;
if (!string.IsNullOrEmpty(_customer.County))
tax = new TaxByCounty();
else
{
if (!stateTaxObjects.Keys.Contains(_customer.StateCode))
{
tax = (ITax)Activator.CreateInstance(Type.GetType("solid.Order+" + _customer.StateCode + "Tax"));
stateTaxObjects.Add(_customer.StateCode, tax);
}
else
tax = stateTaxObjects[_customer.StateCode];
}
return tax;
}
}
public class DateBasedTaxFactory : ITaxFactory
{
Customer _customer;
ITaxFactory _taxFactory;
public DateBasedTaxFactory(Customer c, ITaxFactory cb)
{
_customer = c;
_taxFactory = cb;
}
public ITax GetTaxObject()
{
if (_customer.StateCode == "TX" && DateTime.Now.Month == 4 && DateTime.Now.Day == 4)
{
return new NoTax();
}
else
return _taxFactory.GetTaxObject();
}
}
}
}
看看上面的代码,您可能会想,哇,刚才发生了什么。Tax 对象去哪儿了?CustomerBasedTaxFactory
里的那些东西是什么?编程的一些学派认为,您应该尽量少写 if 语句。这与开闭原则有关,因为每次添加新的州税例程时,您都必须用另一个 if 语句(或 switch)修改 Tax 对象。Tax 类中可能会有很多 if 语句。怎么办? 更多的类!我们可以将每个州的税费例程逻辑放入自己的类中,并删除 Tax 对象。但那样的话,if 语句就会被放在工厂类中。没那么快。我们可以使用反射来根据命名约定动态获取正确的对象。CustomerBasedTaxFactory
将负责这一点。需要考虑的一点是,反射会产生一些额外的开销,所以我们应该缓存创建的每个项,我们将使用一个以州代码为键的静态字典来实现这一点。没有更多的 IF 了!我们也可以对基于县的税费做同样的事情。
我想就到这里吧。希望这对某些人有所帮助。我将在下一篇文章中接续我们上次中断的地方。