There are many ways to accomplish the same thing in programming. Code architecture is basically the study of "which way is better".
Note: In this article, "better" means code that's generally considered easier to read, scale, and maintain.
What's in this article?
This article will briefly introduce three major code architecture subjects, and explain how I've found them helpful in my 10+ years of experience as a full-stack JavaScript developer. This is intended to be a simple and practical introduction, rather than comprehensive and technical.
Subjects:
- Paradigms
- Principles
- Patterns
Paradigms 🏛️
Paradigms are generally the broadest and (in my opinion) most important subject. They're sort of like "styles" to write code in. In my experience, the ones most relevant to web development are declarative/imperative code, Reactive Programming, and Functional Programming.
Declarative/Imperative Code
Declarative code is code that combines more work into fewer statements/expressions, while imperative is the opposite.
More Imperative
let x = 0;
...
if(something){
x = 5;
}
...
return x;
More Declarative
const x = something ? 5 : 0;
...
return x;
In general, more declarative code is considered better. In the above examples, imagine if each ...
were 100 lines of code. It would be much harder to figure out what value is returned in the imperative example.
Reactive Programming
Reactive Programming is about writing code to automatically trigger operations in response to data changing.
Imagine you're making the shopping cart page of a shopping app. The cart has an underlying source of data we'll call items
. Imagine you have a requirement to navigate back to the homepage as soon as there are no items left in the cart.
One Approach
let items = [];
function onRemoveButtonClicked(item) {
items = ...;
if(items.length == 0){
navigateHome();
}
}
function onClearButtonClicked() {
items = ...;
if(items.length == 0){
navigateHome();
}
}
But what should really trigger the navigation? Clicking "Remove Item"? Clicking "Clear Items"? The true answer is "changing the data in items
".
More Reactive
let items = [];
function setItems(newItems){
items = newItems;
if(items.length == 0){
navigateHome();
}
}
function onRemoveButtonClicked(item) {
setItems(...);
}
function onClearButtonClicked() {
setItems(...);
}
A more reactive approach could be to use setItems
in place of items = ...
. That way, "changing the data in items
" will trigger the navigation whenever appropriate.
Fun Fact: This paradigm is where React.js gets its name!
Functional Programming
Functional Programming is basically writing declarative code, among other things. One particularly helpful thing is "pure functions".
A function is "pure" if it...
- Gets everything it needs from arguments only
- Does not affect the outside world in any way
Note: Something that affects the outside world is called a "side effect"
Consider this example...
let z = 0;
console.log(sum(2, 2));
console.log(z);
It's very obvious what should happen. You should see 4
, then 0
in the console. If sum
is a pure function, then that result is guaranteed. But, imagine if sum
was defined like this...
function sum(x, y){
z = x + y;
return x + y;
}
This example is "impure", because z = x + y
affects the outside world. This makes the results less predictable and harder to keep track of, which tends to cause bugs.
Note: Not every function can be pure! UI event handlers, rendering functions, and back-end route handlers, for example, are rarely pure.
Principles 📜
Principles are less broad than Paradigms. They're sort of like rules to follow. The most popular Principles are the so-called SOLID principles.
S - Single Responsibility Principle
O - Open-closed Principle
L - Liskov Substitution Principle
I - Interface Segregation Principle
D - Dependency Inversion Principle
Although these principles were intended to apply specifically to object-oriented programming, they can easily be applied to any type of "module". Below are two principles that I've found particularly helpful...
Note: "Module" is a vague term in this field of study meaning a piece of code like a function, UI component, API route, file, etc. In JavaScript, "module" can also specifically mean a file that imports/exports things.
Single Responsibility Principle
The Single Responsibility Principle says that a module should have only one responsibility.
❌ Single Responsibility
const [area, volume] = getAreaAndVolume(shape);
✔️ Single Responsibility
const area = getArea(shape);
const volume = getVolume(shape);
In the above example, getAreaAndVolume
clearly has more than one responsibility.
Dependency Inversion Principle
The Dependency Inversion Principle says that a module should only depend on generic "interfaces" provided by other modules, not specific details.
Note: "Interface" is another vague term which roughly means "the way to use" a module. In other programming languages, "interface" can also have a similar, but more specific meaning.
Imagine you're making a UI component that needs to save some data, but you don't know or care how the data should be saved yet. Maybe data storage is your co-worker's responsibility, or maybe you just want to worry about it later. In either case, you could make a data storage module and follow the Dependency Inversion Principle.
❌ Dependency Inversion
import { saveDataInLocalStorage } from "../data-store.js";
...
saveDataInLocalStorage(data);
✔️ Dependency Inversion
import { saveData } from "../data-store.js";
...
saveData(data);
In the above example, saveDataInLocalStorage
is more specific than necessary. All we care about in the UI code is that we need to save data. We don't care about the details yet.
Patterns 📦
Patterns are even less broad than Principles. They're sort of like recipes you can use in your code. There's one pattern I've found particularly useful.
Unidirectional Data Flow
Unidirectional Data Flow can be implemented by rendering a UI simply as a reflection of the current state/data of the app.
❌ Unidirectional Data Flow
let items = [];
function onAddButtonClicked(item) {
items = ...;
document.querySelector("#item-list").innerHTML += `
<div>${item.name}</div>
`;
}
function onClearButtonClicked() {
items = ...;
document.querySelector("#item-list").innerHTML = ``;
}
✔️ Unidirectional Data Flow
let items = [];
function onAddButtonClicked(item) {
items = ...;
renderItems(items);
}
function onClearButtonClicked() {
items = ...;
renderItems(items);
}
In the above example, imagine the function renderItems
just renders the current state of items
at any time. Instead of updating individual parts of the UI depending on the situation, we can just call renderItems
whenever items
changes.
Conclusion
There's MUCH more to learn about code architecture out there. These are just a few concepts that I eventually learned throughout the years (sometimes the hard way). Hopefully this helps demystify the subject for others as well.
💬 Feedback and questions are welcome below 👇
Top comments (0)