ASP.NET 设计模式 -- 第3章 应用程序分层与关注点分离 (学习笔记)

源代码在网站下载:http://www.wrox.com

3.1.2分离关注点

(1)新建空白解决方案ASPPatterns.Chap3.Layered
(2)向解决方案,新添加一个类库项目ASPPatterns.Chap3.Layered.Repository
(3)进一步添加下面三个类库项目
ASPPatterns.Chap3.Layered.Model
ASPPatterns.Chap3.Layered.Service
ASPPatterns.Chap3.Layered.Specifications
(4)添加新的Web应用程序ASPPatterns.Chap3.Layered.WebUI
(5)添加相关项目引用
5.1 ASPPatterns.Chap3.Layered.Repository 引用ASPPatterns.Chap3.Layered.Model
5.2 ASPPatterns.Chap3.Layered.Service 引用ASPPatterns.Chap3.Layered.Model 和 ASPPatterns.Chap3.Layered.Repository
5.3 ASPPatterns.Chap3.Layered.Specifications 引用ASPPatterns.Chap3.Layered.Model 和 ASPPatterns.Chap3.Layered.Service
5.4 ASPPatterns.Chap3.Layered.WebUI 引用ASPPatterns.Chap3.Layered.Model、ASPPatterns.Chap3.Layered.Service、ASPPatterns.Chap3.Layered.Specifications、ASPPatterns.Chap3.Layered.Repository

1.业务层

(1)在ASPPatterns.Chap3.Layered.Model添加IDiscountStrategy接口

public interface IDiscountStrategy
{
    decimal ApplyExtraDiscountsTo(decimal OriginalSalePrice);
}

将接口命名为IDiscountStrategy的目的是它实际匹配Strategy设计模式(Stategy模式将算法封装到一个类中,并可以在运行时转换,从而改变对象的行为)。

(2)既然已经有了接口,就可以添加折扣策略的两种实现。创建TradeDiscountStategy

public class TradeDiscountStrategy : IDiscountStrategy 
{        
    public decimal ApplyExtraDiscountsTo(decimal OriginalSalePrice)
    {
        decimal price = OriginalSalePrice;            
        
        price = price * 0.95M;            

        return price;
    }     
}

创建NullDiscountStrategy

public class NullDiscountStrategy : IDiscountStrategy 
{        
    public decimal ApplyExtraDiscountsTo(decimal OriginalSalePrice)
    {
        return OriginalSalePrice;
    }
}

(3)建立折扣策略之后,创建Price对象,创建名为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对象使用依赖注入的设置器形式,这样可以将折扣策略应用到商品的价格。

(4)创建简单的Product类

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Price Price { get; set; }
}

(5)为了让客户指定应用到商品的折扣,需要创建一个枚举类型CustomerType,它将用作服务方法的参数

public enum CustomerType
{
    Standard = 0,
    Trade = 1
}

(6)为了确定哪一种折扣策略用哪种价格,还需要创建一个工厂类,它唯一的职责就是为给定的CustomType返回一个匹配的折扣策略。

创建一个名为DiscountFactory的新类(Factory模式可以让类来委托创建有效对象的责任。)

public static class DiscountFactory
{
    public static IDiscountStrategy GetDiscountStrategyFor(CustomerType customerType)
    {
        switch (customerType)
        {
            case CustomerType.Trade:
                return new TradeDiscountStrategy(); 
            default:
                return new NullDiscountStrategy(); 
        }
    }
}

 (7)服务层将与数据存储交互,以检索商品。使用Repository模式来实现此功能,但只指定资源库接口,这是因为不希望model项目牵涉到诸如使用什么类型的数据存储或使用什么类型的技术来查询等细节。创建一个名为IProductRepository的接口

public interface IProductRepository
{
    IList<Product> FindAll();
}

 (Repository模式充当业务实体的内存集合或仓库,完全将底层数据基础设施抽象出来。)

 (8)服务类需要能够将给定的折扣策略应用到一组商品,可以创建一个自定义集合来实现该功能。

但更喜欢扩展方法的灵活性,因此创建一个名为ProductListExtensionMethods的新类

public static class ProductListExtensionMethods
{
    public static void Apply(this IList<Product> products, IDiscountStrategy discountStrategy)
    {
        foreach (Product p in products)
        {
            p.Price.SetDiscountStrategyTo(discountStrategy);
        }
    }
}

(Separated Interface模式确保客户端完全不知道具体的实现,因此可以促进对抽象的编程(而不是对实现的编程)及依赖倒置原则)

(9)现在可以创建客户端用来与领域交互的服务类,创建名为ProductService的新类(上下两段代码的Apply方法就是接口隔离模式)

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;
    }    
}

现在已经完成了应用程序包含的所有业务逻辑。注意,业务逻辑并没有绑定到特定的数据存储,并使用接口对资源库进行访问来完成所有持久化需要。现在可以以完全与应用程序其他部分隔离的方式来测试业务层,而且业务层不会受到其他层变化的影响。

2.服务层

(Facade模式为一系列复杂的接口和子系统提供了一个简单的接口并控制对其的访问)

(1)向ASPPatterns.Chap3.Layered.Service项目,添加名为ProductViewModel的新类

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; }
}

(视图模型是为特定视图优化的强类型的类,并包含用来辅助完成数据表示的逻辑)

(2)为了让客户端与服务层交互,将使用Request/Response消息模式。Request部分由客户端提供,它将携带所有必要的参数。

在这里,它将包含CustomType枚举变量(在领域模型中定义)。创建名为ProductListRequest的新类

using ASPPatterns.Chap3.Layered.Model;   

namespace ASPPatterns.Chap3.Layered.Service
{
    public class ProductListRequest
    {
        public CustomerType CustomerType { get; set; }
    }
}

(3)对于Response对象,要定义更多属性,以便客户端能够检查请求是否完成。

还会有一个Message属性,如果调用没有成功完成,该属性可以让服务为客户端提供信息。创建名为ProductListResponse的新类

public class ProductListResponse
{
    public bool Success { get; set; }
    public string Message { get; set; }
    public IList<ProductViewModel> Products { get; set; }
}

(4)为了将Product实体转换成ProductViewModel,需要两个方法:一个用来转换单个商品,另一个用来转换商品列表。可以将这些方法添加到领域模型的Product实体中,但他们并非真正属于业务逻辑,因此最好的做法就是将其作为扩展方法,这样在使用他们时,就像是Product实体的优先部分。

在Service项目中创建一个名为ProductMapperExtensionMethods的新类,并添加两个方法

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;
    }
}

(5)最后,添加ProductService类,它将于领域模型服务交互,以检索商品列表;然后将其转换成ProductViewModels列表。

向Service项目中添加名为ProductService的新类

public class ProductService
{
    private Model.ProductService _productService;

    public ProductService(Model.ProductService ProductService)
    {
        _productService = ProductService;
    }

    public ProductListResponse GetAllProductsFor(ProductListRequest productListRequest)
    {
        ProductListResponse productListResponse = new ProductListResponse();

        try
        {
            IList<Model.Product> productEntities = _productService.GetAllProductsFor(productListRequest.CustomerType);

            productListResponse.Products = productEntities.ConvertToProductListViewModel();
            productListResponse.Success = true;
        }
        catch (System.Data.SqlClient.SqlException ex)
        {
            // Log the exception...

            productListResponse.Success = false;
            // Return a friendly error message
            productListResponse.Message = "Check that your database is in the correct place. Hint: Check the AttachDbFilename section within App.config in the project ASPPatterns.Chap3.Layered.Repository.";
        }
        catch (Exception ex)
        {
            // Log the exception...

            productListResponse.Success = false;
            // Return a friendly error message
            productListResponse.Message = "An error occured";
        }

        return productListResponse;
    }
}

该服务类捕获所有错误并向客户端返回一条友好的消息,此时是记录错误信息的理想时机。通过在这里处理所有错误并标清成功标记,使得服务层出现问题时,可以让客户端优雅地进行响应。

3.数据访问层

(1)向ASPPatterns.Chap3.Layered.Repository项目,添加一个新的Linq to SQL类,命名为Shop.dbml

(2)打开Server Explorer窗口,将Products表拖放到设计器上。Visual Studio创建一个名为Product的Linq to SQL实体。

(3)现在可以创建IProductRepository接口(在Model项目中创建过)的具体实现。向Repository项目添加名为ProductRepository的新类:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ASPPatterns.Chap3.Layered.Model;   

namespace ASPPatterns.Chap3.Layered.Repository
{
    public class ProductRepository : IProductRepository 
    {        
        public IList<Model.Product> FindAll()
        {
            var products = from p in new ShopDataContext().Products
                           select new Model.Product
                               {
                                   Id = p.ProductId, 
                                   Name = p.ProductName,                                   
                                   Price = new Model.Price(p.RRP, p.SellingPrice)                                                                        
                               };            

            return products.ToList();
        }
     
    }
}

4.表示层

(1)为了将表示逻辑与用户体验(用户界面)分离,采用Model-View-Presenter(模式-视图-呈现器)模式。

在ASPPatterns.Chap3.Layered.Presentation项目中创建名为IProductListView的新接口

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ASPPatterns.Chap3.Layered.Service; 

namespace ASPPatterns.Chap3.Layered.Presentation
{
    public interface IProductListView
    {
        void Display(IList<ProductViewModel> Products);
        Model.CustomerType CustomerType { get; }
        string ErrorMessage { set; }
    }
}

(2)该接口将由ASPX Web表单实现。通过使用接口,可以再测试时将视图分离出来。创建名为ProductListPresenter的新类

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ASPPatterns.Chap3.Layered.Service; 

namespace ASPPatterns.Chap3.Layered.Presentation
{
    public class ProductListPresenter
    {
        private IProductListView _productListView;
        private Service.ProductService _productService;
                
        public ProductListPresenter(IProductListView ProductListView, Service.ProductService ProductService)
        {
            _productService = ProductService;
            _productListView = ProductListView;
        }

        public void Display()
        {
            ProductListRequest productListRequest = new ProductListRequest();
            productListRequest.CustomerType = _productListView.CustomerType;

            ProductListResponse productResponse = _productService.GetAllProductsFor(productListRequest);

            if (productResponse.Success)
            {
                _productListView.Display(productResponse.Products);
            }
            else 
            {
                _productListView.ErrorMessage = productResponse.Message; 
            }
   
        }
    }
}

呈现器类负责获取数据,处理用户事件并通过视图的接口更新视图。

这样就完成了一个非常瘦但是很简单的表示层。拥有表示层的好处是,现在很容易测试数据库的表示以及用户和系统之间的交互,而不用担心难以测试的Web表单。还可以在应用程序之上添加任何形式的用户体验,比如WPF,Winform或者web表单引用程序。

5.用户体验层

最后,可以实现视图,以便在网页上显示商品。但在处理HTML标记之前,需要一种方式将松散耦合的应用程序粘合到一起,因此创建IProductRepository接口的一个具体实现。为此,使用StructureMap,这是一种IOC容器。(导航到http://sourceforge.net/projects/structuremap 并下载StructureMap的最新版本)

(1)在WebUI项目中创建一个名为BootStrapper的新类

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using StructureMap;
using StructureMap.Configuration.DSL;
using ASPPatterns.Chap3.Layered.Repository;
using ASPPatterns.Chap3.Layered.Model;

namespace ASPPatterns.Chap3.Layered.WebUI
{
    public class BootStrapper
    {
        public static void ConfigureStructureMap()
        {            
            ObjectFactory.Initialize(x =>
            {
                x.AddRegistry<ProductRegistry>();

            });
        }   
    }

    public class ProductRegistry : Registry
    {
        public ProductRegistry()
        {
            ForRequestedType<IProductRepository>().TheDefaultIsConcreteType<ProductRepository>();            
        }

    }

}

BootStrapper类的目的是向StructureMap注册所有的具体依赖类。当客户端代码使用StructureMap来解析某个类时,StructureMap检查该类的依赖类,并根据选中的具体实现(在ProductRegistry中指定)自动注入这些依赖类。

(2)应用程序启动时需要运行ConfigureStructureMap方法,因此可以在global.asax文件中添加对其的引用。global.asax文件默认不存在,因此将其添加到WebUI项目的根目录中。

namespace ASPPatterns.Chap3.Layered.WebUI
{
    public class Global : System.Web.HttpApplication
    {

        protected void Application_Start(object sender, EventArgs e)
        {
            BootStrapper.ConfigureStructureMap();
        }
    }
}

(3)打开default.aspx源代码并编辑HTML标记

<asp:DropDownList AutoPostBack="true" ID="ddlCustomerType" runat="server" >
            <asp:ListItem Value="0">Standard</asp:ListItem> 
            <asp:ListItem Value="1">Trade</asp:ListItem> 
</asp:DropDownList>
                
        <asp:Label ID="lblErrorMessage" runat="server" ></asp:Label> 
    
        <asp:Repeater ID="rptProducts" runat="server" >
            <HeaderTemplate>
                <table>
                    <tr>
                        <td>Name</td>
                        <td>RRP</td>
                        <td>Selling Price</td>
                        <td>Discount</td>
                        <td>Savings</td>
                    </tr>
                    <tr>
                        <td colspan="5"><hr /></td>
                    </tr>
            </HeaderTemplate> 
            <ItemTemplate>
                    <tr>
                        <td><%# Eval("Name") %></td>
                        <td><%# Eval("RRP")%></td>
                        <td><%# Eval("SellingPrice") %></td>
                        <td><%# Eval("Discount") %></td>
                        <td><%# Eval("Savings") %></td>
                    </tr>
            </ItemTemplate> 
            <FooterTemplate>
                </table>
            </FooterTemplate> 
        </asp:Repeater>

(4)转换到该页面的隐藏代码并进行编辑,以实现Presentation项目中的IProductListView接口

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using ASPPatterns.Chap3.Layered.Model;
using ASPPatterns.Chap3.Layered.Repository;
using ASPPatterns.Chap3.Layered.Presentation;
using ASPPatterns.Chap3.Layered.Service;
using StructureMap;

namespace ASPPatterns.Chap3.Layered.WebUI
{
    public partial class _Default : System.Web.UI.Page, IProductListView 
    {
        private ProductListPresenter _presenter;

        protected void Page_Init(object sender, EventArgs e)
        {
            _presenter = new ProductListPresenter(this, ObjectFactory.GetInstance<Service.ProductService>());
            this.ddlCustomerType.SelectedIndexChanged += delegate { _presenter.Display();};
        }


        protected void Page_Load(object sender, EventArgs e)
        {
            if (Page.IsPostBack != true) 
                _presenter.Display(); 
        }
        
        public void Display(IList<ProductViewModel> Products)
        {
            rptProducts.DataSource = Products;
            rptProducts.DataBind(); 
        }

        public CustomerType CustomerType
        {
            get { return (CustomerType)Enum.ToObject(typeof(CustomerType), int.Parse(this.ddlCustomerType.SelectedValue) ); }
        }
   
        
        public string ErrorMessage
        {
            set { lblErrorMessage.Text = String.Format("<p><strong>Error</strong><br/>{0}<p/>", value); }
        }
        
    }
}

 

posted @ 2019-08-07 18:52  FH1004322  阅读(170)  评论(0)    收藏  举报