领域驱动设计之代码优先-领域层设计-7 (翻译)



3.6.- 实体类的领域逻辑(非贫血领域)
 
  在领域驱动设计中需要把逻辑关联到实体的内部操作中。如果实体类只是用来数据结构,所有的领域逻辑都放在
领域服务中,这会构成一个反模式叫做”贫血领域模型“。见http://www.martinfowler.com/bliki/AnemicDomainModel.html 
  
  该反模式解释了对于面向领域应用,实体模型不应包含数据。这个方法对于使用ADO.NET Datasets的人很典型。
但是如果我们审视实体的业务逻辑,我们会发现大多数领域实体都它内部行为。有时只是一些验证数据的方法,
有时是进行计算的业务方法,很多时候是实体内部重要的业务逻辑。如果我们不为实体类添加行为,我们会不得
把行为放在领域服务中,这会是一种不好的设计抉择因为领域逻辑在会更难维护,业务规则的重用会更困难。简言之
我们要做的是提供实体的”面向对象的情景“,就像Martin Fowler在解释”贫血模型“时提到的。


  因此,我们应该在每个实体类中添加相关的内部业务逻辑。如果我们用代码优先的POCO实体类,好消息是可以
直接在实体类中添加领域逻辑。如果使用STE或T4模板的POCO类,就应该添加领域逻辑到部分类中。这是另一个
为什么代码优先更适合领域驱动设计的理由。
  例如,下面的BankAccount实体类在内部添加了领域逻辑。尤其是,进行的操作是”账户提款“,在操作前进行必要
的检查:
//POCO Domain Entity 
// 
// 
public class BankAccount : Entity,IValidatableObject 
{ 
 
public class BankAccount 
                    :Entity,IValidatableObject 
{         
    public string BankAccountNumber { get; set; } 
    public decimal Balance { get; private set; } 
    public bool Locked { get; private set; } 
    public Guid CustomerId { get; set; }        
    public virtual Customer Customer { get; private set; } 
 
    //Ommitted BankAccountActivities 
    //… 
     
    public void Lock() 
    { 
        if (!Locked) 
            Locked = true; 
    } 
POCO-Entity with Domain logic 
Entity data properties 
 
    public void UnLock() 
    { 
        if (Locked) 
            Locked = false; 
    } 
 
    public void WithdrawMoney(decimal amount,string reason) 
    { 
        if ( amount < 0 )  throw new 
ArgumentException(Messages.exception_BankAccountInvalidWithdrawAmount); 
 
        //WithdrawMoney is a term of our Ubiquitous Language.  
        //Means deducting money to this account 
        if (CanBeWithdrawed(amount)) 
        { 
            checked 
            { 
                this.Balance -= amount; 
 
                //anotate activity 
                this.BankAccActivity.Add(new BankAccountActivity() 
                { 
                    Date = DateTime.UtcNow, 
                    Id = IdentityGenerator.NewSequentialGuid(), 
                    Amount = -amount, 
                    ActivityDescription = reason 
                }); 
            } 
        } 
        else 
            throw new 
InvalidOperationException(Messages.exception_BankAccountCannotWithdraw); 
    } 
 
    public bool CanBeWithdrawed(decimal amount) 
    { 
        return !Locked && (this.Balance >= amount); 
    } 
 
 
} 

很多这些实体行为需要改变属性映射,因为任何属性都不是公共或者其他的属性必须是可读但不是直接更新的。
  比如,我们可以扩展最初的实体来添加一些行为。因为OrderDetail是一个子实体,它不应该是直接访问的。另
一方面,所有对OrderDetail的操作都应该通过聚合根实体进行,例如下面:
//POCO Entity Class (Root Entity Class for Order-Aggregate) 
public class Order : Entity, IValidatableObject 
{ 
     
 
    //.Id property is inherited from the Entity base Class 
    HashSet<OrderLine> _Lines; 
    public Guid CustomerId { get; set; } 
    public DateTime OrderDate { get; set; } 
POCO Root Entity Class (For „Aggregate Order‟) 
Business checks / validations 
Business/Domain logic  for  the 
BankAccount Withdraw process  
Domain Entity logic 
    public DateTime? DeliveryDate { get; set; } 
    public virtual ShippingInfo ShippingInformation { get; set; } 
 
    public void AddOrderLine(OrderLine line) 
    { 
        if (line == null) 
            throw new ArgumentNullException("line"); 
 
        //Fix relation 
        line.OrderId = this.Id; 
 
        this._Lines.Add(line); 
    }   
     
    //... 
    //More code omitted for brevity 
    //.. 
} 
 
 
public class OrderLine : Entity, IValidatableObject 
{ 
    public decimal UnitPrice { get; set; } 
    public int Amount { get; set; } 
    public decimal Discount { get; set; } 
 
    public decimal TotalLine 
    { 
        get 
        { 
            return (UnitPrice * Amount) * (1 - Discount); 
        } 
    } 
 
    public Guid OrderId { get; set; } 
 
} 

顺便说一下,使用STE和POCO T4实体并不适合这样灵活的映射改变,隐藏属性等。使用STE可以在T4模板中完成,
但是很多情况并不灵活。这是另一个POCO-代码优先更适合领域驱动设计的理由。

3.7.- 实现领域实体的基类



  推荐使用实体基类这样可以把实体类通用的功能放入。典型的,比较方法或其它和实体标识符相关的操作。

  下面我们展示了一个使用POCO代码优先的”实体基类“:
// Entity Base Class 
//     
public abstract class Entity 
{ 
    int? _requestedHashCode;                 
    public Guid Id { get; set; } 
         
         
 
    public bool IsTransient() 
    { 
        return this.Id == Guid.Empty; 
    }        
 
         
    public override bool Equals(object obj) 
    { 
        if (obj == null || !(obj is Entity)) 
            return false; 
 
        Entity item = (Entity)obj; 
 
        if (item.IsTransient() || this.IsTransient()) 
            return false; 
        else 
            return item.Id == this.Id; 
    } 
 
    public static bool operator ==(Entity left, Entity right) 
    { 
        return left.Equals(right); 
    } 
 
    public static bool operator !=(Entity left, Entity right) 
    { 
        return !left.Equals(right); 
    } 
         
    ... 
    //Other methods 
    ... 
} 

这样我们可以继承实体这样通用的实体行为可以重用。例如下面对BankAccount实体继承自实体基类:

//POCO Domain Entity implementing our Entity Base-Class 
// 
public class BankAccount : Entity,IValidatableObject 
{ 
    //Attributes 
    //     
    public string BankAccountNumber { get; set; }     
    public decimal Balance { get; set; } 
    public int CustomerId { get; set; } 
    public bool Locked { get; set; } 
    ... 
    // 
 
    //Other specific Domain Entity Logic (Methods) 
    // 
    ... 
    ... 
    ... 
} 

使用基类会非常有用尤其对于标识符的管理,比较实例。


3.8.- EF 4.1”代码优先“领域实体的协定(Conventions)


  EF 4.1”代码优先“利用一个编程模式叫做配置上的协定(Conventions)。如果继续使用领域实体,使用一个
简单的EF DbContext,就可以运行你的代码。不需要创建数据库表或任何事先的映射。

// EF DbContext 
// This code should be part of your Data Persistence Infrastructure Layer 
public class MainBCUnitOfWork : DbContext, IMainBCUnitOfWork 
{ 
    //SIMPLIFIED... 
    public DbSet<Customer> Customers { get; set; } 
    public DbSet<BankAccount> BankAccount { get; set; } 
}

DbContext是EF 4.1新出现的,可以通过”代码优先“使用或者也可以使用”模型/数据库优先“。
 
  这就是我们开始存储和获取数据的全部代码。显然需要做更多的事,我们会在下面的章节看到。
同时,在我们的领域驱动架构里我们不使用简单的DbContext,会抽出“Unit of Work‟接口,使用依赖
注入等等。
  这意味着代码优先会假定你的类按照默认的协定(Conventions)。这种情况,EF可以按照具体的细节
来做它的事。下面的代码可以执行。(这是一个孤立概念的证明,不是任何示例)。


// Using ‘Code First’ 
class Program 
{ 
    static void Main(string[] args) 
    { 
        using (var db = new MainBCUnitOfWork()) 
        { 
            // Add a Customer  
            var customer = new Customer {CustomerId = "ALFKI", Name = "Joe Smith"}; 
            db. Customers.Add(customer); 
            int recordsAffected = db.SaveChanges(); 
 
            Console.WriteLine( 
                "Saved {0} entities to the database, press any key to exit.", 
                recordsAffected); 
 
            Console.ReadKey(); 
        } 
    } 
} 

DbContext的协定(Conventions)会在localhost\SQLEXPRESS上创建数据库。数据库由你派生的context
命名。这和数据访问层有关,所以我们会在下面的章节看到如果改变命名。另外,协定(Conventions)对于
”发现模型“工作。DbContext通过DbSet的属性找到包含在模型中的类。然后使用默认的代码优先协定(Conventions)
来找到主键,外键等等。


  例如,DbContext根据前缀带有"Id"的属性推断实体和表的主键。因此,对于BankAccount实体,按照协定
主键会是BankAccountId。如果想有多个或符合键,会有一些方法来做。
  下面是一些有意思的默认协定:
 
表 13.- EF 4.1主要的 ‘默认协定’  


协定 描述


IdKeyDiscoveryConvention   主键属性的协定


ComplexTypeDiscoveryConvention     如果没有主键,没有基本类型,没有导航属性时配置复杂类


ForeignKeyAssociationMultiplicityConvention  基于外键属性的是否为空区分可选和必须的关系


ManyToManyCascadeDeleteConvention     在多对多关系中假如级联删除
 
NavigationPropertyNameForeignKeyDiscoveryCon 发现名字是独立导航属性和主键的组合时的外键
vention 
 
OneToManyCascadeDeleteConvention     关联的级联删除
 
OneToOneConstraintIntroductionConvention     在一对一关系中配置依赖实体类型的主键作为外键
 
PluralizingEntitySetNameConvention           设置实体集合的名字作为实体类型的复数。
 
PluralizingTableNameConvention     设置实体类型复数的表名
 
PrimaryKeyNameForeignKeyDiscoveryConvention  发现符合主键属性名称的外键属性
 
StoreGeneratedIdentityKeyConvention     配置整形的主键作为标识符
 
PropertyMaxLengthConvention     设置属性类型的最大长度
 
TypeNameForeignKeyDiscoveryConvention     发现名称是主要类型和主要类型主键属性组合的外键属性


  还有很多其他的关于自定义模型的协定。
  对于EF 4.1的完整列表和解释请看下面:
http://msdn.microsoft.com/en-us/library/system.data.entity.modelconfiguration.conventions(v=vs.103).aspx   
 
  在我们不要应用默认协定的情况,我们可以忽略它们,这是DbContext自定义的一部分,因此,我们会在
数据访问基础结构的章节解释。


  然而,如果你的类不按照默认的协定,你可以在类上添加配置来匹配已有的数据库。这可以通过两种方法做到:
”数据注解“或”Fluent API“。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值