Project Falcon
Programming...once more with feeling.
“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.” — Martin Fowler
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?”
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.
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:
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.
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.
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.
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.
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.
“It is not the language that makes programs appear simple. It is the programmer that make the language appear simple!” -Robert Martin
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;
"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())
When we build Objects there are some things we should keep in mind:
class keyword is just syntactic sugar. JS is not Classically Inherited, it is prototypically inherited.Customer, WikiPage, Account, and AddressParser. Avoid verbs and key words like Manager, Processor, Data, or Info in the name of a class.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 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() { ... } }
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.
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.
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.
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 { 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; } }
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:
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 { 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.