Skip to content

Instantly share code, notes, and snippets.

@brasten
Last active January 28, 2019 16:06
Show Gist options
  • Save brasten/f87b9bb470973dd5ee9de0760f1c81c7 to your computer and use it in GitHub Desktop.
Save brasten/f87b9bb470973dd5ee9de0760f1c81c7 to your computer and use it in GitHub Desktop.
Default method for invoking objects in JavaScript

Proposal: "Callable" objects via Symbol.apply function

Objects w/ default method can be invoked like a function.

Problem

Objects that are well constrained (single responsibility) can tend to end up with a single method, or at least a single method that is important to most consumers. These methods tend to be named by either verbing the class name (eg. UserCreator.create()) or with some generic handle / perform / doTheObviousThing.

Whatever the name, downstream consumers of the object end up coupled to two implementation details:

  1. this thing-doer is an object and not a function
  2. this thing-doer's doing method is called X

Example

Here we are going to create an object that can be used to create a user later. Note that downstream consumers will only care that this object does one thing: create a user. While it make have other methods eventually for use in some limited contexts, creating a user is its primary (and often sole-) responsibility.

class UserCreator {
  constructor(repository) {
    this.repository = repository; 
  }

  create(name) {
     return this.repository.createUser(name);
  }  
}

const userCreator = new UserCreator(userRepository);

At this point, the userCreator is just a single-method object. It is useful for injecting into other objects that may need to create a user. But the fact that the userCreator is an object with a single useful method is an implementation detail to which consumers become coupled.

// Consumer of `userCreator`. Although this could itself be a
// good example of a "UserCreator"-like object (due to `.handle()`).
//
class UserSignupHandler {
  constructor(userCreator) {
    this.userCreator = userCreator;
  }
  
  handle(userName) {
    // UserSignupHandler is aware of ".create" when it really doesn't have to be.
    //
    return this.userCreator.create(userName);
  }
}

const handler = new UserSignupHandler(userCreator);

Notably, if we were to change the implementation of UserCreator later to be a pure function, we would have to change all consumers of UserCreator when conceptually it shouldn't be needed. There is still a thing-doer that has the same input/output.

Proposed Solution

An object instance can have a default method. This would allow an object to be "invoked" exactly like a function, hiding the implementation detail from consumers.

Note that there are several ways to define how the default method is determined, and this proposal is less concerned with this aspect than with what it looks like to invoke the object. We will demonstrate an option here, but alternatives are welcome.

// This particular implementataion would use a Symbol.
//

class UserCreator {
  constructor(repository) {
    this.repository = repository; 
  }

  [Symbol.apply](name) {
     return this.repository.createUser(name);
  }  
}

const userCreator = new UserCreator(userRepository);

class UserSignupHandler {
  constructor(userCreator) {
    // NOTE: at the consumer, it almost makes more sense to
    // name these with action verbs, as is done here.
    //
    this.createUser = userCreator;
  }
  
  handle(userName) {
    // UserSignupHandler is no longer coupled to the implementation details it doesn't need.
    //
    return this.createUser(userName);
  }
}

const handler = new UserSignupHandler(userCreator);

References

Scala's apply function provides a similar feature in that language: https://blog.matthewrathbone.com/2017/03/06/scala-object-apply-functions.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment