使用设计模式和原则构建 C# 应用程序 - 第二部分
我写这篇文章是为了演示如何从头开始使用 SOLID 原则和常见设计模式构建应用程序
介绍
这是文章的第二部分,文章的起始部分在这里。你可能想先阅读它,否则你将不知道我在说什么 。我将继续使用相同的代码库,同时重构和演示更多编码原则。
更多重构、模式和接口 欢呼
下面是我们之前创建的订单类代码片段。你发现它有什么奇怪的地方吗?
public class Order
{
ITaxFactory _taxFactory;
public Order(Customer c)
: this(new DateBasedTaxFactory(c, new CustomerBasedTaxFactory(c)))
{
}
public Order(ITaxFactory taxFactory)
{
_taxFactory = taxFactory;
}
public 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;
}
}
}
也许你注意到 CalculateTotal 方法有些不对劲。我们有两个独立的 Customer 对象引用,一个在构造函数中,另一个作为 CalculateTotal 的参数。除了它很笨拙之外,Order 可能还在使用两个不同的客户实例,这会导致一些不稳定的行为。再看看 TXTax 类中的代码。发现什么不对劲了吗?CalculateTax 方法需要一个客户参数,但它并没有使用它。我可能会声称这违反了接口隔离原则。这基本上说,类不应该被迫实现它们不需要的接口中的一堆东西。如果它们不使用它,那么它就不应该在那里。这通常适用于接口方法,但也许也适用于参数。你怎么看?无论如何,如果你从逻辑上思考,一个不需要客户的 Tax 对象是完全合理的,例如我们的 NoTax 类。所以这不应该是每个 ITax 实现都必需的。
所以,解释一下,在代码的第一个版本中,我们需要客户来直接在这个方法中实现税收逻辑,用于一个特定情况(基于州)。嗯,我们现在不需要这个了,因为税收对象本身会处理逻辑。所以让我们进行一些重大的代码更改来适应这一点。这看起来可能很多,但请记住,我们仍然是为了学习而设计和编码,而不是更改生产代码
public class Order
{
ITaxFactory _taxFactory;
public Order(Customer c)
: this( new DateBasedTaxFactory(c, new CustomerBasedTaxFactory(c)))
{
}
public Order(ITaxFactory taxFactory)
{
_taxFactory = taxFactory;
}
public List<OrderItem> _orderItems = new List<OrderItem>();
public decimal CalculateTotal()
{
decimal total = _orderItems.Sum((item) =>
{
return item.Cost * item.Quantity;
});
total = total + _taxFactory.GetTaxObject().CalculateTax( total);
return total;
}
public interface ITax
{
decimal CalculateTax( decimal total);
}
public class TXTax:ITax
{
public decimal CalculateTax( decimal total)
{
return total * .08m;
}
}
}
好的,完成了,我们继续做别的事情。让我添加一些代码来非常简单地演示实际使用 Order 对象。通常在电子商务中,我们有一个 Basket 的概念,所以我们将使用它并坚持我们在文章 I 中学到的关于面向抽象和工厂等的知识。
public interface IOrderFactory
{
Order GetOrder(int orderId);
}
public class OrderFactory : IOrderFactory
{
public Order GetOrder(int orderId)
{
Order order = null;//get from repository or wherever
return order;
}
}
public class Basket
{
Order_order;
public Basket(int sessionId,IOrderFactory orderFactory)
{
int orderId = 3333;//Session[sessionId].OrderId;
_order = orderFactory.GetOrder(orderId);
}
public Basket(int sessionId):this(sessionId,new OrderFactory())
{
}
}
太棒了,现在我们拥有了 UI 获取 basket 及其订单所需的一切。
如果我说我们应该创建一个名为 IOrder 的接口,它看起来像这样
public interface IOrder
{
decimal CalculateTotal();
int NumberOfItems();
}
然后我们让 Order 实现它。你可能会想,这有什么意义呢?我们已经有了一个具体的类 Order,除了 THE Order 之外,不会有其他任何东西,为什么我们要为它设置一个接口。好吧,让我们想象一下,我们完成了所有类的编码,测试并通过了,然后将其移至生产环境。但随后(惊喜!)并非所有的业务需求都已提供,我们忘记了一些东西。我们现在需要将运费计算添加到 CalculateTotal 订单中。也许是这样的
public decimal CalculateTotal()
{
decimal shippingCost = 1.3m * _orderItems.Count;
decimal total = _orderItems.Sum((item) =>
{
return item.Cost * item.Quantity;
});
total = shippingCost + total + _taxFactory.GetTaxObject().CalculateTax(total);
return total;
}
太好了,现在我们必须进入 order 类并更改它,但我们不应该这样做!开闭原则。有什么办法可以解决?我们可以使用一种旨在扩展类行为而不必实际更改它的模式,即装饰器。简单来说,装饰器对象接受并维护对实现相同抽象的其他对象的引用,在本例中是 IOrder 接口。然后它们相互使用,在彼此之上添加功能。令人困惑?让我们看看它是如何工作的。我不想更改 order 类,但我们一开始就应该有 IOrder。所以,让我们假装它从一开始就一直存在。
public class Order:IOrder
{
}
现在我们需要新的功能,当需要新功能时,你总是做什么?当然是创建一个类。所以,让我们创建一个处理运费的类,如下所示
public class ShippingOrder : IOrder
{
IOrder _order;
public ShippingOrder(IOrder order)
{
_order = order;
}
public decimal CalculateTotal()
{
decimal shippingCost = 1.3m * _order.NumberOfItems();
return shippingCost + _order.CalculateTotal();
}
public int NumberOfItems()
{
return _order.NumberOfItems();
}
}
那么,这有什么意义呢?嗯,你有你的默认 Order 类,就像它目前存在的那样,然后你可以将它传递给一个 ShippingOrder 对象,它将“包装”它。它会处理计算运费本身,然后将其添加到普通 Order 对象的 CalculateTotal 值中。然后该工厂可以返回一个 ShippingOrder 而不是一个普通的 Order,消费者将永远不会知道区别。所以让我们把它改成它一开始应该有的样子。
public interface IOrderFactory
{
IOrder GetOrder(int orderId);
}
public class OrderFactory : IOrderFactory
{
public IOrder GetOrder(int orderId)
{
Order order = null;//get from repository database or wherever
return new ShippingOrder(order);
}
}
public class Basket
{
IOrder _order;
public Basket(int sessionId,IOrderFactory orderFactory)
{
int orderId = 3333;//Session[sessionId].OrderId; whatever you are using to maintain state
orderFactory.GetOrder(orderId);
}
public Basket(int sessionId):this(sessionId,new OrderFactory())
{
}
}
那么我们这里有什么?Basket 对象正在使用 OrderFactory 来获取对 IOrder 的引用。它不知道该变量是持有对 Order 还是 ShippingOrder 的引用,它也不应该知道。这正是我们想要的。OrderFactory 是唯一了解发生了什么事情的对象。它只是像平常一样从数据库中获取一个 Order 对象,然后将其放入一个 ShippingOrder 对象中,以便添加运费功能,而 Order 对象从未改变。
现在一切都已就位,让我们再看一遍。高层管理者现在希望我们接受优惠券和折扣码。太好了,这将是一个简单的更改,因为我们已经为成功做好了准备。
public class DiscountOrder : IOrder
{
IOrder _order;
public DiscountOrder(IOrder order)
{
_order = order;
}
public decimal CalculateTotal()
{
return _order.CalculateTotal() - 4;
}
public int NumberOfItems()
{
return _order.NumberOfItems();
}
}
所以我们只需要创建一个 DiscountOrder 类,除了工厂类之外,什么都不用改变:
public class OrderFactory : IOrderFactory
{
public IOrder GetOrder(int orderId)
{
Order order = null;//get from repository or wherever
return new ShippingOrder(new DiscountOrder(order));
}
}
是的,我知道我遗漏了很多东西。Basket 对象甚至没有使用 CalculateTotal 方法,如果它调用它,由于 tax factory 为 null,将会出现运行时异常,但你的意思你应该明白了。
好的,现在就到这里。希望这有道理并为大家增加了一些价值。