Design principles for composable software components
Software design principles are abstract principles, advice, and guidelines that help you create better software. This section discusses the guiding principles that help you design complex software effectively, reduce the risk of errors, and improve our product’s maintainability, scalability, and adaptability.
Separation of concerns
The separation of concerns is one of the fundamental principles in software development and design. This principle, likely coined by Edsger W. Dijkstra in his 1974 paper “On the role of scientific thought,” illustrates how to manage complexities by separating the software system into distinct sections, ensuring that each section is responsible for a separate concern.
The separation principle generally states that you should avoid overlapping concerns in a design or code. Mia-Platform excels at separation of concerns by providing packaged business capabilities (PBC) that handle specific business concerns. This modular approach enhances code maintainability and promotes collaboration among development teams.
There are different techniques for achieving effective separation. We’ve listed a few below:
- Module-based design groups related concerns into separate modules with a well-defined interface.
- Data encapsulation hides the internal implementation of modules, exposing only their public interface.
- Dependency injection makes implicit dependencies explicit, providing the receiving object or function with its dependencies by external code (an “injector”), which it’s not aware of.
- Abstraction uses abstract interfaces to hide the concrete implementation of modules.
Designing composable and maintainable software components requires a strong understanding of abstraction. By concealing unnecessary complexities from the developers, abstraction simplifies component interactions and reduces the cognitive load on the developers. It provides users with well-defined and easy-to-use interfaces that they can build on without worrying about the intricacies.
For example, in file systems, users interact with files and directories through abstracted functions like
read() without understanding the intricate details of disk management. Abstraction promotes reusability, making it easier to compose and maintain complex software systems by providing a clean separation between what a component does and how it accomplishes it, simplifying interactions in the process.
In computing and systems design, a loosely coupled system is one in which the components are independent or detached. Changes to one component do not affect other components, which makes the system more flexible and easier to maintain, ultimately promoting composability.
You can minimize coupling between components by doing the following:
- Use well-defined interfaces: Each component should expose a well-defined interface with which other components can interact. The interface should define the methods and properties that the component provides, but it should not reveal the implementation details of the component.
- Avoid sharing state: When possible, avoid sharing state between components. This autonomy among components enhances their reliability and promotes a more predictable and maintainable system.
- Use dependency injection: Dependency injection is a technique that allows you to pass dependencies into components at runtime. This lets you to decouple the components from their dependencies, making them more testable and maintainable.
Cohesion measures how strongly elements within a module work together to fulfill a single well-defined purpose. In highly cohesive systems, the elements have a great relationship with each other and are focused on a single goal, while low cohesion in systems means that the elements are loosely related and serve multiple purposes. A system’s level of cohesiveness is directly proportional to its software quality.
To improve the cohesiveness of a software system, you can take the following actions:
- Split the software system into smaller, more manageable units of functionality, design, and reuse;
- Apply design principles and standards that ensure consistency and compatibility throughout the system;
- Refactor and review the code to improve its design and structure.
When components are considered complete, they encapsulate all the functionality and features required to perform their designated task or fulfill their intended purpose within a system. This autonomy enhances the component’s predictability, making it less likely to cause unexpected issues in the system.
To ensure completeness, you must define specifications stating the component’s purpose, inputs, outputs, and expected behavior. Additionally, reducing external dependencies, implementing robust error handling, and using modular design principles contribute to more complete components.