JavaWorld on OO: Getters/Setters vs Builder

2019-03-13 06:59发布

问题:

Background:

I found this article on JavaWorld, where Allen Holub explains an alternative to Getters/Setters that maintains the principle that the implementation of an object should be hidden (his example code can also be found below).

It is explained that the classes Name/EmployeeId/Money should have a constructor taking a single string - the reasoning is that if you type it as an int, and later need to change it to a long, you will have to modify all the uses of the class, and with this pattern you don't have to.

Question 1:

I was wondering: doesn't this simply move the problem to the parsing of the String parameters being tossed about? For example, if all the code using the EmployeeId (received from the Exporter) parses the String into an int, and suddenly you start exporting long values, you need to modify exactly as many uses... and if you start out parsing it as a long it might well have to change to a double (even though that makes no sense for id's)... and if you can't be sure what to parse the String into, you can't implement anything.

Question 2:

Besides this question, I have another: I realise that the article is over seven years old, so could anyone point me to some recent overviews concerning OO-design, and specifically to ideas concerning the getter/setter and implementation hiding debate?

Listing 1. Employee: The Builder Context


  public class Employee
  {   private Name        name;
      private EmployeeId  id;
      private Money       salary;

      public interface Exporter
      {   void addName    ( String name   );
          void addID      ( String id     );
          void addSalary  ( String salary );
      }

      public interface Importer
      {   String provideName();
          String provideID();
          String provideSalary();
          void   open();
          void   close();
      }

      public Employee( Importer builder )
      {   builder.open();
          this.name   = new Name      ( builder.provideName()     );
          this.id     = new EmployeeId( builder.provideID()       );
          this.salary = new Money     ( builder.provideSalary(),
                                    new Locale("en", "US") );
          builder.close();
      }

      public void export( Exporter builder )
      {   builder.addName  ( name.toString()   );
          builder.addID    ( id.toString()     );
          builder.addSalary( salary.toString() );
      }

      //...
  }

回答1:

Question 1: String parsing seems strange. IMHO you can only do so much to anticipate future enhancements. Either you use a long parameter right from the start to be sure, or consider adding additional constructors later. Alternatively you can introduce an extensible parameter class. See below.

Question 2: There are several scenarios in which the builder pattern can be useful.

  • Complex Object creation

    When you are dealing with very complex object that have lots of properties that you would preferably only set once at object creation, doing this with regular constructors can become hard to read, because the constructor will have a long list of parameters. Publishing this as an API is not good style because everyone will have to read the documentation carefully and make sure they do not confuse parameters.

    Instead when you offer a builder, only you have to cope with the (private) constructor taking all the arguments, but the consumers of your class can use much more readable individual methods.

    Setters are not the same thing, because they would allow you to change object properties after its creation.

  • Extensible API

    When you only publish a multi-parameter constructor for your class and later decide you need to add a new (optional) property (say in a later version of your software) you have to create a second constructor that is identical to the first one, but takes one more parameter. Otherwise - if you were to just add it to the existing constructor - you would break compatibility with existing code.

    With a builder, you simply add a new method for the new property, with all existing code still being compatible.

  • Immutability

    Software development is strongly trending towards parallel execution of multiple threads. In such scenarios it is best to use objects that cannot be modified after they have been created (immutable objects), because these cannot cause problems with concurrent updates from multiple threads. This is why setters are not an option.

    Now, if you want to avoid the problems of the multi-parameter public constructors, that leaves builders as a very convenient alternative.

  • Readability ("Fluent API")

    Builder based APIs can be very easy to read, if the methods of the builder are named cleverly, you can come out with code that reads almost like English sentences.

In general, builders are a useful pattern, and depending on the language you are using, they are either really easy to use (e. g. Groovy) or a little more tedious (e. g. in Java) for the provider of an API. For the consumers, however, they can be just as easy.



回答2:

You can implement Builders is a more concise manner. ;) I have often found writing Builders by hand tedious and error prone.

It can work well if you have a data model which generates your Data Value objects and their Builders (and marshallers). In that case I believe using Builders is worth it.



回答3:

There are many problems with constructors that take arguments (for example, you can't build the object in several steps). Also if you need lots of arguments, you will eventually get confused about parameter order.

The latest idea is to use a "fluent interface". It works with setters that return this. Often, set is omitted from the method name. Now you can write:

User user = new User()
.firstName( "John" )
.familyName( "Doe" )
.address( address1 )
.address( address2 )
;

This has several advantages:

  1. It's very readable.
  2. You can change the order of parameters without breaking anything
  3. It can handle single-value and multi-value arguments (address).

The major drawback is that you don't know anymore when the instance is "ready" to be used.

The solution is to have many unit tests or specifically add an "init()" or "done()" method which does all the checks and sets a flag "this instance is properly initialized".

Another solution is a factory which creates the actual instance in a build() method which must be the last in the chain:

User user = new UserFactory()
.firstName( "John" )
.familyName( "Doe" )
.address( address1 )
.address( address2 )
.build()
;

Modern languages like Groovy turn this into a language feature:

User user = new User( firstName: 'John', familyName: 'Doe',
     address: [ address1, address2 ] )


回答4:

When you require a constructor (consider factories in a similar way) for an object, you force the code using your object to pass the essential requirements to the constructor. The more explicit the better. You can leave the optional fields to be set later (injected) using a setter.