0
\$\begingroup\$

Problem statement

I have entities whose relations form a graph. As an example, let's imagine Users and Groups. Each user has a Set of groups s*he belongs to; each Group has a Set of Users, representing its members. When constructing objects of those types, there is no clear hierachy. One could start by constructing a Group and add Users to the group, as well as start with constructing an User and add Groups to that user.

To ease construction, I want to have a fluent API for constructing those objects. Looking online, the most advanced solutions I have found assume a hieracical (i.e. tree-like) relation model, e.g.:

  • An Order has OrderItems
  • OrderItems have an OrderDescription
  • ....

Implementing this kind of fluent builder is easy enough, since an OrderItemBuilder always needs some kind of callback to an Order, and an Order always needs to have a build() method.

With the User-Group-example, the separation is not as clear. When starting construction a User-objects, a build()-method must be present. Starting a (recursive) Group-construction, the builder must not provide build(), but and() (or some other callback-method) instead. Here is an example usage:

Group root = groupBuilder // may be injected through some means
    .withName("root")
    .withUser()
        .name("John Doe")
        .email("john@doe.com")
        // must not compile:
        // .build()
        .and()
    // must not compile:
    //.and()
    .build();

User jane = userBuilder // may be injected thorugh some means
    .withName("Jane Doe")
    .withGroup()
        .name("default")
        // must not compile:
        // .build()
        .and()
    .withEmail("jane@doe.com")
    // must not compile:
    // .and()
    .build()

My solution

follows the following concept:

  • One UserBuilder for "root"-consturction having a User build()-method,
  • One UserBuilderNested, if a User is constructed within the construction of another object, thus providing an and()-method, which internally calls some callback-function, passing the constructed user to the enclosing builder
  • both versions for the Group-object.

To avoid WET-programming, I created two interfaces UserBuilderCore and GroupBuilderCore, containing only the with...(...) methods for the respective builder. UserBuilder and UserBuilderNested implement UserBuilderCore; GroupBuilder and GroupBuilderNested implement GroupBuilderCore. The code for User and its builders is shown at the end of the question, the code for Group and its builders is analogue. The whole code can be found on bitbucket.

Pros

  • The interfaces are fully functional and type-safe.

Cons

  • For every relation in a class (e.g., lets assume each User gets an additional Set<Post> posts of written posts), one additional generic parameter has to be introduced.
  • As of now, UserImpl and GroupImpl are tightly coupled. I have not yet found a possibility to modify the generic parameters in such a way that I could e.g. inject some UserBuilderNested in GroupImplBuilder.
  • Furhtermore, adding a new User-implementation requires the correct generic wiring, as shown in the example implementation.

Request

I am looking for ways to improve the code in the following way:

  • Avoid adding additional generic parameters for new relationships (if possible).
  • Simplify the current design to allow easier extensability.
  • Decouple the UserBuilder- and GroupBuilder-implementations.
  • Any other remarks are of course welcome, but I know that the question is already pretty comprehensive. The points above are my main concern.

Remarks on the provided code

The code is only a mockup, with no validation or referencial integrity measurements (e.g., when constructing a Group within a User, the User is not added to the constructed Group). This is on purpose.

Source code

User.java:

package com.turing.builder;

import java.util.Set;

public interface User {
  String getName();
  String getEmail();
  Set<Group> getGroups();
  void addGroup(Group group);
}

// UserBuilderCore.java
package com.turing.builder;

public interface UserBuilderCore< // @formatter:off
        S extends UserBuilderCore<S, C>,
        C extends GroupBuilderNested<C, ?, S>> { // @formatter:on
  S withName(String name);
  S withEmail(String email);
  S withGroup(Group group);
  C withGroup();
}

UserBuilder.java:

package com.turing.builder;

public interface UserBuilder< // @formatter:off
        S extends UserBuilder<S, C>,
        C extends GroupBuilderNested<C, ?, S>> // @formatter:on
    extends UserBuilderCore<S, C> {
  User build();
}

UserBuilderNested.java:

package com.turing.builder;

public interface UserBuilderNested< // @formatter:off
        S extends UserBuilderNested<S, C, P>,
        C extends GroupBuilderNested<C, ?, S>,
        P extends GroupBuilderCore<P, ?>> // @formatter:on
    extends UserBuilderCore<S, C> {
  P and();
}

UserImpl.java:

package com.turing.builder.impl;

import com.turing.builder.Group;
import com.turing.builder.User;
import com.turing.builder.UserBuilder;
import com.turing.builder.UserBuilderCore;
import com.turing.builder.UserBuilderNested;
import com.turing.builder.impl.GroupImpl.GroupImplBuilderCore;
import com.turing.builder.impl.GroupImpl.GroupImplBuilderNested;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

public class UserImpl implements User {
  private String name;
  private String email;
  private Set<Group> groups = new HashSet<>();

  @Override
  public String getName() {
    return name;
  }

  private void setName(String name) {
    this.name = name;
  }

  @Override
  public String getEmail() {
    return email;
  }

  private void setEmail(String email) {
    this.email = email;
  }

  @Override
  public Set<Group> getGroups() {
    return groups;
  }

  private void setGroups(Set<Group> groups) {
    this.groups = groups;
  }

  @Override
  public void addGroup(Group group) {
    this.groups.add(group);
  }

  @Override
  public String toString() {
    return String.format("User %s=%s",
        getName(),
        getGroups().stream()
            .map(Group::getName)
            .collect(Collectors.toList()));
  }

  protected abstract static class UserImplBuilderCore< // @formatter:off
          S extends UserImplBuilderCore<S>>
      implements UserBuilderCore<
          /* S = */ S,
          /* C = */ GroupImplBuilderNested<S>> { // @formatter:on
    private UserImpl managed = new UserImpl();

    @Override
    @SuppressWarnings("unchecked")
    public S withName(String name) {
      managed.setName(name);
      return ((S) this);
    }

    @Override
    @SuppressWarnings("unchecked")
    public S withEmail(String email) {
      managed.setEmail(email);
      return ((S) this);
    }

    @Override
    @SuppressWarnings("unchecked")
    public S withGroup(Group group) {
      managed.addGroup(group);
      return ((S) this);
    }

    @Override
    public GroupImplBuilderNested<S> withGroup() {
      return new GroupImplBuilderNested<>(this::withGroup);
    }

    protected User construct() {
      User constructed = this.managed;
      this.managed = new UserImpl();
      return constructed;
    }
  }

  public static class UserImplBuilder extends UserImplBuilderCore<UserImplBuilder>
      implements UserBuilder< // @formatter:off
          /* S = */ UserImplBuilder,
          /* C = */ GroupImplBuilderNested<UserImplBuilder>> { // @formatter:on
    public User build() {
      return construct();
    }
  }

  public static class UserImplBuilderNested<T extends GroupImplBuilderCore<T>>
      extends UserImplBuilderCore<UserImplBuilderNested<T>>
      implements UserBuilderNested< // @formatter:off
          /* S = */ UserImplBuilderNested<T>,
          /* C = */ GroupImplBuilderNested<UserImplBuilderNested<T>>,
          /* P = */ T> { // @formatter:on
    private final Function<User, T> callback;

    public UserImplBuilderNested(Function<User, T> callback) {
      this.callback = callback;
    }

    public T and() {
      return callback.apply(construct());
    }
  }
}
\$\endgroup\$
1
\$\begingroup\$

Drop nested builders and use separate builders for each type of object to be created (GroupBuilder, UserBuilder, WhateverBuilder).

Then you may use the builders like so:

Group root = GroupBuilder
    .withName("root")
    .withUser(UserBuilder.
        .withName("John Doe")
        .withEmail("john@doe.com")
        // ...
        .build())
    .withUser(UserBuilder.
        .withName("Jane Doe")
        .withEmail("jane@doe.com")
        .build())
    .build();

This is easier to implement and probably easier to understand for users.

\$\endgroup\$
  • \$\begingroup\$ I am aware of this solution and it is already implemented in my solution. \$\endgroup\$ – Turing85 Dec 23 '18 at 23:28

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.