Chapter #3 Design Principles — Software Design and Architecture Specialization University of Alberta
This module provides comprehensive principles for assessing the architecture of software solutions, aiming to guarantee their adaptability, reusability, and maintainability. Additionally, it delves into the use of UML state and UML sequence diagrams for modeling the behaviors of objects within the software.
Evaluating Design Complexity
Simplicity is crucial in programming, as excessive design complexity can lead to increased bug occurrences when it surpasses developers’ cognitive capacities. Evaluating design complexity is essential, and it applies to both classes and their methods within the modules. In this context, the term ‘module’ refers to program units encompassing classes and their methods, while a ‘system’ denotes a combination of multiple modules. A poorly designed system restricts module connections to specific counterparts, whereas a well-designed system enables seamless connectivity and reusability among modules. The primary metrics for assessing design complexity are coupling and cohesion.
Coupling
Coupling assesses the intricacy between a module and other modules, with a focus on striking a balance between tight and loose coupling. Tight coupling occurs when a module excessively depends on others, while loose coupling enables seamless connectivity via well-defined interfaces. To evaluate coupling, consider the metrics of degree, ease, and flexibility. Degree refers to the number of connections between the module and others, which should ideally be minimal. Ease emphasizes the simplicity of establishing connections without delving into the implementations of other modules. Flexibility indicates how easily other modules can be substituted in the future. Signs of a tightly coupled system with poor design include excessive parameters or interfaces, challenges in identifying corresponding modules, and limited interchangeability with specific modules.
Cohesion
Cohesion pertains to the intricacy within a module, reflecting the clarity of the module’s responsibilities. It operates between high and low extremes, with high cohesion indicating a module dedicated to a single task or purpose, while low cohesion suggests a module encapsulating multiple purposes, leading to an unclear focus. Maintaining a balance between low coupling and high cohesion is crucial in system design, as both are imperative for effective design. Complex systems require a careful distribution of complexity within and between modules. Simplifying modules for high cohesion may heighten their dependence on other modules, thereby increasing coupling. Conversely, simplifying connections between modules for low coupling might necessitate modules to shoulder more responsibilities, thus reducing cohesion.
Separation of Concerns
The design principle of decomposition involves dividing a whole into distinct parts. To grasp the significance of decomposition, one must explore the principle of separation of concerns, which emphasizes keeping various concerns in the design distinct. Concerns refer to any relevant aspect in resolving a problem. When designing software, different concerns should be handled in separate segments. When implementing abstractions from the problem space, the software can give rise to additional concerns, such as data representation, manipulation, and presentation. To prevent getting overwhelmed by these concerns and subproblems, the design should be structured to carefully address all concerns. This entails segregating different subproblems and concerns into separate sections during the software system’s design and construction, thereby applying the principle of separation of concerns.
Separation of concerns offers several benefits, including independent development and updates of software sections, facilitating changes in one component without affecting others, and not requiring comprehensive knowledge of all code sections for updates. This concept forms the foundation of object-oriented modeling and programming, leading to the creation of more cohesive classes and enforcing design principles like abstraction, encapsulation, decomposition, and generalization. Abstraction involves separating each concept with its attributes and behaviors, while encapsulation involves grouping attributes and behaviors into classes, enabling easy implementation changes while maintaining a consistent interface view. Decomposition allows the division of a class into multiple classes, while generalization recognizes commonalities and incorporates them into a superclass. Implementing separation of concerns in software design enhances maintainability, as each class contains only the necessary code for its tasks, promoting modularity and facilitating class reusability without impacting others. While the delineation of class boundaries may not always be straightforward, making decisions about abstraction, encapsulation, decomposition, and generalization is crucial for designing modular software.
public interface ICamera { public void takePhoto(); public void savePhoto(); public void cameraFlash();
}
public interface IPhone {
public void makePhoneCall();
public void encryptOutgoingSound(); public void deciphereIncomingSound();
}
public class FirstGenCamera implements ICamera { /* Abstracted camera attributes */
public class TraditionalPhone implements IPhone { /* Abstracted phone attributes */}
public class SmartPhone { private ICamera myCamera; private IPhone myPhone;
public SmartPhone( ICamera aCamera, IPhone aPhone ) {
this.myCamera = aCamera;
this.myPhone = aPhone; }
public void useCamera() {
return this.myCamera.takePhoto();
}
public void usePhone() {
return this.myPhone.makePhoneCall();
}
}
Information Hiding
A well-designed software system adheres to several design principles, including the crucial concept of information access. Not all software components need to be aware of each other, and modules should only access the necessary information required for their functions. This restriction of information access, ensuring that modules can utilize only the minimum information necessary and concealing all else, is achieved through information hiding. Information hiding, while commonly associated with sensitive data, is also used in software design to conceal changeable details such as algorithms or data representations. In contrast, assumptions are typically expressed in APIs and interfaces and are not hidden. By employing information hiding, developers can work on modules independently, relying on their interfaces rather than needing to know the module’s implementation details.
Information hiding is closely tied to encapsulation, which involves bundling attributes and behaviors into appropriate classes, providing access to modules through interfaces, and restricting access to specific functions. Encapsulation conceals the implementation of behaviors behind interfaces, ensuring that changes to the implementation do not affect the expected outcome. Additionally, attributes can be hidden to prevent critical information from being changed directly. Access modifiers, such as public, protected, default, and private, enable the realization of information hiding by controlling which classes can access attributes and behaviors and determining the attributes and behaviors shared between superclasses and subclasses.
- Public
Public access modifiers allow any class in the system to access and modify the attributes or call the methods associated with them. However, while the method can be accessed, it does not permit other classes to alter the behavior’s implementation. A publicly accessible method enables other classes to invoke the behavior and receive its output, while the underlying implementation remains concealed through encapsulation.
- Protected
Protected attributes and methods are not universally accessible in the system. They are limited to the encapsulated class itself, all subclasses, and classes within the same package. In Java, packages organize related classes into a single namespace.
- Default
The default access modifier restricts access to attributes and methods solely to subclasses and classes within the same package or encapsulation. Referred to as the “no modifier access,” this access modifier does not require explicit declaration in the code.
- Private
Private attributes and methods are solely accessible within the encapsulating class itself. This restriction implies that these attributes cannot be directly accessed, and the methods cannot be invoked by any other classes.
Conceptual Integrity
Conceptual integrity is a critical concept in creating coherent and consistent software systems. It involves making deliberate design and implementation decisions so that even when multiple individuals work on a project, the result appears unified and consistent, as if guided by a single mind. Achieving this typically involves agreeing on specific design principles and conventions for the software. It’s essential to note that conceptual integrity doesn’t mean ignoring the valuable input of the development team; their ideas should be openly discussed and aligned with the agreed-upon principles and conventions.
There are various approaches to establish and maintain conceptual integrity, including effective communication to facilitate agreement on libraries or methods, code reviews for systematic examination of code to ensure consistency, the use of design principles and programming constructs like Java interfaces, well-defined software architecture, unifying concepts to treat different aspects uniformly, and having a small core group responsible for reviewing and accepting code commits. This small group ensures that changes align with the software’s architecture and design, ultimately promoting consistency and adherence to the agreed-upon principles.
Conceptual integrity is a crucial consideration in system design as it guides the development team in creating software with consistent design and logic, making it comprehensible and maintainable. It serves as a framework for software projects, preventing haphazard and disorganized code, leading to a more structured and organized development process.
Generalization Principles
The previous module in this course emphasized the importance of design principles, including abstraction, encapsulation, decomposition, and generalization, in guiding object-oriented system design. While some design decisions are relatively straightforward, generalization and inheritance can be more challenging in object-oriented programming and modeling.
Inheritance is a potent design tool that can facilitate the creation of clean, reusable, and maintainable software systems. However, misuse of inheritance can lead to poor code quality, potentially introducing more problems than it solves. To identify whether inheritance is being misused, it is advisable to consider a couple of generalization principles.
One principle involves asking whether a subclass serves a meaningful purpose or if it merely inherits attributes or behavior from a superclass without adding anything distinctive. If the answer is “yes” to the latter, it suggests inheritance is being misused, as there is no valid reason for the subclasses to exist, and the superclass should suffice.
For instance, in the case of employees, managers, salespeople, and cashiers, each represents a specific subtype of employee with unique functions, justifying the use of inheritance. However, when creating different kinds of pizza, there is no genuine specialization between them, making subclasses unnecessary.
Another technique to assess inheritance misuse is by examining the Liskov substitution principle, which asserts that a subclass should replace a superclass if and only if it does not alter the superclass’s functionality. If a subclass replaces a superclass but significantly changes the superclass’s behaviors, inheritance is being misused.
For example, if a Whale class, known for swimming behavior, replaces an Animal class and overrides functions such as running and walking, it violates the Liskov substitution principle, as the Whale behaves differently from what is expected of its superclass.
In Java, an example of poor inheritance can be seen in the Stack class, which inherits from the Vector superclass. While a stack should follow the first-in-last-out data structure with specific behaviors like peek, pop, and push, the Stack class inherits additional, non-standard behaviors from Vector, such as indexing and element insertion, which are not expected from a stack, illustrating an instance of problematic inheritance.
Specialized UML Class Diagrams
UML Sequence Diagrams
UML sequence diagrams, a type of UML diagram, serve as a crucial planning tool used before the development phase to showcase how objects in a program interact in task completion. They act as visual representations of conversations between various entities, highlighting the messages exchanged between them. Sequence diagrams aid in conceptualizing the software’s class structure and identifying the necessary functions to be implemented. Additionally, they can reveal previously unnoticed issues within the system.
UML State Diagrams
UML state diagrams represent the behavior and responses of systems, tracking the states of a system or an individual object and showcasing the transitions between these states as events unfold. These diagrams provide a visual depiction of how objects change states in response to various events, with a state representing the condition of an object at a specific moment, determined by its attribute values. A helpful analogy for understanding states is that of a car with an automatic transmission, which can have various states such as park, reverse, neutral, and drive. For instance, when the car is in the reverse state, it can only move backward, necessitating a change to the drive state to move forward. Similarly, objects in a software system undergo various states, determining their behavior as events occur.
Model Checking
Software verification techniques are essential for ensuring the functionality and reliability of a system. In addition to unit testing, beta testing, and simulations, model checking is a systematic approach that scrutinizes the system’s state model in all possible states to uncover errors that other tests might overlook. By simulating various events and their impacts on states and variables, model checking identifies violations of rules within the system’s behavior. This process is typically facilitated by dedicated model checking software available for developers in different programming languages.
The model checking process involves three phases: modeling, running, and analysis. In the modeling phase, the model description, along with the desired properties, is specified, and quick sanity checks are conducted to identify simple errors. During the running phase, the model checker is executed to assess how well the model aligns with the predefined properties. Finally, the analysis phase examines whether the desired properties are satisfied and identifies any violations, referred to as counterexamples. Insight from the model checker aids in refining the software and rectifying any issues, with the process often repeated until the software meets the desired properties.
Model checking not only ensures that software is well-designed but also guarantees that it functions as intended and adheres to the desired properties and behavior.
Ibrahim Can Erdoğan