Andreas Söderlund
Uniting art and engineering in code

Uniting art and engineering in code

DCI tutorial for TypeScript: Part 4

DCI tutorial for TypeScript: Part 4

Andreas Söderlund's photo
Andreas Söderlund
·Nov 5, 2022·

7 min read

Ready for more DCI? If you're new, check out the other parts first.

We talked a lot about Roles in the previous part, concluding that Roles are a quite natural way of expressing ourselves in code. This has led to many attempts at adding Roles to languages without introducing additional syntax. Which has also been a huge source of misunderstanding regarding DCI, so lets clear this one once and for all.

DCI Roles cannot be wrappers

Who needs the underscore RoleMethod convention? Let's use objects instead for the example in Part 3:

const Messages = (function(
  Messages: {
    [Symbol.iterator]: () => IterableIterator<HTMLElement>;
  }
) {
  function set(display: string, name = '') {
    for (const msg of Messages) {
      if (name && msg.dataset.formMessage != name) continue;
      msg.style.display = display;
    }
  }
  return {
    show(name: string) {
      set('unset', name);
    },
    hide() {
      set('none');
    },
  };
})(e.target.querySelectorAll('[data-form-message]'));

// Now we're free to use the Role with normal method syntax:
Messages.hide()

This seems to be working, but unfortunately isn't. This has been the number one obstacle when trying to understand DCI: Failing to understand that you cannot wrap a Role in, or substitute it for another object. By doing that you are violating object identity, which could work in many cases, but not all, and that kind of uncertainty is not what we want in software.

The problem is that any other part of the program, and that can be a huge part considering the number of external dependencies of a project, doing an identity (== or ===) check on the Messages Role will now fail this check, even though it was supposed not to.

This FAQ entry explains the whole thing in more detail, and I've made an example on Stackblitz that shows how these bugs can appear, even in a strongly typed language like TypeScript.

Now that we have gotten this out of the way, let's move on to more interesting things.

Adding actual interaction

A question was asked at the end of Part 3: If there is only one Role, can there be interaction?

Interaction presupposes more than one part, so the answer seems to be no, and this is also the case with DCI. Programs with minimal object interaction are probably just as readable without Roles, data transforming with functional programming comes to mind here as well. But it's worth considering that something simple can grow more complicated, and the more variables introduced, the greater the chance that some interaction happens between them.

Looking at our SubmitForm Context again, is it too simple for Roles?

// @DCI-context
function SubmitForm(e: SubmitEvent) {
  e.preventDefault();
  if (!(e.target instanceof HTMLFormElement)) 
    throw new Error('No form found.');

  // Role
  const Messages: {
    [Symbol.iterator]: () => IterableIterator<HTMLElement>;
  } = e.target.querySelectorAll('[data-form-message]');

  // RoleMethods
  const Messages__set = (display: string, name = '') => {
    for (const msg of Messages) {
      if (name && msg.dataset.formMessage != name) continue;
      msg.style.display = display;
    }
  };

  const Messages_show = (name: string) => Messages__set('unset', name);

  const Messages_hide = () => Messages__set('none');

  {
    // System operation
    const form: HTMLFormElement = e.target as HTMLFormElement;

    Messages_hide();
    fetch(form.action, { method: 'POST' })
      .then((response) => response.json())
      .then((data) => data.errors?.forEach(Messages_show));
  }
}

Well, something seems to interacting in the system operation:

{
  // System operation

  // We're creating a new variable here:
  const form: HTMLFormElement = e.target as HTMLFormElement;

  // This is the actual system operation - a RoleMethod call
  Messages_hide();

  // Form submit behavior
  fetch(form.action, { method: 'POST' })
    .then((response) => response.json())
    // Interaction with Messages.show
    .then((data) => data.errors?.forEach(Messages_show));
}

I'd say we have created a Role without really noticing:

  • An identifier (form) is created and assigned
  • It has behavior (submit)
  • It interacts with the Messages role.

This means that we in principle have two Roles, and therefore interaction! Let's modify the Context to use a Form Role instead:

// @DCI-context
function SubmitForm(e: SubmitEvent) {
  e.preventDefault();
  if (!(e.target instanceof HTMLFormElement)) throw new Error('No form found.');

  // Role
  const Form: {
    action: string;
  } = e.target;

  // RoleMethods
  const Form_submit = () => {
    Messages_hide();
    fetch(Form.action, { method: 'POST' })
      .then((response) => response.json())
      .then((data) => data.errors?.forEach(Messages_show));
  };

  // Messages role omitted, it's unchanged.
  // ...

  {
    // System operation
    Form_submit();
  }
}

It's now more apparent what's going on. The Role contract is literal, so we can immediately see that the object playing the Form Role only needs an action property to do that. And remember that the action property is protected from access by the rest of the Context. If we want to access it, we have to be explicit and use the Form's public RoleMethods.

Distributing the interaction

What we have done now, however, is to create an imperative, all-knowing Role that limits the DCI idea of describing a network of interacting objects. Putting the interactions in one place like this is basically a step back to procedural programming (as was the previous system operation, though perhaps simple enough to get away with it).

Older languages like Fortran and Pascal worked in sequence and told in detail to the computer what to execute. This is not what we want, since objects in the network are non-static entities. By strictly organizing them, telling them what to do and handle their return values, we prevent interaction and communication. This FAQ entry explains in more depth.

Instead of limiting communication, let's think of how our objects could interact and communicate in a more dynamic manner. Here is a breakdown of the code in its current, procedure-like fashion:

  1. Tell the messages to hide
  2. Fetch the form action and return the errors
  3. Tell the messages to show the errors

Let's instead distribute this algorithm in an object-oriented fashion:

DCI distributed algorithm@2x.png

Now the RolePlayers are communicating in a message-passing manner, which is not only more connected to mental modelling and DCI concepts, it is also more polite and friendly:

  1. User: Messages, can you hide?
  2. Messages: (Hiding) Form, can you submit?
  3. Form: (Submitting) Messages, can you show these errors?
  4. Messages: Hey User, here are some error messages.

Now we're asking, not telling. By asking, we get rid of the assumption that the object can always do what we want (a more reasonable error handling, in other words). We will talk more about error handling in the next part.

With this, I think we're ready to update the Context (code on Stackblitz).

// @DCI-context
function SubmitForm(e: SubmitEvent) {
  e.preventDefault();
  if (!(e.target instanceof HTMLFormElement)) 
    throw new Error('No form found.');

  // Role
  const Form: {
    action: string;
  } = e.target;

  // RoleMethods
  const Form_submit = async () => {
    const response = await fetch(Form.action, {
      method: 'POST',
      body: new FormData(e.target as HTMLFormElement),
    });
    const data = await response.json();

    // Role communication/interaction
    data.errors?.forEach(Messages_show);
  };

  // Role
  const Messages: {
    [Symbol.iterator]: () => IterableIterator<HTMLElement>;
  } = e.target.querySelectorAll('[data-form-message]');

  // RoleMethods
  const Messages_hide = () => {
    Messages__set('none');
    // Role communication/interaction
    Form_submit();
  };

  const Messages_show = (name: string) => {
    Messages__set('unset', name);
  };

  const Messages__set = (display: string, name = '') => {
    for (const msg of Messages) {
      if (name && msg.dataset.formMessage != name) continue;
      msg.style.display = display;
    }
  };

  {
    // System operation
    Messages_hide();
  }
}

The interactions now follows our mental model very closely. For further analysis, tracing a code path through the Context for this specific Interaction can be done with a sequence diagram:

DCI sequence diagram@2x.png

"Rewiring" the Context

The flexibility of a distributed algorithm like this will be more evident as the system grows. By avoiding return values (though they are not forbidden), the parts of the system becomes more modular, and as functionality changes we can more easily "rewire" the interactions similar to patching an audio effect rack, where a cable can send messages to another part of the rack.

Rewiring is much harder when we rely on return values to tell the system what to do next. It creates a coupling to the return value type, takes us away from the idea of mental modelling, and invokes the imperative programming style that isn't very object-oriented.

Context visualization

Looking at physical cables we can see how messages could travel, so it can help visualizing the system, until it gets too messy. Being able to pass messages without physical cables avoids the mess, but on the other hand makes debugging less practical. :) Previously we've had trouble determining what part of the system to diagram, since behavior was spread out through multiple abstraction boundaries. The boundary of a Context though, as a distinct unit of system behavior, makes it appropriate for visualization tools to display its interactions, as I've experimented with earlier:

DCI Context visualization.png

The Roles have different colors, the inner circle are RoleMethods, and behind them are the Role contract fields. I can put an interactive diagram online later, if there is enough interest.

In the next part

In the next part of this series, we will start to formalize the mental models, so they can be used more effectively to describe business value. See you soon again!

 
Share this