9

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:

  • Is there a method doing exactly what I want? (problem A)
  • If not, which one is the closest one to start from? (problem B)

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.

vektor
  • 3,312
  • 8
  • 41
  • 71
  • 3
    Would you consider: public TableConfiguration createTableConfiguration(boolean isFilterable, boolean isSortable, boolean isXXX ....); a factory approach. – Ian Mc Feb 01 '16 at 15:00
  • How is this different from a huge constructor? – vektor Feb 01 '16 at 15:36
  • Yes, good point. How about a varargs approach where you only pass the parameters that are valid (these could be modeled as enums) – Ian Mc Feb 01 '16 at 15:45
  • I want to semantically group various param sets, hence 'createFilterable()' etc – vektor Feb 01 '16 at 15:51
  • 1
    Is the answer to create an enum ConfigurationSetType (containing 20 patterns you have chosen)? Add a new type as required. Then the programmer has clear visibility to the 20 sets in one simple enum? – Ian Mc Feb 01 '16 at 15:57
  • 1
    I meant to add that you then pass this ConfigurationSetType to the factory which creates the builder. Now you have a factory, with one parameter, that can work on sets of properties – Ian Mc Feb 01 '16 at 16:04
  • Ad enum: right now, the 20 methods live in a single factory class. Therefore, I don't see much benefit in having a separate enum. Ad second comment: I understand your. But does this "enum" approach help with A and B (marked in the question)? – vektor Feb 01 '16 at 16:10
  • Of all options discussed, the builder is the best one. I see no gaining in either enums or methods with boolean parameters. Perhaps you could have a default builder that already sets the most common flags, and then set the specific flags as per the builder pattern. – fps Feb 01 '16 at 17:37
  • @FedericoPeraltaSchaffner I agree that enums, factories etc. don't help at all. The default builder makes sense, if you write it as an answer, I'll accept it. – vektor Feb 02 '16 at 07:16
  • Two questions 1. Are these tables UI tables? 2. Can you add more examples of the parameters? – AdamSkywalker Feb 02 '16 at 08:36
  • In this example, those really are UI tables with filtering, sorting, paging, data export - you name it. Real-life examples: `isSortable`, `isMultiSortable`, `defaultSort`, `isFullTextSearchable`, `hasFilter`, `hasAggregateRow`, `allowsPaging`, `allowsSelection`, `hasStickyHeader`. – vektor Feb 02 '16 at 08:45

5 Answers5

3

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();
fps
  • 33,623
  • 8
  • 55
  • 110
2

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()
rinde
  • 1,181
  • 1
  • 8
  • 20
1

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");
gudok
  • 4,029
  • 2
  • 20
  • 30
  • Yes, this is a bit more concise way of recording the configuration options. However, please keep in mind that there is usually something like 10-20 of the options present, and that's what makes problem A & B so hard in practice. – vektor Feb 02 '16 at 07:13
  • 1
    While the concept of this is theoretically sound, the form has really been outdated for 10-20 years. This is what `enum` and `EnumSet` were designed for (as mentioned in the comment section under the original question). With the use of an enum and an enumset, though, this _is_ a good option, I think. – Petr Janeček Feb 02 '16 at 09:00
1

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.

Daniel Lovasko
  • 471
  • 2
  • 10
1

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.

vektor
  • 3,312
  • 8
  • 41
  • 71