How can we improve the layering of the shopping list code?
I recommend a three-step process:
-
Identify layers: Categorize the code into presentation, domain and persistence.
-
Create layers: Move each category of code into functions, files or modules.
-
Measure layering: Consider whether the design has improved, and return to step 1 as necessary.
Identifying layers
The first step is to categorize the code to identify potential layers in the code.
For example, one way to perform this categorization is as follows:
-
Presentation: The HTML generation and request handling code is presentation logic because it creates the user interface HTML:
app.get('/', (req, res) => { // Prepare the header let result = `<!DOCTYPE html><title>AIP shopping list</title> <h1>AIP shopping list</h1> <h3>Current items:</h3>`; // Generate the items in the shopping list and a total // or a placeholder if nothing has been added. if (items.length > 0) { ... result += `<ul>`; for (let item of items) { result += `<li>${item.quantity} units of ${item.description}</li>`; ... } result += `</ul>`; result += `<p>Total quantity of units: ${total}.</p>`; } else { result += `<p>No items have been added.</p>` } ...
-
Domain: The calculation of totals is domain logic because it is a function that could be reused by different front-ends or presentation layers:
... let total = 0; ... for (let item of items) { ... total += item.quantity; } ...
-
Persistence: The list array and array insertion operations are persistence logic because they are responsible for storing data:
... // A list of items in the shopping list // Each item is an object: { description: string, quantity: number } let items = []; ... ... // Add to the shopping list items.push({ description, quantity }); ...
Creating layers
In the next step, extract the code for each layer into separate units.
For example, we might create a reusable persistence layer as follows:
// Underlying data store, an array of { description: string, quantity: number }
let itemData = [];
// Retrieve all items in the shopping list
// returns an array of { description: string, quantity: number }
function findAllItems() {
return itemData;
}
// Insert a single item into the shopping list
// description is a string, quantity is an integer
// the parameters are assumed to be valid (non-null, non-empty)
function insertItem(description, quantity) {
itemData.push({ description, quantity });
}
The domain logic layer might be as follows:
// Find all items in the shopping list
function queryItems() {
return findAllItems();
}
// Create a shopping list from a descriptin and an integer quantity
function createItem(description, quantity) {
insertItem(description, quantity);
}
// Compute the total quantity of items
function computeTotalQuantity() {
let total = 0;
for (let item of findAllItems()) {
total += item.quantity;
}
return total;
}
The functions queryItems
and createItem
are extremely simple. I added them to ensure strict adherence to layering. Strict layering means the presentation only uses the domain logic. The presentation layer does not bypass the domain logic to access the persistence layer. The presentation layer must create an item indirectly, by calling createItem
on the domain logic.
However, after writing the domain logic code above, I could (and did) decide that queryItems
and createItem
add unnecessary complexity. In this case, I felt that it would be acceptable for the presentation layer to have direct access to the domain logic layer. When I made this decision, I also noted that I might need to reconsider the decision later. [1]
Finally, I notice that the presentation logic is slightly complex so I decided to improve the presentation logic with some helper functions:
// Render the shopping list as an unordered HTML list
function generateHtmlShoppingList() {
let items = findAllItems();
let total = computeTotalQuantity();
let result = '';
if (items.length > 0) {
result += `<ul>`;
for (let item of items) {
result += `<li>${item.quantity} units of ${item.description}</li>`;
}
result += `</ul>`;
result += `<p>Total quantity of units: ${total}.</p>`;
} else {
result += `<p>No items have been added.</p>`
}
return result;
}
// Render an HTML form for submitting new list items
function generateHtmlShoppingListForm(action) {
return `<h3>Add item:</h3>
<form action="/${action}" method="GET">
<p>
<label>Description:
<input type="text" name="description">
</label>
</p>
<p>
<label>Quantity:
<input type="number" name="quantity">
</label>
</p>
<p>
<input type="submit" value="Add">
</p>
</form>`;
}
Combining these all together, I have have an initial layered version of our code:
const express = require('express');
const app = express();
const port = 3000;
// --------------------------------------
// Persistence layer
// --------------------------------------
// Underlying data store, an array of { description: string, quantity: number }
let itemData = [];
// Retrieve all items in the shopping list
// returns an array of { description: string, quantity: number }
function findAllItems() {
return itemData;
}
// Insert a single item into the shopping list
// description is a string, quantity is an integer
// the parameters are assumed to be valid (non-null, non-empty)
function insertItem(description, quantity) {
itemData.push({ description, quantity });
}
// --------------------------------------
// Domain layer
// --------------------------------------
// Compute the total quantity of items
function computeTotalQuantity() {
let total = 0;
for (let item of findAllItems()) {
total += item.quantity;
}
return total;
}
// --------------------------------------
// Presentation layer
// --------------------------------------
// Render the shopping list as an unordered HTML list
function generateHtmlShoppingList() {
let items = findAllItems();
let total = computeTotalQuantity();
let result = '';
if (items.length > 0) {
result += `<ul>`;
for (let item of items) {
result += `<li>${item.quantity} units of ${item.description}</li>`;
}
result += `</ul>`;
result += `<p>Total quantity of units: ${total}.</p>`;
} else {
result += `<p>No items have been added.</p>`
}
return result;
}
// Render an HTML form for submitting new list items
function generateHtmlShoppingListForm(action) {
return `<h3>Add item:</h3>
<form action="${action}" method="GET">
<p>
<label>Description:
<input type="text" name="description">
</label>
</p>
<p>
<label>Quantity:
<input type="number" name="quantity">
</label>
</p>
<p>
<input type="submit" value="Add">
</p>
</form>`;
}
app.get('/', (req, res) => {
res.send(
`<!DOCTYPE html><title>AIP shopping list</title>
<h1>AIP shopping list</h1>
<h3>Current items:</h3>
${generateHtmlShoppingList()}
${generateHtmlShoppingListForm('/new')}`);
});
app.get('/new', (req, res) => {
// Get the user input
const description = req.query['description'];
const quantity = parseInt(req.query['quantity']);
// Create the shopping list item
insertItem(description, quantity);
// Redirect back to the home page to show the full list
res.redirect('/');
});
app.listen(port, () => console.log(`The shopping list is running on http://localhost:${port}/`));
Is this simpler? Perhaps not yet: it is longer than the original file. However, it does give the application more structure. This structure makes it possible to evolve the application in new ways. For example, I could store data in a database by replacing only findAllItems
and insertItem
. Similarly, I can create a text-based interface just by replacing generateHtmlShoppingList
and generateHtmlShoppingListForm
.
This layered architecture makes other opportunities for design improvement more obvious. For example, some other ways we could improve layering include the following:
-
Moving each layer into separate JavaScript modules (this would make it easier for other developers to understand the layering)
-
Moving the HTML code in
generateHtmlShoppingListForm
into a separate.html
file (this would also allow your IDE to perform syntax highlighting and HTML previews directly) [2]
Measuring layering
Design is a constant process involving trade-offs. When I consider making a design change, I think about three factors:
- Current effort
-
How much work will it take for me to implement the change?
- Complexity
-
How much more or less complex will my code be as a result of the change? i.e., How long will it take my team members to understand this code?
- Next task effort
-
How much work will this change save me on my next task? [3]
A change that reduces complexity and saves time is a “no-brainer”. However, these factors are often in conflict with each other.
A good rule of thumb is to focus on evolving the design to isolate most changes to a small proportion of your application’s files. For example:
-
How many separate files would need to be changed if you decide to add a French language user interface?
-
How many separate files would need to be changed if you decide to migrate from plain text files to a database (or vice-versa)?
-
How many separate files would need to be changed if you decide to add additional business logic rules (e.g., that quantity must be between 1 and 10)?
Can you pinpoint changes to specific places in your code? If so, you have a good design. In the original shopping list application, making a change from English to French would require careful checking of every line of code. In the layered application, the place to make such changes is easy to identify: generateHtmlShoppingList
and generateHtmlShoppingListForm
. There is no need for a developer to understand calculation of totals or data storage while translating English to French.
As a developer, these factors offer guidance into whether it is better to spend more time improving layering or to spend more time developing new features.