Software Engineering Principles: Don’t just write code , Structure them !
Introduction:
What is software Engineering?
Software Engineering is a set of principles , procedures and methods to analyse users requirements and develop effective , reliable and high quality software .It is a set of best practices introduced by some famous industry experts that programmers should follow during software development. To write bug free, maintainable and reusable code , These practice are often adviced.
These principles set aside the major differences between software developers to software engineers
Table of content:
- Advantages of software Engineering principles
- Law of Demeter( The principle of least knowledge)
- Keep it simple, Stupid ( KISS principle )
- Don’t repeat yourself ( DRY principle )
- You Aren’t Gonna Need it ( YAGNI principle )
- SOLID principles
Advantages of software engineering principles:
- It’s Reduces complexity of the development process
- Its helps software teams in avoiding critical errors and mistake
- Its helps to achieve development goals efficiently
- Increase quality and productivity of the development etc .
Let’s go through some top design principles of software engineering.
LAW OF DEMETER ( THE PRINCIPLE OF LEAST KNOWLEDGE ) :
According to clean code book by Robert Martin , The law of Demeter says that a method D of a class A should call only (1. Methods of class A (2. Method of an object created by D etc . The idea is simple and plain : Talk to friends not strangers !
According to this principle it is important to divide responsibilities among classes and encapsulate logic within a class or method .The key recommendation of this principles are
- Keep software entities independent of each other
- Minimize coupling between different classes .
This law promotes independence among classes , Adhering to this principle makes our application maintainable.
KEEP IT SIMPLE, STUPID ( KISS PRINCIPLE ):
This principle was originated in the 1960’s when the US Navy made a valuable observation about the way systems function .They noticed that complex systems tends to perform poorly ,while simple systems tends to work well .This is because complexity can lead to a poor understanding of the system and increase bugs. With this principle we design systems that are easier to understand and more reliable.
The aim of this principle is that software code should be flexible and easily understood .When adding new features to our application we should avoid unnecessary complexity in our software by understanding the usefulness and new dependencies of frameworks and features before implementing them.
DON’T REPEAT YOURSELF (DRY PRINCIPLE):
The main aim of this principle is to avoid redundant code. It helps to maintain code readability, reusability and bug free code. It’s also helps us to avoid common maintenance and modification challenges .When the same code appears in multiple places , making even a small change requires updating the code in all those locations ,If we miss one of this updates it will result to errors that takes time and effort to fix .By following this principle we can avoid this potential pitfall and make our code more efficient and reliable .Having a single reference point or source of truth for each data .This way if we need to change any part of the data , we only need to make changes in one place instead of multiple locations.
YOU AIN’T GONNA NEED IT ( YAGNI PRINCIPLE ):
One of the challenges in software engineering is that we may think we need certain functionality in the future ,but then our requirements change and that functionality becomes unnecessary. This principle advises us to actually implement things when we actually need them , rather than adding functionality to solve potential future problems …
In simple words .. avoid over engineering!
SOLID PRINCIPLES:
SOLID is a group of object-oriented design principles ,where each letter in the acronym “SOLID" represents one of the principles .When applied together ,these principles helps developers create code that is easy to maintain
Let’s go through each SOLID principles one by one :
1. Single Responsibility principle:
This principle states that each class or method should have a clear and well — defined responsibility and that responsibility should be fully encapsulated by the class or method .This means that a class or method should have only one job and only one reason to change, so that if any part of the application needs to be modified it will only affect one class or method by following this principle we can create more modular and maintainable software systems . Designing methods or classes with a single Responsibility makes our code easier to understand,maintain and modify .
2. Open closed principle:
According to this principle we should be able to change the behavior of a class without modifying it.
Open for an extension: We should add new features to the classes without changing the existing code.
Closed for modification: Once the existing code is working ,we shouldn’t change the existing code to add functionality or features.
The main aim of this principle is to help us avoid disrupting existing functionality when we need to add new features to our software .It also helps us to maintain integrity of the codebase and makes it easier to extend our software over time.
3. Liskov substitution principle :
This principle was introduced by Barbara Liskov in 1988. Which states that derived classes should be able to be replaced by their base class, without affecting the correctness of the program .In other words , object of a parent class should be interchangeable with object of a child class.
Example of the code in C++
#include <iostream>
// Superclass
class Shape {
public:
virtual double area() const = 0;
};
// Subclass
class Rectangle : public Shape {
private:
double width;
double height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double area() const override {
return width * height;
}
};
// Subclass
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override {
return 3.14159265359 * radius * radius;
}
};
int main() {
Shape* shape1 = new Rectangle(5, 4);
Shape* shape2 = new Circle(3);
std::cout << "Area of Rectangle: " << shape1->area() << std::endl;
std::cout << "Area of Circle: " << shape2->area() << std::endl;
delete shape1;
delete shape2;
return 0;
}
In this example, we have a superclass Shape with a pure virtual function area(), indicating that any subclass must provide an implementation for area(). We have two subclasses, Rectangle and Circle, both of which inherit from Shape and provide their own implementations of area().
The key point here is that you can replace Shape objects with its subclasses (Rectangle and Circle) without affecting the correctness of the program. This adheres to the Liskov Substitution Principle. In the main function, we create objects of the subclasses and assign them to Shape pointers, demonstrating the interchangeability of objects based on their common superclass.
4. Interface Segregation principle:
This principle suggests that clients should not be forced to depend on interfaces ( contracts ) they do not need. Therefore , the interface Segregation principle implies that one interface is designed for each job type by making it small and focused .
Example of the code in JAVA
// Define an interface
// Define an interface for generic Device
interface IDevice {
void turnOn();
void turnOff();
}
// Define a separate interface for AudioDevices
interface IAudioDevice {
void playAudio();
void stopAudio();
}
// Define a separate interface for VideoDevices
interface IVideoDevice {
void playVideo();
void stopVideo();
}
// Concrete class: Speaker implements IAudioDevice
class Speaker implements IAudioDevice {
public void turnOn() {
System.out.println("Speaker turned on.");
}
public void turnOff() {
System.out.println("Speaker turned off.");
}
public void playAudio() {
System.out.println("Playing audio on the speaker.");
}
public void stopAudio() {
System.out.println("Stopping audio on the speaker.");
}
}
// Concrete class: TV implements IVideoDevice
class TV implements IVideoDevice {
public void turnOn() {
System.out.println("TV turned on.");
}
public void turnOff() {
System.out.println("TV turned off.");
}
public void playVideo() {
System.out.println("Playing video on the TV.");
}
public void stopVideo() {
System.out.println("Stopping video on the TV.");
}
}
public class Main {
public static void main(String[] args) {
// Using the Speaker as an IAudioDevice
IAudioDevice audioDevice = new Speaker();
audioDevice.turnOn();
audioDevice.playAudio();
audioDevice.stopAudio();
audioDevice.turnOff();
// Using the TV as an IVideoDevice
IVideoDevice videoDevice = new TV();
videoDevice.turnOn();
videoDevice.playVideo();
videoDevice.stopVideo();
videoDevice.turnOff();
}
}
We define separate interfaces IAudioDevice and IVideoDevice, each containing methods specific to audio and video devices, respectively.
Concrete classes Speaker and TV implement their respective interfaces, providing their own implementations of the methods.
In the main method, we demonstrate how devices can be used through their respective interfaces. This adheres to the Interface Segregation Principle, as each client (audio or video device) depends only on the methods it needs, and there are no unnecessary dependencies on methods from the other interface.
This separation of interfaces helps ensure that classes adhere to their specific contracts and prevents them from being forced to implement methods that are not relevant to their purpose, thus following the ISP.
5. Dependency Inversion principle:
Dependency Inversion principle states that high level modules/classes should not depend on low level modules/classes but only on their abstraction rather than the details . The interaction between two classes should be thought of as an abstract interaction between them not a concrete one. In simple words it’s suggests that we use interfaces instead of concrete implementations whenever possible .
Example of the code in c++
#include <iostream>
#include <vector>
// Abstraction (interface)
class Switchable {
public:
virtual void turnOn() = 0;
virtual void turnOff() = 0;
};
// Low-level module (implementation)
class LightBulb : public Switchable {
public:
void turnOn() override {
std::cout << "LightBulb turned on." << std::endl;
}
void turnOff() override {
std::cout << "LightBulb turned off." << std::endl;
}
};
// Low-level module (implementation)
class Fan : public Switchable {
public:
void turnOn() override {
std::cout << "Fan turned on." << std::endl;
}
void turnOff() override {
std::cout << "Fan turned off." << std::endl;
}
};
// High-level module
class RemoteControl {
private:
std::vector<Switchable*> devices;
public:
void addDevice(Switchable* device) {
devices.push_back(device);
}
void turnAllOn() {
for (Switchable* device : devices) {
device->turnOn();
}
}
void turnAllOff() {
for (Switchable* device : devices) {
device->turnOff();
}
}
};
int main() {
LightBulb bulb;
Fan fan;
RemoteControl remote;
remote.addDevice(&bulb);
remote.addDevice(&fan);
remote.turnAllOn();
remote.turnAllOff();
return 0;
}
We define an Switchable interface (abstraction) that specifies the methods turnOn() and turnOff().
LightBulb and Fan are low-level modules that implement the Switchable interface.
RemoteControl is a high-level module that operates on devices without knowing their specific implementations. It depends on the Switchable interface, adhering to the Dependency Inversion Principle.
In the main function, we create instances of LightBulb and Fan, add them to the RemoteControl, and control them without having to know the details of their implementations.
By adhering to DIP, we ensure that high-level modules depend on abstractions (Switchable), not on specific implementations. This allows us to change or add new devices without modifying the RemoteControl class
In conclusion, These are some of the best engineering principles that helps software engineers to build reusable, maintainable , flexible and reliable softwares for clients and businesses.