DEV Community

Cover image for Practical Code Architecture 🛠️ for Beginners (in JavaScript)
Joe Boris
Joe Boris

Posted on • Edited on

Practical Code Architecture 🛠️ for Beginners (in JavaScript)

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;
Enter fullscreen mode Exit fullscreen mode

More Declarative

const x = something ? 5 : 0;

...

return x;
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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(...);
}
Enter fullscreen mode Exit fullscreen mode

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...

  1. Gets everything it needs from arguments only
  2. 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);
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

✔️ Single Responsibility

const area = getArea(shape);
const volume = getVolume(shape);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

✔️ Dependency Inversion

import { saveData } from "../data-store.js";

...

saveData(data);
Enter fullscreen mode Exit fullscreen mode

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 = ``;
}
Enter fullscreen mode Exit fullscreen mode

✔️ Unidirectional Data Flow

let items = [];

function onAddButtonClicked(item) {
  items = ...;
  renderItems(items);
}

function onClearButtonClicked() {
  items = ...;
  renderItems(items);
}
Enter fullscreen mode Exit fullscreen mode

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)