走向ASP.NET架构设计——第三章:分层设计,初涉架构(中篇)
1.阐明示例需求
本篇还是用之前的电子商务网站中的一个简单的场景来讲述:在页面上需要显示产品的列表信息。并且根据产品的类型不同,计算出相应的折扣。 在上篇中,我们已经设计项目的逻辑分层。我们再来回顾下:
可能有的朋友认为从Smart UI立刻跳到这种分层设计,似乎快了些。其实也算是一个思想的跳跃吧。下面就来看看这种分层是如何解决之前Smart UI的问题的。
2.业务层设计
记得在之前的Smart UI的例子中,程序的业务逻辑是直接写在了ASPX页面后面的cs代码中的。现在,采用分层的方法,我们采用了领域模型来组织来电子商务中的业务逻辑。有关领域模型的一些东西,我们在后续的文章中会讲解的。注:领域模型模式被设计用来组织复杂的业务逻辑和关系。
下面的类图就反映了我们之前的电子商务的需求中所用到的业务模型。
Product类就代表了电子商务中的每一个产品。Price类将会包含可算折扣的业务逻辑,并且用策略模式来具体实现折扣的算法。在ASPPatterns.Chap3.Layerd.Model添加一个接口类:IDiscountStrategy:
public interface IDiscountStrategy
{
decimal ApplyExtraDiscountsTo(decimal OriginalSalePrice);
}
这个接口就用来实现不同打折的策略,这是策略模式的一种应用。这个模式允许我们在运行的时候更改不同的算法实现。在本例子中,Price类将会根据不同的产品来实现不同的打折策略。在我们之前的那个Smart UI例子中,其实这个打折的算法我们已经写了,但是没有分离出来,导致了每次加一个打折的算法的策略,程序就需要改动,重新编译,部署。也就是说打折的部分是个变化点,我们应该分离出来的。
注:策略模式:用一个类来封装一个算法的实现,并且通过切换算法的实现允许在运行时修改一个对象的行为。
在电子商务中,不是每种商品都会打折的,其实我们要实现的打折策略只有一种。但是如果这样,我们在写代码的时候就要if-else判断是否是打折的商品,其实这里还是暴露了变化点的:如果国庆那天,所有的商品都打折了,那么我们就得修改代码。其实我们可以这样想想:不打折的情况也算是一种打折,其他的商品打折可能是7折,不打折的情况就是10折。
public class TradeDiscountStrategy : IDiscountStrategy
{
public decimal ApplyExtraDiscountsTo(decimal OriginalSalePrice)
{
decimal price = OriginalSalePrice;
price = price * 0.95M;
return price;
}
}
public class NullDiscountStrategy : IDiscountStrategy
{
public decimal ApplyExtraDiscountsTo(decimal OriginalSalePrice)
{
return OriginalSalePrice;
}
}
下面我们来看看Price类的实现。
public class Price
{
private IDiscountStrategy _discountStrategy = new NullDiscountStrategy();
private decimal _rrp;
private decimal _sellingPrice;
public Price(decimal RRP, decimal SellingPrice)
{
_rrp = RRP;
_sellingPrice = SellingPrice;
}
public void SetDiscountStrategyTo(IDiscountStrategy DiscountStrategy)
{
_discountStrategy = DiscountStrategy;
}
public decimal SellingPrice
{
get { return _discountStrategy.ApplyExtraDiscountsTo(_sellingPrice); }
}
public decimal RRP
{
get { return _rrp; }
}
public decimal Discount
{
get {
if (RRP > SellingPrice)
return (RRP - SellingPrice);
else
return 0;}
}
public decimal Savings
{
get{
if (RRP > SellingPrice)
return 1 - (SellingPrice / RRP);
else
return 0;}
}
}
Price类在设计中就是用了“依赖倒置原则”,因为它没有采用某一个具体的打折实现算法,而且依赖于接口抽象,至于之后到底会哪种的打折算法,其实是由商品的类型来决定的。 我们还是继续的看,现在看看Product类。
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public Price Price { get; set; }
}
现在所有的业务实体就已经创建了。至于对商品是否打折,其实这是由客户代码来决定:根据客户代码传入的商品的类型不同,然后调用不同的策略,选择了不同的打折算法计算折扣。所以我们这里来添加一个表示商品类型的枚举:
public enum CustomerType
{
Standard = 0,
Trade = 1
}
我们将会把选择哪种打折的策略的逻辑写在一个单独的地方,也就是说:只要客户代码传入相应的参数信息,我们就自动的创建一个合适的打折策略对象。很明显,这里可以采用工厂方法来实现,如下:
public static class DiscountFactory
{
public static IDiscountStrategy GetDiscountStrategyFor(CustomerType customerType)
{
switch (customerType)
{
case CustomerType.Trade:
return new TradeDiscountStrategy();
default:
return new NullDiscountStrategy();
}
}
}
在上面的逻辑分层中,我们建立了一个Repository的类库,其实我们就是想采用Repository模式来实现”持久化无关性”-----业务类完全不用管如何保存和获取数据。而且由Repository决定数据的来源和保存的地方,可能是数据库,也可能就是内存,但是不管怎么,业务类是不用管这些的。所以下面用一个接口来实现灵活性:
public interface IProductRepository
{
IList<Product> FindAll();
}
如果现在有很多的商品,我们想知道他们的折扣价格,最简单的方法就是遍历他们,判断类型,然后应用不同的打折策略。为了更加的可读,我们可以为商品列表建立扩展方法,如下:
public static class ProductListExtensionMethods
{
public static void Apply(this IList<Product> products, IDiscountStrategy discountStrategy)
{
foreach (Product p in products)
{
p.Price.SetDiscountStrategyTo(discountStrategy);
}
}
}
为了简化客户代码的调用工作,我们提供一个类似门户(gateway),或者是Façade的概念:把复杂的操作逻辑隐藏,留给客户代码一个简单易用的API。我们这里创建一个Service类,如下:
public class ProductService
{
private IProductRepository _productRepository;
public ProductService(IProductRepository productRepository)
{
_productRepository = productRepository;
}
public IList<Product> GetAllProductsFor(CustomerType customerType)
{
IDiscountStrategy discountStrategy = DiscountFactory.GetDiscountStrategyFor(customerType);
IList<Product> products = _productRepository.FindAll();
products.Apply(discountStrategy);
return products;
}
}
只要客户代码(如显示层中的代码)直接调用上面的方法就可以了,而且商品的折扣也根据传入的商品类型不同来计算。
3.服务层设计
服务层就充当应用程序的入口的角色。有时候,可以被认为是façade.不仅如此,因为service分为领域逻辑的service和门户的service。门户的service常常为显示层提供强类型的View Model(有时也称为Presentation Model)。 一个View Model就是给一个专门的View来使用的。在本例中,我们将会建立Product的View Model来显示商品的信息。一般情况下,我们不要把业务类直接暴露给显示层,这样很容易紧耦合,所以在中间就上一个View Model,其实View Model和业务类的结构差不多,只是View Model做了一些调整,便于最后的显示。关于View Model详细的,后文讲述。
注:Façade模式:为内部负责的子系统提供一个简单的接口供外部访问。
下面我们就来看看Product的View Model是如何写的:
public class ProductViewModel
{
public int ProductId { get; set; }
public string Name { get; set; }
public string RRP { get; set; }
public string SellingPrice { get; set; }
public string Discount { get; set; }
public string Savings { get; set; }
}
可以看到,其实View Model就是做了一些显示逻辑的处理。在这里就是多加了一些字段,这些字段就是在UI的GridView中显示用的。我们之前的Smart UI的方法中,还建立了模板列来显示Product类中没有的字段,其实就相当于在UI中作了一定的显示逻辑的处理。这里我们直接显示ViewModel.
大家应该很熟悉Web Service:在客户端和服务使用请求/响应的消息机制进行通信的。我们这里的客户代码和Service也采用这种方法,因为很有可能我们在部署的时候Service的代码和客户代码(显示层)在不同机器上。
请求的消息的结构如下:
public class ProductListRequest
{
public CustomerType CustomerType { get; set; }
}
服务在响应请求的时候也要定义格式,而且我们可以在响应中加入更多的属性来判断这个请求是否成功。所以在下面的代码中,我们加入了Message属性,用来在请求失败的时候显示错误信息,还添加了一个Success属性用来判断请求的状态:
public class ProductListResponse
{
public bool Success { get; set; }
public string Message { get; set; }
public IList<ProductViewModel> Products { get; set; }
}
还有一点不要忘记了:因为Product和它对应的View Model结构不同的,而Service返回的又是ViewModel的响应,那么就需要把获取到的Product转换为View Model的结构。可以把转换的代码写在一个特定的地方(可以认为是个Mapping的过程),为了阅读的方便,我们可以为List<Product>添加扩展方法,直接调用,如下:
public static class ProductMapperExtensionMethods
{
public static IList<ProductViewModel> ConvertToProductListViewModel(this IList<Model.Product> products)
{
IList<ProductViewModel> productViewModels = new List<ProductViewModel>();
foreach(Model.Product p in products)
{
productViewModels.Add(p.ConvertToProductViewModel());
}
return productViewModels;
}
public static ProductViewModel ConvertToProductViewModel(this Model.Product product)
{
ProductViewModel productViewModel = new ProductViewModel();
productViewModel.ProductId = product.Id;
productViewModel.Name = product.Name;
productViewModel.RRP = String.Format("{0:C}", product.Price.RRP);
productViewModel.SellingPrice = String.Format("{0:C}", product.Price.SellingPrice);
if (product.Price.Discount > 0)
productViewModel.Discount = String.Format("{0:C}", product.Price.Discount);
if (product.Price.Savings < 1 && product.Price.Savings > 0)
productViewModel.Savings = product.Price.Savings.ToString("#%");
return productViewModel;
}
}
最后,我们加入一个ProductService来与业务层的Service 类进行交互,业务层的Service会返回商品列表,然后我们现在添加的这个ProductService会把列表转为ProductViewModels。
大家可能觉得奇怪:为什么这里添加了两个ProductService,之前在业务层加一个,现在又加一个,是否命名有问题或者功能重复?其实在上一篇已经提过:有时在业务层类添加一个service层,主要是用来组织业务流程的,常常要几个业务类组合在一起使用,这样主要是为了简化客户程序(也就是调用这个业务层的代码)的调用,实现类似Façade的作用。
我们现在添加的ProductService就是业务层中service层的客户程序,因为我们调用了业务层的service,往往有时候,我们不想把自己系统的业务类的结构直接暴露给外界,如显示层,而且也希望提供更加符合显示层所需的数据结构,那么我们就添加了这个ProductService,提供从业务类到ViewModel的转换。而且在这个ProductSevice中,我们也可以实现一些异常处理机制,如果涉及到了分布式调用,那么我们还可以用这个ProductService类向显示层和UI那边隐藏分布式的信息:实现代理模式。
今天就写在到这里,在写的过程中发现这篇有点长了,所以分为3篇(前,中,后)发布!不明白的地方大家多琢磨一下,也可以告诉我!下篇明天发布!见谅!