Fully-Constructed Objects
As I discussed about a year ago, Smalltalk’s argument-passing syntax is somewhat limited compared to other languages when you want to define a flexible API. By “flexible”, I mean an API where some parameters are optional with reasonable defaults, and the order of the parameters can vary. When it comes to “constructor” methods, there is another option.
Many years ago, a former co-worker decided to experiment with a new
way of creating objects in Smalltalk. Instead of defining class-side
constructor methods, he would just use #new
and a bunch of
instance-side setters. That way, the setters could be called in any
order, and there would be no need to call setters when the default
value was sufficient. This yields an extremely flexible API for
constructing objects without having to define a whole family of
constructor methods. The experiment seemed worthwhile at the time and
we perpetuated the pattern throughout parts of our codebase.
Over the years, as we’ve had to live in the resulting code, we’ve found that this pattern has a number of disadvantages that we can no longer live with:
-
It is nearly impossible to tell how to properly construct the object. Without looking at the code, you don’t know which setters must be called, and which are optional.
-
If you need to perform additional initialization based on more than one of the incoming parameters, there is no convenient place to put that initialization. Instead, each of the affected setters has to do this initialization, but only if the other setters have already been called. This adds complexity.
-
There is a lack of consistency among the clients of the object being constructed. Each client constructs the object in a different way, and it become much harder to understand how the object should be used.
-
It is easy to allow the object to grow additional, disjoint responsibilities. “If I give it these two parameters, it behaves this way. But if I give it these other three, then it behaves that way.”
-
This pattern is most useful with larger objects that are constructed with a lot of parameters. In my opinion, this is a code smell.
Instead of propagating this pattern further, I now always define proper constructor methods for my objects. Each object should spring into the world fully-formed.
If an object seems to need too many variants of its constructor method, I take that as a sign that the object is doing too much and should be broken up into pieces. Maybe I can divide up its responsibilities. Or maybe some of the incoming constructor parameters should be grouped together into another object.
It’s still somewhat early into this new way of doing things, but I haven’t yet run into a case where I had to create more constructor variants than I was comfortable with.
This is another example of the tradeoffs involved in staying at the same job for a long time. You are forced to live with the long-term consequences of your decisions. But you also get to learn from your mistakes and write code that is more maintainable long-term.