5

Currently, in our service layer, we pass an id as well as a new updated value, something akin to

updatePersonName(Person person, Name name)

which, in turn, calls the corresponding repository functions. Now this works fine if we want to update only a single value. But once we start wanting to update multiple values at the same time, we're either stuck calling various service methods in a row or defining a service method that takes multiple arguments to update. It is bearable, but it gets even worse when we need to update multiple values that are forced to be updated together, which means we are forced to define a (maybe new) method in the service that makes sure the combination is met. And if you start having some limitations (perhaps a Person that's marked as non-updatable due to different reasons), it just increases the complexity even further.

Recently I've been thinking of using the builder pattern, but not for object creation, but updating. Something like this (all the methods chosen completely arbitrary, but you get the point):

PersonService.updater(person)
    .setName("newName")
    .addRole(newRoleObject)
    .setFlag(PersonFlag.NOT_UNDERAGE)
    .overrideBlock()
    .withEventDescription("Person changed her name on birthday!")
    .update();

The builder could internally resolve the logic without exposing too much complexity to the outside. The fluent API is easy to use in every other service/component that needs access. I don't need to create a plethora of methods to cover whatever requirement came up. Multiple updates are easy to chain together, and if you want to update something that's now allowed, it could block that internally unless you override said block. And it would be able to force specific fields due to type safety, for example, an EventObject.

More importantly, this could also, in turn, make sure we only make a single trip to the repository, instead of multiple. It would improve runtime, especially in critical algorithms that require many passes to the database otherwise.

I can see a few issues with this approach, as well. It's bulky, is an unconventional API for people not experienced with it, and could lead to some misuse. Implementing it is not trivial while making sure the internal logic keeps together. However, I think the positives outweigh the negatives in my situation.

Am I missing something?

Edit: I know that usually, you have a save() function inside of your DTO that takes care of saving, but due to existing infrastructure, that isn't an option at this point.

5

First of all this is not the GoF Builder Pattern. This might be the Joshua Bloch Builder Pattern. It's used to simulate named arguments in languages (like Java) that don't have them.

Named arguments make long argument lists tolerable by clearly labeling the arguments. They also enable optional arguments so the list doesn't have to include every possible argument the way positional arguments do.

If that's all you're doing then you really are doing object construction because that's what the Joshua Bloch Builder gives you, an immutable object with all those fields set.

If that's not what you're doing then this is an internal or embedded Domain Specific Language (DSL). These have the power to control what does and doesn't come next. They do that because the methods return different types that enable new methods.

This provides you with a LOT of power and enough rope to hang yourself. It's also a lot of work to set up behind the scenes. But it does have good uses. For example JOOQ and Java 8 streams. I've successfully used it to formalize construction of a nightmarish god object that was also used to update the database and was too entrenched to reform.

The key thing with this is the business rules are enforced by the mini language you're building. That's great but it sets them in stone. DSLs are not easy to write and they are not easy to change. Use them when they will be used a LOT and changed very little.

Now that said you can avoid hard coding the implementation with a little dependency injection.

happyNewName(PersionService personService, Person person, Role newRoleObject) {
    personService
        .updater(person)
        .setName("newName")
        .addRole(newRoleObject)
        .setFlag(PersonFlag.NOT_UNDERAGE)
        .overrideBlock()
        .withEventDescription("Person changed her name on birthday!")
        .update()
    ;
}

Done this way the DSLs 'source' and it's implementation can be changed independently.

This lets you create facades that guide coders to follow complex ceremonies that might be necessary. However, often they are not really necessary if you'll just do the work to simplify the system. The danger here is if you do a DSL instead you'll be digging this technical debt hole even deeper. So be sure you really have no better solution, because this is the nuclear option.

This fluent style is trending right now but be advised that you need to design your DSL from end-to-end. No sneaky picking up random classes in the middle of this. That's a huge Law of Demeter violation. Only dot through classes designed to work together like this. Not random ones that happen to be lying about the place.

1

Ultimately it sounds like you have several unique use cases for updating a person, and for this reason you have created a "builder" class that encapsulates the logic of these individual updates. By chaining individual updates together with a fluent API you replicate a use case. The problem here is that you need to know which use case you are executing in order to call the correct sequence of methods.

While this is a novel approach, it feels like a misuse of the builder pattern, which provides a layer of abstraction over object creation. It breaks people's expectations of what a "builder object" does, and for this reason I don't think this is a good pattern.

In Clean Architecture the behavior for a use case is literally called a use case — a class that specializes in coordinating all operations for a particular use case in the application. You get the benefits of naming something similar to a term used in other architectures, and you just plain old get better names.

Consider all the combinations of chained method calls you are making on your "builder" class, and make a "use case" class for each sequence of method calls. Eliminate the unnecessary builder pattern, since you are not constructing a new object here, and have the use case class handle the nuances of which methods and objects are needed from the repository, and how those objects interact:

var useCase = new NameChangeUseCase(repository);

useCase.Execute(person, dateOfChange, "newName");

The execute method of the NameChangeUseCase can set the event description, handle the NOT_UNDERAGE flag based on the dateOfChange and assign roles based on these conditions as well:

public class NameChangeUseCase
{
    private IPersonRepository repository;

    public NameChangeUseCase(IPersonRepository repository)
    {
        this.repository = repository;
    }

    public void Execute(Person person, DateTime dateOfChange, string newName)
    {
        if (person.BirthDate == dateOfChange)
        {
            // Add event with description "Person changed her name on birthday!"
        }

        if (person.CalculateAgeInYears(dateOfChange) >= 18)
        {
            // set NOT_UNDERAGE flag
        }

        // get new role and add to person
        person.ChangeName(newName);
        repository.Save(person);
    }
}

This is much easier to follow as a developer, and a lot harder to screw up.

  • Hey! I definitely agree with a lot of your points. Something I'm slightly afraid of though is the amount of code duplication across use cases. In my current situation (the person example is an arbitrary example!), many use cases are similar enough that it would be quite some duplication, but not different enough to warrant completely seperate implementations. And the "hidden" logic would be simple enough to not make it too complicated, but impactful enough that it'd cause a lot of similar classes. – Joe Nov 8 at 19:24
  • @Joe: Since they are different use cases it is OK to copy code. Don't create shared code unless you think all of the use cases that need this behavior will evolve the same way, and for the same reasons. You can also push this logic into your businesses classes as well, making that logic portable across use cases. – Greg Burghardt Nov 8 at 19:39
  • Something that bugs me about the "use case architecture" is the fact that the team I'm working with is more the kind of group that want a working abstraction on a service layer that they can then piece together. The Use Case implementation would be mentally perceived as a lot of "double code" even though it serves different situations. Sadly this is less a fault of the architecture and just more a psychological one of the team. Especially if they had to write these use cases themselves. – Joe yesterday

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.