Skip to content

Sérgio Kopplin

Clean Code - Objects, Data Structure and SOLID

Notebook, Clean Code3 min read

Objects and Data Structure

Getters and Setters to access data on objects

  • When you want to do more beyond getting an object property, you don't have to look up and change every accessor in your codebase.
  • Makes adding validation simple when doing a set.
  • Encapsulates the internal representation.
  • Easy to add logging and error handling when getting and setting.
  • You can lazy load your object's properties, let's say getting it from a server.

Bad:

1function makeBankAccount() {
2 // ...
3
4 return {
5 balance: 0
6 // ...
7 };
8}
9
10const account = makeBankAccount();
11account.balance = 100;

Good:

1function makeBankAccount() {
2 // this one is private
3 let balance = 0;
4
5 // a "getter", made public via the returned object below
6 function getBalance() {
7 return balance;
8 }
9
10 // a "setter", made public via the returned object below
11 function setBalance(amount) {
12 // ... validate before updating the balance
13 balance = amount;
14 }
15
16 return {
17 // ...
18 getBalance,
19 setBalance
20 };
21}
22
23const account = makeBankAccount();
24account.setBalance(100);

Private members on Objects

This can be accomplished through closures (for ES5 and below).

Bad:

1const Employee = function(name) {
2 this.name = name;
3};
4
5Employee.prototype.getName = function getName() {
6 return this.name;
7};
8
9const employee = new Employee("John Doe");
10console.log(`Employee name: ${employee.getName()}`);
11// Employee name: John Doe
12delete employee.name;
13console.log(`Employee name: ${employee.getName()}`);
14// Employee name: undefined

Good:

1function makeEmployee(name) {
2 return {
3 getName() {
4 return name;
5 }
6 };
7}
8
9const employee = makeEmployee("John Doe");
10console.log(`Employee name: ${employee.getName()}`);
11// Employee name: John Doe
12delete employee.name;
13console.log(`Employee name: ${employee.getName()}`);
14// Employee name: John Doe

ES2015/ES6 classes over ES5 plain functions

Bad:

1const Animal = function(age) {
2 if (!(this instanceof Animal)) {
3 throw new Error("Instantiate Animal with `new`");
4 }
5
6 this.age = age;
7};
8
9Animal.prototype.move = function move() {};
10
11const Mammal = function(age, furColor) {
12 if (!(this instanceof Mammal)) {
13 throw new Error("Instantiate Mammal with `new`");
14 }
15
16 Animal.call(this, age);
17 this.furColor = furColor;
18};
19
20Mammal.prototype = Object.create(Animal.prototype);
21Mammal.prototype.constructor = Mammal;
22Mammal.prototype.liveBirth = function liveBirth() {};
23
24const Human = function(age, furColor, languageSpoken) {
25 if (!(this instanceof Human)) {
26 throw new Error("Instantiate Human with `new`");
27 }
28
29 Mammal.call(this, age, furColor);
30 this.languageSpoken = languageSpoken;
31};
32
33Human.prototype = Object.create(Mammal.prototype);
34Human.prototype.constructor = Human;
35Human.prototype.speak = function speak() {};

Good:

1class Animal {
2 constructor(age) {
3 this.age = age;
4 }
5
6 move() {
7 /* ... */
8 }
9}
10
11class Mammal extends Animal {
12 constructor(age, furColor) {
13 super(age);
14 this.furColor = furColor;
15 }
16
17 liveBirth() {
18 /* ... */
19 }
20}
21
22class Human extends Mammal {
23 constructor(age, furColor, languageSpoken) {
24 super(age, furColor);
25 this.languageSpoken = languageSpoken;
26 }
27
28 speak() {
29 /* ... */
30 }
31}

Method chaining

Bad:

1class Car {
2 constructor(make, model, color) {
3 this.make = make;
4 this.model = model;
5 this.color = color;
6 }
7
8 setMake(make) {
9 this.make = make;
10 }
11
12 setModel(model) {
13 this.model = model;
14 }
15
16 setColor(color) {
17 this.color = color;
18 }
19
20 save() {
21 console.log(this.make, this.model, this.color);
22 }
23}
24
25const car = new Car("Ford", "F-150", "red");
26car.setColor("pink");
27car.save();

Good:

1class Car {
2 constructor(make, model, color) {
3 this.make = make;
4 this.model = model;
5 this.color = color;
6 }
7
8 setMake(make) {
9 this.make = make;
10 // NOTE: Returning this for chaining
11 return this;
12 }
13
14 setModel(model) {
15 this.model = model;
16 // NOTE: Returning this for chaining
17 return this;
18 }
19
20 setColor(color) {
21 this.color = color;
22 // NOTE: Returning this for chaining
23 return this;
24 }
25
26 save() {
27 console.log(this.make, this.model, this.color);
28 // NOTE: Returning this for chaining
29 return this;
30 }
31}
32
33const car = new Car("Ford", "F-150", "red").setColor("pink").save();

Composition over inheritance

As stated famously in Design Patterns by the Gang of Four, you should prefer composition over inheritance where you can.

But, Inheritance over Composition:

  • Your inheritance represents an "is-a" relationship and not a "has-a" relationship (Human->Animal vs. User->UserDetails).
  • You can reuse code from the base classes (Humans can move like all animals).
  • You want to make global changes to derived classes by changing a base class. (Change the caloric expenditure of all animals when they move).

Bad:

1class Employee {
2 constructor(name, email) {
3 this.name = name;
4 this.email = email;
5 }
6
7 // ...
8}
9
10// Bad because Employees "have" tax data.
11// EmployeeTaxData is not a type of Employee
12class EmployeeTaxData extends Employee {
13 constructor(ssn, salary) {
14 super();
15 this.ssn = ssn;
16 this.salary = salary;
17 }
18
19 // ...
20}

Good:

1class EmployeeTaxData {
2 constructor(ssn, salary) {
3 this.ssn = ssn;
4 this.salary = salary;
5 }
6
7 // ...
8}
9
10class Employee {
11 constructor(name, email) {
12 this.name = name;
13 this.email = email;
14 }
15
16 setTaxData(ssn, salary) {
17 this.taxData = new EmployeeTaxData(ssn, salary);
18 }
19 // ...
20}

SOLID

Single Responsibility Principle (SRP)

"There should never be more than one reason for a class to change". It's tempting to jam-pack a class with a lot of functionality, like when you can only take one suitcase on your flight. The issue with this is that your class won't be conceptually cohesive and it will give it many reasons to change. Minimizing the amount of times you need to change a class is important. It's important because if too much functionality is in one class and you modify a piece of it, it can be difficult to understand how that will affect other dependent modules in your codebase.

Bad:

1class UserSettings {
2 constructor(user) {
3 this.user = user;
4 }
5
6 changeSettings(settings) {
7 if (this.verifyCredentials()) {
8 // ...
9 }
10 }
11
12 verifyCredentials() {
13 // ...
14 }
15}

Good:

1class UserAuth {
2 constructor(user) {
3 this.user = user;
4 }
5
6 verifyCredentials() {
7 // ...
8 }
9}
10
11class UserSettings {
12 constructor(user) {
13 this.user = user;
14 this.auth = new UserAuth(user);
15 }
16
17 changeSettings(settings) {
18 if (this.auth.verifyCredentials()) {
19 // ...
20 }
21 }
22}

Open/Closed Principle (OCP)

As stated by Bertrand Meyer, "software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification." What does that mean though? This principle basically states that you should allow users to add new functionalities without changing existing code.

Bad:

1class AjaxAdapter extends Adapter {
2 constructor() {
3 super();
4 this.name = "ajaxAdapter";
5 }
6}
7
8class NodeAdapter extends Adapter {
9 constructor() {
10 super();
11 this.name = "nodeAdapter";
12 }
13}
14
15class HttpRequester {
16 constructor(adapter) {
17 this.adapter = adapter;
18 }
19
20 fetch(url) {
21 if (this.adapter.name === "ajaxAdapter") {
22 return makeAjaxCall(url).then(response => {
23 // transform response and return
24 });
25 } else if (this.adapter.name === "nodeAdapter") {
26 return makeHttpCall(url).then(response => {
27 // transform response and return
28 });
29 }
30 }
31}
32
33function makeAjaxCall(url) {
34 // request and return promise
35}
36
37function makeHttpCall(url) {
38 // request and return promise
39}

Good:

1class AjaxAdapter extends Adapter {
2 constructor() {
3 super();
4 this.name = "ajaxAdapter";
5 }
6
7 request(url) {
8 // request and return promise
9 }
10}
11
12class NodeAdapter extends Adapter {
13 constructor() {
14 super();
15 this.name = "nodeAdapter";
16 }
17
18 request(url) {
19 // request and return promise
20 }
21}
22
23class HttpRequester {
24 constructor(adapter) {
25 this.adapter = adapter;
26 }
27
28 fetch(url) {
29 return this.adapter.request(url).then(response => {
30 // transform response and return
31 });
32 }
33}

Liskov Substitution Principle (LSP)

This is a scary term for a very simple concept. It's formally defined as "If S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., objects of type S may substitute objects of type T) without altering any of the desirable properties of that program (correctness, task performed, etc.)." That's an even scarier definition.

The best explanation for this is if you have a parent class and a child class, then the base class and child class can be used interchangeably without getting incorrect results. This might still be confusing, so let's take a look at the classic Square-Rectangle example. Mathematically, a square is a rectangle, but if you model it using the "is-a" relationship via inheritance, you quickly get into trouble.

Bad:

1class Rectangle {
2 constructor() {
3 this.width = 0;
4 this.height = 0;
5 }
6
7 setColor(color) {
8 // ...
9 }
10
11 render(area) {
12 // ...
13 }
14
15 setWidth(width) {
16 this.width = width;
17 }
18
19 setHeight(height) {
20 this.height = height;
21 }
22
23 getArea() {
24 return this.width * this.height;
25 }
26}
27
28class Square extends Rectangle {
29 setWidth(width) {
30 this.width = width;
31 this.height = width;
32 }
33
34 setHeight(height) {
35 this.width = height;
36 this.height = height;
37 }
38}
39
40function renderLargeRectangles(rectangles) {
41 rectangles.forEach(rectangle => {
42 rectangle.setWidth(4);
43 rectangle.setHeight(5);
44 const area = rectangle.getArea();
45 // BAD: Returns 25 for Square. Should be 20.
46 rectangle.render(area);
47 });
48}
49
50const rectangles = [new Rectangle(), new Rectangle(), new Square()];
51renderLargeRectangles(rectangles);

Good:

1class Shape {
2 setColor(color) {
3 // ...
4 }
5
6 render(area) {
7 // ...
8 }
9}
10
11class Rectangle extends Shape {
12 constructor(width, height) {
13 super();
14 this.width = width;
15 this.height = height;
16 }
17
18 getArea() {
19 return this.width * this.height;
20 }
21}
22
23class Square extends Shape {
24 constructor(length) {
25 super();
26 this.length = length;
27 }
28
29 getArea() {
30 return this.length * this.length;
31 }
32}
33
34function renderLargeShapes(shapes) {
35 shapes.forEach(shape => {
36 const area = shape.getArea();
37 shape.render(area);
38 });
39}
40
41const shapes = [
42 new Rectangle(4, 5),
43 new Rectangle(4, 5),
44 new Square(5)
45];
46renderLargeShapes(shapes);

Interface Segregation Principle (ISP)

JavaScript doesn't have interfaces so this principle doesn't apply as strictly as others. However, it's important and relevant even with JavaScript's lack of type system.

ISP states that "Clients should not be forced to depend upon interfaces that they do not use." Interfaces are implicit contracts in JavaScript because of duck typing.

A good example to look at that demonstrates this principle in JavaScript is for classes that require large settings objects. Not requiring clients to setup huge amounts of options is beneficial, because most of the time they won't need all of the settings. Making them optional helps prevent having a "fat interface".

Bad:

1class DOMTraverser {
2 constructor(settings) {
3 this.settings = settings;
4 this.setup();
5 }
6
7 setup() {
8 this.rootNode = this.settings.rootNode;
9 this.settings.animationModule.setup();
10 }
11
12 traverse() {
13 // ...
14 }
15}
16
17const $ = new DOMTraverser({
18 rootNode: document.getElementsByTagName("body"),
19 animationModule() {}
20 // Most of the time, we won't need to animate when traversing.
21 // ...
22});

Good:

1class DOMTraverser {
2 constructor(settings) {
3 this.settings = settings;
4 this.options = settings.options;
5 this.setup();
6 }
7
8 setup() {
9 this.rootNode = this.settings.rootNode;
10 this.setupOptions();
11 }
12
13 setupOptions() {
14 if (this.options.animationModule) {
15 // ...
16 }
17 }
18
19 traverse() {
20 // ...
21 }
22}
23
24const $ = new DOMTraverser({
25 rootNode: document.getElementsByTagName("body"),
26 options: {
27 animationModule() {}
28 }
29});

Dependency Inversion Principle (DIP)

This principle states two essential things:

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend upon details. Details should depend on abstractions. This can be hard to understand at first, but if you've worked with AngularJS, you've seen an implementation of this principle in the form of Dependency Injection (DI). While they are not identical concepts, DIP keeps high-level modules from knowing the details of its low-level modules and setting them up. It can accomplish this through DI. A huge benefit of this is that it reduces the coupling between modules. Coupling is a very bad development pattern because it makes your code hard to refactor.

As stated previously, JavaScript doesn't have interfaces so the abstractions that are depended upon are implicit contracts. That is to say, the methods and properties that an object/class exposes to another object/class. In the example below, the implicit contract is that any Request module for an InventoryTracker will have a requestItems method.

Bad:

1class InventoryRequester {
2 constructor() {
3 this.REQ_METHODS = ["HTTP"];
4 }
5
6 requestItem(item) {
7 // ...
8 }
9}
10
11class InventoryTracker {
12 constructor(items) {
13 this.items = items;
14
15 // BAD: We have created a dependency
16 // on a specific request implementation.
17 // We should just have requestItems
18 // depend on a request method: `request`
19 this.requester = new InventoryRequester();
20 }
21
22 requestItems() {
23 this.items.forEach(item => {
24 this.requester.requestItem(item);
25 });
26 }
27}
28
29const inventoryTracker = new InventoryTracker(["apples", "bananas"]);
30inventoryTracker.requestItems();

Good:

1class InventoryTracker {
2 constructor(items, requester) {
3 this.items = items;
4 this.requester = requester;
5 }
6
7 requestItems() {
8 this.items.forEach(item => {
9 this.requester.requestItem(item);
10 });
11 }
12}
13
14class InventoryRequesterV1 {
15 constructor() {
16 this.REQ_METHODS = ["HTTP"];
17 }
18
19 requestItem(item) {
20 // ...
21 }
22}
23
24class InventoryRequesterV2 {
25 constructor() {
26 this.REQ_METHODS = ["WS"];
27 }
28
29 requestItem(item) {
30 // ...
31 }
32}
33
34// By constructing our dependencies externally
35// and injecting them, we can easily
36// substitute our request module for
37// a fancy new one that uses WebSockets.
38const inventoryTracker = new InventoryTracker(
39 ["apples", "bananas"],
40 new InventoryRequesterV2()
41);
42inventoryTracker.requestItems();
© 2020 by Sérgio Kopplin.