简体   繁体   中英

DDD (Domain Driven Design) Application Layer

I have been trying to build an application based on DDD but I have some questions.

Some layers that I have: - Presentation Layer - MVC - Application Layer - Domain Layer ...

First of all, I would like to know if I can do this in the ApplicationLayer (get family info > get default message info > send an email > updated the database):

public ApproveFamilyOutput ApproveFamily(ApproveFamilyInput input)
        {
            Family family = _familyRepository.GetFamily(input.Username);
            family.Approve();

            DefaultMessage defaultMessage = _defaultMessageRepository.GetDefaultMessage(MessageTypes.FamilyApproved);

            _email.Send(family.GetEmail(), defaultMessage.Subject, defaultMessage.Message);

            _familyRepository.Update(family);
            bool isSaved = _familyRepository.Save();

            return new ApproveFamilyOutput()
            {
                Errors = Helper.GetErrorIfNotSaved(isSaved)
            };
        }

Am I thinking well? Is the Application layer responsible to do that job?

The second question is: I need to send some data to the presentation layer according to the privileges that the user has. These privileges are defined in the database. Example: - The object Family have the Name, LastName, PhoneNumber, Email properties and the user can show/hide each of the values. How can I handle with this?

Can I do something like in the Application Layer:

public GetFamilyOutput GetFamily(GetFamilyInput input)
        {
            Family family = _familyRepository.GetFamily(input.Username);

            FamilyConfiguration familyConfiguration = _familyConfigurationRepository.GetConfigurations(family.Id);

            //ProcessConfiguration will set to null the properties that I cannot show
            family.ProcessConfiguration(familyConfiguration);

            return new GetFamilyOutput
            {
                //Map Family Object to the GetFamilyOutput
            };
        }

Note: The Family, DefaultMessage and FamilyConfiguration are domain objects created inside the Domain Layer.

What is your opinion?

Thanks :)

Edited: Note: I liked all the answers below and I used a little of all :) (I can´t mark all answers as acceptable)

What your application service is doing in #1 is perfectly valid: it coordinates the workflow with very little to no business logic knowledge.

There are certainly few improvements that could be done however, for instance:

  1. I do not see any transactions? The email should only be sent upon a successfully comitted transaction.

  2. Sending the email could be seen as a side-effect of the family's approval. I suppose the business experts could have stated: " when a family is approved then notify interested parties by email" . Therefore, it could be wise to publish a FamilyApproved domain event and move the email sending logic in an event handler.

    Note that you want the handler to be called asynchronously only after the domain event was persisted to disk and you want to persist the event in the same transaction as the aggregate.

  3. You could probably further abstract the mailing process into something like emailService.send(MessageTypes.FamilyApproved, family.getEmail()) . The application service wouldn't have to know about default messages.

  4. Repositories are usually exclusive to aggregate roots (AR), if DefaultMessage is not an AR then I'd consider naming the DefaultMessageRepository service differently.

As for #2, although authorization checks could be done in the domain, it is much more common to relieve the domain from such task and enforce permissions in the application layer. You could even have a dedicated Identity & Access supporting Bounded Context (BC).

"//ProcessConfiguration will set to null the properties that I cannot show"

That solution wouldn't be so great (just like implementing the IFamilyProperty solution) because your domain model becomes polluted by technical authorization concerns. If you are looking to apply DDD then the model should be as faithful as possible to the Ubiquitous Language (UL) and I doubt IFamilyProperty is something your domain experts would mention or even understand. Allowing properties to become null would probably violate some invariants as well.

Another problem with such solution is that the domain model is rarely adapted for queries (it's built for commands), so it's often preferrable to bypass it entirely and favor going directly to the DB. Implementing authorizations in the domain would prevent you from doing that easily.

For at least these reasons, I think it is preferrable to implement authorization checks outside the domain. There your are free to use whatever implementation you want and suits your needs. For instance, I believe that stripping off values from a DTO could be legitimate.

I also was doubting if it's ok to place some logic to Application Service or not. But things got much cleaner once I read Vladimir Khorikov's Domain services vs Application services article . It states that

domain services hold domain logic whereas application services don't.

and illustrates the idea by great examples. So in your cases I think it's totally fine to place these scenarios to Application Service as it doesn't contain domain logic.

As for #1:

Theoretically, the application layer can do what you have described. However, I personally prefer to separate concerns further: there should be a persistence layer. In your case, a developer needs to know to:

  1. Get the family from the repository.
  2. Call the method to approve the family object.
  3. Update the family back in the repository.
  4. Persist the repository.
  5. Handle any possible errors if there were persistence errors.

I would argue that 2-3-4 should be moved to a persistence layer, to make the code look like:

Family family = _familyRepository.GetFamily(input.Username);
family.Approve().Notify(_email);

This approach gives one more flexibility in how to handle errors and some business logic improvements. For example, you would not be sending an e-mail if you encounter persistence errors.

Of course, you'd need to have some additional types and extension methods implemented (for "Notify()" as an example).

Finally, I'd argue that e-mail service should be implemented using a repository pattern too (so you have two repositories) and have a persistence-level implementation. My point of view: anything persisted outside of the application requires repository & persistence implementation; e-mails are persisted outside of the application in user's mailbox.

As for #2:

I would strongly recommend against nullable properties and clearing them out. It gets really confusing really fast, very hard to unit-test and has a lot of "hidden" caveats. Instead, implement classes for your properties. For example:

public class UserPriviledge { //... your db-defined privileges  }

public interface IFamilyProperty<T>
{
    public string PropertyName { get; }
    public T PropertyValue { get; }
    public List<UserPriviledge> ReadPriviledges { get; }
    public bool IsReadOnly { get; }
}

public class FamilyName : IFamilyProperty<string>
{
    public static string PropertyName => "Name";
    public string PropertyValue { get; }
    public List<UserPriviledge> ReadPriviledges { get; }
    public bool IsReadOnly { get; private set; }

    public FamilyName(string familyName) {
        this.PropertyValue = familyName;
        this.ReadPriviledges.Add(someUserPrivilege);
        this.IsReadOnly = false;
    }

    public void MakeReadOnly() {
        this.IsReadOnly = true;
    }
}

public class Family
{
     public int Id { get; }
     public List<IFamilyProperty> LimitedProperties { get; }
}

With this kind of implementation you can have the same kind of method that removes the values instead of obfuscating the value or applies even more complicated logic:

public void ApplyFamilyPermissions(Family family, UserEntity user)
{
    foreach (var property in family.LimitedProperties) {
        if (property.ReadPriviledges.Intersect(user.Priviledges).Any() == false) {
             family.LimitedProperties.Remove(property);
        } else if (property.IsReadOnly == false && HasPropertyWriteAccess(property, user) == false) {
             property.MakeReadOnly();
        }
    }
}

Note: the code was not verified and I'm pretty sure has some syntax mistakes, but I believe it communicates the idea clearly.

Ad 1
I usually move that logic into domain layer - services .
So then the application layer just calls:

public ApproveFamilyOutput ApproveFamily(ApproveFamilyInput input)
{
    var approveService = diContainer.Get<ApproveService>(); // Or correctly injected by constructor
    var result = approveService.ApproveFamily(input);

    // Convert to ouput
}

And domain service (AppproveService class) looks like:

public ApproveResult ApproveFamily(ApproveFamilyInput input)
{
     var family = _familyRepository.GetFamily(input.Username);
     family.Approve();

     _familyRepository.Update(family);
     bool isSaved = _familyRepository.Save();

     if(isSaved)
         _eventPublisher.Publish(family.raisedEvents);
     // return result
}

To make it work (and following hexagonal/onion architecture), domain layer defines all interfaces for its dependencies (IFamilyRepository, IDefaultMessageRepository, etc) and application layer injects specific implementation into domain layer.

To make it clear:
1. domain layer is independent
2. domain objects are pure - consist of entities, value objects
3. domain objects don't call repositories, it is up to the domain service
4. domain objects raise events
5. unrelated logic is handled by events (event handlers) - for example sending emails, it follows open-closed principle

class FamilyApprovedHandler : IHandle<FamilyApprovedEvent>
{
    private readonly IDefaultMessageRepository _defaultMessageRepository;
    private readonly IEmailSender _emailSender;
    private readonly IEmailProvider _emailProvider;

    // ctor

    public Task Handle(FamilyApprovedEvent event)
    {
        var defaultMessage = _defaultMessageRepository.GetDefaultMessage(MessageTypes.FamilyApproved);

        var email = _emailProvider.Generate(event.Family, defaultMessage.Subject, defaultMessage.Message);

        _emailSender.Send(email);
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM