Open-Closed Principle
by Julian Rubisch
Good software design is - in part - realized by the capability to introduce changes in a way that isn’t painful. (If you’d like to read up on code smells that violate this principle, take a look at this list of change preventers.)
The open-closed principle is a guideline that, if adhered to, makes this simple: Software entities should be
- closed for modification, but
- open to extension.
What does that mean? Let’s look at an example:
Bad
export default class WidgetController extends Controller { static values = { type: String, toggled: Boolean }; async connect() { switch(this.typeValue) { case "toggle": this.toggledValue = false; break; case "dropdown": this.options = await fetch(`...`); break; } } }
Trivial as this case may be, consider what it would entail to add a new type of widget, say a slider
? Correct, we’d have to add a new case
clause each time, further diluting the class’s state (typeValue
, options
) with new properties.
What’s more, if there were child classes of WidgetController
, they’d be exposed to the risk of breaking whenever connect
is altered.
Good
// Base controller for UI widgets export default class WidgetController extends Controller { connect() { this.setup(); } } export default class ToggleController extends WidgetController { static values = { toggled: Boolean }; setup() { super.setup(); this.toggledValue = false; } } export default class DropdownController extends WidgetController { async setup() { super.setup(); this.options = await fetch(`...`); } }
Contrast this to the example above: A specialized setup()
method is overridden in the individual widget classes. The state definitions are private to the child classes, so that adding new widgets is as simple as adding a new class that inherits from WidgetController
.
Omitted here, but common practice, are method definitions in the base class that are to be polymorphically overridden in the descendants - think disabled()
etc.