什么是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);
}

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

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

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

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

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

总结

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

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

NHibernate初学者指南(3):创建Model相关推荐

  1. NHibernate初学者指南(15):使用LINQ to NHibernate提供程序查询数据

    在前面的<NHibernate初学者指南(8):增删查改>一文中简单的提到了查询一个实体的Get<T>和Load<T>方法以及查询实体列表的Query<T&g ...

  2. Docker初学者指南-如何创建您的第一个Docker应用程序

    您是一名开发人员,并且想要开始使用Docker? 本文是为您准备的. (You are a developer and you want to start with Docker? This arti ...

  3. NHibernate初学者指南(6):映射模型到数据库之方式二

    使用Fluent NHibernate自动映射 使用Fluent NHibernate自动映射,首先要将映射的实体放到一个命名空间中,这使得通知AutoMapper哪些实体和值对象包含在映射中更容易. ...

  4. NHibernate初学者指南(2):一个完整的例子

    准备开发环境 操作系统:Windows Vista, Windows 7, Windows Server 2003 或 Windows Server 2008 IDE:VS 2010 Professi ...

  5. NHibernate初学者指南(10):一级和二级缓存

    一级缓存 为了获得更好的性能,NHibernate智能地缓存数据.NHibernate有不同的缓存机制起作用,最重要的就是一级缓存.每个session对象维持一个一级缓存,session对象创建时缓存 ...

  6. NHibernate初学者指南(1):开篇

    这个系列所有的文章都是根据NHibernate 3 Beginner's Guide一书完成的,主要记录自己的学习经历,方便以后的查阅.鉴于<Entity Framework 4 in Acti ...

  7. 使用 OpenAI API 构建 Next.js 应用程序的初学者指南

    欢迎阅读我们的初学者指南,了解如何使用 OpenAI API 构建 Next.js 应用程序.在本教程中,我们将引导您完成利用 OpenAI API 的功能创建功能强大且智能的 Next.js 应用程 ...

  8. Unity增强现实初学者指南视频教程 A Beginner’s Guide to Augmented Reality with Unity

    Unity增强现实初学者指南视频教程 A Beginner's Guide to Augmented Reality with Unity MP4 |视频:h264,1280×720 (部分1920X ...

  9. 《C语言编程初学者指南》一1.5 使用程序语句

    本节书摘来自异步社区<C语言编程初学者指南>一书中的第1章,第1.5节,作者[美]Keith Davenport(达文波特) , M1ichael Vine(维恩),更多章节内容可以访问云 ...

  10. 《Python编程初学者指南》——1.6 回到Game Over程序

    本节书摘来自异步社区<Python编程初学者指南>一书中的第1章,第1.6节,作者[美]Michael Dawson,王金兰 译,更多章节内容可以访问云栖社区"异步社区" ...

最新文章

  1. 来玩Play框架07 静态文件
  2. 架构师的能力模型(图)
  3. linux xia 安装程序,linux更新或安装libzip
  4. Linux SVN一次增加多个文件并批量上传
  5. 机器学习中的相似性度量(转载)
  6. Python 数据可视化之matpotlib画图
  7. C语言实现成语接龙完整版
  8. 数据库设计2-Visio2016画E-R图
  9. 计算机管理 网络连接服务,WZC服务未开启无法连接网络 解决方法及步骤
  10. 2020年好用的BI应用排行榜
  11. 人生中重要抉择:读研还是工作?
  12. 微型6轴惯性导航姿态传感器(IMU)LPMS-BE2 OEM
  13. 一个数据录入/查询的应用程序在登录时错误信息解决过程
  14. 力扣(350.121)补9.3
  15. [译] 现代浏览器内部揭秘(第一部分)
  16. 《Redis设计与实现》笔记
  17. 《Windows操作系统原理》好书推荐
  18. 史上最简单的mybatis-plus快速入门
  19. 查看.tar.gz文件内容(不需要解压)
  20. linux系统基础知识总结

热门文章

  1. java swing 雪花_求用JAVA制作的飘雪花的效果
  2. Linux电源管理-wakeup events framework
  3. Web浏览器已经限制此文件显示
  4. linux内核奇遇记之md源代码解读之十raid5数据流之同步数据流程
  5. Linux CFS调度系统----周期性调度器
  6. 追加文件内容的三种方法
  7. GUI实现学生点名系统
  8. 3810.最长连续休息时间-AcWing题库
  9. 【数据结构】树状数组效果讲解
  10. 【排列组合】只上代码不解释