.net架构设计读书笔记--第三章 第9节 域模型实现(ImplementingDomain Model)
我们长时间争论什么方案是实现域业务领域层架构的最佳方法。最后,我们用一个在线商店案例来说明,其中忽略了许多之前遇到的一些场景。在线商店对很多人来说更容易理解。
一、在线商店项目简介
1. 用例选择
Use-case | Description |
Registers to the site | The user fills in the application form and becomes an official customer of the I-Buy-Stuff site. |
Log in to start using the site | The user enters credentials and logs in. Credentials can be entered directly to the site or via a couple of social networks. |
Subscribe to the newsletter | The user adds an email address to receive the newsletter. |
Search an order | The user indicates the number of one of her orders and reviews details such as estimated delivery date, items, and costs. |
Place an order | The user fills a shopping cart with a list of products and then proceeds with making payment and entering the details related to delivery |
2. 方法选择
在线商城的需求,几乎所有的开发人员都或多或少的了解。在需要分析后和通常语言的定义下,将在线商城清晰的定义为一些实体。可以将项目中的类分开创建为类库。域模型将定义为以下域实体:Admin, Customer, Order, OrderItem, Product, Subscriber, 和FidelityCard。
3. 设计在线商城项目结构
前面的章节中专注于探讨分层结构在DDD中的作用,现在看下实际项目中是如何使用的。下图是VS2013中的项目结构
图中可以的地到Demain Layer,Infrastructure Layer, 和site。IBuyStuff解决方案中,site目录中实际上就是一个asp.net mvc应用程序。从逻辑上划分,可以将系统分为前端和后端两部分。Demain Layer 和Infrastructure Layer是后端的部分,后端尽可能多的在同一个上下文中处理前端的动作。
4. 技术选择
IBuyStuff解决方案使用了一系列.net技术,大部分通过NuGet包引入。前面已经提到,前端是使用了ASP.NET Identity 的asp.net mvc5的应用程序。Twitter Bootstrap技术也用户界中使用。针对于移动视图,使用界面使用了WURFL来感知终端设备,Asp.net通过路由来展现View。
后端的持久化使用了Entity Framework Code First实现。在示例代码中本地的SqlServer实例在web站点的App_Data目录下。最后,项目中所有的依赖注入都是使用Microsoft Patterns and Practices Unity。
5. 边界上下文设计
I-Buy-Stuff在线商城是一个刻意简单小型、上下文边界中业务不需要面对额外的需求。演示一个广义的DDD架构它可能不是最佳的例子,但别一方面,一个太复杂的例子又太难讲解。
在域上下文中分解业务,一个合理简单的方案,将业务分为以下几个边界上下文:
- MembershipBC
- OrderingBC
- CatalogBC
MembershipBC用于身份认证和用户账户管理。OrderingBC用于管理购物车和订单处理,CatalogBC用于后台管理,如产品更新、添加、删除,更新库存等。
OrderingBC最可能是一个Asp.net Mvc应用程序,如在线商城。Membership在同一个应用中作为不同的功能模块独立存在。CatalogBC是管理系统,更多是数据库的CRUD操作,没在必要使用DDD设计。
6. 边界上下文间的通信
Product的增删改查何时通知OrderingBC上下文边界?OrderingBC要对数据尽可能频繁读取来获取CatalogBC对数据库的更新是不太可能的,别外一种方法,当CatalogBC更新时,同时调用一个OrderingBC的RPC。在大型复杂的系统中,你甚至会用Service bus将架构中许多小模块连接起来。
7. 添加其它的上下文边界
随着商城的复杂度成长,你要添加其它的上下文,比如:ShippingBC,PricingBC,PurchaseBC等。你可以将他们放到同下big上下文,可以将他们分割成多个小的上下文。这儿没有什么硬性指标来决策怎么做。
8. I-Buy-Stuff解决方案中的上下文图表
如下图所示,I-Buy-Stuff上下文是一个ASP.NET MVC应用程序,它要在域模型架构上运行,是一个相当大的上下文,国为它包含了会员、订购逻辑、甚至是包含了购买上下文。I-Buy-Stuff上下文承载着大部分业务逻辑,Shipping和Purchase被实现为服务形式。
二、高大尚的域模型建模
现代软件的根本问题是,开发都常常宁愿关注代码而不愿意面对模型设计,这种方法本身没有错,但是你将面对的是难以管理的复杂度。如果你没有进行域模型设计的情况下写出了成功的代码,这样做有错吗?当然没错,但你有可能在复杂度增长时暴露出问题。它可能出现在下一个项目或同一个项目中的需求增长和变更。
1. anemic model 弱模型
DDD权威指出没有模型没有必要是弱模型。软件设计中的面向对象指出,一个类是对数据和行为的封装,而一个模型只是用于映射数据库,基本上没有行为方法,这样的类被称之为弱类 anemic model。
业务逻辑的位置
实际上,面向行为本质上是找出业务逻辑模块的位置。
class Invoice
{
public string Number {get; private set;}
public DateTime Date {get; private set;}
public Customer Customer {get; private set;}
public string PayableOrder {get; private set;}
public ICollection<InvoiceLine> Lines {get; private set;}
...
}
上面的Invoice类中没有方法,是错误的写法?还是弱模型的一部分?如果对象仅没有方法,它也不一定是一个弱模型。弱模型是指将业务逻辑放到了模型外的地方实现。
2. Entities scaffolding
一个DDD实体应该是一个同时包含数据和行为的类。一个实体可能有公有属性,但他不会被用作为一个数据容器。下面是DDD实体的特性
- 良好的唯一性定义
- 行为通过方法来表示,不管是公有方法还是非公有方法
- 通过只读属性公开状态
- 用值对象代替很少用的基元类型
- 使用工厂方法来代替多个构造函数
3.Value objects scaffolding
与entity类似,value object是由一系列数据组成的类。行为对值对象来说是不需要的。值对象也可以有方法,但是这些方法都是一些辅助类方法。于entity不同,value object不需要唯一性,它们没有可变状态,它们只用于存储数据。如DateTime类型的构造函数。
4. 明确合集
将话题转到I-Buy-Stuff这个示例应用程序上,I-Buy-Stuff是一个简单的在线商城,下图反应出应用中实体的关系。在站点中,我们认为只有少量的用例,如:搜索,下单。Customer、Order、Product是合集,Customer和Produc是单件实体合集。
- Customer aggregate
Customer的特点是有一些个人数据,如name,address,username,或payment details。我们明确假设I-Buy-Stuff系统中只有一种类型用户,不用做任何区分。如:法人客户可能和个人客户,法人客户下可能包含多个个人客户。I-Buy-Stuff选择第3种表示方法,不区分。
Customer定义
public class Customer : IAggregateRoot
{
public static Customer CreateNew(Gender gender, string id,string firstname, string lastname, string email)
{
var customer = new Customer {
CustomerId = id,
Address = Address.Create(),
Payment = NullCreditCard.Instance,
Email = email,
FirstName = firstname,
LastName = lastname,
Gender = gender
};
return customer;
}
public string CustomerId { get; private set; }
public string PasswordHash { get; private set; }
public string FirstName { get; private set; }
public string LastName { get; private set; }
public string Email { get; private set; }
public Gender Gender { get; private set; }
public string Avatar { get; private set; }
public Address Address { get; private set; }
public CreditCard Payment { get; private set; }
public ICollection<Order> Orders { get; private set; }
// More methods here such as SetDefaultPaymentMode.
// A customer may have a default payment mode (e.g., credit card), but
// different orders can be paid in different ways.
...
}
其它几个合集定义……
5. 实体持久化 Persisting the model
域模型要支持持久化,常见的是用O/RM来实现,如Entity Framework Code-First 方法实现。本质上Entity Framework有两种实现风格:Database First 与Code First。另外还有第三种风格,Model First,它是一种混合风格。Database-First方法中Entity Framework读取数据库结构,生成和一系列partial类型的弱类(anemic class),可以使用partial来扩展类。Code-First风格是生成一系列类,如I-Buy-Stuff定义的合集,然后添加一个额外的层来映射到数据库表。这个映射层告诉O/RM关于数据库信息,及如何CRUD数据。I-Buy-Stuff示例中使用的是Code-First风格。
Code-First通过继承DbContext类为中心实现持久化。
public class DomainModelFacade : DbContext
{
static DomainModelFacade()
{
Database.SetInitializer(new SampleAppInitializer());
}
public DomainModelFacade() : base("naa4e - 09")
{
Products = base.Set<Product>();
Customers = base.Set<Customer>();
Orders = base.Set<Order>();
FidelityCards = base.Set<FidelityCard>();
}
public DbSet<Order> Orders { get; private set; }
public DbSet<Customer> Customers { get; private set; }
public DbSet<Product> Products { get; private set; }
public DbSet<FidelityCard> FidelityCards { get; private set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
...
}
}
Mapping properties to columns
Code First中有两种方法将properties映射到columns。可以在demain class中使用data annotation attributes,也可以使用Code-First API自带的configuration classes来重写OnModelCreating方法轻松实现。如:
modelBuilder.ComplexType<Money>();
modelBuilder.ComplexType<Address>();
modelBuilder.ComplexType<CreditCard>();
modelBuilder.Configurations.Add(new FidelityCardMap());
modelBuilder.Configurations.Add(new OrderMap());
modelBuilder.Configurations.Add(new CustomerMap());
modelBuilder.Configurations.Add(new OrderItemMap());
modelBuilder.Configurations.Add(new CurrencyMap());
三、实现业务逻辑
很多情况下,并不所有的业务逻辑都需要融入到域模型的类里。而非业务逻辑的功能,如持久化功能也要添加到域模型中。在I-Buy-Stuff项目中有两个主要的业务逻辑:查询订单和订单下单。
1.查询订单
下面的代码中Controller接受一个查找订单的用户请求,controller方法中使用一个Service来获取订单数据。
public ActionResult SearchResults(int id)
{
var model = _service.FindOrder(id, User.Identity.Name);
return View(model);
}
public SearchOrderViewModel FindOrder(int orderId, string customerId)
{
var order = _orderRepository.FindByCustomerAndId(orderId, customerId);
if (order is NullOrder)
return new SearchOrderViewModel();
return SearchOrderViewModel.CreateFromOrder(order);
}
application service中FindOrder方法调用域服务处仓储服务来获取订单。
2. 订单下单
当用户在站点上点击购买时,商城系统反回一个用户界面,用于创建订单,另外,还有添加到购物车等方法
public ActionResult New()
{
var customerId = User.Identity.Name;
var shoppingCartModel = _service.CreateShoppingCartForCustomer(customerId);
shoppingCartModel.EnableEditOnShoppingCart = true;
SaveCurrentShoppingCart(shoppingCartModel);
return View("shoppingcart", shoppingCartModel);
}
//添加到购物车
public ActionResult AddToShoppingCartCommand(int productId, int quantity = 1)
{
var cartModel = RetrieveCurrentShoppingCart();
cartModel = _service.AddProductToShoppingCart(cart, productId, quantity);
SaveCurrentShoppingCart(cartModel);
return RedirectToAction("AddTo");
}
public ShoppingCartViewModel AddProductToShoppingCart(ShoppingCartViewModel cart, int productId, int quantity)
{
var product = (from p in cart.Products where p.Id == productId select p).Single();
cart.OrderRequest.AddItem(quantity, product);
return cart;
}
曾经年少多少事 而今皆付谈笑中!