Principles and Practices that we should follow when coding

Solid Principles
SOLID principles are object-oriented design concepts that are applicable to software development. It is conceptualized by Robert C. Martin. SOLID stands for Single Responsibility Principle, Open-Closed Principle, Liskov Substitution Principle, Interface Segregation Principle, and Dependency Inversion Principle.
SOLID is a systematic design strategy that assures your program is modular, understandable and refactorable. Following SOLID principles also saves time and effort in both development and maintenance. SOLID keeps your code from becoming inflexible and fragile, allowing you to create long-lasting software.
The word SOLID acronym for:
- Single Responsibility Principle (SRP)
- Open-Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
1. Single responsibility principle
Every class should be assigned a single task. To be more specific, there should be only one reason to change classes. The following is an example of a Java class that violates the single responsibility principle (SRP):
public class Vehicle {
public void printVehicleDetails() {}
public double calculatePrice() {}
public void saveDetails() {}
public Vehicle getVehicleDetails(int vehicleId)
}
Above Vehicle class is responsible for three distinct tasks: reporting, computation, and database management.
2. Open-closed principle
Software entities (such as classes, modules, and functions) should be extensible but not modifiable. According to the open-closed principle, a module should be open for extension but closed for modification when new requirements arise. We may use the extension to provide additional functionality to the module.
Consider the below method of the class VehicleCalculations
:
public class VehicleCalculations {
public double calculateAmount(Vehicle vehicle) {
if (vehicle instanceof Car) {
return vehicle.getPrice() * 0.3;
if (vehicle instanceof Bike) {
return vehicle.getPrice() * 0.4;
}
}
Let’s say we want to create a new subclass named Van. We’d have to add another if expression to the aforementioned class, which goes against the Open-Closed Principle.
A better solution would be for the Car and Van subclasses to override the calculateAmount
method:
public interface Vehicle {
public double calculateAmount();
}public class Car implements Vehicle {
public double calculateAmount() {
return this.getPrice() * 0.3;
}public class Bike implements Vehicle{
public double calculateAmount() {
return this.getPrice() * 0.4;
}public class Truck implements Vehicle{
public double calculateAmount() {
return this.getPrice() * 0.4;
}
3. Liskov substitution principle
Barbara Liskov proposed the Liskov Substitution Principle (LSP). It pertains to inheritance in the sense that derived classes must be 100% interchangeable with their source classes. To put it another way, if class A is a subtype of class B, we should be able to substitute B with A without affecting the program’s behavior. Consider the following example of a Rectangle base class and a Square derived class:
public class Rectangle {
private double height;
private double width;
public void setHeight(double height) { this.height = height; }
public void setWidht(double width) { this.width = width; }
}public class Square extends Rectangle { @Override
public void setHeight(double height) {
super.setHeight(height);
super.setWidth(height);
} @Override
public void setWidth(double width) {
super.setHeight(width);
super.setWidth(width);
}
}
Because you can’t replace the Rectangle base class with its derived class Square, above classes don’t follow the Liskov substitution principle. The Square class includes additional requirements, such as the height and width must be equal. As a result, swapping Square for Rectangle may result in unexpected behavior.
4. Interface segregation principle
Clients should not be compelled to rely on interface members they do not utilize, according to the Interface Segregation Principle (ISP). To put it another way, don’t make any client implement an interface that they don’t need.
Assume there’s a vehicle interface and a Bike class:
public interface Vehicle {
public void drive();
public void stop();
public void openDoors();
public void closeDoors();
}
public class Bike implements Vehicle {
// Can be implemented
public void drive() {...}
public void stop() {...}
// Can not be implemented
public void openDoors() {...}
public void closeDoors() {...}
}
As you can see, the openDoors() and closeDoors() methods make no sense for a Bike class because a bike does not have any doors! To address this, ISP offers breaking down the interfaces into several, tiny coherent interfaces so that no class is required to implement any interfaces (and hence methods) that it does not require.
5. Dependency inversion principle
We must employ abstraction (abstract classes and interfaces) rather than actual implementations, according to the dependency inversion principle. High-level modules should not be dependent on low-level modules, but rather on the abstraction. Because abstraction is independent of detail, whereas detail is independent of abstraction. It decouples the software.
Consider the following scenario. Because the Car class is dependent on the concrete Engine class, it does not follow dependency inversion principle.
public class Car {
private Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
public void start() {
this.engine.start();
}
}
public class Engine {
public void start() {...}
}
For the time being, above code will function, but what if we want to add another engine type, such as a diesel engine? The Car class will need to be refactored in order to do this. It can not be done according to the Open / Close principle.
However, by creating a layer of abstraction, we may fix this problem. Let’s provide an interface to the Car instead of relying just on the Engine:
public interface IEngine {
public void start();
}
We can now link any type of Engine to the Car class that implements the Engine interface:
public class Car {
private EngineInterface engine;
public Car(IEngine engine) {
this.engine = engine;
}
public void start() {
this.engine.start();
}
}
public class PetrolEngine implements IEngine {
public void start() {...}
}
public class DieselEngine implements IEngine {
public void start() {...}
}
Practices
1.Unit testing
Unit testing is a testing approach in which individual modules are checked by the developer to see if there are any flaws. It is concerned with the independent modules’ functional soundness.
The basic goal is to isolate each component of the system in order to detect, evaluate, and correct any flaws.
Unit Testing — Advantages:
- Reduces bugs while modifying current functionality or reducing defects in newly built features.
- Defects are caught early in the testing process, which lowers the cost of testing.
- Enhances code reworking and improves design.
- When unit tests are used in conjunction with the build process, the build’s quality is improved.
2.Code quality
The company has standardized and prioritized a set of qualities and standards for code quality. The following are the primary characteristics that may be utilized to determine it.
- Reliability: It should function reliably without crashing frequently.
- Clarity: Should adhere to the language’s coding and naming norms. Anyone who isn’t the code’s creator will find it simple to read and understand. It’s lot easier to maintain and extend code if it’s simple to grasp. It is necessary for people, as well as computers, to comprehend it.
- Maintainability: A good piece of code isn’t excessively complex. If anybody working with the code wants to make any modifications, they must first comprehend the entire context of the code.
- Well Documented: The ideal case scenario is when the code is self-explanatory, although adding comments to the code to clarify its purpose and functions is always suggested. It makes it considerably easier for those who weren’t involved in the code’s creation to comprehend and maintain it.
- Well-tested: The better the quality of the code, the fewer flaws it has. Thorough testing weeds out significant flaws, ensuring that the program performs as expected.
- Extendible: Code must be extendable so that it does not have to be scrapped every few weeks when new needs arise.
- Efficiency: To accomplish the required operation, high-quality code does not consume unneeded resources.
- Secure: Should prevent coding vulnerabilities.
- Lower Technical Debt: Teams with little technical debt may move quickly and build new features without being bogged down by low-quality, non-maintainable code.
How to enforce code quality?
- Code repetition must be avoided.
- The code should be readable.
- Do not reinvent the wheel.
- Commenting and maintaining.
- Completely avoid hard coding.
- Adhere to the stylebook and set code review rules.