22

我正在查看POCO 和 DTO 之间的区别(看起来 POCO 是具有行为(方法?)的 dto),并遇到了 Martin Fowler 关于贫血域模型的这篇文章

由于缺乏理解,我想我已经创建了这些贫血的领域模型之一。

在我的一个应用程序中,我在“dto”dll 中定义了我的业务域实体。它们有很多与 getter 和 setter 相关的属性,其他的不多。我的业务逻辑代码(填充、计算)在另一个“bll”dll 中,而我的数据访问代码在一个“dal”dll 中。“最佳实践”我想。

所以通常我会像这样创建一个 dto:

dto.BusinessObject bo = new dto.BusinessObject(...)

并将其传递给 bll 层,如下所示:

bll.BusinessObject.Populate(bo);

反过来,它执行一些逻辑并将其传递给 dal 层,如下所示:

dal.BusinessObject.Populate(bo);

据我了解,要使我的 dto 成为 POCO,我需要使业务逻辑和行为(方法)成为对象的一部分。因此,而不是上面的代码,它更像是:

poco.BusinessObject bo = new poco.BusinessObject(...)
bo.Populate();

IE。我在对象上调用方法,而不是将对象传递给方法。

我的问题是 - 我怎样才能做到这一点,并且仍然保留关注点的“最佳实践”分层(单独的 dll 等......)。在对象上调用方法不就意味着必须在对象中定义方法吗?

请帮助我的困惑。

4

3 回答 3

23

通常,您不想在域对象中引入持久性,因为它不是该业务模型的一部分(飞机不会自行构建,它会将乘客/货物从一个位置运送到另一个位置)。您应该使用存储库模式ORM 框架或其他一些数据访问模式来管理对象状态的持久存储和检索。

当你在做这样的事情时,贫血的领域模型就会发挥作用:

IAirplaneService service = ...;
Airplane plane = ...;
service.FlyAirplaneToAirport(plane, "IAD");

在这种情况下,飞机状态的管理(是否正在飞行、在哪里、起飞时间/机场是什么、到达时间/机场是什么、飞行计划是什么等)被委派给飞机外部的东西。 . AirplaneService 实例。

POCO 的实现方式是这样设计你的界面:

Airplane plane = ...;
plane.FlyToAirport("IAD");

这更容易被发现,因为开发人员知道在哪里可以让飞机飞行(只需告诉飞机去做)。它还允许您确保在内部管理状态。然后,您可以将当前位置等内容设为只读,并确保仅在一处更改。对于贫血的域对象,由于状态是在外部设置的,随着域规模的增加,发现状态更改的位置变得越来越困难。

于 2009-05-26T17:44:14.350 回答
10

我认为澄清这一点的最好方法是根据定义:

DTO:数据传输对象:

它们通常仅用于表示层和服务层之间的数据传输。不多也不少。通常它被实现为带有gets和sets的类。

public class ClientDTO
{
    public long Id {get;set;}
    public string Name {get;set;}
}

BO:业务对象:

业务对象代表业务元素,自然最佳实践表明它们也应该包含业务逻辑。正如 Michael Meadows 所说,将数据访问与这些对象隔离开来也是一种很好的做法。

public class Client
{
    private long _id;
    public long Id 
    { 
        get { return _id; }
        protected set { _id = value; } 
    }
    protected Client() { }
    public Client(string name)
    {
        this.Name = name;    
    }
    private string _name;
    public string Name
    {
        get { return _name; }
        set 
        {   // Notice that there is business logic inside (name existence checking)
            // Persistence is isolated through the IClientDAO interface and a factory
            IClientDAO clientDAO = DAOFactory.Instance.Get<IClientDAO>();
            if (clientDAO.ExistsClientByName(value))
            {
                throw new ApplicationException("Another client with same name exists.");
            }
            _name = value;
        }
    }
    public void CheckIfCanBeRemoved()
    {
        // Check if there are sales associated to client
        if ( DAOFactory.Instance.GetDAO<ISaleDAO>().ExistsSalesFor(this) )
        {
            string msg = "Client can not be removed, there are sales associated to him/her.";
            throw new ApplicationException(msg);
        }
    }
}

服务或应用程序类 这些类代表用户和系统之间的交互,它们将同时使用 ClientDTO 和 Client。

public class ClientRegistration
{
    public void Insert(ClientDTO dto)
    {
        Client client = new Client(dto.Id,dto.Name); /// <-- Business logic inside the constructor
        DAOFactory.Instance.Save(client);        
    }
    public void Modify(ClientDTO dto)
    {
        Client client = DAOFactory.Instance.Get<Client>(dto.Id);
        client.Name = dto.Name;  // <--- Business logic inside the Name property
        DAOFactory.Instance.Save(client);
    }
    public void Remove(ClientDTO dto)
    {
        Client client = DAOFactory.Instance.Get<Client>(dto.Id);
        client.CheckIfCanBeRemoved() // <--- Business logic here
        DAOFactory.Instance.Remove(client);
    }
    public ClientDTO Retrieve(string name)
    {
        Client client = DAOFactory.Instance.Get<IClientDAO>().FindByName(name);
        if (client == null) { throw new ApplicationException("Client not found."); }
        ClientDTO dto = new ClientDTO()
        {
            Id = client.Id,
            Name = client.Name
        }
    }
}
于 2009-12-02T11:24:50.030 回答
6

就我个人而言,我不觉得那些贫血的领域模型那么糟糕。我真的很喜欢让域对象只代表数据而不是行为的想法。我认为这种方法的主要缺点是代码的可发现性。您需要知道可以使用哪些操作。解决这个问题并仍然保持行为代码与模型分离的一种方法是为行为引入接口:

interface ISomeDomainObjectBehaviour
{
    SomeDomainObject Get(int Id);
    void Save(SomeDomainObject data);
    void Delete(int Id);
}

class SomeDomainObjectSqlBehaviour : ISomeDomainObjectBehaviour
{
    SomeDomainObject ISomeDomainObjectBehaviour.Get(int Id)
    {
        // code to get object from database
    }

    void ISomeDomainObjectBehaviour.Save(SomeDomainObject data)
    {
        // code to store object in database
    }

    void ISomeDomainObjectBehaviour.Delete(int Id)
    {
        // code to remove object from database
    }
}
class SomeDomainObject
{
    private ISomeDomainObjectBehaviour _behaviour = null;
    public SomeDomainObject(ISomeDomainObjectBehaviour behaviour)
    {

    }

    public int Id { get; set; }
    public string Name { get; set; }
    public int Size { get; set; }


    public void Save()
    {
        if (_behaviour != null)
        {
            _behaviour.Save(this);
        }
    }

    // add methods for getting, deleting, ...

}

这样,您可以将行为实现与模型分开。使用注入模型的接口实现也使代码相当容易测试,因为您可以轻松地模拟行为。

于 2009-05-22T05:54:39.070 回答