Thinking In Design Pattern——MVP模式演绎
原文《Thinking In Design Pattern——MVP模式演绎》不知为何丢失了,故重新整理了一遍。
目录
- What Is MVP
- Domain Model
- StubRepositoty
- IView & Presenter
- View
- Ioc容器StructureMap
开篇
忙碌的9月,工作终于落定,新公司里的框架是MVP+Linq,对于MVP虽然不熟,但有MVC的基础,花了两天时间研究了MVP,故作此博文,留作参考。
Model-View-Presenter(模型-视图-呈现器,MVP)模式的重点是让Presenter控制整个表示层的逻辑流。MVP模式由如下三个不同部分组成:
- 模型表示视图显示或者修改的业务数据,包括业务逻辑和领域相关的逻辑。
- 视图通过呈现器显示模型数据,并将用户输入委托给呈现器。
- 呈现器被视图调用来显示从模型中“拉”出来的数据并处理用户输入。
What Is MVP
了解了MVP设计模式后,我以一个简单的例子阐述MVP模式在企业级架构中的应用,如下图给出了企业级分层设计的ASP.NET应用程序的典型体系结构(实际还要更复杂些):
下面的我将以一个简单的案例(出自《ASP.NET》设计模式)详解MVP思想的应用,当然MVP和MVC一样都是属于表现层的设计模式,我将参考上述两幅图中的分层思想来创建应用程序,下图为分层体系结构创建完毕时解决方案目录:
OK,接下来我们从头开始来创建我们的应用程序,首先我们要分清楚需求(建立一个简单的购物流程Demo),了解需求后我们再抽象出模型(Category,Product)。
建立简单的领域模型:
namespace Eyes.MVP.Model { public class Category { public int Id { get; set; } public string Name { get; set; } } }
public class Product { public int Id { get; set; } public Category Category { get; set; } public string Name { get; set; } public decimal Price { get; set; } public string Description { get; set; } }
接着,为Product和Category添加资源库的契约接口,该接口为业务实体持久化提供了标准方法,我建议把这部分代码放到infrastructure层中:
public interface ICategoryRepository { IEnumerable<Category> FindAll(); Category FindBy(int id); } public interface IProductRepository { IEnumerable<Product> FindAll(); Product FindBy(int id); }
最后添加领域服务类ProductService,基于接口编程的思想使用资源库契约接口(IxxxRepository)来协调Product和Category的操作:
public class ProductService { private ICategoryRepository _categoryRepository; private IProductRepository _productRepository; public ProductService(ICategoryRepository categoryRepository, IProductRepository productRepository) { _categoryRepository = categoryRepository; _productRepository = productRepository; } public Product GetProductBy( int id) { return _productRepository.FindBy(id); } public IEnumerable<Product> GetAllProductsIn( int categoryId) { return _productRepository.FindAll().Where(c => c.Category.Id == categoryId); } public Category GetCategoryBy( int id) { return _categoryRepository.FindBy(id); } public IEnumerable<Category> GetAllCategories() { return _categoryRepository.FindAll(); } public IEnumerable<Product> GetBestSellingProducts() { return _productRepository.FindAll().Take(4); } } |
建立Domain Model之后,需要为资源库(仓储)提供数据,所以创建StubRepository:
StubRepositoty
创建名为StubRepositoty的类库,DataContext为我们的资源库提供数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | /// <summary> /// Provider data to repositories /// </summary> public class DataContext { private readonly List<Product> _products; private readonly List<Category> _categories; public DataContext() { _categories = new List<Category>(); var hatCategory = new Category {Id = 1, Name = "Hats" }; var gloveCategory = new Category {Id = 2, Name = "Gloves" }; var scarfCategory = new Category {Id = 3, Name = "Scarfs" }; _categories.Add(hatCategory); _categories.Add(gloveCategory); _categories.Add(scarfCategory); _products = new List<Product> { new Product {Id = 1, Name = "BaseBall Cap" , Price = 9.99m, Category = hatCategory}, new Product {Id = 2, Name = "Flat Cap" , Price = 5.99m, Category = hatCategory}, new Product {Id = 3, Name = "Top Hat" , Price = 9.99m, Category = hatCategory} }; _products.Add( new Product {Id = 4, Name = "Mitten" , Price = 10.99m, Category = gloveCategory}); _products.Add( new Product {Id = 5, Name = "Fingerless Glove" , Price = 13.99m, Category = gloveCategory}); _products.Add( new Product {Id = 6, Name = "Leather Glove" , Price = 7.99m, Category = gloveCategory}); _products.Add( new Product {Id = 7, Name = "Silk Scarf" , Price = 23.99m, Category = scarfCategory}); _products.Add( new Product {Id = 8, Name = "Woolen" , Price = 14.99m, Category = scarfCategory}); _products.Add( new Product {Id = 9, Name = "Warm Heart" , Price = 87.99m, Category = scarfCategory}); } public List<Product> Products { get { return _products; } } public List<Category> Categories { get { return _categories; } } } |
当数据就为之后,我们就可以实现Model项目中定义的资源库契约接口:
public class ProductRepository : IProductRepository { public IEnumerable<Product> FindAll() { return new DataContext().Products; } public Product FindBy( int id) { Product productFound = new DataContext().Products.FirstOrDefault(prod => prod.Id == id); //set discription if (productFound != null ) { productFound.Description = "orem ipsum dolor sit amet, consectetur adipiscing elit." + "Praesent est libero, imperdiet eget dapibus vel, tempus at ligula. Nullam eu metus justo." + "Curabitur sit amet lectus lorem, a tempus felis. " + "Phasellus consectetur eleifend est, euismod cursus tellus porttitor id." ; } return productFound; } } public class CategoryRepository : ICategoryRepository { public IEnumerable<Category> FindAll() { return new DataContext().Categories; } public Category FindBy( int id) { return new DataContext().Categories.FirstOrDefault(cat => cat.Id == id); } } |
这样我们就完成了StubRepository项目,有关Infrastructure和Repository我不做详细介绍,所以我简单处理。当然本片博客的核心是MVP,接下来详解View和Presenter关系。
View & Presenter
切换Presenter项目中,添加IHomeView接口,这个接口定义了电子商务网页的视图,在首页上显示商品目录以及最畅销的商品:
public interface IHomeView { IEnumerable<Category> CategoryList { set; } }
接着,定义一个IHomePagePresenter接口,这个接口的目的是实现代码松散耦合并有助于测试:
public interface IHomePagePresenter { void Display(); }
最后,添加一个HomePagePresenter,这个呈现器从ProductService中检索到的Product和Category数据来填充视图属性,这儿完美体现了Presenter的作用:
public class HomePagePresenter : IHomePagePresenter { private readonly IHomeView _view; private readonly ProductService _productService; public HomePagePresenter(IHomeView view, ProductService productService) { _view = view; _productService = productService; } public void Display() { _view.TopSellingProduct = _productService.GetBestSellingProducts(); _view.CategoryList = _productService.GetAllCategories(); } }
接下来是包含一个分类中所有商品的视图ICategoryProductsView:
public interface ICategoryProductsView { int CategoryId { get; } IEnumerable<Product> CategoryProductList { set; } IEnumerable<Category> CategoryList { set; } }
然后再创建CategoryProductsPresenter,他与HomePagePresenter相似:从ProductService中获取到的分类商品来更新视图,但他稍有不同,他要求视图提供CategoryId:
public class CategoryProductsPresenter : ICategoryProductsPresenter { private readonly ICategoryProductsView _view; private readonly ProductService _productService; public CategoryProductsPresenter(ICategoryProductsView view, ProductService productService) { _view = view; _productService = productService; } public void Display() { _view.CategoryProductList = _productService.GetAllProductsIn(_view.CategoryId); _view.Category = _productService.GetCategoryBy(_view.CategoryId); _view.CategoryList = _productService.GetAllCategories(); } }
接下来我们还要创建下一个视图用来表示Product的详细视图,该视图显示有关特定商品的详细信息并可以添加到购物车中(Session),在该视图之前我们还需要创建一些支撑类:
public interface IBasket { IEnumerable<Product> Items { get ; } void Add(Product product); } public class WebBasket : IBasket { public IEnumerable<Model.Product> Items { get { return GetBasketProducts(); } } public void Add(Model.Product product) { IList<Product> products = GetBasketProducts(); products.Add(product); } private IList<Product> GetBasketProducts() { var products = HttpContext.Current.Session[ "Basket" ] as IList<Product>; if (products == null ) { products = new List<Product>(); HttpContext.Current.Session[ "Basket" ] = products; } return products; } } |
WebBasket类简单的使用当前会话来存放和检索商品集合,接着我们在添加一个Navigation,用来重定向:
public enum PageDirectory { Basket } public interface IPageNavigator { void NaviagateTo(PageDirectory page); } |
实现IPageNavigator:
public void NaviagateTo(PageDirectory page) { switch (page) { case PageDirectory.Basket: HttpContext.Current.Response.Redirect( "/Views/Basket/Basket.aspx" ); break ; default : break ; } } |
编写好辅助类之后,我们在创建商品详细视图,这儿需要注意一下ProduceId这个属性,和之前一样也是只读的,通过QueryString得到ProductId:
public interface IProductDetailView { int ProductId { get ; } string Name { set ; } decimal Price { set ; } string Description { set ; } IEnumerable<Category> CategoryList { set ; } } |
接下来,添加一个相应的ProductDetailPresenter(实现IProductDetailPresenter接口):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | public class ProductDetailPresenter : IProductDetailPresenter { private readonly IProductDetailView _view; private readonly ProductService _productService; private readonly IBasket _basket; private readonly IPageNavigator _pageNavigator; public ProductDetailPresenter(IProductDetailView view, ProductService productService, IBasket basket, IPageNavigator pageNavigator) { _view = view; _productService = productService; _basket = basket; _pageNavigator = pageNavigator; } public void Display() { Product product = _productService.GetProductBy(_view.ProductId); _view.Name = product.Name; _view.Description = product.Description; _view.Price = product.Price; _view.CategoryList = _productService.GetAllCategories(); } public void AddProductToBasketAndShowBasketPage() { Product product = _productService.GetProductBy(_view.ProductId); _basket.Add(product); _pageNavigator.NaviagateTo(PageDirectory.Basket); } } |
最后添加购物车视图,IBasketView接口显示顾客的购物车中的所有商品以及一个用户商品目录导航的商品分类列表:
public interface IBasketView { IEnumerable<Category> CategoryList { set ; } IEnumerable<Product> BasketItems { set ; } } |
相信接下来你已经驾轻就熟了,创建BasketPresenter,用来控制Model和View之间的数据交互:
public class BasketPresenter : IBasketPresenter { private readonly IBasketView _view; private readonly ProductService _productService; private readonly IBasket _basket; public BasketPresenter(IBasketView view, ProductService productService, IBasket basket) { _view = view; _productService = productService; _basket = basket; } public void Display() { _view.BasketItems = _basket.Items; _view.CategoryList = _productService.GetAllCategories(); } } |
这样我们就完成了Presenter项目,接下来我们就可以关注视图实现了,由于篇幅有限,我挑选一个典型模块分析,具体代码可以在此下载:
MVP实现关注点的分离,集中管理相关的逻辑,View关注与UI交互,Model关注与业务逻辑,Presenter协调管理View和Model,是整个体系的核心。Model与View无关,具有极大复用性。
MVP通过将将主要的逻辑局限于Presenter,是它们具有更好的可测试性。至于并行开发,个人觉得在真正的开发中,意义到不是很大,现在开发这大多是多面手,呵!
Presenter通过接口调用View降低了Presenter对View的依赖,但是View依然可以调用Presenter,从而导致了很多开发人员将Presenter当成了一个Proxy,所以我们的目的是降低View对Presenter的依赖。
“所以我更倾向于View并不知道按钮点击后回发生什么事,如Update数据,但是点击后界面有什么光线,水纹,这个应该是View关心的,View应该更注重的是和用户交互的反应。”着正是本文的观点:View仅仅将请求递交给Presenter,Presenter在适当的时候来驱动View!
View:
为了使布局统一和减少冗余代码,我们将创建Master Page和User Control:
CategoryList.ascx,用来显示所有的目录集合:
为了能让Presenter为他绑定数据,我们需要创建一个方法:
public partial class CategoryList : System.Web.UI.UserControl { public void SetCategoriesToDisplay(IEnumerable<Category> categories) { this .rptCategoryList.DataSource = categories; this .rptCategoryList.DataBind(); } } |
接下来,再添加一个用户控件ProductList.ascx,用来显示商品的集合:
public partial class ProductList : System.Web.UI.UserControl { public void SetProductsToDisplay(IEnumerable<Model.Product> products) { this .rptProducts.DataSource = products; this .rptProducts.DataBind(); } } |
最后再添加一个Master Page,并为其添加一个:
<%@ Master Language= "C#" AutoEventWireup= "true" CodeBehind= "Shop.master.cs" Inherits= "Eyes.MVP.UI.Web.Views.Shared.Shop" %> <%@ Register src= "~/Views/Shared/CategoryList.ascx" tagname= "CategoryList" tagprefix= "uc1" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" > <html xmlns= "http://www.w3.org/1999/xhtml" > <head runat= "server" > <title></title> </head> <body> <form id= "form1" runat= "server" > <div> <table width= "70%" > <tr> <td colspan= "3" ><h2><a href= "http://archive.cnblogs.com/Views/Home/Index.aspx" target= "_blank" rel= "nofollow" > |
public partial class Shop : System.Web.UI.MasterPage { public CategoryList CategoryListControl { get { return this .CategoryList1; } } } |
接下来添加一张ProductDetail.aspx页面,因为比较典型,所以我选他来作为分析,让其实现IProductDetailView接口:
public partial class ProductDetail : System.Web.UI.Page, IProductDetailView { private IProductDetailPresenter _presenter; protected void Page_Init( object sender, EventArgs e) { _presenter = new ProductDetailPresenter( this , ObjectFactory.GetInstance<ProductService>(), ObjectFactory.GetInstance<IBasket>(), ObjectFactory.GetInstance<IPageNavigator>()); } protected void Page_Load( object sender, EventArgs e) { _presenter.Display(); } public int ProductId { get { return int .Parse(Request.QueryString[ "ProductId" ]); } } public string Name { set { litName.Text = value; } } public decimal Price { set { litPrice.Text = string .Format( "{0:C}" , value); } } public string Description { set { litDescription.Text = value; } } public IEnumerable<Model.Category> CategoryList { set { Shop shopMaster = (Shop) Page.Master; shopMaster.CategoryListControl.SetCategoriesToDisplay(value); } } protected void btnAddToBasket_Click( object sender, EventArgs e) { _presenter.AddProductToBasketAndShowBasketPage(); } } |
这里我想提一下Ioc容器:StructureMap
Ioc
传统的控制流,从客户端创建服务时(new xxxService()),必须指定一个特定服务实现(并且对服务的程序集添加引用),Ioc容器所做的就是完全将这种关系倒置过来(倒置给Ioc容器),将服务注入到客户端代码中,这是一种推得方式(依赖注入)。术语”控制反转“,即客户放弃代码的控制,将其交给Ioc容器,也就是将控制从客户端代码倒置给容器,所以又有人称作好莱坞原则”不要打电话过来,我们打给你“。实际上,Ioc就是使用Ioc容器将传统的控制流(客户端创建服务)倒置过来,将服务注入到客户端代码中。
总之一句话,客户端代码能够只依赖接口或者抽象类或基类或其他,而不关心运行时由谁来提供具体实现。
使用Ioc容器如StructureMap,首先配置依赖关系(即当向Ioc容器询问特定的类型时将返回一个具体的实现),所以这又叫依赖注入:
public class BootStrapper { public static void ConfigureDependencies() { ObjectFactory.Initialize(x => { x.AddRegistry<ControllerRegistry>(); }); } public class ControllerRegistry : Registry { public ControllerRegistry() { ForRequestedType<ICategoryRepository>().TheDefault.Is.OfConcreteType<CategoryRepository>(); ForRequestedType<IProductRepository>().TheDefault.Is.OfConcreteType<ProductRepository>(); ForRequestedType<IPageNavigator>().TheDefault.Is.OfConcreteType<PageNavigator>(); ForRequestedType<IBasket>().TheDefault.Is.OfConcreteType<WebBasket>(); } } } |
通常我们希望在启动是配置依赖关系,一般在Application_Start事件中调用ConfigureStructureMap方法:
protected void Application_Start( object sender, EventArgs e) <br> { <br> BootStrapper.ConfigureDependencies(); } |
小结
MVP设计模式我匆忙总结了一下,由于经验不足,不足之处,还望多多指点。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~