Principles of Good Software Design
Software design is the most important phase of the software development cycle. Thinking about how to structure code before you start writing it is critical. Changes and updates will inevitably arise. Good software design plans and makes allowances for added features, algorithm changes, and new integrations.
By planning ahead, you’ll save valuable time, headache, and cost on maintenance, upkeep, and extension of the original software. Designing software is an exercise in problem solving. It requires you break a task down into its component parts, deciding how you’ll address each part and how all the components will assemble together to produce the desired functionality.
As such, good design relies on a combination of high-level systems thinking and low-level component knowledge. In modern software design, the best practice revolves around creating modular components that you can call and deploy as needed. This creates software that’s reusable, extensible, and easy to test. But before you can create those components, you need to consider what functionality users (or other software) will need out of the software you’re creating.
Flow Charts & User Experience Stories
Software generally falls into three categories: user-centered, semi-automated, or completely automated.
User-centered software includes an interface where users will interact with the software to produce the desired results. In cases where you’re creating user centered software, you’ll want to consider user experience as the first step in your software design process. Understanding how users will interact with the software will help you determine what functionality to build and how that functionality ought to connect with the interface and the user’s inputs. Typically, user-centered design involves storyboarding the reasons why a user would use your software and how they’d expect to interact with the software.
On the other end of the spectrum, completely automated software has no user or interface. It’s software that talks to other software. In these cases, you won’t need a storyboard, but you’ll want to map all the components out in a flow chart to organize the various components. Sequence diagrams are also a helpful way of documenting the various communications between systems.
A semi-autonomous piece of software sits in-between. You’ll need to consider both the user’s interaction with the software and the processes that are happening autonomously as you’re architecting a solution.
Requirements Always Change
One of the reasons why good software design is so important is the demands on software are always changing. As a result, requirements change constantly as well. Sometimes a client needs new features. Other times you may want to change the libraries or tools you use to accomplish a given task. In fact, in the agile world we commit to delivering a small set of features and functionality in our given iteration and we allow requirements to change if those changes do not interfere with our current sprint commitment. Ultimately, over time, all software becomes legacy and needs updates to keep up with new business needs or technology best practices.
The challenge about designing software from the beginning of a project is the future needs may not be clear at the outset. Software design best practices anticipate a variety of future needs. It implements best practices from the beginning, instead of hacking together a solution every time a new problem arises.
Separation of Concerns: Modularity is Your Friend
The foundation of good software design is separation of concerns. This means that you divide your software into component parts and build each part once. Avoid code repetition. Always place code that you’re likely to use again in a utility class and make it accessible throughout the application. When you need to update that code in the future, you only need to edit it in one place, instead of searching for the various locations where you repeated the code.
When you need a given component, you can call it and use it in an abstraction layer. This separation is called modularity, and it’s a key to scalable, maintainable software architecture.
Modularity has several key benefits:
Testing & Debugging
Since each component is self-contained, you mitigate dependency issues. It becomes easy to test each component in isolation by using a mocking or isolation framework.. This helps us track down bugs and other problems more quickly. It also allows you to divide work among developers, since each component stands on its own.
Another benefit of modularity is reusing the code becomes easy. If you discover you need the same functionality in a new project, you can package the existing functionality into something reusable by multiple projects without copying and pasting the code. This can be accomplished with a web service or a nuget package for example.
Your software now runs as a set of independent components connected by an abstraction layer. This means adding new capabilities is as easy as creating a new component and linking it to the abstraction layer. Incorporating new features is trivial if you’ve architected the application well.
Abstract the Interface from the Algorithms
If each component runs in a modular container, you’ll need an overarching abstraction layer that the user can interact with. Based on the user’s inputs, the abstraction layer will decide which components are needed to fulfill the task. In simple terms, the code that does the work should be separate from the code that serves the features to the user.
We use this approach so that a change in one place won’t necessarily break other parts. Your abstract layer is not likely to change as often as your low-level modules. They’ll get updated, added to, and revised much more frequently. Since they’re contained, a mistake in an update won’t necessarily break the whole application, just the one module. Each module knows about the other modules in the application and what they do. However, it doesn’t know how the other modules do their work.
The goal of software design is simplicity. Each class, method, and module in your code should have a single purpose. Every new task should get its own module that we can use and modify independently. This minimizes regressions and makes the code easier to use.
Embrace simplicity, don’t add complexity where a simpler solution will work. Often, it’s tempting to think you have a brilliant solution, but if there’s a simpler way to accomplish the same task, you should always choose the simpler solution.
Learning to design software obviously gets more complex than the simple rules laid out in this article. However, these fundamentals will apply across all projects big or small. If you’re just starting your journey as a developer, you should expect these design principles to pop up in your code reviews. Even senior developers working on complex projects can often use a reminder on the principles of software design.
If you would like to learn more about Agile Design Principles, click on our free eBook below to download. The eBook covers these topics:
- Liskov Substitution Principle with C# Examples
- The Open Closed Principle with C# Examples
- The Interface Segregation Principle with C# Examples
- The Single Responsibility Principle with C# Examples
- The Dependency Inversion Principle with C# Examples