0

I am designing a java system where users can define some rules in a fluent style.

Rules have many properties, which are partly mutually exclusive. We use a builder pattern with validation for this.

To make the system easier to use, we want to introduce a StepBuilder, to guide the user to all necessary steps.

There are different types of rules, but all share some common properties.

Current System:

abstract BaseBuilder<T extends BaseBuilder<T>> {

   protected String property1;
   protected String property2;
   protected String property3;

   abstract Rule build();

   public T withProperty1(String data) { 
      this.property1 = data; 
      return this; 
   }

   public T withProperty2(String data) { 
       this.property2 = data; 
       return this; 
   }

   public T withProperty3(String data) { 
       //this should only be possible if property2 is not set or similar logic
       this.property3 = data; 
       return this; 
   }
}

//there are a few others like this e.g. SpecialBuilder1-10
class SpecialRuleBuilder extends BaseBuilder<SpecialBuilder> {

       protected String special1;
       protected String special2;

       public T withSpecial1(String data) { 
          this.special1 = data; 
          return this; 
       }

       public T withSpecial2(String data) { 
           this.special2 = data; 
           return this; 
       }

     @Override
     Rule builder() {
        return new SpecialRule(property1, property3, special1, special2, ....);
     }

     static SpecialRuleBuilder builder() {
        return new SpecialRuleBuilder();
     }
    
}

class BuilderTest() {
   
   //User can set anything, no steps are enforced at compile time
   Result result = SpecialBuilder.builder()
                        .property1("some")
                        .special2("thing")
                        .build(); 

}

How can we use a StepBuilder including a hierarchy (parent class), so the user cannot get to the same step twice cannot accidentally go back to a previous base step and set some mutually exclusive properties again.

Ideally the user should not be aware of all the special builders and have the same entry point and guided steps. For example:

Result result = GeneralBuilder.builder()
                              .withBaseProperty1("asdas") <-- Step 1
                              .withBaseProperty2("asd")  <-- Step 2, Step 3 is now not visible, continue with all possible special options
                              .withSpecial1("asd") <-- no we are in the step of one concrete builder, and should not get "out" again, not even to the base methods

I know how to define the interface steps, I just dont know how to include all possible starting steps of the special builders at the end of the base steps, since the higher interface/class should probably not rely on the lower parts of the hierarchy.

Is this somehow possible?

HectorLector
  • 1,851
  • 1
  • 23
  • 33
  • "so the user cannot get to the same step twice" what's the problem you're trying to avoid? – Andy Turner Sep 23 '21 at 14:19
  • The users should be guided step by step, there should be no (or little) possibility to build a wrong rule - since this will only show up at runtime. The same step would actually not be a problem, but if I am in the SpecialBuilderStep and can call again all methods of the base class, I can e.g. set base property3 again, even if i set base property2 before and they are mutually exclusive. The Rules can get quite large and there are thousands of them, so it should be as easy/safe as possible. – HectorLector Sep 23 '21 at 14:23
  • I concur with the first sentence of [this answer](https://stackoverflow.com/a/69301952/3788176): "This is a bad idea; the sheer amount of code you need to write is staggering.". I have approached a similar problem using [Error Prone](http://errorprone.info/), making multiple invocations of the same setter a compiler error. So, you can write the code, invoking whatever setters you like, but it then fails at compile time. It's not a "pure Java" solution, but it's an awful lot easier than what you're trying. – Andy Turner Sep 23 '21 at 14:30

1 Answers1

0

This is a bad idea; the sheer amount of code you need to write is staggering. Also, whilst the appeal of a guided 'auto-complete' experience in an IDE is obvious, realize that sometimes builders are used in a more fluent (heh) fashion: Some code makes a builder, sets half the stuff, and then returns the builder so that other code can pick up where it left off. Or, a utility method that takes a builder as param, and sets a few things on it. These use cases become awkward to impossible when you go down this path. So, you spend a ton of time writing this code and it's not even a clean win, API-wise!

If you want to ignore my advice and do it anyway, well, okay. Your funeral. A warned person counts for two, and all that. It looks like this:

public interface BridgeBuilder1 {
    public BridgeBuilder2 name(String name); // mandatory
}

public interface BridgeBuilder2 {
    public BridgeBuilder3 buildYear(int year);  // mandatory
}

public interface BridgeBuilder3 {
    // one of these two is mandatory, and they are mutually exclusive.
    public BridgeBuilder4 lanes(int lanes);
    public BridgeBuilder4 width(int widthInM);
}

public interface BridgeBuilder4 {
    // optional stuff, and the build itself.
    public BridgeBuilder4 color(Color c);
    public BridgeBuilder4 country(String name);
    public Bridge build();
}

// intentionally private class! Should be an inner class
// of your public Bridge class.
private static class BridgeBuilder implements BridgeBuilder1, BridgeBuilder2, BridgeBuilder3, BridgeBuilder4 {
    private String name;

    public BridgeBuilder2 name(String name) {
        this.name = name;
        return this;
    }

    // the fluent, chaining 'setters' for _ALL_ properties here.
    // Match the return types exactly to how they
    // are defined in the interfaces.

    public Bridge build() {
        return new Bridge(name, year, ....);
    }
}

...

public class Bridge {
    Bridge(String name, ...) {
       // a package private constructor.
    }

    public static BridgeBuilder1 builder() {
        return new BridgeBuilder();
    }
}

That should be sufficient to highlight how this works.

But don't.

NB: The actually good solution is to enshrine the notion of a builder into the editor/lang tooling itself. An eclipse or intellij plugin, in other words. Annotations can be used to identify everything (annotate a builder() method to indicate it makes a builder, annotate a class that it is a builder class, etc), then annotate each method in a builder as being optional or not, and as being multiple or not (imagine a builder method that is intended to be invoked multiple times to e.g. fill a list). Armed with that info a plugin can 'fix' the auto-complete dialog to skip all the crap from j.l.Object, and to show all mandatory fields in bold, all already-set stuff greyed out, all optionals in normal colour, and the build method greyed out until all mandatories are set, and bold once they are. That's the real solution. I invite anybody to write these plugins instead of wasting time maintaining this ridiculous-length API extravaganza to create a hampered take on the idea.

rzwitserloot
  • 85,357
  • 5
  • 51
  • 72