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

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.

Reference