Practical Object-Oriented Design in JavaScript

Adam Waselnuk (@awazels)
Technical Lead at Shopify

The POODJ examples repo

Who here has seen something like this before?


    class Animal {
      constructor(name) {
        this.name = name;
      }

      speak() {
        console.log(`I am ${this.name}.`);
      }
    }

    class Cat extends Animal {
      speak() {
        console.log(`I like to meow and poop on your bed.`);
      }
    }
  

Object-Oriented Design

“The goal of design is to arrange your software so that it does what it is supposed to right now and is also easy to change later.

Creating this quality of easy changeability reveals the craft of programming.” - Sandi Metz

Object-Oriented Design

This is a bad name.

“I'm sorry that I long ago coined the term 'objects' for this topic because it gets many people to focus on the lesser idea.” - Alan Kay


    cat         // object
      .speak()  // message
  

“You don’t send messages because you have objects, you have objects because you send messages”

- Sandi Metz

Some concepts

  1. Single Responsibility Principle
  2. Dependencies
  3. Interfaces

WARNING!!

Production code ahead.

Ish might get real.

Single Responsibility Principle

SRP - Classes

  • Pretend it is a person and ask it questions
  • A better name might be Single Changeability principle

    class EmailForm {
      render() { ... }

      submit() { ... }

      validate() { ... }

      trackUserDataWithoutConsent() { ... }
    }
  

    class EmailForm {
      // Mr. EmailForm how do you look in the DOM?
      // Pretty good thanks
      render() { ... }
      // Mr. EmailForm can you please submit yourself?
      // Yep.
      submit() { ... }
      // Mr. EmailForm can you validate something?
      // Hmm, maybe. Depends what I'm validating.
      validate() { ... }
      // Mr. EmailForm can you please track everything?
      // Maybe not, ask Mr. Tracky
      trackUserDataWithoutConsent() { ... }
    }
  

SRP - Methods and Functions

  • SRP is fractal. Try it on methods too!
  • Make implicit things explicit and avoid the need for comments
  • Encourage reuse
  • Easy to move around to other classes

    function printReport(report) {
      // our printer can't handle emojis so we need to remove them
      report = report.replace(/!&!(%^%^#*)/g, '');
      console.log(report);
    }
  

    function printReport(report) {
      console.log(prepareForPrint(report));
    }

    function prepareForPrint(printable) {
      const BLACKLIST = /!&!(%^%^#*)/g;
      return printable.replace(BLACKLIST, '');
    }
  

* note that my made up regex might not work as advertised

Consider this real world example.

Dependencies

Recognition is your most important first step! An object has a dependency when it knows:

  • The name of another class
  • The name of a message it intends to send to someone other than this
  • Arguments that a message requires
  • The order of arguments that a message requires

    import Turkey from './Turkey.js';
    import Soup from './Soup.js';

    class Dinner {
      serve() {
        new Turkey().serve();
        new Soup().serve(3, true); // 3 scoops with garnish
      }
    }
  

Dependency Injection

  • Helps you not know the name of a class you depend on
  • Makes things very highly decoupled and therefore, much easier to test
  • Opens up the possibility of duck-typing (not covered in this talk but POODR has a whole chapter on it or come talk to me later)

Dependency Injection


    import Turkey from './Turkey.js';

    class Dinner {
      constructor(turkey) {
        this.turkey = turkey;
      }

      serve() {
        this.turkey.serve();
      }
    }
  

Isolate dependencies

  • Constraints or convenience might make it unpractical to remove dependencies
  • Put a useful barrier around your class' dependencies that makes them easy to spot
  • Make it easy to recognize and remove should that moment ever arrive

Isolate dependencies


    import Turkey from './Turkey.js';

    class Dinner {
      serve() {
        this.turkey.serve();
      }

      // Isolate the evil invader
      get turkey() {
        this.turkeyCache = this.turkeyCache || new Turkey();
        return this.turkeyCache;
      }
    }
  

Use named arguments

  • Remove order dependencies
  • Leave a really nice label for programmers
  • Keep in mind we mostly read code

Use named arguments


    // Dependency madness
    soup.serve(3, true) // 3 scoops with garnish added
  

    // So clear
    soup.serve({scoops: 3, withGarnish: true})
  

    // Might be cool?
    soup.serve({scoops: 3, extras: ['garnish']})
  

Let's look at some real code.

Interfaces

“Think about interfaces. Create them intentionally. It is your interfaces more than all of your tests and any of your code, that define your application and determine its future.”

- Sandi, of course

Craft explicit and flexible interfaces

Public interfaces should:

  • Be explicity identified as public
  • Be more about what than how
  • Have names that, as much as you know, won't change

“Private” methods in JavaScript


    // Developers use underscores

    class Spaceship {
      // public
      fly() { ... }
      refuel() { ... }

      // private
      _calculateRemainingFuel() { ... }
      _preflightMusicCheck() { ... }
    }
  

“Private” methods in JavaScript


    // Revealing module pattern

    const spaceship = (() => {
      const fly = () => { ... }
      const refuel = () => { ... }

      // these two functions are only available in closure
      const calculateRemainingFuel = () => { ... }
      const preflightMusicCheck = () => { ... }

      // the IIFE returns the public API
      return {
        fly,
        refuel
      };
    })();
  

“Private” methods in JavaScript


    // The future
    // https://github.com/tc39/proposal-private-methods

    class Spaceship {
      // public
      fly() { ... }
      refuel() { ... }

      // private
      #calculateRemainingFuel() { ... }
      #preflightMusicCheck() { ... }
    }
  

Be more about what than how

  • Size of public interface drops considerably which means less complexity and coupling.
  • Flexibility is improved because now you can trust collaborators to evolve their internals and you don’t need to care about how they did it.

Evolution goes from procedural to OO as trust improves:

  1. I know what I want and I know how you do it
  2. I know what I want and I know what you do
  3. I know what I want and I trust you to do your part

Improving trust


    import Spaceship from './Spaceship.js';

    class Astronaut {
      constructor() {
        this.spaceship = new Spaceship();
      }

      // I know what I want and I know how you do it
      goToSpaceNow() {
        if (this.spaceship.fuel < 10) {
          this.spaceship.refuel();
        }

        if (this.spaceship.flightReady) {
          this.spaceship.fly = true;
        } else (
          this.spaceship.preflightMusicCheck();
          this.spaceship.fly = true;
        )
      }
    }
  

Improving trust


    import Spaceship from './Spaceship.js';

    class Astronaut {
      constructor() {
        this.spaceship = new Spaceship();
      }

      // I know what I want and I know what you do
      goToSpaceNow() {
        this.spaceship.refuel();
        this.spaceship.preflightMusicCheck();
        this.spaceship.startEngine();
        this.spaceship.takeOff();
      }
    }
  

Improving trust


    import Spaceship from './Spaceship.js';

    class Astronaut {
      constructor() {
        this.spaceship = new Spaceship();
      }

      // I know what I want and I trust you to do your part
      // Notice the shrinking public API
      goToSpaceNow() {
        this.spaceship.fly({astronaut: this});
      }
    }
  

One final flight to production code