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?

  • I'm afraid of a good answer would take several pages if not a book. Software engineering is not famous for its rigour or a methodical implementation. There are several (good and "bad") ways to achieve the same result and whether they are good or bad will depend on many things, all related to the context where the project is being implemented. That said, I suggest starting with something simple, get familiar with Clean Architecture principles. – Laiv Sep 12 at 15:04
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.

  • 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 at 18:24

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.