Advanced Software Development

Chapter 4
Advanced Design and Programming Practices

Bachelors Program in Computer Science Engineering
Advanced Software Development   •   Bachelor

Advanced Design and Programming Practices

Introduction

Software systems become difficult to change not because individual statements are complex, but because dependencies, responsibilities, state, and failure modes become entangled. Advanced software development therefore requires more than knowledge of syntax or APIs. It requires design principles that control coupling, patterns that provide reusable solutions, refactoring techniques that improve existing code, and programming models that make behavior predictable under change and concurrency.

This chapter presents a coherent set of practices for building maintainable object-oriented and multi-paradigm software. SOLID principles provide design guidance; design patterns provide recurring structural solutions; dependency injection makes collaborators replaceable; generic and functional programming improve reuse and clarity; asynchronous and concurrent programming improve responsiveness and throughput; immutability and defensive error handling reduce defects.

The practices are complementary rather than independent. For example, dependency injection supports the Dependency Inversion Principle, immutability simplifies concurrent programming, and functional techniques make asynchronous pipelines easier to reason about.

Learning Objectives

By the end of this chapter, a student should be able to:

  1. Explain and apply the SOLID principles.
  2. Select and implement suitable design patterns.
  3. Refactor code while preserving externally observable behavior.
  4. Use dependency injection and generic programming to reduce coupling and duplication.
  5. Apply functional techniques, immutability, concurrency, and asynchronous programming appropriately.
  6. Design defensive error-handling strategies.
  7. Evaluate trade-offs involving performance, reliability, security, and maintainability.

1. Design Quality and Changeability

A well-designed system is not simply one that works today. It should remain understandable and modifiable when requirements, platforms, team members, or operational conditions change. Important quality attributes include:

  • Cohesion: how closely related the responsibilities of a module are.
  • Coupling: how strongly one module depends on another.
  • Separation of concerns: division of different reasons for change into distinct components.
  • Information hiding: exposing stable abstractions while concealing implementation details.
  • Testability: the ease with which behavior can be verified in isolation.

A useful design goal is high cohesion and low coupling. These are qualitative properties, not universal numerical scores. A module with one narrowly defined responsibility is often easier to test and change than a module that handles persistence, validation, presentation, and messaging simultaneously.

A practical design heuristic is to ask: “What is likely to change, and which component should absorb that change?” This question is more useful than mechanically applying patterns.

2. SOLID Principles

2.1 Single Responsibility Principle

The Single Responsibility Principle (SRP) states that a class should have one responsibility and therefore one principal reason to change. It does not mean that a class must contain only one method. A PurchaseOrder object may contain several operations related to order behavior, but report formatting and database persistence are usually separate responsibilities.

A common violation is a class that validates orders, calculates prices, writes files, and sends email. Changes to tax law, file format, or email infrastructure all affect the same class. Splitting these concerns improves cohesion and testability.

2.2 Open–Closed Principle

The Open–Closed Principle (OCP) states that software entities should be open for extension but closed for modification. New behavior should preferably be added through new implementations of an abstraction rather than by repeatedly editing a large conditional statement.

For example, a payment service can depend on a PaymentMethod interface. Adding WalletPayment then requires a new implementation rather than modifying the service’s central algorithm. OCP is not an absolute ban on modification; poorly chosen abstractions can create unnecessary complexity. The principle is most valuable around stable variation points.

2.3 Liskov Substitution Principle

The Liskov Substitution Principle (LSP) requires that objects of a subtype be usable wherever objects of the supertype are expected, without breaking the client’s valid assumptions. A subtype must preserve relevant contracts, including preconditions, postconditions, and invariants.

If a base ReadOnlyCollection promises that read() never changes state, a subtype that unexpectedly clears or mutates the collection violates substitutability. Inheritance should express a genuine behavioral relationship, not merely reuse implementation.

2.4 Interface Segregation Principle

The Interface Segregation Principle (ISP) states that clients should not be forced to depend on methods they do not use. A large Machine interface containing print, scan, fax, and staple methods creates difficulty for simple printers that support only printing. Smaller interfaces such as Printable and Scannable are more precise.

ISP reduces accidental coupling and makes mocks and test doubles smaller.

2.5 Dependency Inversion Principle

The Dependency Inversion Principle (DIP) has two parts:

  1. High-level policy modules should not depend directly on low-level implementation details.
  2. Both should depend on abstractions; abstractions should not depend on details.

A checkout policy should depend on a PaymentGateway abstraction rather than a specific vendor SDK. The concrete adapter implements that abstraction at the system boundary.

3. Design Patterns

A design pattern is a named, reusable solution structure for a recurring design problem. It is not a code template that must be copied mechanically. Patterns communicate design intent, but every pattern introduces concepts and should justify its cost.

3.1 Strategy Pattern

The Strategy pattern encapsulates interchangeable algorithms behind a common interface. A route planner may accept FastestRoute, LowestCostRoute, or AccessibleRoute. The context delegates algorithmic variation to the selected strategy.

Use Strategy when behavior varies independently of the object using it. It replaces growing conditional logic with polymorphism or function parameters. Its cost is additional abstractions and object coordination.

3.2 Factory Pattern

A Factory centralizes object creation when construction is complex, conditional, or tied to infrastructure. It can prevent application code from knowing concrete implementation classes. Factories are particularly useful when configuration determines the implementation.

3.3 Adapter Pattern

An Adapter converts one interface into another expected by clients. It is valuable when integrating legacy systems or third-party libraries. The adapter should isolate external terminology, error conventions, and data formats from the domain model.

3.4 Decorator Pattern

A Decorator wraps an object and adds behavior while preserving the same interface. Logging, caching, authorization, and metrics can be composed as decorators around a service. Decorators support the Open–Closed Principle, but excessive nesting can make execution order difficult to understand.

3.5 Observer and Publish–Subscribe

The Observer pattern notifies dependents when a subject changes. Publish–subscribe generalizes this idea through a broker or event channel. These patterns reduce direct coupling but introduce risks such as event ordering, duplicate delivery, memory leaks, and difficult debugging. Events should have clear ownership, schemas, and delivery guarantees.

4. Dependency Injection

Dependency injection (DI) means that an object receives its collaborators from outside rather than constructing them internally. Constructor injection is generally preferred because it makes required dependencies explicit and ensures that an object cannot normally exist in an incomplete state.

class OrderService {
    constructor(orderRepository, paymentGateway, clock) {
        this.repository = orderRepository
        this.payment = paymentGateway
        this.clock = clock
    }
}

The service can be tested with an in-memory repository, a fake gateway, and a fixed clock. This avoids tests depending on a database, network, or system time.

DI may be manual or managed by a container. A container can construct object graphs, manage lifetimes, and select implementations from configuration. However, a container does not automatically create good design. Problems include service-locator usage, hidden dependencies, circular dependencies, and excessive global scope.

Common lifetime choices are:

LifetimeMeaningTypical use
TransientNew instance for each requestStateless lightweight services
ScopedOne instance within an operation or requestUnit-of-work components
SingletonOne instance for the application lifetimeImmutable configuration or safe shared caches

Singletons must be thread-safe and must not unintentionally retain request-specific or sensitive data.

5. Generic and Functional Programming

5.1 Generic Programming

Generic programming expresses algorithms in terms of types or capabilities rather than concrete data types. A generic sort<T> can operate on any T for which an ordering is supplied. Generics improve reuse while preserving compile-time checking.

A generic abstraction should express meaningful constraints. A function that requires comparison should declare that requirement rather than accepting arbitrary values and failing at runtime. Overly elaborate generic designs, however, can reduce readability and increase compiler complexity.

5.2 Functional Techniques

Functional programming treats computation as the evaluation and composition of functions. Useful techniques include:

  • Pure functions: same inputs produce the same outputs and no observable side effects occur.
  • Higher-order functions: functions accept or return other functions.
  • Map: transform every element of a collection.
  • Filter: retain elements satisfying a predicate.
  • Reduce/fold: combine elements into one result.
  • Function composition: build a larger function from smaller functions.

For example, an order pipeline may filter valid orders, map them to totals, and reduce the totals to a daily sum. Pipelines make transformations explicit, but they should not conceal expensive operations or errors.

Side effects such as database writes and network calls should be isolated at system boundaries. This does not require a purely functional system; it means that effects should be visible and controlled.

6. Immutability and Defensive Design

An immutable object cannot be changed after construction. Instead of modifying a value, a program creates a new value. Immutability reduces aliasing problems: if two components share an immutable object, neither can unexpectedly alter the other’s view.

Use immutable value objects for money, dates, identifiers, configuration, and messages where practical. Defensive copying is necessary when accepting or returning mutable collections. A read-only view may prevent modification through one reference but may still reflect changes made through another reference; a true copy provides stronger isolation.

Immutability is especially useful in concurrent programs because immutable data can be shared without locks. It does not eliminate all concurrency problems: references to mutable resources, external systems, and compound operations still require coordination.

7. Concurrency and Asynchronous Programming

Concurrency means that multiple tasks make progress during overlapping periods. Parallelism means that tasks execute simultaneously, usually on multiple processing units. A program can be concurrent without being parallel.

Typical hazards include:

  • Race condition: result depends on uncontrolled timing.
  • Data race: concurrent access to shared memory includes at least one write without adequate synchronization.
  • Deadlock: tasks wait indefinitely for one another.
  • Starvation: a task cannot obtain needed resources because others continually receive priority.
  • Livelock: tasks remain active but make no useful progress.

A critical section must protect an invariant, not merely a single line of code. A mutex, monitor, semaphore, atomic operation, message queue, or transactional database operation may be appropriate. Lock ordering can prevent deadlock: if all participants acquire multiple locks in a consistent global order, circular wait is avoided.

Asynchronous programming represents waiting without blocking the executing thread. It is especially effective for I/O-bound operations such as network and file access. It does not automatically make CPU-bound work faster; CPU-intensive work may require parallel execution or a worker pool.

Good asynchronous practices include:

  1. Propagate asynchronous results rather than blocking on them.
  2. Define cancellation and timeout behavior.
  3. Limit concurrency when calling rate-limited services.
  4. Preserve context needed for logging and authorization.
  5. Handle exceptions at the appropriate boundary.
  6. Ensure resources are released when cancellation occurs.

A common error is launching tasks and ignoring their results. This can lose exceptions and create uncontrolled background work. Structured concurrency addresses this by making child tasks belong to a defined scope whose completion and failure are managed explicitly.

8. Defensive Error Handling

Errors should be classified according to their source and recovery strategy. A validation error may be returned to a user; a transient network failure may be retried; an invariant violation may require immediate attention; a cancellation should usually not be reported as an unexpected fault.

Defensive error handling includes:

  • validating untrusted input at boundaries;
  • using precise exception or result types;
  • preserving the original cause when translating errors;
  • avoiding sensitive data in logs and error messages;
  • applying timeouts to external calls;
  • retrying only transient and idempotent operations;
  • using exponential backoff with a maximum delay and jitter where appropriate;
  • making operations idempotent when retries are possible;
  • failing safely when security or correctness cannot be assured.

A retry is not universally beneficial. Retrying a non-idempotent payment request can duplicate a charge unless an idempotency key or equivalent protocol is used. Error handling should therefore be designed with the operation’s side effects and external guarantees in mind.

9. Refactoring and Technical Debt

Refactoring is the disciplined restructuring of internal code without changing its externally observable behavior. It is not the same as rewriting a system or adding new functionality.

A safe refactoring process is:

  1. Establish characterization or unit tests around current behavior.
  2. Make one small structural change.
  3. Run automated tests and static analysis.
  4. Review the resulting design.
  5. Commit in small, comprehensible steps.

Common refactorings include extracting a method or class, renaming misleading identifiers, replacing conditionals with Strategy, introducing a parameter object, simplifying nested logic, and removing duplication. Refactoring should address a real design smell such as long methods, shotgun surgery, feature envy, or inappropriate intimacy.

Tests are essential, but passing tests do not prove that behavior is correct if important behavior is untested. Contract tests, integration tests, property-based tests, and static analysis complement unit tests.

10. Integrated Illustrative Case: University Course Registration

Background

A university registration system initially contains one RegistrationManager class. It validates prerequisites, calculates fees, writes directly to a relational database, sends email, and calls a payment provider.

Problem

A new payment provider must be added, registration rules differ by program, and intermittent provider timeouts cause duplicate payment attempts. Tests are slow because every test uses the database and live provider.

Analysis

The class violates SRP and DIP. Direct construction of the database and provider prevents isolated tests. Conditional logic for programs violates OCP. Mutable registration objects are shared across asynchronous tasks, producing inconsistent status updates.

Approach and Solution

The system is reorganized into:

  • RegistrationPolicy, an abstraction for program-specific rules;
  • PaymentGateway, implemented by provider adapters;
  • RegistrationRepository, implemented by a database adapter;
  • NotificationService, isolated from registration policy;
  • immutable RegistrationRequest and Money value objects;
  • an injected Clock for reproducible time-dependent tests.

A Strategy selects the appropriate registration policy. Constructor injection supplies repositories, gateways, and policies. A payment request carries an idempotency key. The gateway adapter translates provider-specific failures into domain-level categories. Asynchronous notification is placed after successful registration and is monitored separately from the transaction that records enrollment.

Outcome

Unit tests use fakes and complete quickly. Adding a provider requires a new adapter rather than changes to registration policy. Timeout handling no longer blindly repeats non-idempotent requests. Immutable request data eliminates a class of race conditions, while explicit event and error boundaries make operations observable.

Learning Points

The case demonstrates that patterns work best as a system of reinforcing decisions. SOLID principles identify structural problems; DI enables substitution; adapters isolate infrastructure; Strategy isolates policy variation; immutability and idempotency make asynchronous behavior safer.

11. Professional, Ethical, and Contemporary Considerations

Maintainability has professional consequences. Poor error handling can expose personal information, inaccessible interfaces can exclude users, and unsafe concurrency can corrupt academic, financial, or medical records. Logs should follow data-minimization principles, access should be authorized, and retention should be justified by policy and applicable law.

AI-assisted code generation can accelerate prototyping, but generated code requires review for licensing, security vulnerabilities, hallucinated APIs, biased assumptions, and inappropriate data handling. Developers remain responsible for correctness, testing, provenance, and compliance. AI tools should not receive confidential source code or personal data unless organizational controls permit it.

Contemporary practice increasingly emphasizes observability, structured concurrency, automated dependency scanning, supply-chain security, infrastructure as code, and platform-independent domain design. These developments extend rather than replace the principles in this chapter.

Best-Practice Checklist

  • Design around responsibilities and stable abstractions.
  • Prefer composition when inheritance does not express a behavioral subtype.
  • Inject required collaborators through constructors.
  • Keep side effects at explicit boundaries.
  • Use immutable values for shared data where practical.
  • Bound asynchronous work with cancellation, timeouts, and concurrency limits.
  • Make retry behavior compatible with idempotency.
  • Refactor in small, tested increments.
  • Measure performance before optimizing.
  • Treat security, privacy, accessibility, and observability as design requirements.

Chapter Summary

Advanced software development is primarily the management of change, dependencies, state, and failure. SOLID principles guide responsibility allocation, extensibility, substitutability, interface design, and abstraction boundaries. Design patterns such as Strategy, Factory, Adapter, Decorator, and Observer provide vocabulary and reusable structures, but should be applied only when their trade-offs are justified.

Dependency injection makes collaborators explicit and replaceable. Generic programming improves type-safe reuse, while functional techniques make transformations and side effects clearer. Immutability reduces aliasing and simplifies sharing. Concurrency and asynchronous programming require careful treatment of races, cancellation, resource limits, and failure propagation. Defensive error handling must consider recovery, security, idempotency, and operational observability. Finally, refactoring is a disciplined, test-supported method for reducing technical debt without changing behavior.

Key Terms

SOLID principles: Five principles for maintainable object-oriented design.

Single Responsibility Principle: A module should have one principal reason to change.

Open–Closed Principle: Entities should support extension without repeated modification.

Liskov Substitution Principle: Subtypes must preserve the behavioral expectations of their supertypes.

Interface Segregation Principle: Clients should not depend on unused interface methods.

Dependency Inversion Principle: High-level and low-level modules should depend on abstractions.

Dependency injection: Supplying collaborators from outside an object.

Design pattern: A named solution structure for a recurring design problem.

Strategy: A pattern that encapsulates interchangeable algorithms.

Adapter: A component that converts one interface into another.

Decorator: A wrapper that adds behavior while preserving an interface.

Generic programming: Writing reusable algorithms over types or capabilities.

Pure function: A function without observable side effects whose output depends only on inputs.

Immutability: The property of not changing an object after construction.

Concurrency: Overlapping progress of multiple tasks.

Parallelism: Simultaneous execution of multiple tasks.

Race condition: Timing-dependent behavior caused by uncontrolled coordination.

Asynchronous programming: Structuring waiting operations without blocking the executing thread.

Idempotency: The property that repeating an operation has the same intended effect as performing it once.

Refactoring: Behavior-preserving restructuring of internal code.

Review Questions

Short Answer Questions

  1. What is the difference between high cohesion and low coupling?
  2. State the five SOLID principles.
  3. Why is constructor injection often preferred to service location?
  4. Distinguish concurrency from parallelism.
  5. Why can retrying a payment request be unsafe?

Long Answer Questions

  1. Explain how the SOLID principles can guide the redesign of a monolithic service.
  2. Compare Strategy, Adapter, and Decorator patterns, including one suitable use for each.
  3. Discuss the relationship between immutability and concurrent programming.
  4. Describe a disciplined refactoring process and explain the role of tests.
  5. Explain how asynchronous programming, cancellation, timeout, and error propagation should be designed together.

Analytical / Application Questions

  1. A service directly constructs a database repository, payment client, and system clock. Identify the design problems and propose a DI-based redesign.
  2. A multithreaded inventory system sometimes reports negative stock. Analyze likely causes and propose synchronization, immutability, or transactional strategies.
  3. Design an error and retry policy for an asynchronous order service that communicates with a rate-limited external API. Address idempotency, backoff, cancellation, logging, and security.

Multiple Choice Questions with Answers

Question 1

Which SOLID principle recommends that high-level policy depend on abstractions rather than concrete infrastructure?

A. Single Responsibility Principle

B. Dependency Inversion Principle

C. Interface Segregation Principle

D. Liskov Substitution Principle

Correct Answer: B. Dependency Inversion Principle

Question 2

Which pattern encapsulates interchangeable algorithms behind a common interface?

A. Adapter

B. Decorator

C. Strategy

D. Factory

Correct Answer: C. Strategy

Question 3

What is a principal advantage of constructor dependency injection?

A. It hides all dependencies from callers

B. It makes required dependencies explicit

C. It guarantees parallel execution

D. It eliminates the need for interfaces

Correct Answer: B. It makes required dependencies explicit

Question 4

Which property means that an object cannot be changed after construction?

A. Polymorphism

B. Idempotency

C. Immutability

D. Encapsulation

Correct Answer: C. Immutability

Question 5

Which statement best distinguishes asynchronous programming from parallelism?

A. Asynchronous programming always uses multiple processors

B. Asynchronous programming avoids waiting by blocking all threads

C. Asynchronous programming can represent waiting without blocking the executing thread

D. Parallelism is possible only for I/O-bound tasks

Correct Answer: C. Asynchronous programming can represent waiting without blocking the executing thread

Question 6

Which condition occurs when program behavior depends on uncontrolled timing between tasks?

A. Race condition

B. Compilation error

C. Type erasure

D. Encapsulation

Correct Answer: A. Race condition

Question 7

Why is an adapter useful when integrating a third-party payment library?

A. It removes the need for testing

B. It isolates the external interface from the domain model

C. It guarantees that payments are idempotent

D. It converts synchronous code into parallel code

Correct Answer: B. It isolates the external interface from the domain model

Question 8

What is the main purpose of refactoring?

A. Adding features without testing

B. Replacing all existing code

C. Restructuring internal code while preserving observable behavior

D. Increasing the number of abstractions in every module

Correct Answer: C. Restructuring internal code while preserving observable behavior

Question 9

Which operation is generally most appropriate for retry with backoff?

A. A non-idempotent charge without an idempotency key

B. A transient, idempotent network request

C. A detected security-policy violation

D. A failed invariant caused by corrupt state

Correct Answer: B. A transient, idempotent network request

Question 10

Which pattern adds behavior by wrapping an object while preserving its interface?

A. Decorator

B. Factory

C. Observer

D. Strategy

Correct Answer: A. Decorator

Advanced Software Development
Chapter 4 of 15