µObjects: Code Free Constructors
One of the big practices I got from Elegant Objects is the concept of a code free constructor.
Your constructor should have zero logic. Let's see why!
Always born
My favorite reason for this is that you can never fail to instantiate the object. It's always a valid object because it can always exist.
It's not always the most compelling to others because they're happy to have it fail to construct. That's what they're used to.
We should be able to instantiate any class within our system in our tests. This isn't claiming we can DO everything from our test with this "default" version; but we should be guaranteed to instantiate.
For a code-integration test; you need everything to instantiate. You should only inject at the BookEnds. That's the only place you "fake" - when the flow would leave your code. Set up the HttpInterceptor. Add a static 'TestConfig' method that the test preps via.
While smelly - and probably a better way; this is what I do. Using the Dependency Constructors I'll be able to just do a new TopLevelClass()
and use it like it'll really be used. I'll be able to see the complex behavior from the combination of my multitude of simple µObjects.
The Dependency Constructors are what allow us a default constructor to call.
This is something I mis-understood from Elegant Objects that I like a lot better. I took the Secondary Constructor concept and got it wrong. I use secondary constructors for wrapping things that need touching without touching them in the constructor.
Dependency Constructors simplify the code base by allowing everyone else to be ignorant of how things are accomplished.
When I ask an object to do soemthing for me; I'm not allowed to care how. I don't care what error handling happens. I don't care what REST endpoint auth-refresh happens, or how. I don't need to know what or when a redirect happens.
These aren't important things when I'm asking an object to do something. It won't know how they are all done either. I know that ConcreteBehavior
will do the behavior I want; I new
it up in my Dependency Constructor. It will know what it needs to do what I'm asking - It new
s whatever it needs up. All the way down the chain.
This behaviorless constructor allows us to reliably and safely construct objects. Always able to ensure out instantiation happens. It drives object creation to represent behavior. If we need a string
as an int
; we create a StringToInt
that implements INumber
and we store that. StringToInt
won't do work until we ask for the Number()
.
Only needed
Another aspect I like about logicless constructors is that you will only do work that is needed; and only WHEN it's needed.
I've worked on a a number of client apps that had start up performance issues. A lot of objects were instantiated and doing a lot of stuff. There's not offloading that work; You create an object that does work in the constructor - It's doing all that work on whatever thread you called from. Unless you do thread madness inside a constructor... bleh.
You lose control over when and what-thread the logic is happening at/on. You're giving up control over your object execution time.
It's going to be much harder to refactor or thread the behavior when it's done only at instantiation.
Is the creator of the object going to know the best time/place/way to execute any expensive operations?
Mobile apps have requirements that certain activities are performed on non-IU threads. Why should instantiation of an object require thread handling? That's a pretty bad design.
I've done it - it's bad.
If we do work in the constructor - it always; without choice - will do that work.
By moving the behavior into an object; that performs the work when we ask - A nice "on demand" feature - we have greater flexibility over how we use that data.
We'll perform the logic everytime we call - unless we introduce a cache decorator.
We get the same performance that the busy constructor had - doing it just once; but more options.
I do this for almost every book end in my project at work. We cache the response. Some of the caches time out - and will re-run the execution. If it's done in the constructor we would have to reinstantiate. By doing it outside the constructor; we can re-fetch network data. Or disk data, or do whatever a second time - when our cache expires. Or never. Some we never expire.
Transparency & Guarantees
When you new up a class - What's it doing?
Without looking at it - What does it do? What behavior is it executing?
In my projects I can guarantee you the answer - Nothing.
You can know exactly what the class does when it's instantiated - Nothing.
When you put code into the constructor; you've lost the ability to have this guarantee about the object. An object dependency could change it's behavior when YOU instantiate it without you ever changing your interactions with it.
You are no longer guaranteed anything about the object instantiation. You're guaranteed by interface contract the behavior result. When you put code into the constructor; you take away your consumers ability to control what they cause to happen.
Maintainability
Doing work in the constructor hinders maintainability. Temporally coupled things may be happening in the constructor. If/When there needs to be a refactor - If this temporally coupled behavior get's refactored out of the constructor; your consumers may not be configured or calling in a manner that ensures all those operations are happening in the correct fashion.
You end up forced to change the consumers or have code rot in this class. Refactoring it away further becomes a much bigger and broader change to the code.
Code in the constructor causes non-isolated changes. Updating the constructor-code class will, potentially, force changes to all consumers.
Especially if a behavior is refactored to require new/different arguments - EVERY consumer must be updated - not just the ones that use that behavior.
You're creating a maintainance nightmare by putting code into the constructor.
Leave the constructor code free; the result is a more configurable, transparent, guaranteed, and maintainable class.