Inside and Outside
Messages flowing into and within the boundaries of a bus
Previously, we reached an awkward half way point.
We sent our Cart
messages describing events in time.
import { Cart } from "./Cart.js";
const cart = Cart();
// 2 yoyos
cart(Message(AddedToCart, { …yoyo, quantity: 2 }));
// 1 skpping rope
cart(Message(AddedToCart, { …skippingRope, quantity: 1 }));
// checkout
const cartTotals = cart(Message(AtCheckout));
console.log(cartTotals.items, "items, total cost:", cartTotals.total);
But, we coupled our script to cart
’s implementation of handling AtCheckout
.
Instead of relying on messages, we implied a contract. We depended on the cart
to return a cartTotals message. In effect we commanded the cart
to calculate the totals for us.
Message passing allows us to react to messages instead of commanding an action:
import shopping from "./objects/shopping.js";
// 2 yoyos
shopping(AddedToCart({ ...yoyo, quantity: 2 }));
// 1 skpping rope
shopping(AddedToCart({ ...skippingRope, quantity: 1 }));
// checkout
shopping(AtCheckout());
Now we don’t even have direct access to the cart
object. But we do have access to a shopping
object.
Inside and Outside
Our shopping
object represents a boundary of sorts. From outside, we can send messages to the object, but other objects exist inside the boundary:
// objects/shopping.js
import { Cart } from “../factories/Cart.js”;
import { Bus } from "../factories/Bus.js";
import { Checkout } from "../factories/Checkout.js";
export default Bus([
Cart(),
Checkout()
]);
Unbeknownst to other objects, our shopping object is a Bus
. Outwardly, it receives messages just like all our other objects.
Internally, it contains one of our original Cart
objects, and a new Checkout
object.
Message flow within the bus
When the bus receives a message, it immediately does two things:
- it places the message in a queue.
- it executes a loop to process the queue.
Queue processing sends the message to each of the objects internal to the bus. But, it also places any resulting messages back into the queue.
The bus continues to process messages until the queue empties.
// factories/Bus.js
export function Bus(components) {
const queue = [];
return message => {
queue.push(message);
processMessageQueue();
};
function processMessageQueue() {
while(queue.length) {
// next message to dispatch
const next = queue.shift();
// call all components with the next message;
const messages = components.flatMap(component => component(next));
// push all resulting messages on to the queue
queue.push(...messages.filter(x => x));
}
}
}
The Checkout
Our bus enabled the outside control logic to rely entirely on message dispatch. We send messages noting the addition of products to the cart. This results in other messages flowing within the bus.
The checkout object is responsible for the checkout process. But, to do this it listens out for another message flowing within the context of shopping
.
// factories/Checkout.js
import { AtCheckout, CartTotalsUpdated } from "../taxonomy.js";
export function Checkout() {
let latestTotals;
return message => {
switch (true) {
case message instanceof CartTotalsUpdated:
latestTotals = message;
break;
case message instanceof AtCheckout:
logCartTotals(latestTotals);
break;
}
};
}
function logCartTotals({ items, total }) {
console.log(items, "items, total cost:", total);
}
Any time it detects a CartTotalsUpdated
message, it stores the message away in preparation. Finally, when the AtCheckout
message finally arrives, it fulfils its responsibility.
Note that the Checkout
object is entirely unaware of any Cart
object. All it knows are the messages it can receive and send.
A cognitive boundary
Eric Evans, talking about Bounded Contexts, says
Model expressions, like any other phrase, only have meaning in context.
The same can be said of messages flowing within our bus. When creating this aggregate object, I chose a context boundary within which messages are well understood.
As this context grows, it might become harder to understand what a message means, what data it holds or what schema it uses. If it happens, this is a sign that we should rethink the divisions, or introduce new subdivisions.
Conclusion
We’re now free of dependencies between the different objects in our system. Each object is free to send and receive messages without any external contract, other than the messages themselves.
This is a loosely coupled system. And, using contexts like shopping
we can create high cohesion too.
https://github.com/goofballLogic/modd/tree/article-2023/articles/003-OutsideAndInside