博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
NHibernate初学者指南(3):创建Model
阅读量:5942 次
发布时间:2019-06-19

本文共 11614 字,大约阅读时间需要 38 分钟。

什么是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方法,返回值是三个独立属性组合的哈希值。通过下面的链接:获得如何构造哈希值的详细描述。注意,如果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
where T : Entity
{ }}

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
lhs, Entity
rhs){ return Equals(lhs, rhs);}public static bool operator !=(Entity
lhs, Entity
rhs){ return !Equals(lhs, rhs);}

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

现在,让我们实现一个继承自基实体的真正实体。我们可以集中精力于描述实体的属性和描述实体行为的方法上了。

1. 在Domain文件夹中添加一个新类Customer,让它继承自Entity基类。

public class Customer : Entity
{}

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;public IEnumerable
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
{ public Name Name { get; set; }}

6. 额外给Customer添加一个Address类型的Address属性。同时添加一个只读的集合Orders。

public Address Address { get; set; }private readonly List
orders;public IEnumerable
Orders{ get { return orders; }}

7. 给Order类添加如下属性:Customer(类型Customer),Reference(Employee),OrderDate(DateTime)和OrderTotal(decimal)。同时添加一个只读集合LineItems和构造函数。

public class Order : Entity
{ public Customer Customer { get; set; } public Employee Employee { get; set; } public DateTime OrderDate { get; set; } public decimal OrderTotal { get; set; } private readonly List
lineItems; public IEnumerable
LineItems { get { return lineItems; } } public Order(Customer customer) { lineItems = new List
(); Customer = customer; OrderDate = DateTime.Now; } }

8.  给Product类添加如下属性:Name(string),Description(string),UnitPrice(decimal),ReorderLevel(int)和Discontinued(bool)。

public class Product : Entity
{ 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
{ 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
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);}

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

总结

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

转载于:https://www.cnblogs.com/nianming/archive/2011/11/12/2246343.html

你可能感兴趣的文章
阿里云CentOS7安装Oracle11GR2
查看>>
nginc+memcache
查看>>
从拼多多优惠券事件看到的一些反思
查看>>
mac下完全卸载postgresql的方法
查看>>
20个纯css3写的logo
查看>>
四周第四次课 6.1 压缩打包介绍 6.2 gzip压缩工具 6.3 bzip2压缩工具 6.4 x
查看>>
交换机自动学习vlan
查看>>
三层交换配置与原理
查看>>
Nginx ssl、rewrite配置
查看>>
thinkphp-查询数据-基本查询
查看>>
bootstrap-自适应导航
查看>>
SQL-4查找所有已经分配部门的员工的last_name和first_name(自然连接)
查看>>
查找最近修改的SP
查看>>
linux交换空间
查看>>
加入马帮,马到功成
查看>>
使用cpau.exe让不是管理员的用户也有权限运行哪些需要管理员权限的软件。
查看>>
编译安装mariadb-10.0.10
查看>>
UML类图
查看>>
nginx0.8 + php-5.3.4 + memcached
查看>>
YUM部署高版本LNMP环境
查看>>