POCO、DTO、DLL和贫血领域模型

22

我在查看 POCO和DTO的区别 (似乎POCO是带有行为(方法?)的DTO),并发现Martin Fowler关于无力的域模型的这篇文章

由于理解不足,我认为我创建了一个无力的域模型。

在我的应用程序中,我将业务域实体定义在'dto' dll中。它们有很多具有getter和setter但没有太多其他内容的属性。我的业务逻辑代码(填充、计算)在另一个'bll' dll中,数据访问代码在'dal' dll中。我认为这是“最佳实践”。

因此,通常我会这样创建一个dto:

dto.BusinessObject bo = new dto.BusinessObject(...)
并将其传递给 BLL 层,如下所示:

and pass it to the bll layer like so:

bll.BusinessObject.Populate(bo);

接着执行一些逻辑并将其传递到dal层,如下所示:

dal.BusinessObject.Populate(bo);

根据我的理解,要将我的DTO转换为POCO,我需要将业务逻辑和行为(方法)作为对象的一部分。因此,与上面的代码不同,应该更像:

poco.BusinessObject bo = new poco.BusinessObject(...)
bo.Populate();
例如,我在对象上调用方法而不是将对象传递给方法。
我的问题是 - 如何做到这一点并仍保留“最佳实践”的关注层次(分离的dll等)。 在对象上调用方法是否意味着该方法必须在对象中定义?
请帮助我解开疑惑。
3个回答

23

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

而贫血领域模型则在您执行以下操作时发挥作用:

IAirplaneService service = ...;
Airplane plane = ...;
service.FlyAirplaneToAirport(plane, "IAD");
在这种情况下,飞机状态的管理(无论它是否在飞行中,在哪里,出发时间/机场,到达时间/机场,飞行计划等)被委托给飞机外部的某个实体...即AirplaneService实例。
使用POCO的一种实现方式是按照以下方式设计您的接口:
Airplane plane = ...;
plane.FlyToAirport("IAD");

这种方法更易于发现,因为开发人员知道在哪里让飞机起飞(只需告诉飞机即可)。它还允许您确保状态仅在内部管理。然后,您可以使诸如当前位置之类的事物只读,并确保它仅在一个地方更改。使用贫血的领域对象,由于状态是在外部设置的,随着领域规模的增加,发现状态何时被更改变得越来越困难。


4
你知道有没有这种设计的源代码示例?我觉得这些原则很有道理,但当我开始实现FlyToAirport时,如果需要更新多张表格,特别是如果我不使用存储过程,最终会得到一个与数据库交互频繁的界面。 - Scott McKenzie
1
当涉及到外部交易时,使用服务更有意义。您应该尽可能将其粗粒度化,以便在域模型中嵌入尽可能多的行为。不幸的是,我并没有看到它被充分利用。当然,在我编写任何非遗留生产代码时都会使用它,并且我曾经参与过几个使用这种方法的项目。 - Michael Meadows

10

我认为最好的方法是通过定义来澄清这个问题:

DTO:数据传输对象:

它们只用于数据传输,通常在表示层和服务层之间。没有更多或更少。通常实现为一个具有获取和设置方法的类。

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
        }
    }
}

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, ...

}

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


2
你如何管理行为类似于策略模式:(http://en.wikipedia.org/wiki/Strategy_pattern)。当实际行为可能需要在运行时确定时,它非常好用,但在其他情况下使用可能会导致过度设计。我喜欢这种模式,因为它使行为可重用于类层次结构之外。然而,我必须有意识地避免使用它,除非必要,以避免使解决方案过于复杂。 - Michael Meadows
是的,这就像将行为委托给一个单独的类,但调用代码仍然通过您的域对象访问该行为。(也许您可以将行为类设置为内部以确保这一点) - Rodi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接