Software Design and Reusable ArchitecturesIntroductionSoftware design is the disciplined process of transforming requirements into a structure that can be implemented, tested, maintained, extended, and reused. Programming syntax alone does not produce dependable software. A program may compile and appear to work while still being difficult to change because its responsibilities are mixed, its dependencies are uncontrolled, and its components cannot be reused in another context. Software design addresses these problems through abstraction, modularity, cohesion, coupling, design principles, design patterns, architectural styles, APIs, libraries, frameworks, and dependency management. These ideas apply across C++, Java, and Python, although each language expresses them differently. A well-designed system does not merely contain many classes or files. It has clear responsibilities, stable boundaries, understandable dependencies, and interfaces that hide unnecessary implementation details. Reusability is therefore not accidental copying of code; it is the result of deliberate design. Learning ObjectivesAfter studying this chapter, a learner should be able to: - explain abstraction, encapsulation, cohesion, coupling, modularity, and separation of concerns;
- apply the five SOLID principles at diploma level;
- select and implement suitable design patterns;
- distinguish a library from a framework and an API;
- describe layered architecture and dependency direction;
- manage dependencies and assess their risks;
- design reusable components in C++, Java, or Python;
- evaluate maintainability, testability, extensibility, and ethical risks in a software design.
1. Foundations of Maintainable Design1.1 Abstraction and encapsulationAbstraction represents the essential features of an entity while omitting irrelevant implementation details. A PaymentGateway abstraction may expose authorize(amount) without revealing whether payment is sent through a bank API, a file, or a test double. Encapsulation places data and the operations that protect it behind a controlled boundary. A bank account should not expose its balance for unrestricted modification. Instead, operations such as deposit() and withdraw() enforce valid state transitions. In Java, encapsulation is commonly implemented with private fields and public methods. In C++, classes can place implementation details in private sections or use the Pimpl idiom. Python uses conventions such as a leading underscore and properties, but the language permits more direct access than Java or C++. Abstraction answers “what can this component do?” Encapsulation answers “how is its state protected?” Together they reduce the amount of knowledge that other components require. 1.2 Modularity and separation of concernsA module is a unit with a defined responsibility and interface. A module may be a function, class, package, library, or service. Separation of concerns means that different reasons for change are represented in different parts of the system. User-interface formatting, database persistence, business rules, and authentication should not be tangled in one method. Good modularity provides: - Local reasoning: a developer can understand one component without reading the entire system.
- Independent testing: a component can be tested through its interface.
- Replaceability: an implementation can change without changing clients.
- Reuse: a component can be applied in more than one context.
- Parallel development: teams can work against stable contracts.
1.3 Cohesion and couplingCohesion measures how strongly the responsibilities within a module belong together. High cohesion is desirable. A TaxCalculator that calculates tax is cohesive; a Utility class containing tax calculation, image resizing, database access, and email delivery is not. Coupling measures the degree of dependency between modules. Low coupling is generally desirable because changes in one module then cause fewer changes elsewhere. Coupling is not always bad: a useful dependency on a stable interface is preferable to duplicated logic. The design goal is controlled, explicit coupling rather than zero coupling. Common forms of undesirable coupling include: - Content coupling: one component directly changes another component’s internal data.
- Common coupling: many components share mutable global data.
- Control coupling: one component passes flags that dictate another component’s internal algorithm.
- Data coupling: components exchange only the data needed for a task; this is usually healthier.
- Interface coupling: components depend on a clearly defined abstraction; this is often desirable.
A useful design review asks: “What must change if this requirement changes?” If one requirement causes edits throughout unrelated modules, cohesion or coupling is probably poor. 2. SOLID Design PrinciplesSOLID is a group of five object-oriented design principles. They are guidelines, not laws. Applying them mechanically can produce unnecessary abstractions, so design decisions should reflect actual change and complexity. 2.1 Single Responsibility PrincipleThe Single Responsibility Principle (SRP) states that a class should have one responsibility and therefore one primary reason to change. It does not mean that every class must contain only one method. A ReportGenerator may contain several operations related to report generation, but it should not also manage database connections and send email. A practical decomposition might be: OrderCalculator: calculates totals and discounts;OrderRepository: stores and retrieves orders;InvoiceRenderer: produces PDF or HTML output;EmailService: sends messages.
SRP improves testing and reduces accidental effects, but excessive decomposition can make a simple program difficult to follow. 2.2 Open–Closed PrincipleThe Open–Closed Principle (OCP) says that software entities should be open for extension but closed for modification. Existing, tested behavior should not require repeated editing whenever a new variation is added. For example, instead of placing a growing chain of if statements for every discount type in an order class, define a discount abstraction: interface DiscountPolicy {
double discountFor(Order order);
}
final class NoDiscount implements DiscountPolicy {
public double discountFor(Order order) { return 0.0; }
}
final class StudentDiscount implements DiscountPolicy {
public double discountFor(Order order) {
return order.subtotal() * 0.10;
}
}A new policy can be added without modifying the order calculation algorithm. OCP is most useful where variation is expected; it should not be used to predict every imaginable future requirement. 2.3 Liskov Substitution PrincipleThe Liskov Substitution Principle (LSP) requires that objects of a subtype be usable wherever objects of the base type are expected without breaking correctness. A subtype must honor the promises of the abstraction, including valid inputs, outputs, and observable behavior. If a Bird abstraction promises that every bird can fly, then making Penguin a subtype creates a misleading model. The problem is not that penguins are unusual; it is that the abstraction contains an invalid promise. A better model may separate Bird from FlyingBird. Violations often appear when a subclass throws an exception for an operation that the base class promises to support, changes units, weakens guarantees, or returns an incompatible meaning. 2.4 Interface Segregation PrincipleThe Interface Segregation Principle (ISP) states that clients should not be forced to depend on methods they do not use. Several focused interfaces are often better than one large interface. For example, a printer client that only scans should depend on Scanner, not on an interface requiring print(), fax(), and scan(). In Java and C++, small interfaces can be expressed directly. Python can use protocols or abstract base classes where explicit contracts are valuable. 2.5 Dependency Inversion PrincipleThe Dependency Inversion Principle (DIP) has two related ideas: - high-level policy should not depend directly on low-level details;
- both should depend on abstractions.
For example, an order service should depend on a PaymentGateway interface rather than directly constructing a particular bank client. The concrete gateway is supplied through dependency injection. class OrderService:
def __init__(self, payment_gateway):
self.payment_gateway = payment_gateway
def pay(self, order):
return self.payment_gateway.authorize(order.total)A production gateway and a test fake can now be supplied independently. Dependency injection can be performed through constructors, setters, method parameters, or a composition root. Constructor injection is usually easiest to reason about because required dependencies are visible and the object can be valid immediately after construction. 3. Design PatternsA design pattern is a named, reusable solution structure for a recurring design problem. It is not a copy-and-paste class and is not a complete application. Patterns communicate design intent and provide a vocabulary for technical discussion. 3.1 Strategy patternThe Strategy pattern encapsulates interchangeable algorithms behind a common interface. A shopping application might use different shipping algorithms for standard, express, and international delivery. Use Strategy when: - several algorithms perform the same conceptual task;
- the algorithm should vary independently of the client;
- conditional logic is growing or being repeated.
The principal benefit is extensibility and testability. A possible cost is additional classes or objects. 3.2 Factory patternA Factory centralizes object creation. It is useful when construction is complex, depends on configuration, or should be separated from business logic. class ParserFactory:
@staticmethod
def create(file_name):
if file_name.endswith(".json"):
return JsonParser()
if file_name.endswith(".csv"):
return CsvParser()
raise ValueError("Unsupported format")The factory itself must not become an unmaintainable conditional catalogue. If product types are configured dynamically, a registry can associate names with constructors. 3.3 Adapter patternAn Adapter converts one interface into another expected by a client. Suppose an application expects send(message), but a vendor library exposes publish(topic, payload). An adapter translates between them. Adapter is valuable when an external library cannot be changed and prevents vendor-specific calls from spreading through the application. 3.4 Observer patternThe Observer pattern allows one subject to notify multiple observers when its state changes. User-interface events and domain notifications are common uses. It reduces direct coupling but introduces risks: forgotten subscriptions, notification order problems, memory leaks, and difficult debugging. In large systems, a message broker may provide stronger delivery and monitoring guarantees. 3.5 Decorator patternA Decorator adds behavior to an object without modifying its class. Logging, caching, authorization, and retry behavior can be wrapped around a service. Decorators are especially natural in Python and are also supported structurally in Java and C++. They should preserve the wrapped component’s contract; otherwise the wrapper becomes a source of surprising behavior. Pattern selectionChoose a pattern because a real problem exists, not because the pattern is fashionable. The following questions help: - What variation or instability must be isolated?
- Which object owns the responsibility?
- Does the pattern reduce coupling or merely move complexity?
- Can a simpler function or module solve the problem?
- How will the design be tested and monitored?
4. Layered ArchitectureLayered architecture organizes a system into levels with defined responsibilities. A typical application has: - Presentation layer: user interfaces, controllers, request validation, and response formatting.
- Application layer: use cases, workflow coordination, and transaction boundaries.
- Domain layer: business concepts, rules, and domain services.
- Infrastructure layer: databases, files, networks, operating-system services, and external providers.
A request may travel from presentation to application to domain, while infrastructure implements interfaces required by the inner layers. The domain should not need to know whether data is stored in PostgreSQL, a file, or memory. The traditional rule is that dependencies point toward more stable, central policy. In a strict layered design, an upper layer calls a lower layer. In dependency-inverted designs, an inner layer may define a repository interface while an outer infrastructure layer implements it. This preserves the domain’s independence while allowing the application to use infrastructure. A layered architecture provides understandable boundaries and supports testing, but it can become rigid. Small applications may not need four formal layers. Layers should correspond to meaningful responsibilities rather than empty wrappers. 5. APIs, Libraries, Frameworks, and ContractsAn API (Application Programming Interface) is a contract through which software components interact. It includes names, parameters, return values, errors, data formats, authentication requirements, and behavioral expectations. An API may be local, such as a class interface, or remote, such as an HTTP REST endpoint. A good API should have: - clear names and consistent conventions;
- explicit valid and invalid inputs;
- predictable error behavior;
- minimal exposure of implementation details;
- documentation and examples;
- compatibility and versioning rules;
- security controls and rate limits where appropriate.
A library is called by the application. The application controls the main flow. A framework usually controls the application flow and calls application-provided components. This distinction is known as inversion of control. For example, a web framework may receive an HTTP request and invoke a developer’s controller. API design should consider semantic versioning where appropriate. Under the common convention, a version has the form MAJOR.MINOR.PATCH: incompatible changes increase MAJOR, backward-compatible functionality increases MINOR, and backward-compatible fixes increase PATCH. Teams must still define what “compatible” means for their API. 6. Libraries and Dependency ManagementA dependency is an external component that a system requires. Dependencies may provide useful capabilities, but they introduce maintenance, security, licensing, and compatibility risks. A professional dependency process includes: - identifying a genuine need before adding a package;
- checking project activity, documentation, license, and community health;
- declaring exact or bounded versions in a manifest;
- committing a lock file when reproducible installation is required;
- scanning for known vulnerabilities;
- reviewing transitive dependencies;
- updating regularly in a controlled branch;
- removing unused packages;
- testing after upgrades;
- recording important architectural decisions.
Examples include Maven or Gradle in Java, CMake and package managers such as vcpkg or Conan in C++, and pip with pyproject.toml or a lock mechanism in Python. Tool names and syntax change, but the underlying principles remain. Direct dependencies are chosen by the project. Transitive dependencies are brought in by those direct dependencies. A dependency graph can contain version conflicts, excessive size, or a vulnerable package several levels below the application. A software bill of materials (SBOM) improves visibility into component provenance. Dependency management is also a governance issue. A package license may impose obligations, and downloading an untrusted package can introduce malicious code. Organizations should use approved repositories, verify integrity where possible, and protect credentials used by package managers. 7. Worked Design Example: A Reusable Notification ServiceConsider a college platform that sends assignment reminders by email, SMS, or an in-application notification. ProblemAn initial implementation places all channel logic in one method: if channel == "email":
...
elif channel == "sms":
...
elif channel == "app":
...Every new channel requires editing the same method. Testing requires real external services, and provider-specific details leak into the rest of the application. AnalysisThe design has low cohesion because one class manages message composition, channel selection, provider authentication, and delivery. It has high coupling because the application knows concrete providers. It violates SRP and tends to violate OCP and DIP. ApproachDefine a small port and inject its implementation: from typing import Protocol
class Notifier(Protocol):
def send(self, recipient: str, message: str) -> None:
...
class EmailNotifier:
def send(self, recipient, message):
# Call the selected email provider here.
pass
class ReminderService:
def __init__(self, notifier: Notifier):
self.notifier = notifier
def send_reminder(self, student_email, assignment_title):
message = f"Reminder: submit {assignment_title}."
self.notifier.send(student_email, message)A test double can record calls without sending a message: class FakeNotifier:
def __init__(self):
self.messages = []
def send(self, recipient, message):
self.messages.append((recipient, message))The composition root chooses EmailNotifier, SmsNotifier, or another adapter. The service remains focused on the reminder use case. A Strategy-like selection mechanism can choose a channel when the choice is a business rule. OutcomeThe design supports isolated tests, provider replacement, and incremental extension. It does not eliminate all complexity: delivery failures, retries, privacy, duplicate messages, and observability still require decisions. Learning points- abstractions should reflect stable business needs;
- external providers belong behind adapters or infrastructure boundaries;
- dependency injection improves testability;
- reusable design must include operational behavior, not only class structure.
8. Quality Attributes and Design Trade-offsReusable architecture should be evaluated through quality attributes: | Attribute | Design question |
|---|
| Maintainability | How easily can a requirement change be implemented? | | Testability | Can behavior be tested without unavailable infrastructure? | | Reusability | Can the component be used in another valid context? | | Reliability | How does the system behave under failure? | | Security | Are trust boundaries and privileges explicit? | | Performance | Are abstractions and calls efficient enough for the workload? | | Usability | Does the API communicate correctly with its users? | | Deployability | Can components be built and released consistently? |
There are unavoidable trade-offs. More abstraction can improve replaceability but increase cognitive overhead. A remote service may provide independent deployment but introduce latency and network failure. A framework may accelerate delivery but create vendor lock-in. Good design makes these trade-offs visible and proportionate to the system’s needs. 9. Testing and Documentation of Reusable ComponentsA reusable component needs more than a clean class diagram. Its contract should document inputs, outputs, errors, side effects, concurrency assumptions, configuration, and resource ownership. Testing should include: - unit tests for local rules;
- contract tests for interfaces and external providers;
- integration tests for database or network boundaries;
- compatibility tests for public APIs;
- security and failure tests where risk warrants them.
Mocks are useful when testing interaction, but excessive mocking can test implementation details instead of behavior. Fakes and in-memory implementations often provide more realistic tests. Public APIs should be tested as consumers use them, not only through internal methods. 10. Ethical, Security, and Governance ConsiderationsArchitecture influences who can access information and who is accountable when the system fails. Designers should apply least privilege, protect secrets, validate untrusted input, and avoid logging personal or confidential data unnecessarily. Notification services, for example, must consider consent, accessibility, message confidentiality, and the possibility of sending to an incorrect recipient. Reusable libraries can propagate vulnerabilities at scale. Teams should track provenance, licenses, vulnerability advisories, and update responsibility. Automated code-generation or AI-assisted programming can suggest patterns and dependencies, but generated code must be reviewed for security defects, license concerns, incorrect assumptions, and leakage of confidential source material. Human developers remain responsible for the resulting system. 11. Contemporary DevelopmentsModern systems increasingly combine modular monoliths, microservices, event-driven components, serverless functions, and managed platform services. These technologies do not replace fundamental design principles. A microservice with unclear responsibilities has poor cohesion, and a serverless function with uncontrolled dependencies is not automatically reusable. Domain-driven design emphasizes bounded contexts and language aligned with business concepts. Ports-and-adapters architecture separates core policy from external mechanisms. Platform engineering and internal developer platforms standardize dependency, deployment, and observability practices. Package registries, automated vulnerability scanning, SBOMs, and software supply-chain controls are now important parts of professional architecture. For diploma-level practice, the most valuable sequence is usually: begin with clear modules, define stable interfaces, inject dependencies, test behavior, and introduce more advanced architectural machinery only when the problem justifies it. Best-Practice ChecklistBefore approving a design, ask: - Is each component’s responsibility clear?
- Are public interfaces smaller than necessary rather than larger than necessary?
- Can implementation details change without widespread edits?
- Do dependencies point toward stable abstractions?
- Are external systems isolated behind adapters?
- Is the selected pattern solving a demonstrated problem?
- Can the important behavior be tested deterministically?
- Are package versions, licenses, and vulnerabilities managed?
- Are security, privacy, accessibility, and failure behavior documented?
- Is the architecture simple enough for the team to understand and operate?
Chapter SummarySoftware design creates structures that make change safer and reuse practical. Abstraction and encapsulation hide unnecessary detail, while modularity and separation of concerns organize responsibilities. High cohesion and controlled, low coupling support maintainability. SOLID principles provide guidance for assigning responsibilities, extending behavior, preserving contracts, keeping interfaces focused, and depending on abstractions. Design patterns such as Strategy, Factory, Adapter, Observer, and Decorator describe recurring solutions, but they should be selected only when they reduce a real design problem. Layered architecture separates presentation, application, domain, and infrastructure concerns. APIs define contracts; libraries are called by applications, whereas frameworks commonly control application flow. Dependency management requires version control, security review, license awareness, and reproducible builds. A professional design balances maintainability, testability, security, performance, reliability, and simplicity. Reuse is achieved not by abstraction alone, but by clear contracts, appropriate boundaries, documentation, tests, and responsible governance. Key TermsAbstraction: Representation of essential behavior while hiding unnecessary detail. API: A defined contract through which software components interact. Adapter: A component that converts one interface into another expected interface. Cohesion: The degree to which responsibilities within a module belong together. Coupling: The degree of dependency between modules. Dependency injection: Supplying a component’s dependencies from outside the component. Encapsulation: Protecting state and implementation behind a controlled interface. Factory: A component or method that centralizes object creation. Framework: Software that controls application flow and invokes application code. Layered architecture: An architecture that separates a system into responsibility-based layers. Library: Reusable software called by an application. Modularity: Organization of software into components with defined boundaries. Observer: A pattern in which subscribers are notified of subject changes. Open–Closed Principle: The principle that entities should be extensible without repeated modification. Protocol: A documented set of interaction rules or an interface contract. Reusable architecture: A structure designed for application across related contexts. Single Responsibility Principle: The principle that a component should have one primary reason to change. Strategy: A pattern that encapsulates interchangeable algorithms. SOLID: Five principles for maintainable object-oriented design. Transitive dependency: A dependency introduced indirectly through another dependency. Review QuestionsShort Answer Questions- Distinguish abstraction from encapsulation.
- Why are high cohesion and controlled coupling desirable?
- State the purpose of dependency injection.
- What is the difference between a library and a framework?
- Name two risks associated with third-party dependencies.
Long Answer Questions- Explain the five SOLID principles and provide one practical example for each.
- Describe a layered architecture and explain how dependency inversion can protect the domain layer.
- Compare the Strategy, Adapter, and Factory patterns, including a suitable use case for each.
- Explain how API design, documentation, testing, and versioning contribute to reuse.
- Discuss the benefits and limitations of introducing abstractions into a small software project.
Analytical / Application Questions- A single Java class validates orders, calculates discounts, writes to a database, and sends email. Analyze its design using cohesion, coupling, and SOLID principles, then propose a modular redesign.
- A Python application directly calls three payment-provider SDKs throughout its codebase. Design an abstraction and dependency-injection approach that permits provider replacement and automated testing.
- A team proposes converting a small desktop application into ten microservices to improve reuse. Evaluate the proposal using quality attributes, operational costs, dependency management, and the principle of proportional design.
Multiple Choice Questions with AnswersQuestion 1Which design property means that the responsibilities inside a module strongly belong together? A. High cohesion B. High coupling C. Inheritance depth D. Interface pollution Correct Answer: A. High cohesion Question 2A service receives a payment gateway through its constructor instead of creating a concrete gateway internally. Which principle is most directly illustrated? A. Liskov Substitution Principle B. Dependency Inversion Principle C. Observer pattern D. Factory Method only Correct Answer: B. Dependency Inversion Principle Question 3Which pattern converts an existing vendor interface into the interface expected by an application? A. Strategy B. Observer C. Adapter D. Decorator Correct Answer: C. Adapter Question 4Under the Open–Closed Principle, a new discount algorithm should preferably be added by: A. Editing every existing order calculation B. Adding an implementation of a discount abstraction C. Making all fields public D. Duplicating the entire order class Correct Answer: B. Adding an implementation of a discount abstraction Question 5Which statement best distinguishes a framework from a library? A. A library cannot contain classes B. A framework is always written in Java C. A library calls the operating system directly, while a framework does not D. A framework commonly controls application flow and invokes application code Correct Answer: D. A framework commonly controls application flow and invokes application code Question 6A class that manages database access, HTML formatting, and email delivery most clearly risks violating which principle? A. Single Responsibility Principle B. Liskov Substitution Principle C. Interface Segregation Principle only D. Open–Closed Principle only Correct Answer: A. Single Responsibility Principle Question 7Which is a transitive dependency? A. A package explicitly selected in the project manifest B. A package required indirectly by a selected dependency C. A local variable inside a function D. A test case with no imported packages Correct Answer: B. A package required indirectly by a selected dependency Question 8Why is a test double useful for the notification case described in the chapter? A. It guarantees network delivery B. It removes the need for an interface C. It permits testing without sending real external messages D. It automatically encrypts all notifications Correct Answer: C. It permits testing without sending real external messages Question 9Which layer normally contains database adapters and external service clients in a layered architecture? A. Presentation B. Application C. Domain D. Infrastructure Correct Answer: D. Infrastructure Question 10What is the main purpose of the Strategy pattern? A. To encapsulate interchangeable algorithms B. To guarantee zero coupling C. To replace all interfaces with global variables D. To store package versions Correct Answer: A. To encapsulate interchangeable algorithms
|