Clean Code

Programming...once more with feeling.

Programming and Good Programming

“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.”—Martin Fowler


It works, isn't that good enough?

We're developers -- we write software and do a lot of programming. We know how to make code that works. We can even write code that works well. In fact, there are many who might say:

“My code is working well, the website I built is looking great, and my client is happy. So why would I still care about writing clean code?”


Clean Code Shows We Care

As developers, it's our goal to not just program, but to program with intent and care. Well written and cared for code can help us, and others, down the road. Well written code can speak for us when we aren't there to explain it.

Writing clean code will go a long way towards improving time management, advancing your career, and helping you become a better developer.


What is Clean Code?

  • Elegant — Clean code should be pleasing to read. Reading it should make you smile the way a well-crafted music box or well-designed car would.

  • Focused — Each function, each class, each module exposes a single-minded attitude that remains entirely undistracted and unpolluted by the surrounding details.

  • Cared For - Someone has taken the time to keep it simple and orderly. They have paid appropriate attention to details. They have cared.

  • Tested - It is high quality. It passes all the tests.

  • Simple - It contains no duplication and minimizes the number of entities such as classes, methods, and functions.


But, Really...Why Should I Care?

  • Less Wasted Time - You may understand how your code works now, but you probably won't in a few weeks. Messy code takes longer for you and your teammates to read and understand.

  • Less Documentation - If your code is clean there is minimal need for comments or extensive documentation to explain it.

  • Bugs Can't Hide - Messy, spaghetti code gives bugs places to hide. Clean code allows you to debug faster.

  • Maintainable - In the future your project will probably evolve and features will be added and changed. Clean code makes maintenance faster and easier, and therefore, less expensive.

  • Pride - You will feel good about producing quality work, and your client or employer will appreciate it.


SOLID Design Principles

SOLID is a term describing a collection of design principles for good code that was invented by Robert C. Martin, also known as Uncle Bob. It is intended to make software design more understandable, flexible and maintainable.

SOLID means:

  • Single Responsibility Principle

  • Open/Closed Principle

  • Liskov Substitution Principle

  • Interface Segregation Principle

  • Dependency Inversion Principle


Single Responsibility Principle

A class should only have a single responsibility.

Every class should be responsible for one thing, and that thing should be entirely contained within that class. Classes should have one reason to change.


Open/Closed Principle

Software entities should be open for extension, but closed for modification.

In other words, we want to be able to change what the entities do, without changing the source code of the entity. How do we do this? With abstractions.


Liskov Substitution Principle

Subclasses should be substitutable for their base classes.

The user of a base class should continue to function properly if a subclass or derived class is passed to it.


Interface Segregation Principle

Many client-specific interfaces are better than one general-purpose interface.

A client should never be forced to implement an interface that it doesn't use or clients shouldn't be forced to depend on methods they do not use.


Dependency Inversion Principle

High-level modules should not depend on low-level modules. Both should depend on abstractions.

This strategy states that modules should depend on interfaces or abstract functions and classes, rather than concrete functions and classes. Why? Because concrete things change a lot and abstract things change less frequently.

High level modules deal with the high level policies of the application and care little about the details that implement them, so we decouple the dependency.


How do we write clean code?

“It is not the language that makes programs appear simple. It is the programmer that make the language appear simple!” -Robert Martin


Meaningful Names

Choosing good names takes time but saves more than it takes. The name of a variable, function, or class, should answer all the big questions- why it exists, what it does, and how it is used.

If a name requires a comment, then the name does not reveal its intent. [[[ For example.

let d // elapsed time in days.

A better name would be:

let elapsedTime

For more clarity, you could use:

let elapsedTimeInDays

Comments

"A long descriptive name is better than a short enigmatic name. A long descriptive name is better than a long descriptive comment."

If you are writing comments to prove your point, you're probably making your code harder to read at a glance. Ideally, comments are not required at all. Our code should explain everything. Modern programming languages allow ways through which we can easily explain our point.

For example, this:

// Check to see if the employee is eligible for full benefits
if (employee.isRetired && employee.age > 65)

Becomes this when the naming is clarified:

if (employee.isEligibleForFullBenefits())

Class Syntax

When we build Objects there are some things we should keep in mind:

  • The class keyword is just syntactic sugar. JS is not Classically Inherited, it is prototypically inherited.

  • Objects should have noun or noun phrase names written in PascalCase, like Customer, WikiPage, Account, and AddressParser. Avoid verbs and key words like Manager, Processor, Data, or Info in the name of a class.

  • Order Matters

    • Variables, Instances, Functions, Utility methods (After functions that call them.)
  • Objects should be concise and adhere to Single Responsibility Principle

    • If you can't derive a good class name, the class is probably too large.
    • Multipurpose classes force us to look through code we don't need to know about.

Method Names

Methods should have verb or verb phrase names written in camelCase, like postPayment, deletePage, or save.

Accessors and mutators should be named for their value and prefixed with the get or set keywords.


Pick One Word per Concept

Pick one word for each abstract concept in your classes and stick with it.

For instance, it’s confusing to have fetch, retrieve, and get as equivalent methods in different classes. How do you remember which method name goes with which class?

Likewise, it’s confusing to have a Controller and a Manager and a Driver in the same code base. What is the essential difference between a DeviceManager and a ProtocolController?


See an example of Class, Property, and Method names below:

class VirtualPet {
  constructor (name, type, health, hunger, boredom) {
    this._name = name
    this._type = type
    this._health = health
    this._hunger = hunger
    this._boredom = boredom
  }

  checkStatus()
  {
    ...
  }

  feed()
  {
    ...
  }

  play()
  {
    ...
  }
}

Small Functions

The first rule of functions is that they should be small.

The second rule of functions is that they should be smaller than that.

This implies that the blocks within if statements, else statements, while statements, and so on should be one line long. That line should probably be a function call. Not only does this keep the enclosing function small, but it also adds documentary value because the function called within the block can have a nicely descriptive name.


Few Function Arguments

A function shouldn’t have more than 3 arguments. When a function needs more than two or three arguments, it is likely that some of those arguments ought to be wrapped into a class of their own.

sum(values, startIndex, endIndex) {
  let total = 0;
  for (let index = startIndex; index <= endIndex; index++) {
    total += values[index];
  }

  return total;
}

This makes the logic crystal clear. Function names easily describe what we are trying to achieve.


Objects and Data Structures

First we need to clarify the difference between Object and Data Structures. One is just about storing data and other allows us to manipulate that data.

  • Objects hide their data behind abstractions and expose methods that operate on that data.

  • Data Structures expose their data and have no meaningful methods.


The Shape class below operates on the three shape classes. The shape classes are simple data structures without any behavior. All the behavior is in the Shape class.

class Shape {
  const PI = 3.14
  constructor() {}

  get area() {
    if(this.constructor === Square) {
      return this._sideLength * this._sideLength
    }
    if(this.constructor === Circle) {
      return PI * this._radius * this._radius
    }
  }
}

class Square extends Shape {
  constructor(sideLength) {
    this._sideLength = sideLength;
  }
}

class Circle extends Shape{
  constructor(radius) {
    this._radius = radius;
  }
}

In our code

Think about the previous example. Consider what would happen if a perimeter() function were added to Shape. The Shape Objects would be unaffected. On the other hand, if I add a new shape, I must change all the functions in Shape to deal with it.

This code is flawed according to Liskov's Substitution Principle Which states:

If S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program.

Put plainly. We shouldn't have to make drastic changes to incorporate new functionality.


class Shape {
  constructor() {}

  get area() {
    throw new Error("Abstract method must be implemented in child class...")
  }
}

class Square {
  constructor(sideLength) {
    this._sideLength = sideLength;
  }

  get area() {
    return this._sideLength * this._sideLength
  }
}

class Circle {
  const PI = 3.14

  constructor(radius) {
    this._radius = radius;
  }

  get area() {
    return PI * this._radius * this._radius
  }
}

Now we can easily add new Shapes as compared to the previous case. If we have to add perimeter() function in only one Shape, we are forced to implement that function in all the Shapes as Shape Object is an interface containing area() and perimeter() function.