vektor
vektor

Reputation: 2926

Class with many parameters, beyond the Builder pattern

Context

Suppose you have a component with a great many options to modify its behavior. Think a table of data with some sorting, filtering, paging, etc. The options could then be isFilterable, isSortable, defaultSortingKey, etc etc. Of course there will be a parameter object to encapsulate all of these, let's call it TableConfiguration. Of course we don't want to have a huge constructor, or a set of telescopic constructors, so we use a builder, TableConfigurationBuilder. The example usage could be:

TableConfiguration config = new TableConfigurationBuilder().sortable().filterable().build();   

So far so good, a ton of SO questions deals with this already.

Moving forward

There is now a ton of Tables and each of them uses its own TableConfiguration. However, not all of the "configuration space" is used uniformly: let's say most of the tables is filterable, and most of those are paginated. Let's say, there are only 20 different combinations of configuration options that make sense and are actually used. In line with the DRY principle, these 20 combinations live in methods like these:

public TableConfiguration createFilterable() {
  return new TableConfigurationBuilder().filterable().build();
}

public TableConfiguration createFilterableSortable() {
  return new TableConfigurationBuilder().filterable().sortable().build();
}

Question

How to manage these 20 methods, so that developers adding new tables can easily find the configuration combination they need, or add a new one if it does not exist yet?

All of the above I use already, and it works reasonably well if I have an existing table to copy-paste ("it's exactly like Customers"). However, every time something out of the ordinary is required, it's hard to figure out:

I tried to give the methods some very descriptive names to express what configuration options are being built in inside, but it does not scale really well...

Edit

While thinking about the great answers below, one more thing occurred to me: Bonus points for grouping tables with the same configuration in a type-safe way. In other words, while looking at a table, it should be possible to find all its "twins" by something like go to definition and find all references.

Upvotes: 9

Views: 1488

Answers (5)

fps
fps

Reputation: 34460

I think that if you are already using the builder pattern, then sticking to the builder pattern would be the best approach. There's no gaining in having methods or an enum to build the most frequently used TableConfiguration.

You have a valid point regarding DRY, though. Why setting the most common flags to almost every builder, in many different places?

So, you would be needing to encapsulate the setting of the most common flags (to not repeat yourself), while still allowing to set extra flags over this common base. Besides, you also need to support special cases. In your example, you mention that most tables are filterable and paginated.

So, while the builder pattern gives you flexibility, it makes you repeat the most common settings. Why not making specialized default builders that set the most common flags for you? These would still allow you to set extra flags. And for special cases, you could use the builder pattern the old-fashioned way.

Code for an abstract builder that defines all settings and builds the actual object could look something like this:

public abstract class AbstractTableConfigurationBuilder
                      <T extends AbstractTableConfigurationBuilder<T>> {

    public T filterable() {
        // set filterable flag
        return (T) this;
    }

    public T paginated() {
        // set paginated flag
        return (T) this;
    }

    public T sortable() {
        // set sortable flag
        return (T) this;
    }

    public T withVeryStrangeSetting() {
        // set very strange setting flag
        return (T) this;
    }

    // TODO add all possible settings here

    public TableConfiguration build() {
        // build object with all settings and return it
    }
}

And this would be the base builder, which does nothing:

public class BaseTableConfigurationBuilder 
    extends AbstractTableConfigurationBuilder<BaseTableConfigurationBuilder> {
}

Inclusion of a BaseTableConfigurationBuilder is meant to avoid using generics in the code that uses the builder.

Then, you could have specialized builders:

public class FilterableTableConfigurationBuilder 
    extends AbstractTableConfigurationBuilder<FilterableTableConfigurationBuilder> {

    public FilterableTableConfigurationBuilder() {
        super();
        this.filterable();
    }
}

public class FilterablePaginatedTableConfigurationBuilder 
    extends FilterableTableConfigurationBuilder {

    public FilterablePaginatedTableConfigurationBuilder() {
        super();
        this.paginated();
    }
}

public class SortablePaginatedTableConfigurationBuilder 
    extends AbstractTableConfigurationBuilder
            <SortablePaginatedTableConfigurationBuilder> {

    public SortablePaginatedTableConfigurationBuilder() {
        super();
        this.sortable().paginated();
    }
}

The idea is that you have builders that set the most common combinations of flags. You could create a hierarchy or have no inheritance relation between them, your call.

Then, you could use your builders to create all combinations, without repeting yourself. For example, this would create a filterable and paginated table configuration:

TableConfiguration config = 
    new FilterablePaginatedTableConfigurationBuilder()
       .build();

And if you want your TableConfiguration to be filterable, paginated and also sortable:

TableConfiguration config = 
    new FilterablePaginatedTableConfigurationBuilder()
       .sortable()
       .build();

And a special table configuration with a very strange setting that is also sortable:

TableConfiguration config = 
    new BaseTableConfigurationBuilder()
       .withVeryStrangeSetting()
       .sortable()
       .build();

Upvotes: 3

vektor
vektor

Reputation: 2926

What I would have done, if I didn't know SO

Assumption: There probably is way less than 2^(# of config flags) reasonable configurations for the table.

  1. Figure out all the configuration combinations that are currently used.
  2. Draw a chart or whatever, find clusters.
  3. Find outliers and think very hard why they don't fit into those clusters: is that really a special case, or an omission, or just laziness (no one implemented full text search for this table yet)?
  4. Pick the clusters, think hard about them, package them as methods with descriptive names and use them from now on.

This solves problem A: which one to use? Well, there is only a handful of options now. And problem B as well: if I want something special? No, you most probably don't.

Upvotes: 1

rinde
rinde

Reputation: 1263

I would remove your convenience methods that call several methods of the builder. The whole point of a fluent builder like this is that you don't need to create 20 something methods for all acceptable combinations.

Is there a method doing exactly what I want? (problem A)

Yes, the method that does what you want is the new TableConfigurationBuilder(). Btw, I think it's cleaner to make the builder constructor package private and make it accessible via a static method in TableConfiguration, then you can simply call TableConfiguration.builder().

If not, which one is the closest one to start from? (problem B)

If you already have an instance of TableConfiguration or TableConfigurationBuilder it may be nice to pass it into the builder such that it becomes preconfigured based on the existing instance. This allows you to do something like:

TableConfiguration.builder(existingTableConfig).sortable(false).build()

Upvotes: 2

Daniel Lovasko
Daniel Lovasko

Reputation: 481

How about having a configuration string? This way, you could encode the table settings in a succinct, yet still readable way.

As an example, that sets the table to be sortable and read-only:

defaultTable().set("sr");

In a way, these strings resemble the command-line interface.

This could be applicable to other scenarios that support the table re-use. Having a method that creates the Customers table, we can alter it in a consistent way:

customersTable().unset("asd").set("qwe");

Possibly, this DSL could be even improved by providing a delimiter character, that would separate the set and unset operations. The previous sample would then look as follows:

customersTable().alter("asd|qwe");

Furthermore, these configuration strings could be loaded from files, allowing the application to be configurable without recompilation.

As for helping a new developer, I can see the benefit in a nicely separated subproblem that can be easily documented.

Upvotes: 1

gudok
gudok

Reputation: 4179

If almost all configuration options are booleans, then you may OR them together:

public static int SORTABLE = 0x1;
public static int FILTERABLE = 0x2;
public static int PAGEABLE = 0x4;

public TableConfiguration createTable(int options, String sortingKey) {
  TableConfigurationBuilder builder = new TableConfigurationBuilder();
  if (options & SORTABLE != 0) {
    builder.sortable();
  }
  if (options & FILTERABLE != 0) {
    builder.filterable();
  }
  if (options & PAGEABLE != 0) {
    builder.pageable();
  }
  if (sortingKey != null) {
    builder.sortable();
    builder.setSortingKey(sortingKey);
  }
  return builder.build();
}

Now table creation doesn't look so ugly:

TableConfiguration conf1 = createTable(SORTEABLE|FILTERABLE, "PhoneNumber");

Upvotes: 1

Related Questions