NHibernate初学者指南(3):创建Model
什么是Model
我这里简单的用一句话概括什么是model:
model是对现实的描述,它可以被开发人员、业务分析师、客户所理解,它不是UML图或者其他任何以开发者为中心描述domain的方式。
model的元素
实体(Entity)
实体是这样一个对象:由它的属性组合唯一标识以及有定义好的生命周期。通常实体包含一个ID或key属性,用于唯一标识它。
两个具有相同类型和相同标识符的实体被认为是相同的实体。
在Line of Business(LOB)应用程序中典型的实体有:customer,product,order,supplier等等。拿一个电子商务程序作为例子,通过唯一标识符来区分customer是非常重要的。
在现实生活中,我们通常使用人可读或可理解的标识符处理实体。这样的标识符也称为自然键(natural keys)。典型的例子有:美国公民的社会安全码(SSN),产品的产品代码,银行账户号码,订单的订单号等等。
在程序中使用人工标识符唯一标识实体很重要。这样的人工标识符也称为代理键(surrogate keys)。这个标识符的其他定义是持久化对象标识符(POI)。
为什么不只使用自然键呢?我们都知道,在实际生活中,由于这样那样的原因,自然键可能发生改变。一件产品可能接受一个新的产品代码或者SSN被重新发放。然而,在程序中,我们需要在实体的整个生命周期都保持不变的标识符,使用代理键可以得到保证。
值对象(Value object)
在model中,对象可能不需要定义生命周期以及不需要通过ID或key唯一标识而存在。这种类型的对象称为值对象(value objects)。相同类型的值对象的两个实例,如果它们的属性都相同就说它们是相同的。
上面值对象的定义带来的直接影响是值对象不可变。也就是说,一旦定义了值对象,就不能再修改了。
虽然在银行应用中,账户是一个实体,需要使用ID唯一标识,但是也存在money的概念,它是由值和货币符号组成的对象。这个money对象正是值对象的一个典型例子。两个有相同数值和相同货币符号的money对象是相同的,它们之间没有区别。一个对象可以由另一个对象替换,不会有其他的影响。
其他值对象的例子:
- person实体的Name。Name值对象由person对象的surname,given name和middle name组成。
- GIS应用中的地理坐标。这个值对象由经纬坐标的值组成。
- 比色法应用中的Color。颜色对象由red,green,blue和alpha通道的值组成。
- 客户关系管理(CRM)中的Address作为customer实体的一部分。地址对象可能包含地址1和地址2,邮政编码,城市的值。
值对象从不单独存在。在model中,它们总是作为实体的一部分。如前面提到的,银行账号有一个balance属性,它是money类型的。
实战时间–创建一个Name值对象
1. 在VS中创建一个类库项目,名字为OrderingSystem,将默认添加的Class1.cs删除。
2. 在项目中添加一个Domain文件夹
3. 在Domain文件夹中添加一个Name类,添加如下属性:
public class Name { public string FirstName { get; private set; } public string MiddleName { get; private set; } public string LastName { get; private set; } }
4. 添加一个带有三个参数:firstName,middleName和lastName(类型都为string)的构造函数,分配参数给每个属性,firstName和lastName不允许传null值,代码如下:
public Name(string firstName, string middleName, string lastName) { if (string.IsNullOrWhiteSpace(firstName)) { throw new ArgumentException("First name must be defined."); } if (string.IsNullOrWhiteSpace(lastName)) { throw new ArgumentException("Last name must be defined."); } FirstName = firstName; MiddleName = middleName; LastName = lastName; }
5. 重写GetHashCode方法,返回值是三个独立属性组合的哈希值。通过下面的链接:http://msdn.microsoft.com/zh-cn/library/system.object.gethashcode.aspx获得如何构造哈希值的详细描述。注意,如果MiddleName为null,将0作为它的哈希值。
public override int GetHashCode() { unchecked { var result = FirstName.GetHashCode(); result = (result * 397) ^ (MiddleName != null ? MiddleName.GetHashCode() : 0); result = (result * 397) ^ LastName.GetHashCode(); return result; } }
6. 要完成,现在我们必须重写Equals方法,它接收一个object类型的参数。然而,我们首先要添加一个接受Name类型参数的Equals的方法。在这个方法中,完成三步工作:
- 检查传递的参数是否为null,如果是,那么这个实体和比较的实体不相等,返回false。
- 然后,检查这个实体和其他实体是不是同一个实例,如果是,返回true。
- 最后,单独比较每个属性。如果所有的属性值都匹配,那么返回true,否则,返回false。
public bool Equals(Name other) { if (other == null) return false; if (ReferenceEquals(this, other)) return true; return Equals(other.FirstName, FirstName) && Equals(other.MiddleName, MiddleName) && Equals(other.LastName, LastName); }
7. 现在重写Equals方法,调用前面的重载方法即可:
public override bool Equals(object other) { return Equals(other as Name); }
恭喜,已经成功的创建了第一个值对象类型。它的类图如下面的截图:
实战时间–创建一个基实体
首先,为所有类型的实体创建一个基类。
1. Domain文件夹中添加一个Entity泛型抽象类。代码如下:
namespace OrderingSystem.Domain { public abstract class Entity<T> where T : Entity<T> { } }
2. 在类中添加一个类型为Guid的自动属性ID。属性的setter器为private。这是实体的唯一标识符。
public Guid ID { get; private set; }
3. 重写类的Equals方法。按照下面三种情况:
- 其他实体(和这个实体比较的实体)不是相同类型,在这种情况下实体不相同,简单的返回false。
- 这个实体和其他实体都是新的对象,尚未保存进数据库。这种情况,仅当在内存中它们指向同一个实例或者使用.net术语,它们的引用相等,我们才认为两个对象是相同的实体。
- 如果我们比较的两个实体是相同的类型但不是新的实体,那么我们简单的比较它们的ID来比较它们是否相同。
public override bool Equals(object obj) { var other = obj as T; if (other == null) return false; var thisIsNew = Equals(ID, Guid.Empty); var otherIsNew = Equals(other.ID, Guid.Empty); if (thisIsNew && otherIsNew) return ReferenceEquals(this, other); return ID.Equals(other.ID); }
4. 每当我们重写Equals方法,同时还必须提供一个GetHashCode方法的实现。在这个方法中,我们仅仅返回ID的哈希值。有一个特殊情况要单独对待。这种情况来自只要实体在内存中,它的哈希值就永远不会改变的事实。实体已经成为一个未定义的新实体和其他(如HashSet<T>或Dictionary<K,T>)已经请求了它的哈希值时正是如此。稍后,实体将会获得一个ID。在本例中,当一个实体仍是未定义ID的实体时,它不能仅仅返回ID的哈希值,而是返回经过计算的哈希值。考虑到这一特殊情况,我们的代码如下所示:
private int? oldHashCode; public override int GetHashCode() { // once we have a hashcode we'll never change it if (oldHashCode.HasValue) return oldHashCode.Value; // when this instance is new we use the base hash code // and remember it, so an instance can NEVER change its // hash code. var thisIsNew = Equals(ID, Guid.Empty); if (thisIsNew) { oldHashCode = base.GetHashCode(); return oldHashCode.Value; } return ID.GetHashCode(); }
5. 最后,重写==和!=操作符,这样我们可以比较两个实体而不用使用Equals方法了。在内部,两个方法只是调用Equals方法。
public static bool operator ==(Entity<T> lhs, Entity<T> rhs) { return Equals(lhs, rhs); } public static bool operator !=(Entity<T> lhs, Entity<T> rhs) { return !Equals(lhs, rhs); }
实战时间–创建一个Customer实体
现在,让我们实现一个继承自基实体的真正实体。我们可以集中精力于描述实体的属性和描述实体行为的方法上了。
1. 在Domain文件夹中添加一个新类Customer,让它继承自Entity基类。
public class Customer : Entity<Customer> { }
2. 在Customer类中添加如下自动属性:
public string CustomerIdentifier { get; private set; } public Name CustomerName { get; private set; }
3. 实现一个ChangeCustomerName方法,带有firstName,middleName和lastName参数。该方法修改类的Customer属性。
public void ChangeCustomerName(string firstName, string middleName, string lastName) { CustomerName = new Name(firstName, middleName, lastName); }
4. 在下面的截图中,我们可以看见刚刚实现的Customer实体的类图,以及基类和Name值对象。
定义实体间的关系
实体是model的关键概念,然而,实体并不是孤立存在的,它们与其他实体相关联。
拥有和包含
值对象永远不能单独存在。它们只有和实体一起才会有意义。一个实体可以拥有或者包含0到多个值对象。在前面Customer的例子中,值对象Name被Customer实体拥有或者包含。这种关系是由箭头从实体指向值对象表示的,如下面的截图。
注意没有箭头从Name指回到Customer。Name值对象不知道它的的拥有者。
在代码中这种关系是在Customer类中通过实现类型为Name的属性定义的。如下面的代码:
public Name CustomerName { get; private set; }
1对多
我们看一下上一篇中用到的两个实体,它们是如何关联彼此的呢?
1. 每个产品都完全属于一个类别。因此我们可以在Product类中定义一个Category类型的Category属性。这个属性是对产品类别的一个引用,它可以用来从product导航到它关联的category。product和category之间的这种关系在下面的截图中用箭头从Product指向Category。属性的名称(Category)用来从Product指向Category,被标记在靠近箭头的一方。代码如下:
public Category Category { get; private set; }
2. 每个类别还很多关联的产品。因此,我们可以在Category类中定义一个Products属性,它是产品的一个集合。这种关系使用从Category指向Product的双箭头标记,如下图。同样,属性的名称(Products)用来从Category导航到它关联的产品,被标记在靠近箭头的一方。代码如下:
private List<Products> products; public IEnumerable<Product> Products { get { return products; } }
在现实生活中的库存应用中,你可能会在Category实体中避免使用Products集合,因为一个category可能有成百上千的products。加载给定类别的整个products集合是不明智的,这会导致程序的响应时间欠佳。 |
1对1
有时,我们会遇到一个实体扮演不同角色的情况。拿Person实体作为例子,一个Person可以是大学里一个学院的Professor,同时还可以是另一个学院的Student。这种关系可以定义为1对1的关系。
同一个domain中的另一个1对1关系会是Professor和HeadOfDepartment中的一个。
下面的截图是前面提到的实体和关系的类图。
注意我们可以通过Professor属性从Person对象导航到它关联的Professor对象。我们也可以使用professor实体的Person属性从professor导航到对应的person对象。这在前面的截图中是用两个箭头表示的,一个从Person指向Professor,另一个方向相反。同样的方式,我们可以从Person导航到Student以及返回,从Professor到HeadOfDepartment也是如此。
多对多
最后一个要讨论的关系类型是多对多关系。让我们看具体的例子:order和product之间的关系。客户要订购产品。可是,客户不想只订购一种产品,而是几种不同的产品。所以,一个订单可以包含多种产品。另一方面,多个不同的客户可以下一个包含单个相同产品的订单。因此,一个产品可以属于多个订单。另一个例子是书和作者的关系。一个作者可以写多本不同的书,同时一本书可以有多个作者。这两个关系都是多对多关系的例子,如下面的截图:
不过,两者也有细微的差别。我们不管后一个关系,因为它是真正的多对多关系。然而,我们需要多讨论一下product和order之间的关系。多考虑一下下订单的过程,我们会意识到缺少了一些概念。一个客户可能不只订购一种产品的一件,还可能是多件。除此之外,我们可能想知道下订单时产品的单价和应用到指定商品的折扣。突然,一个新的中间实体诞生了。我们通常称这个实体为a line item of an order。我们可以修改一下我们的类图,如下图所示:
实战时间–实现订单输入model
该模型的上下文是一个订单输入系统。该模型将作为帮助输入订单到系统的基本解决方案。
1. 我们用到的模型如下面的截图所示:
2. 在VS中,打开OrderingSystem。
3. 首先,创建模型中定义的值对象。
- 我们已经定义了Name类,它是一个值对象,包含三个属性:FirstName,MiddleName和LastName。(Employee和Customer实体都有一个Name类型的属性)。
- 在Domain文件夹中添加一个Address类,它有以下属性(都是string类型的):Line1,Line2,ZipCode,City和State。重写Equals和GetHashCode方法。代码如下:
public class Address { public string Line1 { get; set; } public string Line2 { get; set; } public string ZipCode { get; set; } public string City { get; set; } public string State { get; set; } public override bool Equals(object obj) { return Equals(obj as Address); } public bool Equals(Address other) { if (other == null) return false; if (ReferenceEquals(this, other)) return true; return Equals(other.Line1, Line1) && Equals(other.Line2, Line2) && Equals(other.ZipCode, ZipCode) && Equals(other.City, City) && Equals(other.State, State); } public override int GetHashCode() { unchecked { var result = Line1.GetHashCode(); result = (result * 397) ^ (Line2 != null ? Line2.GetHashCode() : 0); result = (result * 397) ^ ZipCode.GetHashCode(); result = (result * 397) ^ City.GetHashCode(); result = (result * 397) ^ State.GetHashCode(); return result; } } }
4. 项目中已经定义了Entity<TEntity>,我们将使用它作为其他实体的基类。在项目中的Domain文件夹中为每个实体添加一个类,它们都继承自Entity<TEntity>:
Employee,Customer(前面已经添加了),Order,LineItem,Product
5. 给Employee类添加一个Name类型的属性:Name。
public class Employee : Entity<Employee> { public Name Name { get; set; } }
6. 额外给Customer添加一个Address类型的Address属性。同时添加一个只读的集合Orders。
public Address Address { get; set; } private readonly List<Order> orders; public IEnumerable<Order> Orders { get { return orders; } }
7. 给Order类添加如下属性:Customer(类型Customer),Reference(Employee),OrderDate(DateTime)和OrderTotal(decimal)。同时添加一个只读集合LineItems和构造函数。
public class Order : Entity<Order> { public Customer Customer { get; set; } public Employee Employee { get; set; } public DateTime OrderDate { get; set; } public decimal OrderTotal { get; set; } private readonly List<LineItem> lineItems; public IEnumerable<LineItem> LineItems { get { return lineItems; } } public Order(Customer customer) { lineItems = new List<LineItem>(); Customer = customer; OrderDate = DateTime.Now; } }
8. 给Product类添加如下属性:Name(string),Description(string),UnitPrice(decimal),ReorderLevel(int)和Discontinued(bool)。
public class Product : Entity<Product> { public string Name { get; set; } public string Description { get; set; } public decimal UnitPrice { get; set; } public int ReorderLevel { get; set; } public bool Discontinued { get; set; } }
9. 给LineItem类添加如下属性:Order(Order),Product(Product),Quantity(int),UnitPrice(decimal)和Discount(decimal)。并且添加一个构造函数。
public class LineItem : Entity<LineItem> { public Order Order { get; set; } public Product Product { get; set; } public int Quantity { get; set; } public decimal UnitPrice { get; set; } public decimal Discount { get; set; } public LineItem(Order order, int quantity, Product product) { Order = order; Quantity = quantity; Product = product; UnitPrice = product.UnitPrice; if (quantity>=10) { Discount = 0.05m; } } }
10. 添加一个LineInfo类,作为数据传输对象(DTO),代码如下:
public class LineInfo { public int ProductId { get; set; } public int Quantity { get; set; } }
11. 在Order类中定义一个AddProduct方法。这个方法在内部创建一个新的LineItem对象并将它添加到order的line item集合。如下面的代码所示:
public void AddProduct(Customer customer, Product product, int quantity) { Customer = customer; var line = new LineItem(this, quantity, product); lineItems.Add(line); }
12. 为Customer类添加一个PlaceOrder(下订单)方法。在方法内部,创建一个新的order,并为每个传递过来的LineInfo包含的产品都添加到order。
public void PlaceOrder(LineInfo[] lineInfos, IDictionary<int, Product> products) { var order = new Order(this); foreach (var lineInfo in lineInfos) { var product = products[lineInfo.ProductId]; order.AddProduct(this, product, lineInfo.Quantity); } orders.Add(order); }
至此,我们已经成功的定义了一个简单的订单输入系统。
总结
这一篇文章主要讲解了一些简单的概念,这样能更好的帮助我们设计应用程序的领域模型。我们还一步一步的完成了一个简单的模型。下一篇,讲解定义数据库结构。