Jump to content

Config builder for frameworks with concrete configurations


fixthissite

Recommended Posts

This was a design I use in cases where configuration should be set by the client (person using your code) for a framework.
 
The idea is to allow the client to specify the configuration, but prevent the client from storing an instance of the Config so it can be modified later on; a truely immutable config.
 
This is a design I came across myself through experimenting. It started by simply passing the client a Config object, which is used to specify type-safe configurations as well as guide the client by showing them their options (which properties should/can be set) through templates. This means when you put a period after config, it'll give you all the available methods you can choose from, leaving out any irrelevant methods.
public final class Config {
     //details needed by the framework, but not by the client


     Config() { }


    //declare setters so the client can specify
    //declare getters so the framework can access
}
public abstract class Framework {


     public final void start() {
          //check if running


           Config config = new Config();
           init(config);
           //use config
     }


     protected abstract void init(Config config);
}
public final class MyApp extends Framework {
     protected void init(Config config) {
          //set config through setter methods
     }
}
The idea was to hide the ability of creating a Config from the client. The framework would pass in the config, ensuring the client couldn't create "garbage" config objects.
 
The problem with this is that MyApp can store the config object in a field, allowing the client to mutate it (change it's state) later. Our Config is not immutable, because we NEED to set the values in a place other than the constructor. We could implement validations in the config's setter methods, to ensure each field is only set once, but that would be quite a bit to manage.
 
To fix this, I implemented a Builder for Config. The Builder pattern allows the developer to specify the properties of an object, then build it afterwards, allowing immutability after building. To do this, we pass the responsibility of creating the Config class to it's builder. The developer uses the builder to specify the properties (which are stored in the builder). Once you decide to build() the object, the Builder passes itself to the constructor of the object we want, initializing the fields with what we specified in the builder:
public final class Config {
     //private final fields


     private Config(Builder builder) {
          //use builder to init final fields
     }


     public static final class Builder {
          //config properties; will be transfered to Config


          public Builder setProperty(...) {
               //assign to field
               return this;
          }


          public Config build() {
               return new Config(this);
          }
     }
}
Instead of passing a Config object to the client, we would pass a Config.Builder object. This will allow them to set the properties of the config, while ensuring the properties are immutable. There's a downside to this: Framework doesn't have any access to the config the client builds right now; it simply creates the builder and passes it to the client. We need to have the client return it:
public abstract class Framework {
     public final void start() {
          Config.Builder builder = new Config.Builder();
          init(builder);
     }


     protected abstract void init(Config.Builder builder);
}
public final class MyApp extends Framework {
     protected void init(Config.Builder builder) {
          return builder.setProperty(...).build();
     }
}
The client can hold a reference to the Config, but is unable to mutate it. Although this works, we can see that the client doesn't have any reason to know about the Config. We can remove the client's ability to access the Config by removing their ability to build it, and forcing them to return the builder after they specified the properties (allowing the framework to build it). Simply protect the Builder#build method (as well as Builder's constructor; no reason the client should be able to instantiate it). The final API design looks like:
 
public final class Config {
     private Config(Builder builder) {


      }


      public static final class Builder {
           Builder() { }


           Config build() {
                return new Config(this);
           }
      }
}
abstract class Framework {
     public final void start() {
          Config config = init(new Config.Builder()).build();
          //use config
     }


     protected abstract Config.Builder init(Config.Builder builder);
}
public final class MyApp extends Framework {
     protected Config.Builder init(Config.Builder builder) {
          return builder.setProperty(...);
     }
}

I don't expect this to be used, seeing how a system like this is not needed in scripting. I just thought some programmers may be interested in such development processes.

 

I also criticise this as being verbose, but it's a step up from "no design". Responsibilities, encapsulation and enforcing a "hard to misuse API".

 

This also may seem like a complete overkill to those who don't care for strong design, so if you're a "if it works, good enough" kind of developer, this is obviously not a topic for you :P Feel free to leave feedback!

  • Like 2
Link to comment
Share on other sites

My god so much effort tongue.png I'm personally more a fan of: don't touch the variables/methods starting with an underscore, if you do anyway it's your fault.

That doesn't enforce any compile-time safety, which is a pretty big factor in API design. Imagine if the entire JDK was built without compile-time saftey measures; the cognition required to write a program would be rediculous. It's best to inform the developer of a problem as soon as it happens (compile-time). If not possible, inform them at runtime (excpetions). Logical errors should be prevented at all costs, reducing the amount of "wtf moments" (not being able to easily explain the result of the code) and undocumented rules

Edited by fixthissite
Link to comment
Share on other sites

I just used a map with a forced key type of Enum<E> for O(1) get, put, and contains but this is really nice.

That used to be the prefered way of avoiding tons of parameters (pass a map) until the builder pattern came around. A map doesn't enforce concrete configuration. When the client is setting the config using a map, they are unaware of what configuration options are available; it requires you to document what SHOULD go in the map.

 

I actually came across a question on StackOverflow about this. The answers all suggest using a more type-safe solution. Maps aren't bad, and they are definitely less verbose than the builder pattern. But compile-time safety is a HUGE aspect. It's the reason why Oracle is choosing Jigsaw over OSGi for modularizing the JDK; OSGi enforces runtime modularization while Jigsaw enforces compile-time modularization

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...