In his book on called Clean Architecture, Robert Martin, describes an architectural pattern which when used decouples the main entities/objects of a system from their use cases, and more importantly from the frameworks we use to build applications. This achieves two things:
- a decoupled framework is easier to change if the application needs to change or if a better framework comes along
- improves maintainability, (I'm sure we've all seen an express.js app with business logic and SQL in the route handlers)
Clean architectural can be boiled down to this diagram:
The dependencies all point inwards, the frameworks on the outside depend on the controllers which depend on the use cases which depend on the entities. The entities which are our business logic depend on nothing, and unless our business is rapidly changing should require no changes even if we swap from an express web app which stores data in a Postgres DB to an Electron desktop app using MongoDB.
Great, so go do that!
And so we should, but did you see that little bit in the bottom right, presenter, controller, and the red wiggly line, whats all that mean? The Entities depend upon the database right? They need to store themselves, well that doesn't fit with clean architecture. So the red wiggle line shows how the flow of control goes from the controller through an interface, to the presenter. This can be very confusing but in node we can strip away that green layer, and still get an app which is cleanly architected.
These example make strong use of ES6 JavaScript and features only available in Nodejs 12 and above.
Project Structure
I chose to use express as the main interface framework for the list app, however as we shall see later this can be swapped out of any other framework with little trouble.
The projects starts with the standard files, and folders generated by running the `express` command, Added to this is the helpers directory into which I created 3 further directories:
- Entities
- Store
- UseCase.
Don't worry we will be going through the contents of these in detail soon. (There is also a Middleware directory, but that's where I store express middleware functions and is not relevant here).
Entities
Entities are the main Stuff (capital 'S'), of our app, they should be self validating, change little, and have no dependencies.
The entities have everything you would expect from a class, static functions to create, search, and load an entity. Getter functions to get data out of the entity, setter functions to alter data, and functions to access data which the entity owns.
I like to declare all properties as private and declare getters and setters for them, this way I'm ensuring that I cannot accidentally alter the data on the entity in a use case. Using setters is a great way to immediately persist any changes into the database.
Here we can see a `List` entity:
class List {
#id;
#name;
#items;
constructor({ id, name, items }) {
this.#id = id;
this.#name = name;
this.#items = items;
}
get id() {
return this.#id;
}
get name() {
return this.#name;
}
async setName(name) {
const cachedName = this.#name;
this.#name = name;
try {
await List.#store.updateList(this);
return this;
} catch (err) {
this.#name = cachedName;
throw err;
}
}
async getItems() {
if (!this.#items) {
this.#items = await List.#store.getItems(this);
}
return this.#items;
}
static #store = null;
static setStore(store) {
List.#store = store;
}
static async getList({ id }) {
const res = await List.#store.getList(id);
if (!res) {
throw new Error("List not Found");
}
return res;
}
static async newList({ name }) {
const list = new List({ name });
return List.#store.newList(list);
}
static async getAll() {
return List.#store.getAllLists();
}
}
module.exports = List;
The List entity can own 1 or more items, but the list as no knowledge of an item other than a function on how to retrieve them, this keeps the implementation of the entities separate whilst maintaining that ownership relationship.
The only different thing in this class it the way it accesses the store through a static private property called store. There is also a static function which sets the store. We will get onto these later.
I have a preference towards using index.js files, not in all directories, but where the directory contains modules relating to a set of functionality or like in this case a group of module type (entities). Here it serves to purposes its allowing us to manipulate our classes before exporting them to the rest of the project, and it avoids long `require("...")` statements.
Here is the index file for the entities:
const entities = {
Item: require("./Item"),
List: require("./List"),
};
const store = require("../Store")(entities);
Object.values(entities).forEach((el) => el.setStore(store));
module.exports = entities;
Slightly more is happening here:
1. We import all our entities into an object.
2. We import the store/database and pass the entities our entities into the store.
3. We loop over out entities setting the store on them.
So why are we doing this? Our entities are not allowed to depend on the store/DB, we cannot simply import the database functions into the entity file, instead we use the setStore static function we defined earlier.
But it doesn't stop there, our entities are not allowed to know the structure of the database tables so they cannot deserialise the database results to create a new entity. This is why we pass our entities into the database/store.
Database/Store
For my example I'm using a (badly written) json store which doesn't persist the results if the program stops, but the actual storage method doesn't matter, swap the contents of the functions out for a Postgres/MySQL/MongDB/... it doesn't matter. The important thing is, these functions return new entities.
const conn = require("./conn");
function build({ Item }) {
function updateItem(item) {
const storeItem = conn.items.find((el) => el.id = item.id);
if (storeItem) {
storeItem.name = item.name;
storeItem.done = item.done;
storeItem.listID = storeItem.list;
return new Item(storeItem);
}
}
function getItem(id) {
const storeItem = conn.items.find((el) => el.id = id);
if (storeItem) {
storeItem.listID = storeItem.list;
return new Item(storeItem);
}
}
function newItem(item) {
conn.items.push({
name: item.name,
done: item.done,
list: item.listID,
id: Math.floor(Math.random() * 1000)
});
const res = conn.items[conn.items.length] - 1
res.listID = res.list;
return new Item(res);
}
function getItems(list) {
const res = conn.items
.filter((el) => el.list === list.id)
.map((el) => {
el.listID = el.list;
return new Item(el)
});
return res;
}
return {
updateItem,
getItem,
newItem,
getItems,
};
}
module.exports = build;
Every function here returns either nothing or an instance of the Item entity (`{void|Item}`), to do this we need to deserialise the result. In our Item store the ID of the list the item belongs to is called `list`, but in the Item entity its `listID`. We could have named it the same, however there will be situations where this is not possible e.g. SQL where we are changing from one naming convention to another and we need to convert `my_variable_name` to `myVariableName`. So our database/store functions depend on our entities, both in knowing the interface for the entity and for creating new entities.
The `conn` is a module which would export the database functions, this would be `db.query` etc this can be imported here, or if an additional layer of abstraction is needed we could export a conn module with standard interface so it can easily be changed, to a different database.
The `build` function is used to pass in the entities, in this module we only need the List entity so its destructured out.
These are all related modules, they deal with extracting data from a store and they require manipulation before they can be used in another module, so we're using another index file:
function build(entities) {
return {
...require("./item")(entities),
...require("./list")(entities),
};
};
module.exports = build;
We have a function which takes in the entities, imports all the store modules, passes the entities into the store build functions and returns then destructured into an object. (The destructuring is not necessary, and it runs the risk of overwriting some functions, its just a personal preference of mine).
Use Case
Use cases, now we are getting to the application logic, away from the business and storage logic, these files contain what the application does.
For our application we want to be able to create an item on a list, show items on a list, and update an item. (We would also want to do stuff with List and there is a use case file for that but here I will only go over the item use cases).
const { List, Item } = require("../Entities");
async function createItem(listID, name, done = false) {
const list = await List.getList({ id: listID });
const item = await Item.newItem({ name, done, list: list.id });
return {
name: item.name,
done: item.done,
};
}
async function listItems(listID) {
const items = await List.getList({ id: listID })
.then((list) => list.getItems());
return items.map((el) => {
return {
name: el.name,
done: el.done,
};
});
}
async function updateItem(id, name, done) {
const item = await Item.getItem({ id });
await item.setName(name);
await item.setDone(done);
return {
name: item.name,
done: item.done,
};
}
module.exports = {
createItem,
listItems,
updateItem,
}
Here we can see that we are importing our entities, we need both List and Item. For our `createItem` function we first get the list the item is to belong to, then we create the item, in our `listItems` function we are first getting our list, then calling getItems on that list.
Every function then serialises the data which we will want to present to the user or send to a front end and returns it.
Notice that there is no reference to expressjs, our chosen web framework, this mean if we need to change the to koajs or using commanderjs to create a cli application we can use the same use cases, the same entities all we need to do is change the files are called from.
Express
Our Express handlers are now simply a matter of importing our use cases, passing in the required data from the request, returning the response and passing any errors onto the error handler.
const router = require("express").Router();
const {
getAllLists,
listItems,
createList
} = require("../helpers/UseCases");
router.get("/lists", async (req, res, next) => {
try {
return res.json(await getAllLists());
} catch (err) {
return next(err);
}
});
router.get("/list/:id", async (req, res, next) => {
try {
return res.json(await listItems(req.params.id));
} catch (err) {
return next(err);
}
});
router.post("/list", async (req, res, next) => {
try {
return res.json(await createList(req.body.name));
} catch (err) {
return next(err);
}
});
module.exports = router;
Our use cases are already returning the data we data we wish to return from the API so there is no need to transform it, all we need it some try catches to handle errors.
Summary
By developing a system which is decoupled from use cases and frameworks it allows us to easily swap out those layers,
and quickly react to changing requirements.
We end up with a cleaner application which is easier to expand, test, and refactor.
All the code used in this example is available here: GitLab.
Robert Martin's blog on the clean architecture style can be found here: Clean Coder.
Thumbnail Image: "JavaScript" by Matt Ephraim is licensed with CC BY-NC 2.0. To view a copy of this license, visit https://creativecommons.org/licenses/by-nc/2.0/