为什么要将域实体与表示层隔离?

领域驱动设计中有一部分似乎没有太多细节,那就是你应该如何以及为什么把你的域模型从你的界面中分离出来。我试图说服我的同事这是一个很好的做法,但我似乎没有取得多大进展..。

他们在表示层和接口层中随心所欲地使用域实体。当我向他们争辩说他们应该使用显示模型或 DTO 来将域层与接口层隔离开来时,他们反驳说他们没有看到这样做的业务价值,因为现在你要维护一个 UI 对象以及原始的域对象。

因此,我正在寻找一些具体的理由来支持这一观点。具体来说:

  1. 为什么我们不应该在表示层中使用域对象?
    (如果答案是显而易见的“脱钩”,那么请解释一下为什么在这种情况下这很重要)
  2. 我们是否应该使用其他对象或构造来将域对象与接口隔离?
13993 次浏览

Quite simply, the reason is one of implementation and drift. Yes, your presentation layer needs to know about your business objects to be able to represent them properly. Yes, initially it looks like there is a lot of overlap between the implementation of the two types of objects. The problem is, as time goes on, things get added on both sides. Presentation changes, and the needs of the presentation layer evolve to include things that are completely independent of your business layer (color, for example). Meanwhile, your domain objects change over time, and if you don't have appropriate decoupling from your interface, you run the risk of screwing up your interface layer by making seemingly benign changes to your business objects.

Personally, I believe the best way to approach things is through the strictly enforced interface paradigm; that is, your business object layer exposes an interface that is the only way that it can be communicated with; no implementation details (i.e. domain objects) about the interface are exposed. Yes, this means that you have to implement your domain objects in two locations; your interface layer and in your BO layer. But that reimplementation, while it may initially seem like extra work, helps enforce the decoupling that will save TONS of work at some point in the future.

You do it for the same reason you keep SQL out of your ASP/JSP pages.

If you keep only one domain object, for use in the presentation AND domain layer, then that one object soon gets monolithic. It starts to include UI validation code, UI navigation code, and UI generation code. Then, you soon add all of the business layer methods on top of that. Now your business layer and UI are all mixed up, and all of them are messing around at the domain entity layer.

You want to reuse that nifty UI widget in another app? Well, You have to create a database with this name, these two schemas, and these 18 tables. You must also configure Hibernate and Spring ( or your frameworks of choice ) to do the business validation. Oh, you must also include these 85 other non-related classes because they are referenced in the business layer, which just happens to be in the same file.

We are using the same model in the server and on the ui. And it's a pain. We have to refactor it some day.

The problems are mainly because the domain model needs to be cut into smaller pieces to be able to serialize it without having the whole database referenced. This makes it harder to use on the server. Important links are missing. Some types are also not serializable and can't be sent to the client. For instance 'Type' or any generic class. They need to be non-generic and Type needs to be transferred as string. This generates extra properties for serialization, they are redundant and confusing.

Another problem is that the entities on the UI don't really fit. We are using databinding and many entities have lots of redundant properties only for ui purposes. Additionally there are many 'BrowsableAttribute' and others in the entity model. This is really bad.

At the end, I think it is just a matter of which way is easier. There might by projects where it just works fine and where is no need to write another DTO model.

It's about dependencies for the most part. The core functional structure of the organization has its own functional requirements, and the UI should enable people to modify and view the core; but the core itself should not be required to accommodate the UI. (If it needs to happen, it's usually an indication the core is not property designed.)

My accounting system has a structure and content (and data) that are supposed to model the operation of my company. That structure is real and exists regardless of what accounting software I use. (Inevitably a given software package contains structure and content for its own sake, but part of the challenge is minimizing this overhead.)

Basically a person has a job to do. The DDD should match the flow and content of the job. DDD is about making explicit all the jobs that need being done ad completely and independently as possible. Then the UI hopefully facilitates getting the job done as transparently as possible, as productively as possible.

Interfaces are about the inputs and views provided for the properly modeled and invariant functional core.

Perhaps you are not conceptualizing the UI layer in broad enough terms. Think in terms of multiple forms of response (web pages, voice response, printed letters etc) and in terms of multiple languages (English, French etc.).

Now suppose that the speech engine for the telephone call-in system runs on a completely different type of computer (Mac for example) from the computer that runs the website (Windows perhaps).

Of course it is easy to fall into the trap "Well in my company we only care about English, run our website on LAMP (Linux, Apache, MySQL and PHP) and everyone uses the same version of Firefox". But what about in 5 or 10 years?

The only sensible reason for adding additional mapping between generalized and domain specific semantics is that you have (access to) an existing body of code (and tools) that are based on a generalized (but mappable) semantics distinct from your domain semantics.

Domain driven designs work best when used in conjunction with an orthogonal set of functional domain frameworks (such as ORM, GUI, Workflow, etc.). Always remember that it is only in the outer layer adjacencies that domain semantics need to be exposed. Typically this is the front end (GUI) and persistent back-end (RDBM,ORM). Any effectively designed intervening layers can and should be domain invariant.

Dammit, I swear this said persistence.

Anyway, it's one more instance of the same thing: Parnas's law says a module should keep a secret, and the secret is a requirement that can change. (Bob Martin has a rule that's another version of this.) In a system like this, the presentation can change independently of the domain. Such as, for example, a company that maintains prices in Euros and uses French in the company offices, but wants to present prices in dollars with text in Mandarin. The domain is the same; the presentation can change. So, to minimize the brittleness of the system — that is, the number of things that must be changed to implement a change in requirements — you separate the concerns.

I have struggled with this myself. There are cases where a DTO makes sense to use in presentaton. Let's say I want to show a drop down of Companies in my system and I need their id to bind the value to.

Well instead of loading a CompanyObject which might have references to subscriptions or who knows what else, I could send back a DTO with the name and id. This is a good use IMHO.

Now take another example. I have an object which represents an Estimate, this estimate might be made up labor, equipment etc, it might have lots of calculations that are defined by the user which take all these items and sum them up (Each estimate could be different with different types of calculations). Why should I have to model this object twice? Why can't I simply have my UI enumerate over the calculations and display them?

I generally do not use DTO's to isolate my domain layer from my UI. I do use them to isolate my domain layer from a boundary that is outside of my control. The idea that someone would put navigation information in their business object is ridiculous, don't contaminate your business object.

The idea that someone would put validation in their business object? Well I say that this is a good thing. Your UI should not have sole responsability to validate your business objects. Your business layer MUST do its own validation.

Why would you put UI generation code in a busienss object? In my case I have seperate objects which generates the UI code seperatley from the UI. I have sperate objects which render my business objects into Xml, the idea that you have to seperate your layers to prevent this type of contamination is so alien to me because why would you even put HTML generation code in a business object...

Edit As I think a little bit more, there are cases where UI information might belong in the domain layer. And this might cloud what you call a domain layer but I worked on a multi-tenant application, which had very different behavior both UI look and feel and functional workflow. Depending on various factors. In this case we had a domain model that represented the tenants and their configuration. Their configuration happened to include UI information (Label's for generic fields for example).

If I had to design my objects to make them persistable, should I also have to duplicate the objects? Keep in mind if you want to add a new field now you have two places to add it. Perhaps this raises another question if your using DDD, are all persisted entities domain objects? I know in my example they were.

See also the section "Data propagation between layers" in the following, which I think presents compelling arguments:

http://galaxy.andromda.org/docs/andromda-documentation/andromda-getting-started-java/java/index.html

I disagree.

I think the best way to go is to start with domain objects in your presentation layer UNTIL IT MAKES SENSE TO DO OTHERWISE.

Contrary to popular belief, "Domain Objects" and "Value Objects" can happily co-exist in the presentation layer. And this is the best way to do it - you get the benefit of both worlds, reduced duplication (and boilerplate code) with the domain objects; and the tailoring and conceptual simplification of using value objects across requests.

Your presentation may reference your domain layer, but there should be no binding directly from your ui to your domain objects. Domain objects are not intended for UI usage since they are often, if properly designed, based around behaviors and not data representations. There should be a mapping layer between the UI and the Domain. MVVM, or MVP, is a good pattern for this. If you try to directly bind your UI to the Domain, you will probalby create a lot of headache for yourself. They have two different purposes.

With the help of tool like 'Value Injecter' and the concept of 'Mappers' in the presentation layer while working with views, it's much more easy to understand each piece of code. If you have a little bit of code, you will not see the advantages immediately but when your project will growing more and more, you will be very happy while working with the views to don't have to enter into the logic of the services, repositories to understand the view model. View Model is another guard in the vast world of anti-corruption layer and worth its weight in gold in a long term project.

The only reason why I see no advantage of using view model is if your project is small and simple enough to have views binded directly to each property of your model. But if in the futur, the requirement change and some controls in the views will not be binded to the model and you don't have a view model concept, you will start adding patches in many places and you will begin having a legacy code that you will not appreciate. Sure, you can do some refactoring to transform your view-model in view-viewmodel and follow the YAGNI principle while not adding code if you don't need it but for myself, it's much more a best practice that I must follow to add a presentation layer exposing only view-model objects.

Here's a real example as to why I find it good practice to separate domain entities from the view.

A few months ago I created a simple UI to show the values of Nitrogen, Phosphorus and Potassium in a soil sample through a series of 3 gauges. Each gauge had a red, green and red section, i.e. you could either have too little or too much of each component, but there was a safe green level in the middle.

Without much thought, I modelled my business logic to supply data for these 3 chemical components and a separate data sheet, containing data about the accepted levels in each of the 3 cases (including which measurement unit was being used, i.e. moles or percentage). I then modelled my UI to use a very different model, this model was concerned about gauge labels, values, boundary values and colours.

This meant when I later had to show 12 components, I just mapped the extra data into 12 new gauge view models and they appeared on the screen. It also meant I could reuse the gauge control easily and have them display other data sets.

If I'd had coupled these gauges directly into my domain entities, I would not have any of the above flexibility and any future modifications would be a headache. I've come across very similar issues when modelling calendars in the UI. If there is a requirement for a calendar appointment to turn red when there are 10+ attendees, then the business logic to handle this should remain in the business layer and all the calendar in the UI needs to know, is that it has been instructed to turn red, it should not need to know why.

Answer depends on scale of your application.


Simple CRUD (Create, Read, Update, Delete) application

For basic crud applications you do not have any functionality. Adding DTO on top of entities woudl be a waste of time. It would increase complexity without increasing scalability.

enter image description here


Moderately complicated Non-CRUD application

In this size of application you will have few entities which have true lifeclycle and some business logic associated with them.

Adding DTOs on this case is a good idea for few reasons:

  • Presentation layer can see only subset of fields which entity has. You encapsulate Entities
  • No coupling between backend and frontent
  • If you have business methods inside entities, but not in DTO then adding DTOs means that outside code can't ruin state of your entity.

enter image description here


Complicated Enterprise Application

Single entity might need multiple ways of presentation. Each of them will need different set of fields. In this case you encounter the same issues as in previous example plus need to control amount of fields visible for each client. Having separate DTO for each client will help you to chose what should be visible.

enter image description here

I'm trying to convince my colleagues that this is a good practice

A good practice for good practices sake is not always business efficient. If justified, I'm more than happy to argue against using CQRS, Event Sourcing, DDD, etc. For example, on smaller projects that have one or two use-cases.

While the other answers are excellent, the concerns that convinced me that decoupling is important hasn't been mentioned: Your repository (ORM) may populate data unexpectedly.

For example, consider these two actions accessing an Entity Framework DbContext: (I've not tested this code, but it should get the point across)


public JsonResult GetUser(string id) {
return new JsonResult(_ctx.Users.FindOne(u => u.id == id));
}


public JsonResult AddUser(string id) {
var currentUser = _ctx.Users.Include(u => u.Roles).FindOne(currentUserId); // <--
if (!currentUser.Roles.Contains("Administrator")) throw new Exception("User not authorized to add new users");


_ctx.Users.Add(new User(id));


return new JsonResult(_ctx.Users.FindOne(u => u.id == id));
}

While both actions return a JSON object of a User, due to Entity Framework's Object Caching, the second action will include the Roles property—without explicitly asking for it on the final line.

If your application or persistence/infrastructure layer conditionally Includes properties, then your Domain object properties will be conditionally instantiated and you may get unexpected results.

Due to this, and the great reasons provided by other answers, I'm convinced to no longer expose my domain objects to my presentation layer.

Finally, I'm not against using Anonymous objects in place of Dto's—especially when type safety is lost with JsonResult.

For example:

public JsonResult GetUser(string id) {
var user = _ctx.Users.FindOne(u => u.id == id);
return new JsonResult(new {
id = user.id,
name = user.name,
})
}


public JsonResult AddUser(string id) {
var currentUser = _ctx.Users.Include(u => u.Roles).FindOne(currentUserId); // <--
if (!currentUser.Roles.Contains("Administrator")) throw new Exception("User not authorized to add new users");
  

_ctx.Users.Add(new User(id));


var user = _ctx.Users.FineOne(u => u.id == id);


return new JsonResult(new {
id = user.id,
name = user.name,
})
}