Bad:

    Good:

    1. constructor(user) {
    2. this.user = user;
    3. }
    4. verifyCredentials() {
    5. // ...
    6. }
    7. }
    8. class UserSettings {
    9. constructor(user) {
    10. this.user = user;
    11. this.auth = new UserAuth(user);
    12. }
    13. changeSettings(settings) {
    14. if (this.auth.verifyCredentials()) {
    15. // ...
    16. }
    17. }
    18. }

    As stated by Bertrand Meyer, “software entities (classes, modules, functions,
    etc.) should be open for extension, but closed for modification.” What does that
    mean though? This principle basically states that you should allow users to
    add new functionalities without changing existing code.

    Bad:

    1. class AjaxAdapter extends Adapter {
    2. constructor() {
    3. super();
    4. this.name = 'ajaxAdapter';
    5. }
    6. }
    7. class NodeAdapter extends Adapter {
    8. constructor() {
    9. super();
    10. this.name = 'nodeAdapter';
    11. }
    12. }
    13. class HttpRequester {
    14. constructor(adapter) {
    15. this.adapter = adapter;
    16. }
    17. fetch(url) {
    18. if (this.adapter.name === 'ajaxAdapter') {
    19. return makeAjaxCall(url).then((response) => {
    20. // transform response and return
    21. });
    22. } else if (this.adapter.name === 'httpNodeAdapter') {
    23. return makeHttpCall(url).then((response) => {
    24. // transform response and return
    25. });
    26. }
    27. }
    28. }
    29. function makeAjaxCall(url) {
    30. // request and return promise
    31. }
    32. function makeHttpCall(url) {
    33. // request and return promise
    34. }

    Good:

    The best explanation for this is if you have a parent class and a child class,
    then the base class and child class can be used interchangeably without getting
    incorrect results. This might still be confusing, so let’s take a look at the
    classic Square-Rectangle example. Mathematically, a square is a rectangle, but
    if you model it using the “is-a” relationship via inheritance, you quickly
    get into trouble.

    Bad:

    1. class Rectangle {
    2. constructor() {
    3. this.width = 0;
    4. this.height = 0;
    5. }
    6. setColor(color) {
    7. // ...
    8. }
    9. render(area) {
    10. // ...
    11. }
    12. setWidth(width) {
    13. this.width = width;
    14. }
    15. setHeight(height) {
    16. this.height = height;
    17. }
    18. getArea() {
    19. return this.width * this.height;
    20. }
    21. }
    22. class Square extends Rectangle {
    23. setWidth(width) {
    24. this.width = width;
    25. this.height = width;
    26. }
    27. setHeight(height) {
    28. this.width = height;
    29. this.height = height;
    30. }
    31. }
    32. function renderLargeRectangles(rectangles) {
    33. rectangles.forEach((rectangle) => {
    34. rectangle.setWidth(4);
    35. rectangle.setHeight(5);
    36. const area = rectangle.getArea(); // BAD: Returns 25 for Square. Should be 20.
    37. rectangle.render(area);
    38. });
    39. }
    40. const rectangles = [new Rectangle(), new Rectangle(), new Square()];
    41. renderLargeRectangles(rectangles);

    Good:

    1. class Shape {
    2. setColor(color) {
    3. // ...
    4. }
    5. render(area) {
    6. // ...
    7. }
    8. }
    9. class Rectangle extends Shape {
    10. constructor(width, height) {
    11. super();
    12. this.width = width;
    13. this.height = height;
    14. }
    15. getArea() {
    16. return this.width * this.height;
    17. }
    18. }
    19. class Square extends Shape {
    20. constructor(length) {
    21. super();
    22. this.length = length;
    23. }
    24. return this.length * this.length;
    25. }
    26. function renderLargeShapes(shapes) {
    27. shapes.forEach((shape) => {
    28. const area = shape.getArea();
    29. shape.render(area);
    30. });
    31. }
    32. const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
    33. renderLargeShapes(shapes);

    JavaScript doesn’t have interfaces so this principle doesn’t apply as strictly
    as others. However, it’s important and relevant even with JavaScript’s lack of
    type system.

    ISP states that “Clients should not be forced to depend upon interfaces that
    they do not use.” Interfaces are implicit contracts in JavaScript because of
    duck typing.

    Bad:

    Good:

    1. class DOMTraverser {
    2. constructor(settings) {
    3. this.settings = settings;
    4. this.options = settings.options;
    5. this.setup();
    6. }
    7. setup() {
    8. this.rootNode = this.settings.rootNode;
    9. this.setupOptions();
    10. }
    11. setupOptions() {
    12. if (this.options.animationModule) {
    13. // ...
    14. }
    15. }
    16. traverse() {
    17. // ...
    18. }
    19. }
    20. const $ = new DOMTraverser({
    21. rootNode: document.getElementsByTagName('body'),
    22. options: {
    23. animationModule() {}
    24. }
    25. });

    This principle states two essential things:

    1. High-level modules should not depend on low-level modules. Both should
      depend on abstractions.
    2. Abstractions should not depend upon details. Details should depend on
      abstractions.

    This can be hard to understand at first, but if you’ve worked with AngularJS,
    you’ve seen an implementation of this principle in the form of Dependency
    Injection (DI). While they are not identical concepts, DIP keeps high-level
    modules from knowing the details of its low-level modules and setting them up.
    It can accomplish this through DI. A huge benefit of this is that it reduces
    the coupling between modules. Coupling is a very bad development pattern because
    it makes your code hard to refactor.

    As stated previously, JavaScript doesn’t have interfaces so the abstractions
    that are depended upon are implicit contracts. That is to say, the methods
    and properties that an object/class exposes to another object/class. In the
    example below, the implicit contract is that any Request module for an
    InventoryTracker will have a requestItems method.

    1. class InventoryRequester {
    2. constructor() {
    3. this.REQ_METHODS = ['HTTP'];
    4. }
    5. requestItem(item) {
    6. // ...
    7. }
    8. }
    9. class InventoryTracker {
    10. constructor(items) {
    11. this.items = items;
    12. // BAD: We have created a dependency on a specific request implementation.
    13. // We should just have requestItems depend on a request method: `request`
    14. this.requester = new InventoryRequester();
    15. }
    16. requestItems() {
    17. this.items.forEach((item) => {
    18. this.requester.requestItem(item);
    19. });
    20. }
    21. }
    22. const inventoryTracker = new InventoryTracker(['apples', 'bananas']);
    23. inventoryTracker.requestItems();

    Good: