سؤال

After reading some of the Q/As here on Stackoverflow, I am still confused about the correct implementation of DTOs in my web application. My current implementation is a (Java EE based) multi-tier architecture (with persistence, service and presentation layer) but with a "common" package used by all layers, containing (amongst others) domain objects. In this case the layers can not really be considered as independent.

In a re-design I am planning to remove the "common" package step by step.

Questions

During re-design I encounter various challenges/questions:

  1. Assume the persistence layer would use a class myproject.persistence.domain.UserEntity (a JPA based entity) to store and load data to/from the database. To show data in the view I would provide another class myproject.service.domain.User. Where do I convert them? Would the service for the users be responsible to convert between the two classes? Would this really help to improve the coupling?
  2. How should the User class look like? Should it contain only getters to be immutable? Wouldn't it be cumbersome for the views to edit existing users (create a new User, use the getters of the existing User object etc.)?
  3. Should I use the same DTO-classes (User) to send a request to the service to modify an existing user/create a new user or should I implement other classes?
  4. Wouldn't the presentation layer be very dependent on the service layer by using all the DTOs in myproject.service.domain?
  5. How to handle my own exceptions? My current approach rethrows most "severe" exceptions until they are handled by the presentation layer (usually they are logged and the user is informed that something went wrong). On the one hand I have the problem that I have again a shared package. On the other hand I am still not sure that this can be considered "best practice". Any ideas?
هل كانت مفيدة؟

المحلول

Having some packages among different layers is not uncommon, however it is usually done only for cross-cutting concerns such as logging. Your model should not be shared by different layers, or changes to the model would require changes in all those layers. Typically, your model is a lower layer, close to data layer (over, under, or intertwined, depending on the approach).

Data Transfer Objects, as their name imply, are simple classes used to transfer data. As such, they are usually used to communicate between layers, specially when you have a SOA architecture which communicates through messages and not objects. DTOs should be immutable since they merely exist for the purpose of transferring information, not altering it.

Your domain objects are one thing, your DTOs are a different thing, and the objects you need in your presentation layer are yet another thing. However, in small projects it may not be worth the effort of implementing all those different sets and converting between them. That just depends on your requirements.

You are designing a web application but it may help your design to ask yourself, "could I switch my web application by a desktop application? Is my service layer really unaware of my presentation logic?". Thinking in these terms will guide you towards a better architecture.

Answers to your questions

  1. Assume the persistence layer would use a class myproject.persistence.domain.UserEntity (a JPA based entity) to store and load data to/from the database. To show data in the view I would provide another class myproject.service.domain.User. Where do I convert them? Would the service for the users be responsible to convert between the two classes? Would this really help to improve the coupling?

The service layer knows its classes (DTOs) and the layer below it (let's say persistence). So yes, the service is responsible for translating between persistence and itself.

  1. How should the User class look like? Should it contain only getters to be immutable? Wouldn't it be cumbersome for the views to edit existing users (create a new User, use the getters of the existing User object etc.)?

The idea behind DTOs is that you only use them for transfer, so operations like creating a new user are not required. For that you need different objects.

  1. Should I use the same DTO-classes (User) to send a request to the service to modify an existing user/create a new user or should I implement other classes?

The service methods might express the operation, the DTOs being its parameters containing just the data. Another option is using commands which represent the operation and also contain the DTOs. This is popular in SOA architectures where your service may be a mere command processor for instance having one single Execute operation taking a ICommand interface as parameter (as opposed to having one operation per command).

  1. Wouldn't the presentation layer be very dependent on the service layer by using all the DTOs in myproject.service.domain?

Yes, the layer over the service layer will be dependent on it. That is the idea. The upside is that only that layer is dependent on it, no upper or lower layers so changes only affect that layer (unlike what happens if you use your domain classes from every layer).

  1. How to handle my own exceptions? My current approach rethrows most "severe" exceptions until they are handled by the presentation layer (usually they are logged and the user is informed that something went wrong). On the one hand I have the problem that I have again a shared package. On the other hand I am still not sure that this can be considered "best practice". Any ideas?

Each layer can have its own exceptions. They flow from one layer to another encapsulated into the next kind of exception. Sometimes, they will be handled by one layer which will do something (logging, for instance) and maybe then throw a different exception that an upper layer must handle. Other times, they might be handled and the problem might be solved. Think for instance of a problem connecting to the database. It would throw an exception. You could handle it and decide to retry after a second and maybe then there is success, thus the exception would not flow upwards. Should the retry also fail, the exception would be re-thrown and it may flow all the way up to the presentation layer where you gracefully notify the user and ask him to retry layer.

نصائح أخرى

Loose coupling is indeed the recommended way to go, which means you will end up with huge, boring to write, painful to maintain converters in your business logic. Yes, they belong in the business logic: the layer between the DAOs and the views. So the business layer will end up depending on both the DAO DTOs and the view DTOs. And will be full of Converter classes, diluting your view of the actual business logic...

If you can get away with having immutable view DTOs, that's great. A library you use for serializing them might require them to have setters though. Or you might find them easier to build if they have setters.

I have gotten away just fine with using the same DTO classes for both the views and the DAOs. It is bad, but honestly, I did not have the feeling that the system was more decoupled otherwise, since business logic, the most essential part, has to depend on everything anyway. This tight coupling provided for great conciseness, and made it easier to sync the view and DAO layers. I could still have things specific just to one of the layers and not seen in the other by using composition.

Finally, regarding exceptions. It is a responsibility of the outermost layer, the view layer (the Controllers if you are using Spring) to catch errors propagated from the inner layers be it using exceptions, be it using special DTO fields. Then this outermost layer needs to decide if to inform the client of the error, and how. The fact is that down to the innermost layer, you need to distinguish between the different types of errors that the outermost layer will need to handle. For example if something happens in the DAO layer, and the view layer needs to know if to return 400 or 500, the DAO layer will need to provide the view layer with the information needed to decide which one to use, and this information will need to pass through all intermediary levels, who should be able to add their own errors and error types. Propagating an IOException or SQLException to the outermost layer is not enough, the inner layer needs to also tell the outer layer if this is an expected error or not. Sad but true.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top