Skip to content

Latest commit

 

History

History
1258 lines (808 loc) · 22 KB

command-pattern-2017.md

File metadata and controls

1258 lines (808 loc) · 22 KB

footer: © 2017 Reginald Braithwaite. Some rights reserved. slidenumbers: true autoscale: true

^ https://www.flickr.com/photos/fatedenied/7335413942

^ The Command Pattern

^ Note to the reader: This document is written in Markdown, a somewhat human-readable markup language. It is processed by a program called DeckSet, which turns it into a presentation. DeckSet does various things such as treating every paragraph that begins with a caret (^) into a speaker's note that is not visible to the audience.

^ You may be looking at a partially-rendered version of this document, thanks to GitHub's ability to render basic MarkDown in the browser as formatted text. If so, you can look at the original document by clicking the Raw link above.

^ The fully rendered slides are on SpeakerDeck, and you can download a PDF there as well.

^ Some people have pointed out that DeckSet's MarkDown is not an excellent format for reading the material as compared to an essay. This is true. In its favour, you can't read a PowerPoint or KeyNote presentation's raw file at all, nor can you do interesting things like submit your own pull request on GitHub, so MarkDown does have some interesting affordances.

^ Thanks for reading this, and feel free to post an Issue.


First-Class Commands

an unexpectedly fertile design pattern

^ https://www.flickr.com/photos/fatedenied/7335413942


why do we care about commands?


original

^ https://www.flickr.com/photos/fatedenied/7335413942

^ Let's get started


the canonical example:

mutable data


class Buffer {
  constructor (text = '') { this.text = text; }

  replaceWith (replacement, from = 0, to = this.text.length) {
    this.text = this.text.slice(0, from) +
                  replacement +
                  this.text.slice(to);
    return this;
  }

  toString () { return this.text; }
}

let buffer = new Buffer();

buffer.replaceWith(
  "The quick brown fox jumped over the lazy dog"
);
buffer.replaceWith("fast", 4, 9);
buffer.replaceWith("canine", 40, 43);
 //=> The fast brown fox jumped over the lazy canine

buffer

is an object


we treat objects as first-class entities

^ https://www.flickr.com/photos/mwichary/2406482529


replaceWith

is a method


we can treat methods as first-class entities

^ https://www.flickr.com/photos/tompagenet/8580371564

^ e.g. decorators


buffer.replaceWith("fast", 4, 9)

is an invocation


what does it mean to treat an invocation as a first-class entity?

^ https://www.flickr.com/photos/ooocha/2869485136

^ Talk about invocations being ephemeral and tied up with mutable state, some visible, some hidden.


storing invocations

^ https://www.flickr.com/photos/oskay/2550938136


class Edit {
  constructor (buffer, {replacement, from, to}) {
    this.buffer = buffer;
    Object.assign(this, {replacement, from, to});
  }

  doIt () {
    this.buffer.text =
      this.buffer.text.slice(0, this.from) +
      this.replacement +
      this.buffer.text.slice(this.to);
    return this.buffer;
  }
}

class Buffer {
  constructor (text = '') { this.text = text; }

  replaceWith (replacement, from = 0, to = this.text.length) {
    return new Edit(this, {replacement, from, to});
  }

  toString () { return this.text; }
}

let buffer = new Buffer(), jobQueue = [];

jobQueue.push(
  buffer.replaceWith(
    "The quick brown fox jumped over the lazy dog"
  )
);
jobQueue.push( buffer.replaceWith("fast", 4, 9) );
jobQueue.push( buffer.replaceWith("canine", 40, 43) );

while (jobQueue.length > 0) {
  jobQueue.shift().doIt();
}
 //=> The fast brown fox jumped over the lazy canine

^ Presto, a job queue!


job queues

^ https://www.flickr.com/photos/eschipul/1219204898) ^ Deferred execution ^ Asynchronous execution


fit

^ chaining and handling serialization ^ cancel ^ ember concurrency: task instances are like commands, tasks are like job queues


querying invocations

^ https://www.flickr.com/photos/baccharus/4474584940


class Edit {

  netChange () {
    return this.from - this.to + this.replacement.length;
  }

}

let buffer = new Buffer();

buffer.replaceWith(
    "The quick brown fox jumped over the lazy dog"
).netChange();
 //=> 44

buffer.replaceWith("fast", 4, 9).netChange();
 //=> -1

command state


fit

^ status ^ queue control versus debouncing functions


command history


fit

^ logs as first-class entities ^ tracking behaviour

^ traditional logs are terrible and opaque ^ why litter our UX code with tracking invocations?


transforming invocations

^ https://www.flickr.com/photos/micurs/4906349993

^ combinators, combinators, combinators


class Edit {

  reversed () {
    let replacement = this.buffer.text.slice(this.from, this.to),
        from = this.from,
        to = from + this.replacement.length;
    return new Edit(buffer, {replacement, from, to});
  }

}

let buffer = new Buffer(
  "The quick brown fox jumped over the lazy dog"
);

let doer = buffer.replaceWith("fast", 4, 9),
    undoer = doer.reversed();

doer.doIt();
  //=> The fast brown fox jumped over the lazy dog

undoer.doIt();
  //=> The quick brown fox jumped over the lazy dog

all together now

^ https://www.flickr.com/photos/purdman1/2875431305


class Buffer {

  constructor (text = '') {
    this.text = text;
    this.history = [];
    this.future = [];
  }

}

class Buffer {

  replaceWith (replacement, from = 0, to = this.length()) {
    let doer = new Edit(this, {replacement, from, to}),
        undoer = doer.reversed();

    this.history.push(undoer);
    this.future = [];
    return doer.doIt();
  }

}

undo

^ https://www.flickr.com/photos/daryl_mitchell/15427050433


class Buffer {

  undo () {
    let undoer = this.history.pop(),
        redoer = undoer.reversed();

    this.future.unshift(redoer);
    return undoer.doIt();
  }

}

let buffer = new Buffer(
  "The quick brown fox jumped over the lazy dog"
);

buffer.replaceWith("fast", 4, 9)
  //=> The fast brown fox jumped over the lazy dog

buffer.replaceWith("canine", 40, 43)
  //=> The fast brown fox jumped over the lazy canine

buffer.undo()
  //=> The fast brown fox jumped over the lazy dog

buffer.undo()
  //=> The quick brown fox jumped over the lazy dog

redo

^ https://www.flickr.com/photos/the00rig/3753005997


class Buffer {

  redo () {
    let redoer = this.future.shift(),
        undoer = redoer.reversed();

    this.history.push(undoer);
    return redoer.doIt();
  }

}

buffer.redo()
  //=> The fast brown fox jumped over the lazy dog

buffer.redo()
  //=> The fast brown fox jumped over the lazy canine

that's the basic command pattern

^ https://www.flickr.com/photos/robbie1/8656027235


invocations as first-class entities:

we stored them;

we queried them;

we transformed them.

^ https://www.flickr.com/photos/mwichary/2406489333


question!

^ https://www.flickr.com/photos/pedrosimoes7/17386505158


class Buffer {

  replaceWith (replacement, from = 0, to = this.length()) {
    let doer = new Edit(this, {replacement, from, to}),
        undoer = doer.reversed();

    this.history.push(undoer);
    this.future = [];
    return doer.doIt();
  }

}

why do we have to throw the future away?

^ https://www.flickr.com/photos/a-barth/2846621384


class Buffer {

  replaceWith (replacement, from = 0, to = this.length()) {
    let doer = new Edit(this, {replacement, from, to}),
        undoer = doer.reversed();

    this.history.push(undoer);
    // this.future = [];
    return doer.doIt();
  }

}

let buffer = new Buffer(
  "The quick brown fox jumped over the lazy dog"
);

buffer.replaceWith("fast", 4, 9);
  //=> The fast brown fox jumped over the lazy dog

buffer.undo();
  //=> The quick brown fox jumped over the lazy dog

buffer.replaceWith("My", 0, 3);
  //=> My quick brown fox jumped over the lazy dog

what happens when we evaluate buffer.redo()?

^ https://www.flickr.com/photos/mleung311/9468927282


"My qfastbrown fox jumped over the lazy dog"

^ https://www.flickr.com/photos/bludgeoner86/5590795033

^ because our commands are coupled to ephemeral state, changing the state breaks the command


original


let's consider commands as a history

^ https://www.flickr.com/photos/49024304@N00/


let buffer = new Buffer("The quick brown fox jumped over the lazy dog");

"The quick brown fox jumped over the lazy dog"

// PAST

// FUTURE

buffer.replaceWith("fast", 4, 9)

"The fast brown fox jumped over the lazy dog"

// PAST
replaceWith("fast", 4, 9)

// FUTURE

buffer.undo()

"The quick brown fox jumped over the lazy dog"

// PAST

// FUTURE
replaceWith("fast", 4, 9)

buffer.replaceWith("My", 0, 3)

"My quick brown fox jumped over the lazy dog"

// PAST
replaceWith("My", 0, 3)

// FUTURE
replaceWith("fast", 4, 9)

buffer.redo()

"My qfastbrown fox jumped over the lazy dog"

// PAST
replaceWith("My", 0, 3)
replaceWith("fast", 4, 9)

// FUTURE

every command depends on the history of commands preceding it

^ https://www.flickr.com/photos/29143375@N05/4575806708


prepending a command into its history alters the command

^ https://www.flickr.com/photos/30239838@N04/4268147953


let buffer = new Buffer(
  "The quick brown fox jumped over the lazy dog"
);

let fast = new Edit(
    buffer,
    { replacement: "fast", from: 4, to: 9 }
  );

let my = new Edit(
    buffer,
    { replacement: "My", from: 0, to: 3 }
  );

let buffer = new Buffer(
  "The quick brown fox jumped over the lazy dog"
);

let fast = new Edit(
    buffer,
    { replacement: "fast", from: 4, to: 9 }
  );

let my = new Edit(
    buffer,
    { replacement: "My", from: 0, to: 3 }
  );

class Edit {

  isBefore (other) {
    return other.from >= this.to;
  }

}

fast.isBefore(my);
  //=> false

my.isBefore(fast);
  //=> true

class Edit {

  prependedWith (other) {
    if (this.isBefore(other)) {
      return this;
    }
    else if (other.isBefore(this)) {
      let change = other.netChange(),
          {replacement, from, to} = this;

      from = from + change;
      to = to + change;
      return new Edit(this.buffer, {replacement, from, to})
    }
  }

}

my.prependedWith(fast)
  //=> buffer.replaceWith("My", 0, 3)

fast.prependedWith(my)
  //=> buffer.replaceWith("fast", 3, 8)

my.prependedWith(fast)
  //=> buffer.replaceWith("My", 0, 3)

fast.prependedWith(my)
  //=> buffer.replaceWith("fast", 3, 8)

class Buffer {

  replaceWith (replacement, from = 0, to = this.length()) {
    let doer = new Edit(this, {replacement, from, to}),
        undoer = doer.reversed();

    this.history.push(undoer);
    this.future = this.future.map(
      (edit) => edit.prependedWith(doer)
    );
    return doer.doIt();
  }

}

let's start over

^ https://www.flickr.com/photos/benetd/4429314827


let buffer = new Buffer(
  "The quick brown fox jumped over the lazy dog"
);

buffer.replaceWith("fast", 4, 9);
  //=> The fast brown fox jumped over the lazy dog

buffer.undo();
  //=> The quick brown fox jumped over the lazy dog

buffer.replaceWith("My", 0, 3);
  //=> My quick brown fox jumped over the lazy dog

buffer.redo();

"My fast brown fox jumped over the lazy dog"

^ https://www.flickr.com/photos/katiethebeau/16670836007


what did fixing redo teach us about invocations as first-class entities?


"People assume that time is a strict progression of cause to effect, but actually from a non-linear, non-subjective viewpoint—it's more like a big ball of wibbly wobbly… time-y wimey… stuff."

^ https://www.flickr.com/photos/shimgray/2811100997

^ https://www.youtube.com/watch?v=mDsN5lWLKU0


reversed() and prependedWith() show us we can change both the direction and order of time.


^ "Alice B. Toklas and Bob Fosse are editing a script"


let alice = new Buffer(
  "The quick brown fox jumped over the lazy dog"
);

let bob = new Buffer(
  "The quick brown fox jumped over the lazy dog"
);

for simplicity, we'll omit undo , redo and reversed

class Buffer {

  constructor (text = '') {
    this.text = text;
    this.history = [];
  }

}

class Buffer {

  replaceWith (replacement, from = 0, to = this.length()) {
    let edit = new Edit(this,
                   {replacement, from, to}
                 );

    this.history.push(edit);
    return edit.doIt();
  }

}

alice.replaceWith("My", 0, 3);
  //=> My quick brown fox jumped over the lazy dog

bob.replaceWith("fast", 4, 9);
  //=> The fast brown fox jumped over the lazy dog


class Buffer {

  append (theirEdit) {
    this.history.forEach( (myEdit) => {
      theirEdit = theirEdit.prependedWith(myEdit);
    });
    return new Edit(this, theirEdit).doIt();
  }

}

class Buffer {

  appendAll(otherBuffer) {
    otherBuffer.history.forEach(
      (theirEdit) => this.append(theirEdit)
    );
    return this;
  }

}

alice.appendAll(bob);
  //=> My fast brown fox jumped over the lazy dog

bob.appendAll(alice);
  //=> My fast brown fox jumped over the lazy dog

🐛

^ "We have a little bug."


fit

^ "Okay, a big bug! We can't appendAll more than once. Shared mutable data everywhere."


let GUID = () => ???

class Buffer {

  constructor (text = '', history = []) {
    let befores = new Set(history.map(e => e.guid));
    history = history.slice(0);
    Object.assign(this, {text, history, befores});
  }

  share () {
    return new Buffer(this.text, this.history);
  }

}

^ We're going to use guids and a set of before guids in the buffer and the edits

^ let GUID = () => { ^ let _p8 = (s) => { ^ let p = (Math.random().toString(16)+"000000000").substr(2,8); ^ ^ return s ? "-" + p.substr(0,4) + "-" + p.substr(4,4) : p ; ^ } ^ return _p8() + _p8(true) + _p8(true) + _p8(); ^ }


class Edit {

  constructor (buffer,
    { guid = GUID(), befores = new Set(),
      replacement, from, to }) {
    this.buffer = buffer;
    befores = new Set(befores);

    Object.assign(this,
                  {guid, replacement, from, to, befores});
  }

}

class Buffer {

  has (edit) { return this.befores.has(edit.guid); }

  perform (edit) {
    if (!this.has(edit)) {
      this.history.push(edit);
      this.befores.add(edit.guid);
      return edit.doIt();
    }
  }

}

^ now we check edits before we perform them


class Buffer {

  replaceWith (replacement,
               from = 0, to = this.length()) {
    let befores = this.befores,
    let edit = new Edit(this,
                   {replacement, from, to, befores}
                 );
    return this.perform(edit);
  }

}

^ simplifies replaceWith, append, and appendAll


class Buffer {

  append (theirEdit) {
    this.history.forEach( (myEdit) => {
      theirEdit = theirEdit.prependedWith(myEdit);
    });
    return this.perform(new Edit(this, theirEdit));
  }

}

class Buffer {

  appendAll(otherBuffer) {
    otherBuffer.history.forEach(
      (theirEdit) =>
        this.has(theirEdit) || this.append(theirEdit)
    );
    return this;
  }

}

class Edit {

  prependedWith (other) {
    if (this.isBefore(other) ||
        this.befores.has(other.guid) ||
        this.guid === other.guid) return this;

    let change = other.netChange(),
        {guid, replacement, from, to, befores} = this;

    from = from + change;
    to = to + change;
    befores = new Set(befores);
    befores.add(other.guid);

    return new Edit(this.buffer, {guid, replacement, from, to, befores});
  }

}

^ "Alice, Bob, and Carol are editing a script"


let alice = new Buffer(
  "The quick brown fox jumped over the lazy dog"
);

let bob = alice.share();
  //=> The quick brown fox jumped over the lazy dog

alice.replaceWith("My", 0, 3);
  //=> My quick brown fox jumped over the lazy dog

let carol = alice.share();
  //=> My quick brown fox jumped over the lazy dog

bob.replaceWith("fast", 4, 9);
  //=> The fast brown fox jumped over the lazy dog

alice.appendAll(bob);
  //=> My fast brown fox jumped over the lazy dog

bob.appendAll(alice);
  //=> My fast brown fox jumped over the lazy dog

alice.replaceWith("spotted", 8, 13);
  //=> My fast spotted fox jumped over the lazy dog

bob.appendAll(alice);
  //=> My fast spotted fox jumped over the lazy dog

carol.appendAll(bob);
  //=> My fast spotted fox jumped over the lazy dog

"Unfortunately, implementing OT sucks. There's a million algorithms with different tradeoffs, mostly trapped in academic papers. The algorithms are really hard and time consuming to implement correctly."

^ https://www.flickr.com/photos/wordridden/4308645407

^ Joseph Gentle, from https://en.wikipedia.org/wiki/Operational_transformation

^ too much responsibility in edits, and we have an XY problem


perhaps we should borrow a trick from react, and periodically scan a "shadow buffer" for diffs that we exchange with collaborators…

^ congratulations, now we're talking differential synchronization. See Electron, Google Docs

^ See also: https://neil.fraser.name/writing/sync/


differential synchronization


how does differential synchronization differ from the command pattern?

^ lose the semantics

^ but more flexible!


this is a very big problem space


original

^ https://www.flickr.com/photos/sidelong/18620995913

^ There are only two hard problems in computer science: Cache invalidation, and naming things--Phil Karleton

^ leads into discussing what we call merging and version control


class Buffer {

  replaceWith () { ... }

  share () { ... }

  append () { ... }

  appendAll () { ... }

}

class Branch {

  commit () { ... }

  fork () { ... }

  cherryPick () { ... }

  merge () { ... }

}

distributed version control

^ git is differential

^ what we lose is semantics like "rename this variable"

^ what we win is flexibility: any tool can perform edits


with invocations as first-class entities, we can build distributed algorithms and protocols; we can master time and change


original

^ https://www.flickr.com/photos/stawarz/3848824508

^ today's users expect real-time and collaborative applications


fit

^ real-time and interactive is the default. we really need to take another look at EVERY single-user batch-and-submit UX interaction and rethink it.

^ Why can't support help me fill out a form?

^ Why can't multiple people collaboratively edit a form?

^ Why don't things we save have version control?

^ Why can't we merge changes from multiple forks of a form?


down, tiger!

^ https://www.flickr.com/photos/mwichary/3338901313

^ These are excellent ideas, but somewhat "boil the ocean."

^ What can we do modestly? How do we get started?


separation of concerns

^ https://www.flickr.com/photos/churchhatestucker/2472583642

^ this is the big wrap-up: get a lot of code out of the UX and out of the persistence mechanism


Reg Braithwaite
PagerDuty, Inc.

right, fit

^ https://leanpub.com/javascriptallongesix