POCO's, DTO's, DLL's and Anaemic Domai

2019-03-08 10:35发布

问题:

I was looking at the differences between POCO and DTO (It appears that POCO's are dto's with behaviour (methods?))and came across this article by Martin Fowler on the anaemic domain model.

Through lack of understanding, I think I have created one of these anaemic domain models.

In one of my applications I have my business domain entities defined in a 'dto' dll. They have a lot of properties with getter's and setter's and not much else. My business logic code (populate, calculate) is in another 'bll' dll, and my data access code is in a 'dal' dll. 'Best practice' I thought.

So typically I create a dto like so:

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

and pass it to the bll layer like so:

bll.BusinessObject.Populate(bo);

which in turn, performs some logic and passes it to the dal layer like so:

dal.BusinessObject.Populate(bo);

From my understanding, to make my dto's into POCO's I need to make the business logic and behaviour (methods) part of the object. So instead of the code above it is more like:

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

ie. I am calling the method on the object rather than passing the object to the method.

My question is - how can I do this and still retain the 'best practice' layering of concerns (separate dll's etc...). Doesn't calling the method on the object mean that the method must be defined in the object?

Please help my confusion.

回答1:

Typically, you don't want to introduce persistence into your domain objects, since it is not part of that business model (an airplane does not construct itself, it flies passengers/cargo from one location to another). You should use the repository pattern, an ORM framework, or some other data access pattern to manage the persistent storage and retreival of an object's state.

Where the anemic domain model comes in to play is when you're doing things like this:

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

In this case, the management of the airplane's state (whether it's flying, where it's at, what's the departure time/airport, what's the arrival time/airport, what's the flight plan, etc) is delegated to something external to the plane... the AirplaneService instance.

A POCO way of implementing this would be to design your interface this way:

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

This is more discoverable, since developers know where to look to make an airplane fly (just tell the airplane to do it). It also allows you to ensure that state is only managed internally. You can then make things like current location read-only, and ensure that it's only changed in one place. With an anemic domain object, since state is set externally, discovering where state is changed becomes increasingly difficult as the scale of your domain increases.



回答2:

I think the best way to clarify this is by definition:

DTO: Data Transfer Objects:

They only serve for data transportation typically between presentation layer and service layer. Nothing less or more. Generally it is implemented as class with gets and sets.

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

BO: Business Objects:

Business objects represents the business elements and naturally the best practice says they should contain business logic also. As said by Michael Meadows, it is also good practice to isolate data access from this objects.

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

Service or Application Class These classes represent the interaction between User and the System and they will make use of both ClientDTO and 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
        }
    }
}


回答3:

Personally I don't find those Anaemic Domain Models so bad; I really like the idea of having domain objects that represent only data, not behaviour. I think the major downside with this approach is discoverability of the code; you need to know which actions that are available to use them. One way to get around that and still keep the behaviour code decoupled from the model is to introduce interfaces for the behaviour:

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

}

That way you can keep the behaviour implementation separated from the model. The use of interface implementations that are injected into the model also makes the code rather easy to test, since you can easily mock the behaviour.