Essential Software Development Principles.
Maintaining Software Development Principles is essential for writing simple and maintainable code. These are concepts to follow while coding to write better future proof code. Let’s take a glimpse on the core principles.
S.O.L.I.D principles:
- Single Reponsitibily Principle: “A class should only have one reason to change.” This means that a class should have only one job and do one thing. You’ll be tempted to put similar classes together but the problem is as the class needs to be modified later, you’ll need to be on a constant lookout on the dependencies inside that class; may even need to refractor other methods as well. So, create different classes each with a clear and specific goal only.
- Open/Closed Principle: “Software entities (classes, functions, modules etc.) should be open for extension but closed for modification.” It is open for extension means you can extend what the module can do and closed for modification means you cannot change the source code, even though you can extend the behavior of a module or entity. In short, a class, module, function, and other entities can extend their behavior without modifying their source code. In other words, an entity should be extendable without modifying the entity itself. How?
const iceCreamFlavors = ['chocolate', 'vanilla'];
class makeIceCream {
constructor(flavor) {
this.flavor = flavor;
}
make() {
if (iceCreamFlavors.indexOf(this.flavor) > -1) {
console.log('Great success. You now have ice cream.');
} else {
console.log('Epic fail. No ice cream for you.');
}
}
}
The code above fails the open/closed principle. Why? Well, because the code above is not open to an extension, meaning for you to add new flavors, you would need to directly edit the iceCreamFlavors
array. There is no method to add or remove or extend the functionality unless directly modifying the iceCreamFlavors
array. Doing so would make the code is no longer closed to modification as the array may have various values if the main code is changed.
To fix this, you would need an extra class or entity to handle addition, so you no longer need to modify the code directly to make any extension. Doing so, you’ll give the user the power to modify the array and maintain it’s value.
const iceCreamFlavors = ['chocolate', 'vanilla'];
class makeIceCream {
constructor(flavor) {
this.flavor = flavor;
}
make() {
if (iceCreamFlavors.indexOf(this.flavor) > -1) {
console.log('Great success. You now have ice cream.');
} else {
console.log('Epic fail. No ice cream for you.');
}
}
}
// extending the features of the makeIceCream class.
class addIceCream {
constructor(flavor) {
this.flavor = flavor;
}
add() {
iceCreamFlavors.push(this.flavor);
}
}
Thus, the user can modify the iceCreamFlavors
array through creating the variable using new addIceCream
and the add()
method to add new flavors. Thus extending the makeIceCream
class (open for extension) and restricting any kind of modification to the makeIceCream
class. So, the parent class is following the open/closed principle.
- Liskov Substitution Principle: “Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.” The principle defines that in an inheritance, objects of a superclass (or parent class) should be substitutable with objects of its subclasses (or child class) without breaking the application or causing any error. In very plain terms, the objects of your child’s class to behave in the same way as the objects of your parent’s class. So, that when operations are performed on the lot of child classes, each operation synchronizes perfectly with the single parent.
For example, a parent named ball
with weight and radius. Then, children like cricketball
, football
, vollyball
, rugbyball
each with various properties like design, color, shape etc. But when it comes to weight and radius, as these these 4 children extends the ball class, changing/replacing any of their value will sync. Not like weight, radius can of different unit or type across children. Doing that would fail to follow the Liskov Substitution principle. It’s like the weight, radius properties in each children are replaced by the weight, radius property of the parent.
- Interface Segregation Principle: “Many client-specific interfaces are better than one general-purpose interface.” Like the Single Responsibility Principle(SRP), the target of the Interface Segregation Principle is to reduce the side effects and frequency of required changes by splitting the code into multiple/independent parts. A client should never be forced to an interface that he doesn’t use or to depend on methods they do not use. Segregation(the action of setting someone or something apart from others) is all about keeping things separated, meaning maintaining separate the interfaces for client each with exact purpose or call to action.
- Dependency Inversion Principle: “Entities must depend on abstractions, not on concretions. High-level module must not depend on the low-level module, but they should depend on abstractions.” High-level modules (which provide complex logic) are easily reusable and unaffected by changes in low-level modules (which provide utility features). High-level modules should not import anything from low-level modules. Both should depend on abstractions (for example, interfaces). Abstractions should be independent of details and details (concrete implementations) should depend on abstractions. In plain terms, this principle states that your classes should depend upon interfaces or abstract classes instead of concrete classes and functions. This makes your classes open to extension, following the open-closed principle. And let’s you track dependencies just by checking the interfaces. Any extension will have to made through the interface not directly to the parent or High level module. This is dependency inversion where dependency is in invert order; instead of low to high level dependency where input at low level decides the actions at high level, here the high is the most independent and lower modules become more dependent on the high level modules. A simple example is, would you shoulder a lamp directly to the electrical wiring in the wall?! or set a socket to the wall and then place any lamp to the socket.
YAGNI (You ain’t gonna need it) :
“Always implement things when you actually need them never implements things before you need them.” The principle helps developers avoid wasted effort on features that are assumed to be needed at some point. The idea is that this assumption often ends up being incorrect. Even if a feature ends up being desired, it still may turn out that the implementation is not necessary. The argument is for developers to not waste time on creating extraneous elements that may not be necessary and can hinder or slow the development process. As YAGNI helps avoid spending time on features that may not be used, the main features of a program are better developed and less total time is spent on each release. Much like the Worse is Better principle, YAGNI is against the development of extra features and helps avoid feature creep.
DRY (Don’t repeat yourself) :
“Each small pieces of knowledge (code) may only occur exactly once in the entire system.” The root cause of all these problems is the creation of duplicate representations. There might be unintended duplications where developers don't even realize they're duplicating information. This is more common in large organizations, where entire functionalities might be duplicated. The most common reason for this is a lack of collaboration between teams. Sometimes the environment demands code duplication, and the developer needs to show enough ingenuity to avoid such duplication.
KISS (Keep it simple, stupid!) :
“Try to keep each small piece of software simple and unnecessary complexity should be avoided.” Simple in this context doesn’t necessarily mean easy, It simply means producing the same results or a better result with less effort or complexity. In the concept of KISS, we neither want just less nor more, we simply wish to have only as much as is required. If you’re building a product that moves an object from point A to point B, then do that as efficiently as possible, while having simplicity at the back of your mind.
In the programming context, there are a few points to note whenever we want to reduce complexity.
Ensure your variable names describes the variable it holds properly.
Ensure your method names translates to the purpose of that method.
Write comments within your method where necessary.
Ensure your classes has a single responsibility.
Avoid global states and behaviors like as much as you can.
Delete instances, methods or redundant processes within the code base that are not in use.
These are the core or most used software development principles. Even though these are only theory, absorbing them internally helps streamline the development process.