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.
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();
}
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...
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.
You can have constructors with multiple parameters, but also have a constructor with only the minimum parameters.
This method has four parameters: the loan amount, the interest rate, the future value and the number of periods.
No parameters can be passed in the default constructor. The answer to the above question is zero or no parameters. We cannot pass parameters to a default constructor. The only purpose of a default constructor is to instantiate the object.
In the Real World The simplest real world example of the builder pattern, which most of us are familiar with, is when we make (or order) a pizza. The pizza toppings cannot be added in any random order or the whole thing is likely to come out a mess. Instead, there is a step-by-step process that is followed.
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();
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With