I've been following this principle for a long time, but I haven't heard a name for it or anyone talk about it. If I'm first to talk about this, I wouldn't mind it being called Levo's Principle
My Favorite Principle
After an object has been constructed, its behavior should never change
This allows objects to be used more easily, and it's less bug-prone. Let's go over some violations of the rule, then an example.
var obj = new MyObject()
// all violations
obj.init(...);
obj.setup(...)
obj.setOneOrMoreDependencies(...);
obj.setConfig(configFileOrStruct)
obj.setOptions(ObjectOpt::SomeOption)
As an example, let's say we want to create an object where we can add folders to it, and get an iterator that walks over all the files. Two option flags for this object can be
- WalkSubdirectories: Automatically include subfolders without explicitly adding them to the object
- IgnoreDirectories: Do not include directories in the iterator
Using both flags together would mean we don't see folders in the iterator, but the path is added, so the object, so it'll iterate over those files too. Here's an example of not following the rule
var obj = new DirectoryWalker()
obj.addFolder(path1)
obj.setOptions(WalkOpt::WalkSubdirectories | WalkOpt::IgnoreDirs) // wrong
obj.addFolder(path2)
for(var fileEntry in obj.iterator()) { ... }
One big problem with this code is how the options are applied. Does WalkSubdirectories apply to path1 since it's set before the iterator? Was it intentional to set the options after adding the first folder?
Following the rule, you'll get this
var obj = new DirectoryWalker(WalkOpt::WalkSubdirectories | WalkOpt::IgnoreDirs)
obj.addFolder(path1, WalkOpt::None); // Not walking subfolders is intentional
obj.addFolder(path2) // uses the options set in the constructor
for(var fileEntry in obj.iterator())
This code is so much clearer.
- Set options doesn't exist, so we aren't confused if they retroactively apply to previous folders
- It's clearly intentional that path1 should not include directories
- We can refactor and reorder the addFolder calls without breaking anything (unless the code depends on the iterator being in a specific order)
What Happens When You Ignore The Principle
When setup code is separate from the constructor, over time, it tends to get farther and farther from the construction site. There can be multiple places where you call setup, and if setup is allowed to be called numerous times, it will be. Changing one call site may affect more than you intend, and it becomes messy.
When you can change settings, you'll end up in a position where moving a block of code completely breaks it. This could be because a flag you were expecting was never set, because the new location (temporarily) changed flags and the code never properly set them, or because, by chance, all the code leading to the old path was constrained and the block of code accidentally was written to only work with those specific conditions. When refactoring code, you never want to deal with why a seemingly unrelated change suddenly broke a block of code
A significant amount of code isn't part of a library, and much of it isn't documented. When a person has to use an object that doesn't follow the rule, it can be hard to figure out why the object doesn't work the way it does in other parts of the codebase. A person would have to follow construction, potential initialization (or setup) code, configuration code, and less obvious methods that change behavior, etc. It becomes very confusing.
The Only Exception
The only exception I can think of is when the object is meant to be used in a procedural way. For example, a stream parser class. It doesn't make much sense to reorder any part of the code. Most likely, the object will stay within a function.
var r = new StreamParser(filename)
r.expect("version ")
int version = r.Int();
r.expectNL();
r.setAutoWhitespace(1) // will consume 1 or many spaces, tabs, etc
while (!r.EOF()) {
string keyName = r.Var();
r.expect('=');
// handle value
}
I, however, would prefer one change to the above code when possible, but only in complex code. I would prefer complex code to have different objects for different settings. Here we have 'reader' as the initial object, and a second object that auto consumes whitespace.
var reader = new StreamParser(filename)
reader.expect("version ")
int version = reader.Int();
reader.expectNL();
var r = reader.Clone(Opt::AutoConsumeWhitespace)
while (!r.EOF()) {
string keyName = r.Var();
r.expect('=');
// handle value
}
If the code was several hundred lines and spanned multiple functions, it may be easier to have different variables for different setting. This would also make it slightly easier since you wouldn't need to set/clear setting as often.