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对象是相同的,它们之间没有区别。一个对象可以由另一个对象替换,不会有其他的影响。

其他值对象的例子:

  1. person实体的Name。Name值对象由person对象的surname,given name和middle name组成。
  2. GIS应用中的地理坐标。这个值对象由经纬坐标的值组成。
  3. 比色法应用中的Color。颜色对象由red,green,blue和alpha通道的值组成。
  4. 客户关系管理(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);
}

恭喜,已经成功的创建了第一个值对象类型。它的类图如下面的截图:

image

实战时间–创建一个基实体

首先,为所有类型的实体创建一个基类。

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值对象。

image

定义实体间的关系

实体是model的关键概念,然而,实体并不是孤立存在的,它们与其他实体相关联。

拥有和包含

值对象永远不能单独存在。它们只有和实体一起才会有意义。一个实体可以拥有或者包含0到多个值对象。在前面Customer的例子中,值对象Name被Customer实体拥有或者包含。这种关系是由箭头从实体指向值对象表示的,如下面的截图。

image

注意没有箭头从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; } }

image

note 在现实生活中的库存应用中,你可能会在Category实体中避免使用Products集合,因为一个category可能有成百上千的products。加载给定类别的整个products集合是不明智的,这会导致程序的响应时间欠佳。

1对1

有时,我们会遇到一个实体扮演不同角色的情况。拿Person实体作为例子,一个Person可以是大学里一个学院的Professor,同时还可以是另一个学院的Student。这种关系可以定义为1对1的关系。

同一个domain中的另一个1对1关系会是Professor和HeadOfDepartment中的一个。

下面的截图是前面提到的实体和关系的类图。

image

注意我们可以通过Professor属性从Person对象导航到它关联的Professor对象。我们也可以使用professor实体的Person属性从professor导航到对应的person对象。这在前面的截图中是用两个箭头表示的,一个从Person指向Professor,另一个方向相反。同样的方式,我们可以从Person导航到Student以及返回,从Professor到HeadOfDepartment也是如此。

多对多

最后一个要讨论的关系类型是多对多关系。让我们看具体的例子:order和product之间的关系。客户要订购产品。可是,客户不想只订购一种产品,而是几种不同的产品。所以,一个订单可以包含多种产品。另一方面,多个不同的客户可以下一个包含单个相同产品的订单。因此,一个产品可以属于多个订单。另一个例子是书和作者的关系。一个作者可以写多本不同的书,同时一本书可以有多个作者。这两个关系都是多对多关系的例子,如下面的截图:

image

不过,两者也有细微的差别。我们不管后一个关系,因为它是真正的多对多关系。然而,我们需要多讨论一下product和order之间的关系。多考虑一下下订单的过程,我们会意识到缺少了一些概念。一个客户可能不只订购一种产品的一件,还可能是多件。除此之外,我们可能想知道下订单时产品的单价和应用到指定商品的折扣。突然,一个新的中间实体诞生了。我们通常称这个实体为a line item of an order。我们可以修改一下我们的类图,如下图所示:

image

实战时间–实现订单输入model

该模型的上下文是一个订单输入系统。该模型将作为帮助输入订单到系统的基本解决方案。

1. 我们用到的模型如下面的截图所示:

image

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

至此,我们已经成功的定义了一个简单的订单输入系统。

总结

这一篇文章主要讲解了一些简单的概念,这样能更好的帮助我们设计应用程序的领域模型。我们还一步一步的完成了一个简单的模型。下一篇,讲解定义数据库结构。

posted @ 2011-11-12 11:59  BobTian  阅读(3844)  评论(5编辑  收藏  举报