Skip to main content
deleted 1 character in body
Source Link
biziclop
  • 3.4k
  • 22
  • 22
  • Do youwe only instantiate the object from one or two places? Is this likely to change?
  • Maybe youwe should enforce these constraints by refactoring the original object into a composition of smaller objects?
  • Would we end up passing the builder around from method to method?
  • Could Iwe just use a static factory method instead?
  • Is this part of an external API, that needs to be as foolproof and slick, as possible?
  • Approximately how many of our current backlog of bugs could've been avoided if we had builders?
  • How much extra documentation writing would we save?
  • Do you only instantiate the object from one or two places? Is this likely to change?
  • Maybe you should enforce these constraints by refactoring the original object into a composition of smaller objects?
  • Would we end up passing the builder around from method to method?
  • Could I just use a static factory method instead?
  • Is this part of an external API, that needs to be as foolproof and slick, as possible?
  • Approximately how many of our current backlog of bugs could've been avoided if we had builders?
  • How much extra documentation writing would we save?
  • Do we only instantiate the object from one or two places? Is this likely to change?
  • Maybe we should enforce these constraints by refactoring the original object into a composition of smaller objects?
  • Would we end up passing the builder around from method to method?
  • Could we just use a static factory method instead?
  • Is this part of an external API, that needs to be as foolproof and slick, as possible?
  • Approximately how many of our current backlog of bugs could've been avoided if we had builders?
  • How much extra documentation writing would we save?
added 705 characters in body
Source Link
biziclop
  • 3.4k
  • 22
  • 22
// business objects

interface CharRange {
   public char getCharacter();
   public int getFrom();
   public int getTo();
}

class MultiCharRange implements CharRange {
   final char ch;
   final int from;
   final int to;

   public char getCharacter() { return ch; }
   public int getFrom() { return from; }
   public int getTo() { return to; }
}

class SingleCharRange implements CharRange {
   final char ch;
   final int position;

   public char getCharacter() { return ch; }
   public int getFrom() { return position; }
   public int getTo() { return position; }
}

// builders

class Builder1 {
   public Builder2 character(char ch) {
      return new Builder2(ch);
   }
}

class Builder2 {
   final char ch;
   int from;
   int to;

   public Builder2 range(int from, int to) {
      if (from > to) throw new IllegalArgumentException("from > to");
      this.from = from;
      this.to = to;
      return this;
   }

   public Builder2 zeroStartRange(int to) {
      this.from = 0;
      this.to = to;
      return this;
   }

   public CharRange build() {
      if (from == to)
         return new CharRangeSingleCharRange(ch,from);
      else 
         return new MultiCharRange(ch,from,to);
   }
}

This is a noddy example that doesn't make much sense but it demonstrates the principleall three properties: here the character is always set first, the range limits are always set and validated together, and always after the characteryou choose your implementation at build time.

  • Do you only instantiate the object from one or two places? Is this likely to change?
  • Maybe you should enforce these constraints by refactoring the original object into a composition of smaller objects?
  • Would we end up passing the builder around from method to method?
  • Could I just use a static factory method instead?
  • Is this part of an external API, that needs to be as foolproof and slick, as possible?
  • Approximately how many of our current backlog of bugs could've been avoided if we had builders?
  • How much extra documentation writing would we save?
class CharRange {
   final char ch;
   final int from;
   final int to;
}

class Builder1 {
   public Builder2 character(char ch) {
      return new Builder2(ch);
   }
}

class Builder2 {
   final char ch;
   int from;
   int to;

   public Builder2 range(int from, int to) {
      if (from > to) throw new IllegalArgumentException("from > to");
      this.from = from;
      this.to = to;
      return this;
   }

   public Builder2 zeroStartRange(int to) {
      this.from = 0;
      this.to = to;
      return this;
   }

   public CharRange build() {
      return new CharRange(ch,from,to);
   }
}

This is a noddy example that doesn't make much sense but it demonstrates the principle: here the range limits are always set and validated together, and always after the character.

  • Do you only instantiate the object from one or two places? Is this likely to change?
  • Maybe you should enforce these constraints by refactoring the original object into a composition of smaller objects?
  • Would we end up passing the builder around from method to method?
  • Is this part of an external API, that needs to be as foolproof and slick, as possible?
  • Approximately how many of our current backlog of bugs could've been avoided if we had builders?
  • How much extra documentation writing would we save?
// business objects

interface CharRange {
   public char getCharacter();
   public int getFrom();
   public int getTo();
}

class MultiCharRange implements CharRange {
   final char ch;
   final int from;
   final int to;

   public char getCharacter() { return ch; }
   public int getFrom() { return from; }
   public int getTo() { return to; }
}

class SingleCharRange implements CharRange {
   final char ch;
   final int position;

   public char getCharacter() { return ch; }
   public int getFrom() { return position; }
   public int getTo() { return position; }
}

// builders

class Builder1 {
   public Builder2 character(char ch) {
      return new Builder2(ch);
   }
}

class Builder2 {
   final char ch;
   int from;
   int to;

   public Builder2 range(int from, int to) {
      if (from > to) throw new IllegalArgumentException("from > to");
      this.from = from;
      this.to = to;
      return this;
   }

   public Builder2 zeroStartRange(int to) {
      this.from = 0;
      this.to = to;
      return this;
   }

   public CharRange build() {
      if (from == to)
         return new SingleCharRange(ch,from);
      else 
         return new MultiCharRange(ch,from,to);
   }
}

This is a noddy example that doesn't make much sense but it demonstrates all three properties: the character is always set first, the range limits are always set and validated together, and you choose your implementation at build time.

  • Do you only instantiate the object from one or two places? Is this likely to change?
  • Maybe you should enforce these constraints by refactoring the original object into a composition of smaller objects?
  • Would we end up passing the builder around from method to method?
  • Could I just use a static factory method instead?
  • Is this part of an external API, that needs to be as foolproof and slick, as possible?
  • Approximately how many of our current backlog of bugs could've been avoided if we had builders?
  • How much extra documentation writing would we save?
Source Link
biziclop
  • 3.4k
  • 22
  • 22

While all the other answers are very useful (more useful than mine is going to be), there is a property of builders you haven't mentioned yet: by turning the creation of an object into a process, you can enforce complex logical constraints.

You can mandate for example that certain fields are set together, or in a specific order. You can even return a different implementation of the target object depending on those decisions.

class CharRange {
   final char ch;
   final int from;
   final int to;
}

class Builder1 {
   public Builder2 character(char ch) {
      return new Builder2(ch);
   }
}

class Builder2 {
   final char ch;
   int from;
   int to;

   public Builder2 range(int from, int to) {
      if (from > to) throw new IllegalArgumentException("from > to");
      this.from = from;
      this.to = to;
      return this;
   }

   public Builder2 zeroStartRange(int to) {
      this.from = 0;
      this.to = to;
      return this;
   }

   public CharRange build() {
      return new CharRange(ch,from,to);
   }
}

This is a noddy example that doesn't make much sense but it demonstrates the principle: here the range limits are always set and validated together, and always after the character.

It's easy to see how this can lead to more readable and reliable user code but as you can also see, there's a downside to it: lots and lots of builder code (and I even left out the constructors to shorten the snippets). So you try to calculate the trade-off, by asking questions like:

  • Do you only instantiate the object from one or two places? Is this likely to change?
  • Maybe you should enforce these constraints by refactoring the original object into a composition of smaller objects?
  • Would we end up passing the builder around from method to method?
  • Is this part of an external API, that needs to be as foolproof and slick, as possible?
  • Approximately how many of our current backlog of bugs could've been avoided if we had builders?
  • How much extra documentation writing would we save?

Your colleagues simply decided that the trade-off wouldn't be beneficial. You should discuss the reasons openly and honestly, preferably without resorting to abstract principles, but concentrating on the practical implications of this solution or that.