Practical Code Architecture 🛠️ for Beginners (in JavaScript)
Tags: architecture, javascript, programming, beginners
Published: 2024-04-30 15:36:10

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 <u>briefly</u> 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

```javascript
let x = 0;

...

if(something){
  x = 5;
}

...

return x;
```

#### More Declarative

```javascript
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

```javascript
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

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

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

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

```javascript
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](https://www.digitalocean.com/community/conceptual-articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design).

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 other types of "modules". Below are two 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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)._

### Single Responsibility Principle

The Single Responsibility Principle says that a module should have only one responsibility.

#### ❌ Single Responsibility

```javascript
const [area, volume] = getAreaAndVolume(shape);
```

#### ✔️ Single Responsibility

```javascript
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](https://docs.oracle.com/javase/tutorial/java/concepts/interface.html)._

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

```javascript
import { saveDataInLocalStorage } from "../data-store.js";

...

saveDataInLocalStorage(data);
```

#### ✔️ Dependency Inversion

```javascript
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

```javascript
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

```javascript
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 👇