10 Design Principles in Software Engineering

10 Design Principles in Software Engineering

People working at deskPhoto by Marvin Meyer on Unsplash.

How many times have you been in a situation where you wanted to add a new feature to your application but could not because it was rigid? How many times have you had to rewrite code to make it testable? How many times have you had to add more code to make your code optimal for mobile?

It all comes down to design principles. In this article, I will share ten design principles all software engineers/programmers need to know.

These principles relate to how you develop the design for a system, how you implement specific components, and how you should write your code. They are not things that you need to live by. They are just guidelines that you want to think about when you are designing or implementing a system. In other words, they will make your life a lot easier in the future if you do decide to follow them.

1. Divide and Conquer

One of the core principles in any problem-solving situation, including system design, is divide and conquer. It means breaking a problem into smaller bite-sized subproblems. The idea behind this is that those problems are hard to solve due to their complexity. To make it easier, you can divide these problems into smaller problems. Solving those smaller bits will make it easier to solve the larger problem in the long run.

In a coding scenario, you may have a system that has some subsystems that have different packages that have classes with different methods that use some external dependencies or some functions. I have just broken this entire system into subsections by writing down that statement. If said subdivisions had issues, you would figure out if there were problems with the methods used in the classes, then move up to the different classes and check if they were the problem, then move up to the packages, then to the subsystems. Before you know it, you would have solved the issues.

Think of it like a Russian doll type of situation.

2. Increase Cohesion

Cohesion means grouping things that make sense together — sort of as one package. From a development point of view, you can choose to design your packages, modules, or classes cohesively. Think about the math package in Python, for example. The math package is cohesive because it has everything related to mathematical operations within it. You will not find anything within it that is not mathematical.

Cohesion brings about the organization of your code, and it will make it much easier to find things, thus simplifying the system. Everything will make more sense. This way, you will not go into the dictionary package to look for machine learning-related functions or anything like that.

3. Reducing Coupling

In simple terms, coupling occurs when packages, modules, classes, or files are very interdependent. I am not sure about you, but I know this is not the best implementation of any system. Why? If one package has some changes to it or breaks, the whole system could be compromised because some of its parts depend on the package that has malfunctioned.

I am aware that having a highly coupled system is easier to implement in the beginning, but it will be much harder to debug and fix should any component fail.

Oftentimes, you have systems that are so highly coupled that it is very difficult to determine where the coupling is occurring. You may have a package depending on another package that is dependent on another eight. Do you see how problematic that would be? You may be in a situation where you have no idea why one package is not working when another one hidden under four levels of coupling might be malfunctioning. I hope you see the idea.

If you can make your components as independent as possible, you will have an easier time debugging. That is a better design.

4. Increase Abstraction

Something is more abstract if it is a simplified version of something technical. In a coding sense, something that is more abstract is more generalized. Let me bring this home with an example: Assume you have a function that takes only a triangle as an input and displays it on the screen. What would happen if you had different shapes to display on the screen? You would have to write a function for every shape you had. Abstraction would be better if you had one function that took in any shape and displayed a specific shape based on its properties. This would be a more abstract design.

In this example, the triangle is a concrete implementation of a shape, whereas a shape is a general idea. Abstraction reduces code duplication while increasing security.

5. Increase Reusability

This is pretty straightforward and intuitive, but we want to make sure that whenever we’re writing code, we’re thinking about how we can make this code as reusable as possible. It goes hand in hand with abstraction. Rather than writing a specific function that does one thing well but only works in maybe one specific implementation, we can make it general — a bit more abstract. That way, we allow reusability in different contexts. That is the main idea behind reusability.

Think of it like making a small sacrifice up front to save time later, instead of having to go back and understand this code, rewrite it, or make another function.

6. Design for Flexibility

Designing for flexibility comes down to anticipating changes to your system in the future. Your system may be simple in the present but get more complex in the future. You may want to add a lot more stuff. You may want to swap out an implementation of an object or an item for a better one. A lot of times, people design systems for today’s use. They don’t think about the fact that in a month or maybe in a year, they’re going to scale the project up.

There is nothing worse for a developer than redeveloping something they already developed before, but we all have to face our mistakes to learn. In such a case, patching up the code base with new features will not cut it. It is easier to develop an entirely new design than to try to modify the existing one. I’m sure you have experienced this before.

7. Anticipate Obsolescence

If you are using external dependencies in your project or system, you need to be very careful about which dependencies you are using. Could they become obsolete in the future? There are many different ways through which an external dependency can fade into oblivion. It could become deprecated, it may no longer work for a specific version of your programming language after an update, or it may not be supported or maintained on, say, Windows, Linux, or Mac. You get the point.

You need to make sure that you don’t have a project that relies on hundreds of external dependencies because if one of those things goes wrong, you will not be smiling for a while. To be on the safe side, avoid early releases of software, use software from reputable companies, keep the use of external dependencies to a minimum, and keep away from poorly documented or maintained projects.

8. Design for Portability

When designing a system, you need to remember that it may be used on a different platform or device than what you’re currently targeting. If you are making a web application just for the web, it will be costly and time-consuming if you ever want to turn this into an iOS app, an Android app, a Windows desktop application, or something like that. That may involve creating an entirely new system to be able to port.

So, you want to keep this in mind when you design the system.

9. Design for Testability

This is something that almost every developer is guilty of at some point because we all start from small projects. You should know that designing for testability becomes very important when working on large-scale systems and large code bases. Think about a company like Google or Microsoft. How many tests do you think they run in a day? Even better, how many tests do you think they run on every single pull request? Probably somewhere in the high 5,000s. Why? They need to make sure that whatever you submitted will not break anything.

How are they able to run that many tests? The developers who wrote those massive code bases made them a way such that they were able to test the stuff that they write. The takeaway is that if you write a lot of code in a huge code base, ensure that it is testable.

10. Design Defensively

There is no way to ease into this, but it simply means idiot-proofing your code or layman-proofing your code. You can even think of it as beginner-proofing your code. That is the best way that I can explain this.

You have to imagine that anyone using your code, framework, classes, or whatever is a beginner. You have to imagine that. You have to make sure you have good error messages. You have to ensure that you are handling all of the invalid inputs anyone throws at it. Make it robust. Make sure it has valid error messages and possible notifications to help the user use it properly.

If you start messing around with syntax in any language, you will get pretty good error messages. You will know what it is you are doing wrong, and if you look that up on Google or Stack Overflow, you will figure out what you have done wrong in your program. The only reason you’re able to do that is that the person who wrote that code designed it defensively. They knew that people like you and I will use the framework or language, and they designed it in such a way that it would help us use it.

Conclusion

As I said at the beginning, these principles are not things that you always need to follow. There are exceptions to these principles. The main idea here is that you need to be aware of these principles. Think about them. If you want to break one of them, make sure you can justify why and understand why that principle may not apply to your situation.

I hope this helps you.