我写这篇文章,主要是要演示一下如何利用SOLID原则和常用的设计模式从头开始构建一个应用程序。
让我们先从典型的电子商务类型开始。我们需要一个对象来表示订单,订单项,和客户。给定订单对象,有一个方法来计算总订单项的费用,这样做是编码实现的最简单也是最糟糕的方式么?当然是把税费的程序逻辑放在订单类里面
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 => item.Cost * item.Quantity);
decimal tax;
if (customer.StateCode == "TX")
tax = total * 0.08m;
else if (customer.StateCode == "FL")
tax = total * 0.09m;
else
tax = 0.03m;
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编程规则中的第一个原则:单一原则。换句话说,订单对象只应该负责做和订单项管的事情,例如订单项的总成本。它不应该去管理基于不同州的常规税费。可以进一步说,他不应该负责统计订单项,他唯一的职责就是协调统计订单项的流程,增加税费和加运费或者做折扣。但是我们现在先让它保持简单一些。
好的,还有其他的问题么?代码又违反了另外一个SOLID原则:开闭原则。换句话说,只要一个类定义了,他就应该保持永远不变(它是关闭的).只能通过继承来扩展或者在运行时变化(对扩展开放).这看起来很奇怪,或者有点儿不切实际。但是在理想情况下,如果业务规则中的一切,从收集到设计,再到编码都能正确的完成,一旦它通过测试和批准生产,你将永远都不用再去改变类中的源代码。在这里我们看到事实并非如此。任何时候一个州的税费被添加或改变,订单类必须要改变。那怎么去做?让我们改一些代码,开始使用一种设计模式。
namespace Solid
{
public class Order
{
List<OrderItem> _orderItems = new List<OrderItem>();
public decimal CalculateTotal(Customer customer)
{
decimal total = _orderItems.Sum(item => item.Cost * item.Quantity);
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 * 0.08m;
else if (customer.StateCode == "FL")
tax = total * 0.09m;
else
tax = 0.03m;
return tax;
}
}
public class OrderItem
{
public int Quantity;
public string Code;
public decimal Cost;
}
public class Customer
{
public string StateCode;
public string ZipCode;
public string County;
}
}
我们所做的一切就是创建了一个新类Tax,并把税费的程序逻辑放在这里。现在Order对象不再负责税费程序逻辑,Tax对象来处理它。
这是一个简单的策略模式的例子。简单的定义一下它,就是把所有的单独逻辑都放在它自己的类中。太好了,但是现在又有什么问题呢?我们又违反了另一个SOLID原则:依赖倒置。依赖倒置就是说类应该依赖于抽象,而不应该依赖于具体实现。在C#中,抽象通过一个接口或者一个抽象类来表示。在我们上面的代码中,我们通过 new Tax()的方式依赖了Tax类的具体实现。更不用说只要税费程序逻辑能够被添加或修改,Tax对象依然违反开闭原则,我们还没有做的很好,让我们继续重构。
namespace Solid
{
public class Order
{
List<orderItem> _orderItem = new List<orderItem>();
public decimal CalculateTotal(Customer customer)
{
decimal total = _orderItem.Sum(item => item.Cost * item.Quantity);
ITax tax = new ITax();
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 * 0.08m;
else if (customer.StateCode == "FL")
tax = total * 0.09m;
else
tax = 0.03m;
return tax;
}
}
}
我们现在做的就是创建一个tax接口的抽象实现,现在稍微好一点儿了,但是我们还是实例化了一个具体类。Order类不应该为此负责。它不应该关心它在于什么样儿的ITax的对象工作,只是表示带有一个CalculateTax方法的ITax的实例而已。根据上面的代码我们知道它创建了一个Tax实例,这是不好的,我们需要其他的一些东西来负责选择创建什么样儿的ITax对象。
namespace Solid
{
public class Order
{
List<OrderItem> _orderItems = new List<OrderItem>();
public decimal CalculateTotal(Customer customer)
{
decimal total = _orderItems.Sum(item => item.Cost * item.Quantity);
total += new TaxFactory().GetTaxObject().CalculateTax(customer, total);
return total;
}
}
public class Tax
{
public decimal CalculateTax(Customer customer, decimal total)
{
decimal tax;
if (customer.StateCode == "TX")
tax = total * 0.08m;
else if (customer.StateCode == "FL")
tax = total * 0.09m;
else
tax = 0.03m;
return tax;
}
}
public interface ITax
{
decimal CalculateTax(Customer customer, decimal total);
}
public class TaxFactory
{
public ITax GetTaxObject()
{
return new Tax();
}
}
public class Tax:ITax
{
public decimal CalculateTax(Customer customer, decimal total)
{
decimal tax;
if (customer.StateCode == "TX")
tax = total * 0.08m;
else if (customer.StateCode == "FL")
tax = total * 0.09m;
else
tax = 0.03m;
return tax;
}
}
public class OrderItem
{
public int Quantity;
public string Code;
public decimal Cost;
}
public class Customer
{
public string StateCode;
public string ZipCode;
public string County;
}
}
简单的定义一下,这个模式就是说,针对任何给定的场景都应该有一个类,这个类的唯一职责就是根据一些正在变化的条件创建其他的类。利用工厂模式我们创建了类来负责创建Tax对象。现在Order类是完全忽视我们是如何创建Tax对象的或者ITax是具体怎么实现的。它只关心有一个包含CalculateTax方法的ITax对象。这时也许你会好奇这样做的意义是什么,这只是用了更多的代码和一个TaxFactory来返回一种类型的Tax对象而已,不是很有用。
让我们引入一个新的问题:如果一个新的业务逻辑被引入,这个逻辑的意思是,如果一个County存在就用一个特殊的税费程序逻辑,否则就是用State的税费程序逻辑。现在需要改变我们的代码,让我们先用不好的方式来编码,我们也许会这样做:
namespace Solid
{
public class Order
{
List<OrderItem> _orderItems = new List<OrderItem>();
public decimal CalculateTotal(Customer customer)
{
decimal total = _orderItems.Sum(item => item.Cost * item.Quantity);
total += new TaxFactory().GetTaxObject().CalculateTax(customer, total);
return total;
}
}
public class Tax
{
public decimal CalculateTax(Customer customer, decimal total)
{
decimal tax;
if(!string.IsNullOrEmpty(customer.Country))
{
if (customer.Country == "Travis")
tax = total * 0.085m;
else if (customer.Country == "Hays")
tax = total * 0.095m;
else
tax = 0.03m;
}
else
{
if (customer.StateCode == "TX")
tax = total * 0.08m;
else if (customer.StateCode == "FL")
tax = total * 0.09m;
else
tax = 0.03m;
}
return tax;
}
}
public interface ITax
{
decimal CalculateTax(Customer customer, decimal total);
}
public class TaxFactory
{
public ITax GetTaxObject()
{
return new Tax();
}
}
public class Tax:ITax
{
public decimal CalculateTax(Customer customer, decimal total)
{
decimal tax;
if (customer.StateCode == "TX")
tax = total * 0.08m;
else if (customer.StateCode == "FL")
tax = total * 0.09m;
else
tax = 0.03m;
return tax;
}
}
public class OrderItem
{
public int Quantity;
public string Code;
public decimal Cost;
}
public class Customer
{
public string StateCode;
public string ZipCode;
public string County;
}
}
我们放置了一些条件在Tax对象里来检查County属性,然而,我们破坏了更多的规则。我们改变了一个本不该被改变的类(开闭原则).现在Tax对象来处理决定是使用County还是State税费程序逻辑的职责(单一原则)。如果业务逻辑更新了我们该怎么做,不得不对这个类再次做出改变。这听起来像是我们需要添加另外一个类。
namespace Solid
{
public class Order
{
List<OrderItem> _orderItems = new List<OrderItem>();
public decimal CalculateTotal(Customer customer)
{
decimal total = _orderItems.Sum(item => item.Cost * item.Quantity);
total += new 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 * 0.08m;
else if (customer.StateCode == "FL")
tax = total * 0.09m;
else
tax = 0.03m;
return tax;
}
}
public class TaxByCounty:ITax
{
public decimal CalculateTax(Customer customer, decimal total)
{
decimal tax;
if (customer.County == "Travis")
tax = total * 0.08m;
else if (customer.County == "Hays")
tax = total * 0.095m;
else
tax = 0.03m;
return tax;
}
}
public class TaxFactory
{
public ITax GetTaxObject()
{
return new Tax();
}
}
public class OrderItem
{
public int Quantity;
public string Code;
public decimal Cost;
}
public class Customer
{
public string StateCode;
public string ZipCode;
public string County;
}
}
这里我们添加了另外一个基于County的来处理税费程序处理的类。太好了,现在我们有另一个类来处理这一切,但是需要一个对象来处理我们是使用TaxCounty对象还是正常的Tax对象,谁来做这件事?TaxFactory看起来是一个不错的候选对象。Factory对象现在来决策和决定哪个类应该返回。但是等一等,现在我们必须要改变TaxFactory,因为为了做这个决策,它没有任何引用的Customer。现在让我们回过头来做一些我们应该做的事。
namespace Solid
{
public class Order
{
List<OrderItem> _orderItem = new List<OrderItem>();
public decimal CalculateTotal(Customer customer)
{
decimal total = _orderItem.Sum(item => item.Quantity * item.Cost);
ITax tax = new TaxFactory().GetTaxObject();
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 * 0.08m;
else if (customer.StateCode == "FL")
tax = total * 0.09m;
else
tax = 0.03m;
return tax;
}
}
public class TaxByCounty:ITax
{
public decimal CalculateTax(Customer customer, decimal total)
{
decimal tax;
if (customer.County == "Travis")
tax = total * 0.08m;
else if (customer.County == "Hays")
tax = total * 0.09m;
else
tax = 0.03m;
return tax;
}
}
public interface ITaxFactory
{
ITax GetTaxObject();
}
public class TaxFactory:ITaxFactory
{
public ITax GetTaxObject()
{
return new Tax();
}
}
public class CustomBasedTaxFactory:ITaxFactory
{
Customer _customer;
public CustomBasedTaxFactory(Customer customer)
{
this._customer = customer;
}
public ITax GetTaxObject()
{
if (!string.IsNullOrEmpty(_customer.Country))
return new TaxByCounty();
else
return new Tax();
}
}
public class OrderItem
{
public int Quantity;
public string Code;
public decimal Cost;
}
public class Customer
{
public string StateCode;
public string ZipCode;
public string County;
}
}
你可能已经注意到一个问题依然存在Order类当中,明白了么?是的,我们还是有一个引用TaxFactory类的实例化对象。让我们创建另外一个抽象工厂类,ITaxFactory。现在我们创建一个新的工厂,它的构造函数有一个Customer参数,CustomerBasedTaxFactory。现在该怎么办呢?我们希望使用CustomerBasedTaxFactory但是我们不被允许创建一个Order实例,因此我们该做点儿什么。这似乎是我们需要另外一个工厂提供工厂然后我们还需要一个工厂。我们越来越陷入无限循环,我们能做写什么?
namespace Solid
{
public class Order
{
ITaxFactory _taxFactory;
public Order(ITaxFactory taxFactory)
{
this._taxFactory = taxFactory;
}
List<OrderItem> _orderItem = new List<OrderItem>();
public decimal CalculateTotal(Customer customer)
{
decimal total = _orderItem.Sum(item => item.Quantity * item.Cost);
ITax tax = new TaxFactory().GetTaxObject();
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 * 0.08m;
else if (customer.StateCode == "FL")
tax = total * 0.09m;
else
tax = 0.03m;
return tax;
}
}
public class TaxByCounty:ITax
{
public decimal CalculateTax(Customer customer, decimal total)
{
decimal tax;
if (customer.Country == "Travis")
tax = total * 0.08m;
else if (customer.Country == "Hays")
tax = total * 0.09m;
else
tax = 0.03m;
return tax;
}
}
public interface ITaxFactory
{
ITax GetTaxObject();
}
public class TaxFactory:ITaxFactory
{
public ITax GetTaxObject()
{
return new Tax();
}
}
public class CustomBasedTaxFactory:ITaxFactory
{
Customer _customer;
public CustomBasedTaxFactory(Customer customer)
{
this._customer = customer;
}
public ITax GetTaxObject()
{
if (!string.IsNullOrEmpty(_customer.Country))
return new TaxByCounty();
else
return new Tax();
}
}
public class OrderItem
{
public int Quantity;
public string Code;
public decimal Cost;
}
public class Customer
{
public string StateCode;
public string ZipCode;
public string Country;
}
}
依赖注入允许一个类来告诉消费者的类它需要什么来正常操作。在Order类的情况下需要一个Tax工厂来工作,或者一个ITaxFactory对象。DI能够通过三种方式来实现,我比较喜欢通过构造函数的方法,因为他显示的告诉消费者它需要的类。你可以看到在上面的代码中,我们添加了一个构造函数,该函数接收一个ITaxFactory类型参数,然后存储它的引用。在CalculateTotal方法中,它只是通过ITaxFactory的引用来调用GetTaxObject方法。它不知道它用的是什么TaxFactory或者用的是什么Tax对象。无知便是福!但是接下来谁来负责决定哪个TaxFactory使用呢?让我们用几种技术来实现,如下所示
public class Order
{
ITaxFactory _taxFactory;
public Order(ITaxFactory taxFactory)
{
this._taxFactory = taxFactory;
}
public Order(Customer c):this(new CustomBasedTaxFactory(c))
{
}
public Order():this(new TaxFactory())
{
}
List<OrderItem> _orderItem = new List<OrderItem>();
public decimal CalculateTotal(Customer customer)
{
decimal total = _orderItem.Sum(item => item.Quantity * item.Cost);
ITax tax = _taxFactory.GetTaxObject();
total += tax.CalculateTax(customer, total);
return total;
}
}
又是被称为"穷人的依赖注入"是使用一个构造函数来硬编码一个默认工厂来使用,如果没有指定的话。还有一个叫做IOC的容器会自动注入运行时所需的依赖。这在运行时可以基于一些条件例如配置等建立起来。因此让我们介绍另外一个业务逻辑:也许大多数天你都想用正常的税费逻辑,但是在特殊日期所有的在Texas的税费都被覆盖。
namespace Solid
{
public class Order
{
ITaxFactory _taxFactory;
public Order(ITaxFactory taxFactory)
{
this._taxFactory = taxFactory;
}
public Order(Customer c):this(new DateBasedTaxFactory(c, new CustomBasedTaxFactory(c)))
{
}
public Order():this(new TaxFactory())
{
}
List<OrderItem> _orderItem = new List<OrderItem>();
public decimal CalculateTotal(Customer customer)
{
decimal total = _orderItem.Sum(item => item.Quantity * item.Cost);
ITax tax = _taxFactory.GetTaxObject();
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 * 0.08m;
else if (customer.StateCode == "FL")
tax = total * 0.09m;
else
tax = 0.03m;
return tax;
}
}
public class TaxByCounty:ITax
{
public decimal CalculateTax(Customer customer, decimal total)
{
decimal tax;
if (customer.County == "Travis")
tax = total * 0.08m;
else if (customer.County == "Hays")
tax = total * 0.09m;
else
tax = 0.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 CustomBasedTaxFactory:ITaxFactory
{
Customer _customer;
public CustomBasedTaxFactory(Customer customer)
{
this._customer = customer;
}
public ITax GetTaxObject()
{
if (!string.IsNullOrEmpty(_customer.Country))
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();
}
}
public class OrderItem
{
public int Quantity;
public string Code;
public decimal Cost;
}
public class Customer
{
public string StateCode;
public string ZipCode;
public string County;
}
}
在上面的代码中,你可以看到我们使用非空类型模式来创建一个新的名为NoTax的ITax对象,它总是返回0.我们也使用了装饰器模式来改变TaxFactory的行为。我们创建了一个名为DateBasedTaxFactory的新工厂,需要使用一个默认的ITaxFactory实例和一个Customer对象。DateBasedTaxFactory负责检查时间来决定是否使用NoTax对象。我们为Order的构造函数注入这个工厂。现在他将自动来决策是否使用NoTax对象或者让CustomerBasedTaxFactory做出使用什么的决策。
现在事情看起来好多了,所有的逻辑都被封装在它自己的类中。但是我们进一步思考,看看Tax对象,看出有什么问题么?每次你添加一个新状态,你都要添加更多的if语句,如果逻辑发生变化你必须要更新Tax类。你永远都不想去改变它,因此可以做点儿什么呢?
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语句写代码。这是因为与开闭原则关联了起来。在每次你添加一个新的税费程序逻辑的时候,你都需要修改Tax对象与另外一个if语句。这有可能在Tax类中有非常多的if语句。我们可以怎么做呢?更多的类呢?我们可以把每个State的税费程序逻辑都放在它自己的类中,并摆脱Tax对象。但是如果if语句都被放在了Factory类中,这样做不好。我们可以使用反射基于命名约定来动态获取正确的对象。CustomerBasedTaxFactory将来处理它。有一件事需要考虑,反射导致一些额外的开销,所以我们应该缓存我们创建的每一项,我们使用一个基于StateCode的key的static字典来完成。就这些了!我们也可以使用基于Taxes的County来做这件事。