0

I am trying to create an example project Web API to see how "clean" I can remake the Delphi(Pascal) API we are developing on my job.

I have created a solution which as of now contains 3 different projects.

  • WebApi (the main interface to the application logic)
  • ObjectLibrary (Models)
  • DataAccess (Repository style data access layer)

If I want to keep my business logic as separate as possible, where should I put following logic?

  • I have a person which can exist with or without an employment.
  • A person can have 0..n Employment
  • When a person is employed it can affect other business objects, such as his OvertimeAccount, VacationAccout etc. which would need to be created and maintained throughout his employment.

I could use a PersonDataController of some sort, but this would leave me with a DataController which would need to be tightly coupled to objects Person, Employment, VacationAccount - furthermore this would mean that my PersonRepository would also be dependant on my EmploymentRepository and possibly others.

Another approach (and possibly the most straightforward) is to keep business logic in the DataObject Person so I could call Person.Hire(); Which makes the most sense to me, but the issue remains that my Hire function needs to be dependant on the employment object and that my PersonRepository would be dependant on my EmploymentRepository.

Question

Where would I put the business function Hire(Person) in a way that avoids tightly coupling my data objects and repositories?

0

Your question is fairly devoid of existing architecture and implementations, but I assume that because this is Delphi, it's going to be a fairly classic codebase with little architectural frills (such as DDD etc).

If that is wrong, the answer might not apply, but then you should really just be following whatever architecture you already have in place. Consistency is key to a maintainable codebase.


The problem with repositories

DataAccess (Repository style data access layer)

Repositories have received a lot of flak the last few years because they don't play nice with relational data, and often end up being a lump categorization of methods that don't have anything to do with each other other than them both focusing on a particular entity type.

That being said, when you forget relational data and databases for a moment, I really like repositories. They act as neat little SPOCs for each entity type.

The real issue with repositories only emerges when you want to perform multi-entity-type operations, e.g. fetching a Foo with all its related Bar entities. Classic repositories would have you fetch the foos and the bars separately (each from their own repo) but that massively flies in the face of relational database performance.

If you then create a single query to fetch both the foos and the bars, where you put it? The foo repository? The bar repository? You might think this example is still easy to decide, but in reality this will become a difficult consideration.

And when you think about it, this is not a technical problem. You can write the repository method just fine. The problem is centered on developer expectations of where the method should be placed, even though its place doesn't really affect how it is implemented.

Your question effectively focuses on what developers think is best; not on what is technically superior.


Shifting developer expectations

The best solution here is to shift how developers categorize the data handling methods, to remove the apparent clash of trying to fit a multi-entity-type query into a single-entity-type repository.

The simplest way to accomplish this is by using query and command objects. It's not even that hard, because query/command objects are really just one-method-repositories.

The difference is that a repository telegraphs "I handle all logic for this specific entity type", whereas a query/command object telegraphs "I handle all logic for this specific operation (no matter how many entity types that requires".

If you have a certain query/command which depends on three different entity types, then those dependencies are obviously required. You can't avoid that. But at least now you're not pushing them into a repository that starts bulging from all the dependencies required for all the methods it exposes.

Note
It's perfectly fine to use both repositories and command/query objects at the same time. All single-entity-type operations can be put in their appropriate repositories, and any operations that require more than one entity type are put in their own personal command/query object.


Direct answers

I could use a PersonDataController of some sort, but this would leave me with a DataController which would need to be tightly coupled to objects Person, Employment, VacationAccount

This is not too dissimilar from what I'm already suggesting. The only differnce is that your PersonDataController would still contain multiple methods for all person data operations. If you split this class into a unique class for each public method, you've essentially got it.

furthermore this would mean that my PersonRepository would also be dependant on my EmploymentRepository and possibly others.

Well, not. Even in your suggested PersonDataController, your PersonRepository would not depend on your EmploymentRepository; but your PersonDataController would depend on both PersonRepository and EmploymentRepository. That is a subtle but very important difference.

PersonRepository and EmploymentRepository don't depend on each other as they have completely separate responsibilities. But PersonDataController, whose responsibility touches on both people and employments, needs to depend on these two separate repositories in order to fulfill its own tasks.

Another approach (and possibly the most straightforward) is to keep business logic in the DataObject Person so I could call Person.Hire(); Which makes the most sense to me, but the issue remains that my Hire function needs to be dependant on the employment object and that my PersonRepository would be dependant on my EmploymentRepository.

First of all, if you do this, PersonRepository would not be dependant on EmploymentRepository, but Person would be dependant on EmploymentRepository

But you've already hit the nail on the head here on what the issue is. You're losing out on the separation between a person and their employments.


Conclusion

Where would I put the business function Hire(Person) in a way that avoids tightly coupling my data objects and repositories?

Using query/command object, this logic would become its own class. Forgive the C# syntax but I'm no Delphi dev.

public class HirePersonCommand
{
    public int PersonId;

    public string JobTitle;
    public DateTime ContractStartDate;
    public int YearlySalary;
}

public class HirePersonResult
{
    public int EmploymentId;
}

public class HirePersonCommandHandler
{
    private EmploymentRepository _employmentRepository;
    // + other repositories you require

    // Constructor omitted - I suggest dependency injection

    public HirePersonResult Handle(HirePersonCommand command)
    {
        var employment = new Employment()
        {
            PersonId = command.PersonId,
            Title = command.JobTitle,
            StartDate = command.ContractStartDate,
            Salary = command.YearlySalary
        }

        var employmentId = _employmentRepository.Create(employment);

        // you can perform logic on additional repositories (e.g. vacationaccount) here

        return new HirePersonResult() { EmploymentId = employmentId };
    }
}

Also, you don't really need to depend on the PersonRepository here. Having a reference to the person suffices, which would obviously always be information you already have since you know which person you're hiring.

Note that I've omitted a lot of frills to keep the intention clear. In practice, handlers often implement a generic interface so that you can rely on reusable logic for handling all query and command handlers.

If you go this route, read up on CQRS. There are many tutorials online.

| improve this answer | |
  • I guess I was not clear enough, I am rewriting the thing in c# so the syntax was perfect. Thank you – Matt Baech Sep 12 '19 at 18:24
0

If you are dealing with HR Human Resources move to ES CQRS pattern. HR is inherently event-based, "standard" object model just won't fit. Say an employee requested holidays, HR accepted, the employee requested a change, HR accepted, HR requested to cancel a part of holidays, the employee accepted. The holiday "account" in this case is an aggregate of all the n requests. Each request/accept/reject is strictly write-only (excluding cool-off), as the request is actually sent and no longer under our control, just like a paper request.

| improve this answer | |
  • If you read carefully the question, you will realise that this answer doesn't address the question. The question boils down to "Where do we place each responsibility or logic? More precisely, where do we place the logic of cross-curring concerns?" It's indeed a question about modelling and design, but it's too broad and there're different ways to approach the design and all are as good or as bad as they fit in OP's actual context. On the other hand, you didn't argument why ES and why CQRS and in which way these two architectural styles address the problem. – Laiv Jan 2 at 8:08
  • The CQRS part is covered by the previous answer. The CQRS model fits best when you deal with complex hierarchies, i.e. have complex relationships between entities. HR (Human Resources) is the domain that has a very high complexity that cannot be omitted due to many legal and practical issues. If you try to model all of the documents/entities as standard domain objects, you will quickly get stuck in a web of dependencies. Been there, done that. On the other hand, if you look at it from the intents/actions/workflows/commands point of view and don't mix it with queries, it gets manageable. – Hobbyist Jan 6 at 14:26
  • The ES part is about the HR nature: it "consists of" a bunch of actions which on one hand need to be traced as is while on the other hand need to be aggregated for practical use. E.g. a labour contract is an aggregate over events contract concluded, contract changed; actual hours worked is an aggregate over schedule, vacations, sick leaves and other types of events etc. The ES CQRS model fits the requirement perfectly. However, it requires different code organization than trivial UI - BL - DAL. See for detailed description: hackernoon.com/1-year-of-event-sourcing-and-cqrs-fb9033ccd1c6 – Hobbyist Jan 6 at 14:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.