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

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

    总结

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

    作者:BobTian
    出处http://nianming.cnblogs.com/
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
    欢迎访问我的个人博客:程序旅途
  • 相关阅读:
    虚拟机docker开启服务,本地无法进行访问
    make编译提示:make cc Command not found 解决办法
    yum -y install git 无法安装...提示There are no enabled repos.
    linux 安装mysql
    linux 配置环境变量
    HTML5第三天 无序有序列表、相对绝对路径
    JavaScript第一天
    HTML第二天
    mysql流程控制语句
    mysql存储过程和函数
  • 原文地址:https://www.cnblogs.com/nianming/p/2246343.html
Copyright © 2020-2023  润新知