- definition
- taxonomy
- patterns
- refactorings
An edge is a discontinuity along some dimension in our system code.
An edge can exist as essential complexity in the domain. Or an edge can exist as accidental complexity we have added to our code.
There are different types of edges in our system. Let's take a look at some possible ways to classify edges.
Control flow statements, such as an if
, cause a split in the control flow. This we can call a conditional edge
.
Throwing an exception creates an exceptional edge
where the normal flow is handled in one code block and the exceptional condition is handled in another.
This differs from a conditional edge in that the exception handling code may be in a different file and might even cross the system boundary.
Temporal edges occur when some part of our code needs to wait, creating an edge between the now
code and the future
code.
These edges can be challenging to reason about. Javascript promises
are an example of an edge mitigation where the complexity of handling asynchronous callbacks has been simplified. Or, as discussed below the edge has been pushed from our code into a promise
.
Essential edges
exist in our domain regardles of how we choose to express them in our code.
Discounts can only be applied to sales over $100.
In this case, an essential edge
divides the processing rules for sales over $100, which can have discounts, from other sales.
An accidental edge
is an edge that we have introduced in our code. These edges don't exist in our domain but have emerged due to the way we wrote the code.
An essential edge in our domain can only be removed by redefining our domain without the edge.
Similar edges may appear throughout our code and can be problematic:
- they tend to create a proliferation of duplicate checking logic
- they make it more difficult to reason about program flow
By pulling the edge out
we may be able to simplify our code and remove duplication.
function process(operation, connection) {
if (operation === 'remove') {
remove(connection);
} else if (operation === 'update') {
update(connection);
}
}
function remove(connection) {
if (connection === undefined) {
throw Error;
} else {
connection.remove(...);
...
}
};
function update(connection) {
if (connection === undefined) {
throw Error;
} else {
connection.update(...);
...
}
};
function process(operation, connection) {
if (connection === undefined) {
throw Error;
if (operation === 'remove') {
remove(connection);
} else if (operation === 'update') {
update(connection);
}
}
function remove(connection) {
connection.remove(...);
...
};
function update(connection) {
connection.update(...);
...
};
We have moved the conditional edge up one level in our code. It may be possible to continue this outwards until we reach our system boundary.
Often we need to validate data coming into our system.
We are writing an API to return geographic data based on a postal code. We want to be sure the postal code is valid.
// processor.js
function isValid(postalCode) {
return /^[A-Z]\d[A-Z]\d[A-Z]\d$/.test(postalCode);
}
function process(postalCode) {
if (isValid(postalCode)) {
// valid postal code => process the request
} else {
// invalid postal code
throw new Error();
}
};
Here we see a conditional edge
between the code for valid and invalid postal codes. More importantly, the way in which the error is handled, in this case by throwing an exception, is likely not part of the domain. So, we would like to pull out
the edge into a protective shell that validates the postal code before calling process()
.
// shell.js
const processor = require('processor');
function isValid(postalCode) {
return /^[A-Z]\d[A-Z]\d[A-Z]\d$/.test(postalCode);
}
function process(postalCode) {
const processor = ...;
if (isValid(postalCode)) {
processor.process(postalCode);
} else {
// invalid postal code
throw new Error();
}
};
// processor.js
module.exports.process = postalCode => {
// process the request
};
We have simplified the code in the processor.js
file. However, it seems like an SRP violation to have the domain validity checking in a shell layer as this check is really a domain concept. In other words, we would prefer to define domain validations within our core domain but invoke them from the shell.
// validator.js
module.exports.isValid = postalCode => {
return /^[A-Z]\d[A-Z]\d[A-Z]\d$/.test(postalCode);
}
// shell.js
const validator = require('validator');
const processor = require('processor');
function process(postalCode) {
if (validator.isValid(postalCode)) {
processor.process(postalCode);
} else {
// invalid postal code
throw new Error();
}
};
// processor.js
module.exports.process = postalCode => {
// process the request
};