Open–closed principle

The open–closed principle (OCP) states the following:

Software entities (classes, modules, functions, and so on) should be open for extension, but closed for modification
                                                                                                                                                                            -Meyer, Bertrand (1988)

When crafting abstractions, we should enable them to be open to extension so that other developers can come along and build upon their behavior, adapting the abstraction to suit their needs. Extension, in this context, is best thought of as a broad term that encompasses all types of adaptation. If a module or function does not behave as we require it to, it would be ideal for us to be able to adapt it to our needs without having to modify it or create our own alternative.

Consider the following Event class and renderNotification method from our Calendar application:

class Event {

renderNotification() {
return `
You have an event occurring in
${this.calcMinutesUntil()} minutes!
`;
}

// ...

}

We may wish to have a separate type of event that renders a notification prefixed with the word Urgent! to ensure that the user pays more attention to it. The simplest way to achieve this adaptation is via inheritance of the Event class, as follows:

class ImportantEvent extends Event {
renderNotification() {
return `Urgent! ${super.renderNotification()}`;
}
}

We are prefixing our urgent message by overriding the renderNotification method and calling the super class's renderNotification to fill in the remainder of the notification string. Here, via inheritance, we have achieved extension, adapting the Event class to our needs.

Inheritance is only one way that extension can be achieved. There are various other approaches that we could take. One possibility is that, in the original implementation of Event, we foresee the need for custom notification strings and implement a way to configure a renderCustomNotifcation function:

class Event {

renderNotification() {
const defaultNotification = `
You have an event occurring in
${this.calcMinutesUntil()} minutes!
`;
return (
this.config.renderCustomNotification
? this.config.renderCustomNotification(defaultNotification)
: defaultNotification
);
}

// ...

}

This code presumes that there is a config object available. We are optionally calling the renderCustomNotification and passing the default notification string. If it hasn't been configured, then the default notification string is used anyway. This is crucially different from the inheritance approach in that the Event class itself is prescribing the itself is prescribing the possibilities possibilities extension that exist.

Providing adaptability via configuration means that users don't need to worry about the internal implementation knowledge required when extending classes. The path to adaptation is simplified:

new Event({
title: 'Doctor Appointment',
config: {
renderCustomNotification: defaultNotification => {
return `Urgent! ${defaultNotifcation}`;
}
}
});

This approach requires that your implementation can foresee its most likely adaptations and that those adaptations are predictably internalized into the abstraction itself. However, it is impossible to foresee all needs, and even if we tried to, we would likely end up creating such a complicated and large configuration that users and maintainers would suffer. So there is a balance to strike here: adaptability is a good thing, but we also must balance it with a focused and cohesive abstraction that has a constrained purpose. 

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.190.182