Basic Principles of Good Software Engineering

EnjoyAlgorithms Blog Cover Image

Software engineering principles are recommendations that programmers should follow during software development to write clear and maintainable code. It is a set of approaches and best practices introduced by some famous industry experts and authors. In this blog, we will go through some critical software engineering principles that will help you develop quality software.

Separation of Concerns

When specifying the behavior of a class, we need to deal with two fundamental things: basic functionality and data integrity. A class is often easier to use if these two concerns are divided as much as possible into separate sets of methods. It is helpful to clients if the documentation treats the two concerns separately. 

This principle allows code reusability because each method is written to handle a separate task and can be reused in the future for similar objectives. These are two specializations of this principle:

  • The principle of modularity: This is an idea of separating software into components according to functionality and responsibility so that development will be faster and easier.
  • The principle of abstraction: Failure to separate behavior from implementation is a common cause of unnecessary coupling. So abstraction helps us in separating software components' behavior from their implementation. We need to think of software components from two points of view: what it does? and, how it does it?

Law of Demeter or the Principle of Least Knowledge

The basic idea here is to divide the areas of responsibility between classes and encapsulate the logic within a class or method. Here are several recommendations from this principle:

  • We need to keep software entities independent of each other. 
  • Minimum coupling: We should reduce the number of connections or coupling between different classes.
  • Cohesion: We need to put related classes in the same package, module, or directory to achieve cohesion.

The law of Demeter makes classes independent of their functionalities and reduces inter-dependability between classes. Following this idea allows our application to be more maintainable, understandable, and flexible. Here is a quote from the book Clean Code by Robert Martin:

"...There is a well-known heuristic called the Law of Demeter that says a module should not know about the innards of the objects it manipulates. As we saw in the last section, objects hide their data and expose operations. This means that an object should not expose its internal structure through accessors because to do so is to expose, rather than to hide, its internal structure..."

Avoid Premature Optimization

Optimization is necessary to build faster applications and reduce the consumption of system resources. But everything has its own time. If we do optimization at the early stages of development, it may do more harm than good. The idea is simple: developing the optimized code requires more time and effort. Even we need to verify the code's correctness constantly.

So it is better to use a simple but not the most optimal method at first. Later, we can estimate the method's performance and decide to design a faster or less resource-intensive algorithm. Let's understand it from another perspective!

We all agree that optimization speeds up the development process and reduces resource consumption. But suppose we initially implemented the most effcient algorithm and our requirements get changed. What will happen? Our efforts to design an efficient code will be useless, and the program becomes difficult to change. So it would be best if you did not waste your time on premature optimization.

Keep it Simple, Stupid (KISS principle)

This principle came into the picture in 1960 when U.S. NAVY found an insight about the system functioning: the complicated system works worst, and the simple system works best! They observed that complexity causes a poor system understanding and generates more bugs.

So the idea of the KISS principle is: simple software code is easy to understand and flexible in modification or extending new features. So we need to avoid unnecessary complexity while building our software. The idea looks obvious, but we often complicate things by using fancy features. As a result, we ended up adding several dependencies.

  • So whenever we add a new dependency by using a new framework or adding a new feature or some other way, we should think: whether this complexity is worth it or not! In other words, we first consider the usefulness of adding another method/class/tool, etc.
  • Our methods need to be small and designed to solve only one problem. If there are many conditions, try to break them into a smaller block of codes.
  • Following the KISS principle makes our code cleaner and less likely to have bugs. In other words, simple code is always easy to debug and maintain.

Don’t Repeat Yourself (DRY Principle)

The DRY principle states that repeating the same code at different places is not a good idea! It helps us promote code reusability and makes it more maintainable, extensible, and less buggy. This principle originates from the book “The Pragmatic Programmer” by Andy Hunt and Dave Thomas.

Let's understand it in a better way! In software systems, there is always a need to maintain and modify the code later. If some part of code is repeated at several places, it leads to a critical challenge: a minor change in the source code will trigger a change to the same code in several places. Suppose someone misses one of the changes, they will face several errors in the application. These bugs may cost additional time, effort, and focus! 

The solution idea is simple:

  • We should not repeat ourselves while writing code or avoid copy-pasting code in different places. Otherwise, future maintenance will be complex.
  • If any code block occurs more than twice, we should move that common logic into a separate method.
  • Every piece of data should have a single reference point or source of truth, such that changing a single part of that data doesn’t require changing related code at other places.

You Aren't Gonna Need It (YAGNI Principle)

There is a famous problem in developing software: sometimes we may feel that we need that functionality in the future. But a lot of times, we may not even need it due to the changing software requirements. In the end, some or most of these functionalities become useless.

YAGNI comes from the software development methodology called Extreme Programming (XP).

So, according to the YAGNI principle: we should not add functionality to solve a future problem that we don’t need right now. Always implement things when you need them. In other words, this principle aims to avoid the complexity that arises from adding functionality that we think we may need in the future.

SOLID Principles

“SOLID” is a group of object-oriented design principles. Each letter in the acronym “SOLID” represents one of the principles. When applied together, these principles help developers create code that is easy to maintain and extend over time. It consists of design principles that first appeared in Robert C. Martin’s 2000 paper entitled Design Principles and Design Patterns.

Let’s go through each of the above software engineering principles one by one:

SRP (Single Responsibility Principle)

This principle is that every class or method should have responsibility for a single functionality provided by the software, and that class or method should entirely encapsulate responsibility. In other words: a class or method should have only one responsibility and only one reason to change, such that only one part of an application should be able to affect the class if that part is changed.

When we design our methods or classes by making them responsible for a single functionality, our code becomes easier to understand, maintain, and modify. Whenever we want to make any changes to functionality, we exactly know the place where we need to change the code.

  • The SRP principle makes the code more organized and improves code readability.
  • If we have short and focused functions or classes, we can reuse them easily. So it contributes a lot to the code reusability.

OCP (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/modules 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.

Let's understand this from a different perspective! We started the development journey by implementing many functionalities, testing them, and releasing them to the users. But when there is a need to develop new functionalities later, the last thing we want is to make changes to the existing functionality that is working well. So we try to build the new functionality on top of the existing functionality.

LSP (Liskov Substitution Principle) 

In a 1988 conference keynote address titled "data abstraction and hierarchy", Barbara Liskov introduced this principle. She stated that: derived classes should be replaceable by their base class(es). 

In other words, an object of a parent/base class must be interchangeable with an object of a child/derived class without changing the program. So the objects of our subclass should behave in the same way as the objects of our superclass.

  • An inherited class should complement, not replace, the behavior of the base class. 
  • We should be able to substitute the child for the parent class and expect the same basic behavior.

ISP (Interface Segregation Principle)

This principle was defined by Robert C. Martin while consulting Xerox. Xerox had designed a new printer software to perform various tasks such as stapling and faxing. As the software grew, making modifications became more and more difficult so that even the slightest change would take a redeployment cycle of an hour, which made development nearly impossible.

The design problem was that almost all tasks used a single Job class. A call was made to the Job class whenever a print job or a stapling job needed to be performed. This resulted in a 'fat' class with several specific methods for various clients. Because of this design, a staple job would know about all the methods of the print job, even though there was no use for them.

The suggested solution by Martin is called the Interface Segregation Principle. Instead of having one large Job class, a Staple Job interface or a Print Job interface was created that would be used by the Staple or Print classes, respectively, calling methods of the Job class. Therefore, one interface was designed for each job type, which was all implemented by the Job class.

So the Interface Segregation Principle states that a client should never be forced to depend on methods it does not use. We achieve this by making our interfaces small and focused. It would be best to split large interfaces into more specific ones focused on a particular set of functionalities so that the clients can choose to depend only on the functionalities they need.

  • Like the single responsibility principle, the goal of this principle is to reduce the side effects and frequency of required changes by splitting the software into multiple, independent parts.
  • It’s more practical to have separate interfaces that handle different tasks than one that handles many. In other words, Many client-specific interfaces are better than one general-purpose interface.

DIP (Dependency Inversion Principle)

Dependency inversion says that high-level modules should not depend on low-level modules but only on their abstractions. The interaction between the two modules should be thought of as an abstract interaction between them, not a concrete one. In simple words, It suggests that we should use interfaces instead of concrete implementations wherever possible.

So, what is the reason behind this principle? The answer is simple: abstractions don’t change a lot. Therefore, we can easily change the behavior of our closed or open-source code and boost its future evolution.

  • It also allows programmers to work smoothly at the interface level, not the implementation level.
  • This decouples a module from the implementation details of its dependencies. The module only knows about the behavior it depends on, not how it is implemented. This allows you to change the implementation whenever possible without affecting the module itself.

Some other best practices and principles

  • Measure twice and cut once: As we know, good software development project planning can produce a better result. So Before building functionality, we should first consider choosing the right problem, the right solution approach, the right tools, assembling the right team, perfect metrics to measure, and monitoring the end solution. It also requires high-level thinking about application architecture.
  • The principle of consistency: following a consistent coding style helps us understand and read the code in an effcient manner. It saves a lot of programmers time in dealing with more critical issues. Remember that: complex code might look better, readable code is always better!
  • The principle of generality: It is essential to design software free from unnatural restrictions and limitations. In other words, we should develop the project so that it should not be limited or restricted to some of the cases/functions. This would help us provide service to the customer broadly based on their general needs.
  • Remember open source: There are so many open-source options out there. So one of the biggest time wasters in software engineering is building code to do something someone has already written.
  • Follow modern programming practices: To enter current technology trends, modern programming practices are essential to meet users' requirements in the latest and advanced way.
  • Develop a clear understanding of requirements: Understanding user requirements via a well-defined requirement analysis process is critical for good software engineering.
  • Define a project vision: Designing and maintaining the project's vision is one of the most important things throughout the complete development process and critical for success.
  • Write Good documentation: when other developers work on another’s code, they should not be surprised and not waste their time getting code. So providing better documentation for each step of development is a good way of building software projects.
  • Add sensible logging. Make sure you have a way of logging / tracing the execution of your code. Make sure that the mechanism is lightweight and has various log levels (e.g., informational, warning, error) to let you control how much is logged.

Enjoy learning! Enjoy software engineering!

We'd love to hear from you

More content from EnjoyAlgorithms

Our weekly newsletter

Subscribe to get free weekly content on data structure and algorithms, machine learning, system design, oops and math. enjoy learning!