Ch. 09 Lab-10 Chapter 10 · Software Maintenance and Evolution

Introduction to Software Engineering · Chapter 10

Software Maintenance and Evolution

Most of the cost of software comes after its first release. This chapter covers how systems change over time and how to keep them healthy: maintenance categories, technical debt, refactoring, versioning, change control and release management.

ISO/IEC/IEEE 14764:2022 Lehman's laws · SemVer 2.0.0 · Keep a Changelog C++20 · CMake · GoogleTest

Navigate with ← → · Space next · Home/End · G jump to slide · O overview · F fullscreen
Hover the bottom of the screen to reveal the navigation panel.

Agenda

What we will cover

      Click any item to jump directly to that topic.

      Introduction

      Software is delivered once and maintained for years

      • Software maintenance: every modification of a software product after delivery, to fix faults, adapt it to a new environment, or improve it (SWEBOK Guide v4.0, Software Maintenance knowledge area).
      • Software evolution: the long view of the same work. How a system grows, changes shape and ages over many releases.
      • Studies since the 1970s report that maintenance takes most of the life-cycle cost, typically quoted as 60 to 80 %, sometimes more. The exact share depends on the system and on what is counted, so treat any single number with care.
      • ITC Club Hub: your team builds it in weeks 9 to 14. If the clubs office adopts it, next year's students will maintain code they did not write. Everything in this chapter is about making that possible.
      Cost-of-change link (Chapter 01): the later a change is made, the more it costs. Maintenance is where every shortcut taken during development is finally paid for.
      %%{init: {'themeVariables': {'pie1': '#3d8bfd', 'pie2': '#20c997', 'pieSectionTextColor': '#ffffff', 'pieStrokeColor': '#ffffff', 'pieOuterStrokeWidth': '0px'}}}%%
      pie showData
        title Life-cycle cost, typical split (illustrative)
        "Maintenance and evolution" : 70
        "Initial development" : 30
      

      Illustrative split at the middle of the 60 to 80 % range reported in the literature; not a measurement of a real project.

      Topic 1 · Types of maintenance

      Four categories, sorted by two questions

      • ISO/IEC/IEEE 14764:2022 classifies maintenance by why a change is made, not by how big it is.
      • Question 1: is something wrong (correction), or is something new or better needed (enhancement)?
      • Question 2: was it triggered by a failure or an outside change (reactive), or did we act before anyone noticed (proactive)?
      • The category drives priority and version: corrective work is often urgent and becomes a PATCH; perfective work is planned in the backlog and usually becomes a MINOR release.
      Where the effort goes (classic survey)Share
      Perfective (new features, improvements)about 50 %
      Adaptive (environment changes)about 25 %
      Corrective (bug fixing)about 21 %
      Otherabout 4 %

      Lientz and Swanson (1980). Bug fixing is only about one fifth of maintenance.

      Correction Enhancement Proactive we act before users notice Reactive triggered by a failure or an outside change Preventive detect and fix latent faults before they become failures Club Hub: review finds a seat counter that can go negative Perfective improve performance, usability, maintainability; add features * Club Hub: waiting list, faster event list, split a 400-line file Corrective fix a failure that was reported after delivery Club Hub: cancel rejected at exactly 24 h before the start Adaptive keep the product usable in a changed or changing environment Club Hub: GCC 14 on the lab PCs, student IDs grow to 10 characters ISO/IEC/IEEE 14764:2022 · * the 2022 edition also names new features "additive" maintenance

      Topic 1 · Worked example

      Worked example: ten Club Hub change requests, classified

      CRRequestTriggerCategory
      01The CLI crashes when events.csv has an empty description fielduser reportCorrective
      02Cancelling exactly 24 h before the start is rejected, although the rule allows ituser reportCorrective
      03Code review: the seat counter can go negative if cancel is called twice; no user has hit it yetfound by usPreventive
      04clang-tidy reports a use-after-move in the reminder code; tests still passfound by usPreventive
      05Lab PCs move to GCC 14; the build fails on a missing #include <algorithm>environmentAdaptive
      06The institute changes student IDs from 9 to 10 charactersenvironmentAdaptive
      07The IT office requires JSON instead of CSV for all data filesenvironmentAdaptive
      08Add a waiting list for full events (a Should feature)users want morePerfective
      09Export the attendance of an event as CSVusers want morePerfective
      10Split the 400-line Cli.cpp into small command handlersmaintainersPerfective
      • Tally: 2 corrective, 2 preventive, 3 adaptive, 3 perfective.
      • CR-02 and CR-03 are both faults. CR-02 was reported (reactive, corrective); CR-03 was found before any failure (proactive, preventive).
      • CR-06 feels like a bug to the users, but the software did not change: the environment did. That makes it adaptive.
      • CR-08 and CR-09 add functionality; the 2022 edition of the standard calls this additive, a kind of perfective enhancement.
      Classify by the reason, not by the code. The same edit in RegistrationService.cpp can be corrective in one request and perfective in another.
      Many textbooks call refactoring (CR-10) preventive. ISO/IEC/IEEE 14764 files "improve maintainability" under perfective and keeps preventive for latent faults. Say which convention your team uses.

      Topic 2 · Lehman's laws

      Lehman's laws: what happens to software that is actually used

      #Law (year)In plain words
      IContinuing change (1974)A used system must keep changing, or it becomes less and less useful.
      IIIncreasing complexity (1974)As it changes, its complexity grows unless work is done to reduce it.
      IIISelf-regulation (1974)Growth follows regular, predictable trends over releases.
      IVConservation of organisational stability (1978)The effective work rate stays about constant; more people barely speed it up.
      VConservation of familiarity (1978)Each release can only add so much; a release with too much new content causes trouble.
      VIContinuing growth (1991)Functionality must keep growing to keep users satisfied.
      VIIDeclining quality (1996)Quality seems to decline unless the system is actively adapted.
      VIIIFeedback system (1996)Evolution is a feedback process: users, market and team react to each release.

      Lehman (1974 to 1996), for E-type systems: programs embedded in the real world, like Club Hub. An S-type program is fully defined by a specification (a sort function) and does not evolve this way.

      releases over time size and complexity v0.1 v0.2 v1.0 v1.1 v2.0 gap: technical debt functionality (Law VI, continuing growth) complexity without preventive work (Law II) complexity with regular refactoring
      For Club Hub: Law I says the waiting list and CSV export will come; Law II says each one makes the code harder to change unless the team spends time on refactoring. That is why every sprint from now on reserves time for it.

      Topic 3 · Legacy systems

      Legacy systems: old, valuable and hard to change

      • A legacy system is an older system that the organisation still depends on, but that is costly and risky to change.
      • Typical signs: outdated language or platform, the original developers have left, little or no documentation, no automated tests, business rules that exist only in the code, data in formats nobody else reads.
      • Michael Feathers (Working Effectively with Legacy Code, 2004) gives a practical definition: legacy code is code without tests. By that definition a two-month-old student project can already be legacy.
      • Why not simply throw it away? It works, it encodes years of decisions, and a rewrite must reproduce all of that before it delivers any value.
      Scenario: the institute's student registry was written in 2009 as a desktop application with its own database. One staff member understands it. Club Hub needs student names and emails from it. What do we do with the registry?

      First assess, then decide: rate each legacy system on business value and technical quality (after Sommerville, Software Engineering).

      Low technical qualityHigh technical quality
      High business valueModernise: wrap, refactor, replatform or replace step by step. The student registry.Keep maintaining it normally.
      Low business valueRetire it; move the few users elsewhere.Retain with minimal effort, or replace with an off-the-shelf product when convenient.
      Assessment questionStudent registry
      How many people and processes depend on it?whole institute: high value
      Can we build it and run tests today?builds only on one old PC, no tests
      Who understands it?one person: bus factor 1
      Is the platform still supported?no security updates since 2020

      Topic 3 · Modernisation options

      Five modernisation options, from least to most change

      more change: higher cost and risk, more long-term benefit Retain keep it running, fix only what breaks registry as is Wrap put an adapter or API in front; code unchanged StudentDirectory Refactor improve structure in small steps, same platform Cli command map Replatform move to a new OS, compiler or host; few edits old PC to a VM Replace rewrite or buy, ideally piece by piece new registry Club Hub CLI StudentDirectory facade (routes calls) legacy registry (shrinks) new service (grows) Strangler fig (Fowler, 2004): the facade moves one feature at a time from old to new
      Choosewhen
      Retainit works, changes are rare, and it will retire soon
      Wrapnew systems need its data or functions, but its inside is too risky to touch
      Refactorthe platform is fine, but the code is hard to change; tests can be added
      Replatformthe code is acceptable, but the hardware, OS or compiler is dying
      Replacevalue is high, quality is low, and the rules are well understood
      Decision for the registry: wrap now (Club Hub reads students only through a StudentDirectory interface), and plan a replace later with the strangler fig, one feature at a time.
      Big-bang rewrites are the riskiest option: nothing ships until everything is reproduced, and the old system keeps changing meanwhile.

      Topic 4 · Technical debt

      Technical debt: shortcuts that charge interest

      quadrantChart
        title Technical debt quadrant, Club Hub examples
        x-axis Inadvertent --> Deliberate
        y-axis Reckless --> Prudent
        quadrant-1 Prudent and deliberate
        quadrant-2 Prudent and inadvertent
        quadrant-3 Reckless and inadvertent
        quadrant-4 Reckless and deliberate
        CSV now, JSON later, logged: [0.78, 0.8]
        Cli should not read the clock: [0.26, 0.68]
        Copy-pasted CSV parser: [0.24, 0.3]
        Skip tests to finish Sprint 1: [0.74, 0.2]
      

      Quadrant after Martin Fowler (2009); the metaphor is Ward Cunningham's (1992).

      • Technical debt: the future cost of choosing an easy solution now instead of a better one. Like money debt it has a principal (effort to fix it properly) and interest (extra effort every change costs while it stays).
      • Deliberate debt is a conscious trade-off; inadvertent debt is discovered later ("now we know how we should have done it"). Prudent debt is recorded with a plan; reckless debt is not.
      • Only prudent, deliberate debt is a healthy engineering decision. Inadvertent prudent debt is normal learning; reckless debt is the one to stop.
      Common causesKinds of debt
      deadline pressure (the sprint demo)code: smells, duplication
      missing knowledge or experiencedesign: wrong responsibilities
      requirements that changed after designtest: missing or weak tests
      no time planned for refactoringdocumentation: outdated diagrams
      dependencies never updatedbuild: old compiler, pinned libraries

      Topic 4 · Measurement and repayment

      Worked example: a technical-debt register with interest estimates

      IdDebt item (file)PrincipalInterest per sprintInterest / principalDecision
      TD-0141-line if-else chain in Cli::handleCommand, CC 11 (src/Cli.cpp)6 h2 h (every new command edits it)0.33repay in Sprint 2
      TD-02magic number 24 in three places1 h0.5 h + risk of an inconsistent rule0.50repay now
      TD-03CSV splitting copied into two repositories4 h1 h (fix bugs twice)0.25repay in Sprint 2
      TD-04no tests for the reminder scheduler5 h1 h + high risk0.20next sprint
      TD-05hard-coded path "data/events.csv"2 h0.2 h0.10accept, revisit

      Rule of thumb: repay the items with the highest interest-to-principal ratio first, and items in code you are about to change anyway.

      Measuring: technical debt ratio

      debt ratio = remediation cost ÷ development cost

      Club Hub: principal 6 + 1 + 4 + 5 + 2 = 18 h; development so far 5 students × 60 h = 300 h.

      18 ÷ 300 = 6 %. SonarQube's default scale rates up to 5 % as A and 6 to 10 % as B.

      • Make it visible: one register row per item, linked from a // TD-03 comment in the code and a backlog item.
      • Pay continuously: reserve a fixed share of each sprint (for example 15 to 20 %) instead of a "refactoring sprint" that never comes.
      • Boy Scout rule: leave every file a little cleaner than you found it.
      Interest is an estimate. Its value is in the ranking and the conversation with the Product Owner, not in the decimals.

      Topic 5 · Code smells

      Code smells: surface symptoms of deeper design problems

      SmellWhat it looks like in Club HubTypical refactoring
      Long FunctionCli::handleCommand: 41 lines, five branchesExtract Function
      Duplicated Codethe same usage check in three branches; CSV splitting in two repositoriesExtract Function, then call it
      Repeated Switchesif-else chains on command strings or status values in several placesReplace Conditional with Polymorphism, or a lookup map
      Mysterious Nameok(), t, n2, tmpRename Function, Rename Variable
      Magic numbers24, 3600, 86400 scattered in the codeReplace Magic Literal with a named constant
      Long Parameter ListcreateEvent(title, date, time, room, cap, desc, clubId)Introduce Parameter Object (EventDraft)
      Primitive Obsessionstudent ID as a raw std::string, validated in five placesReplace Primitive with Object (StudentId)
      Feature EnvyCli computes whether an event is full from its fieldsMove Function (to Event::isFull())
      Shotgun Surgeryadding a registration status touches six filesMove Function, Combine Functions into Class
      Large ClassRegistrationService also parses CSV and prints outputExtract Class
      Global Dataglobals such as g_events in old report codeEncapsulate Variable, pass as parameter

      Names from Fowler, Refactoring, 2nd ed. (2018), except "magic numbers", a common name for a special case of Mysterious Name.

      • A code smell is not a bug: the program works. It is a hint that the code will be expensive to change.
      • Smells are judged in context: a 50-line function that reads like a recipe may be fine.
      • Tools find many smells for you. The .clang-tidy file from Lab-09 can flag long and complex functions and magic numbers:
      Checks: >
        readability-function-size,
        readability-function-cognitive-complexity,
        readability-magic-numbers
      CheckOptions:
        - key: readability-function-size.LineThreshold
          value: 40
        - key: readability-function-cognitive-complexity.Threshold
          value: 15

      Topic 5 · Refactoring

      Refactoring: small steps behind a safety net of tests

      Tests green start from a clean state One small refactoring Extract Function, Rename Build + run all tests cmake --build · ctest Commit refactor: extract cancel() red: undo (git restore .) green next small step Safety net: GoogleTest suite from Lab-09 + CI on every push · observable behaviour must not change
      Refactoring: a change to the internal structure of software that makes it easier to understand and cheaper to modify without changing its observable behaviour (Fowler, Refactoring, 2018). Kent Beck's two hats: either you add behaviour or you refactor, never both in one commit.
      // tests/CliTest.cpp: pins the CLI behaviour BEFORE refactoring
      class CliTest : public ::testing::Test {
      protected:
          // in-memory fakes from Lab-09
          InMemoryEventRepository events;
          InMemoryRegistrationRepository registrations;
          RegistrationService service{events, registrations};
          std::ostringstream out;
          Cli cli{service, events, out};
      };
      
      TEST_F(CliTest, UnknownCommandIsReported) {
          cli.handleCommand("dance");
          EXPECT_EQ(out.str(), "unknown command: dance\n");
      }
      
      TEST_F(CliTest, CancelWithoutEventIdPrintsUsage) {
          cli.handleCommand("cancel e20230001");
          EXPECT_EQ(out.str(), "usage: cancel <student> <event>\n");
      }
      
      TEST_F(CliTest, EmptyLineIsAnUnknownCommand) {
          cli.handleCommand("");
          EXPECT_EQ(out.str(), "unknown command: \n");
      }

      The third test pins an odd case on purpose: if the refactored code silently ignores empty lines, that is a behaviour change, and the test says so.

      Topic 5 · Refactoring

      Three everyday refactorings: rename, extract function, name the constant

      // BEFORE: what is ok? what is t? why 24?
      bool ok(const Event& e, TimePoint t) {
          return e.start - t >= std::chrono::hours{24};
      }
      // ...and in printReport(), the same expression three times:
      double r = reg == 0 ? 0.0 : 100.0 * in / reg;
      
      // AFTER 1: Rename Function, Rename Variable,
      //          Replace Magic Literal with a named constant
      constexpr auto kCancellationWindow = std::chrono::hours{24};
      
      bool isCancellationOpen(const Event& event, TimePoint now) {
          return event.start - now >= kCancellationWindow;
      }
      
      // AFTER 2: Extract Function (one definition, three callers,
      //          and now it can have its own unit test)
      double attendanceRate(int checkedIn, int registered) {
          if (registered == 0) return 0.0;
          return 100.0 * checkedIn / registered;
      }
      RefactoringMechanics in short
      Renameuse the IDE's rename (it updates every caller); build; test
      Replace Magic Literaladd a constexpr constant with a unit-carrying type (std::chrono::hours); replace each literal; test
      Extract Functioncopy the fragment into a new function; pass what it reads as parameters; replace the fragment with a call; test
      # one refactoring = one commit, tests green each time
      git commit -m "refactor: rename ok() to isCancellationOpen()"
      git commit -m "refactor: add kCancellationWindow constant"
      git commit -m "refactor: extract attendanceRate()"
      Refactoring preserves behaviour, even wrong behaviour. If you notice a bug while refactoring, write it down, finish the refactoring, then fix the bug test-first in its own commit.

      Topic 5 · Worked example, part 1

      Before: one function knows every command

      // src/Cli.cpp (before)
      void Cli::handleCommand(const std::string& line) {
          std::istringstream in{line};
          std::string cmd, sid;
          int eid = 0;
          in >> cmd;
          if (cmd == "list") {
              for (const Event& e : events_.findAll()) {
                  out_ << e.id << ' ' << e.title << '\n';
              }
          } else if (cmd == "register") {
              if (!(in >> sid >> eid)) {
                  out_ << "usage: register <student> <event>\n";
                  return;
              }
              service_.registerStudent(sid, eid);
              out_ << "registered\n";
          } else if (cmd == "cancel") {
              if (!(in >> sid >> eid)) {
                  out_ << "usage: cancel <student> <event>\n";
                  return;
              }
              const Event e = events_.findById(eid);
              if (e.start - Clock::now() < std::chrono::hours{24}) {
                  out_ << "too late to cancel\n";
                  return;
              }
              service_.cancel(sid, eid);
              out_ << "cancelled\n";
          } else if (cmd == "checkin") {
              if (!(in >> sid >> eid)) {
                  out_ << "usage: checkin <student> <event>\n";
                  return;
              }
              service_.checkIn(sid, eid);
              out_ << "checked in\n";
          } else if (cmd == "help") {
              out_ << "list | register | cancel | checkin | help\n";
          } else {
              out_ << "unknown command: " << cmd << '\n';
          }
      }
      Smell foundEvidence
      Long Function41 lines, cyclomatic complexity 11
      Repeated Switches, Divergent Changeevery new command edits this function
      Duplicated Codethree identical "read student and event, else usage" blocks
      Magic number24 hours, also used elsewhere
      Feature Envya business rule (the 24 h window) lives in the user interface

      Plan: five small steps, one commit each

      1. Pin behaviour: Lab-09 tests + the new CliTest cases; all green.
      2. Extract each branch into a member function (list, registerStudent, cancel, checkIn, help).
      3. Extract the duplicated parsing into studentAndEvent().
      4. Replace the if-else chain with a command map.
      5. Move the 24 h rule behind isCancellationOpen() and kCancellationWindow.
      Build and run ctest after every step. If a step turns red and the reason is not obvious in two minutes, git restore . and take a smaller step.

      Topic 5 · Worked example, part 2

      After: small functions and a command map, same behaviour

      // include/clubhub/Cli.hpp (after)
      using Args = std::vector<std::string>;
      using Handler = std::function<void(const Args&)>;
      
      struct StudentEvent {
          std::string studentId;
          int eventId = 0;
      };
      
      class Cli {
      public:
          Cli(RegistrationService& service, const EventRepository& events,
              std::ostream& out);
          void handleCommand(const std::string& line);
      
      private:
          void list(const Args& args);
          void registerStudent(const Args& args);
          void cancel(const Args& args);
          void checkIn(const Args& args);
          void help(const Args& args);
          std::optional<StudentEvent> studentAndEvent(const Args& args,
                                                      std::string_view usage);
      
          RegistrationService& service_;
          const EventRepository& events_;
          std::ostream& out_;
          std::unordered_map<std::string, Handler> commands_;
      };
      Adding a command now means one new member function and one map entry; handleCommand never changes again. All Lab-09 tests and the new CliTest cases stay green, so the behaviour is unchanged.
      // src/Cli.cpp (after, excerpt)
      Cli::Cli(RegistrationService& service, const EventRepository& events,
               std::ostream& out)
          : service_{service}, events_{events}, out_{out},
            commands_{
                {"list",     [this](const Args& a) { list(a); }},
                {"register", [this](const Args& a) { registerStudent(a); }},
                {"cancel",   [this](const Args& a) { cancel(a); }},
                {"checkin",  [this](const Args& a) { checkIn(a); }},
                {"help",     [this](const Args& a) { help(a); }},
            } {}
      
      void Cli::handleCommand(const std::string& line) {
          const Args args = splitWords(line);
          const std::string cmd = args.empty() ? std::string{} : args[0];
          const auto it = commands_.find(cmd);
          if (it == commands_.end()) {
              out_ << "unknown command: " << cmd << '\n';
              return;
          }
          it->second(args);
      }
      
      void Cli::cancel(const Args& args) {
          const auto se = studentAndEvent(args, "cancel <student> <event>");
          if (!se) return;
          const Event event = events_.findById(se->eventId);
          if (!isCancellationOpen(event, Clock::now())) {
              out_ << "too late to cancel\n";
              return;
          }
          service_.cancel(se->studentId, se->eventId);
          out_ << "cancelled\n";
      }

      Topic 6 · Reverse engineering

      Reverse engineering: recovering the design from the code

      Requirements Design and UML models C++ code forward engineering reverse engineering: redocumentation, design recovery restructuring (refactoring) re-engineering = reverse engineer + change + forward engineer

      Terms after Chikofsky and Cross (1990). Reverse engineering analyses a system to build higher-level descriptions; it does not change the system.

      • Redocumentation: produce missing documentation at the same level (API docs from headers). Design recovery: rebuild class and sequence diagrams and the reasons behind them.
      • Sources: the code, the build files, the tests, the Git history, issue tracker, and the people who remember.
      • Characterization tests (Feathers, 2004) record what the code does today, right or wrong, so you can change it safely.
      # who changed this function, when, and why?
      git log -L :handleCommand:src/Cli.cpp --oneline
      git blame -L 40,80 src/Cli.cpp
      
      # API documentation and class graphs from the headers
      doxygen -g Doxyfile      # then set EXTRACT_ALL = YES
      doxygen Doxyfile         # HAVE_DOT = YES draws class graphs
      
      # where are the complex parts? (CC above 10)
      lizard src/ -C 10 -w

      Topic 6 · Worked example

      Worked example: recover a class diagram from undocumented C++

      // include/clubhub/ReportGenerator.hpp
      struct ClubStats {
          std::string clubName;
          int events = 0;
          int registrations = 0;
          int checkIns = 0;
      };
      
      struct MonthlyReport {
          std::chrono::year_month month;
          std::vector<ClubStats> clubs;
      };
      
      class ReportGenerator {
      public:
          ReportGenerator(const EventRepository& events,
                          const RegistrationRepository& regs);
          MonthlyReport build(std::chrono::year_month m) const;
          void print(const MonthlyReport& r,
                     std::ostream& out) const;
      private:
          const EventRepository& events_;
          const RegistrationRepository& registrations_;
      };
      
      // include/clubhub/CsvEventRepository.hpp
      class CsvEventRepository : public EventRepository {
          // ...
      };
      classDiagram
        direction LR
        class ReportGenerator {
          -events_ : EventRepository
          -registrations_ : RegistrationRepository
          +build(month) MonthlyReport
          +print(report, out) void
        }
        class EventRepository {
          <<interface>>
          +findAll() vector~Event~
        }
        class RegistrationRepository {
          <<interface>>
          +findByEvent(eventId) vector~Registration~
        }
        class CsvEventRepository
        class MonthlyReport {
          +month : year_month
        }
        class ClubStats {
          +clubName : string
          +events : int
          +registrations : int
          +checkIns : int
        }
        ReportGenerator --> EventRepository : reads
        ReportGenerator --> RegistrationRepository : reads
        ReportGenerator ..> MonthlyReport : creates
        MonthlyReport *-- "0..*" ClubStats : clubs
        EventRepository <|.. CsvEventRepository
      
      C++ constructUML element (UML 2.5.1)
      class with only pure virtual functions«interface»
      public inheritance from an interfacerealization (dashed line, hollow triangle)
      member held by value, or std::vector of valuescomposition (filled diamond), with multiplicity
      reference or pointer memberassociation (navigable arrow)
      only used as parameter or return typedependency (dashed arrow)

      Topic 7 · Configuration management

      Configuration management: know exactly what you shipped

      Configuration items src/ · include/ (C++ code) tests/ (GoogleTest) CMakeLists.txt (VERSION) .github/workflows/*.yml docs: SRS, UML, README CHANGELOG.md GoogleTest 1.15 (pinned tag) commit 1a2b3c4 all items together tag v0.2.0 = baseline immutable, reproducible release: clubhub 0.2.0 binary + notes v0.1.0 v0.2.0 v0.2.1 main: baselines are tagged commits SWEBOK Guide v4.0, Software Configuration Management · IEEE 828-2012
      • A configuration item (CI) is anything whose version matters to rebuild a release: code, tests, build files, CI workflows, documents, pinned dependencies.
      • A baseline is an approved, frozen set of CI versions. In Git: a tagged commit.
      • Four activities (SWEBOK Guide v4.0): identification (what is a CI, how is it named), change control (who may change it: pull requests and reviews), status accounting (what is in which version), audit (does the release match its baseline?).
      git tag -a v0.2.0 -m "Sprint 2 release"
      git push origin v0.2.0
      git describe --tags   # v0.2.0-3-g1a2b3c4
      # = 3 commits after v0.2.0, at commit 1a2b3c4
      GIT_TAG main in FetchContent is not a baseline: the build changes when someone else pushes. Pin a release tag.

      Topic 7 · Semantic versioning

      Semantic versioning: the number tells users what changed

      flowchart LR
        A([Changes since<br>last release]) --> B{"Breaks the<br>public API?"}
        B -- no --> C{"Adds compatible<br>functionality<br>or deprecates?"}
        C -- no --> D{"Compatible<br>bug fix?"}
        B -- yes --> M["MAJOR<br>1.4.2 to 2.0.0"]
        C -- yes --> N["MINOR<br>1.4.2 to 1.5.0"]
        D -- yes --> P["PATCH<br>1.4.2 to 1.4.3"]
        D -- no --> Z["no release<br>tests, docs, CI"]
        classDef major fill:#dc354533,stroke:#dc3545
        classDef minor fill:#0d6efd33,stroke:#0d6efd
        classDef patch fill:#19875433,stroke:#198754
        class M major
        class N minor
        class P patch
      
      • SemVer 2.0.0: version MAJOR.MINOR.PATCH. The rules only work once you have declared a public API.
      • Club Hub's public API: the CLI commands and their output, the CSV file formats, and the headers in include/clubhub/.
      • When several changes go into one release, the largest bump wins.
      • 0.y.z is initial development: anything may change. Club Hub stays at 0.x until the week-14 submission, which can become 1.0.0.
      • Pre-release and build labels: 1.0.0-rc.1 comes before 1.0.0; 1.0.0+build.57 adds metadata that does not affect order.
      Once a version is released, its contents must never change. A mistake in 1.4.3 is fixed in 1.4.4, not by moving the tag.

      Topic 7 · Worked example

      Worked example: four changes, four version decisions

      Suppose Club Hub is at 1.3.2, a year after the course. Each change is judged on its own:

      ChangePublic API effectNext
      Fix: cancelling exactly 24 h before the start was rejectednone; behaviour now matches the documented rulePATCH 1.3.3
      Add a waiting list, switched on per eventnew command waitlist; old commands and files unchangedMINOR 1.4.0
      Rename CSV column student_id to studentId, old files no longer loadbreaks every existing data fileMAJOR 2.0.0
      Refactor handleCommand into a command mapnone; output identicalPATCH 1.3.3, or no release on its own
      The CSV rename becomes MINOR if the new version still reads the old column name. Compatibility is a design choice, and it is usually cheaper than a MAJOR release.
      # CMakeLists.txt: the single source of the version number
      cmake_minimum_required(VERSION 3.28)
      project(clubhub VERSION 1.4.0 LANGUAGES CXX)
      
      configure_file(include/clubhub/Version.hpp.in
                     ${PROJECT_BINARY_DIR}/generated/clubhub/Version.hpp
                     @ONLY)
      target_include_directories(clubhub_core PUBLIC
                                 ${PROJECT_BINARY_DIR}/generated)
      // include/clubhub/Version.hpp.in
      #pragma once
      #include <string_view>
      
      namespace clubhub {
      inline constexpr std::string_view kVersion = "@PROJECT_VERSION@";
      }
      
      // src/main.cpp: clubhub --version  prints  clubhub 1.4.0
      std::cout << "clubhub " << clubhub::kVersion << '\n';

      Topic 8 · Change requests

      A change request has a life cycle, and every state has an owner

      stateDiagram-v2
        [*] --> Submitted
        Submitted --> Triaged: PO checks it is complete
        Triaged --> Analysed: impact analysis
        Analysed --> Approved: PO or CCB decides
        Analysed --> Rejected
        Analysed --> Deferred
        Deferred --> Analysed: next sprint planning
        Approved --> InProgress: in Sprint Backlog
        InProgress --> InReview: pull request
        InReview --> InProgress: changes requested
        InReview --> Done: merged, CI green
        Done --> Released: tag vX.Y.Z
        Released --> [*]
        Rejected --> [*]
      
      CHANGE REQUEST CR-14                              Status: Approved
      Title       Waiting list for full events
      Requested   Robotics Club leader, week 12, after the Sprint 1 review
      Category    Perfective (additive) · MoSCoW: Should
      Problem     Popular events fill within minutes. When a student cancels,
                  the seat stays empty because nobody knows it is free.
      Proposal    When an event is full, "register" puts the student on a
                  waiting list (first come, first served). A cancellation
                  promotes the first waitlisted student to Registered.
      Acceptance  Given a full event, when a student registers,
                  then the status is Waitlisted and the position is shown.
      Impact      See impact analysis IA-14: about 14 h, 5 classes, 9 tests
      Size        M (5 story points) · Urgency: medium
      Decision    Approved by the Product Owner for Sprint 2; target v0.2.0
      • Who may approve? In Scrum the Product Owner orders the backlog; changes that affect other systems or the release date may need a change control board (CCB): here PO, Scrum Master and the instructor.
      • Rejected and deferred requests are recorded with a reason, never silently dropped. The IT Project Management course covers integrated change control in depth.

      Topic 8 · Change impact analysis

      Change impact analysis: follow the ripples before you change

      CR-14 waiting list RegistrationService Event Repository Registration · Waitlisted CsvRegistrationRepository Cli output reminders registrations.csv user story · SRS · acceptance criteria tests +9 cases UML class, state README · CHANGELOG · help text
      • Change impact analysis: identify the potential consequences of a change, or estimate what must be modified, before making it (Bohner and Arnold, 1996).
      • Ring 1 (red to blue): code that changes directly, the starting impact set. Ring 2 (yellow): code and data that depend on it. Ring 3 (grey): tests, models and documents.
      • Two techniques: traceability (requirement → design → code → test links from Lab-05 and Lab-06) and dependency analysis (who includes, calls or stores what).
      • After the change, compare the estimated set with the files the pull request really touched; the difference improves the next estimate.
      # dependency analysis with plain tools
      grep -rln "RegistrationStatus" include src tests
      grep -rn "registerStudent(" src tests | wc -l

      Topic 8 · Worked example

      Worked example: impact analysis IA-14 "allow events to have a waiting list"

      ArtifactAffected itemsChangeEst.
      Requirementscapacity rule; new user story "join the waiting list" with Given/When/Then; SRS section on registrationamend, add1 h
      UMLclass diagram (Event::waitlistEnabled, repository operation); state diagram of Registration: Waitlisted → Registered on promotion, Waitlisted → Cancelledupdate1 h
      Codeinclude/clubhub/Event.hpp, RegistrationService.hpp/.cpp (registerStudent returns the status, cancel promotes), RegistrationRepository.hpp (findWaitlisted(eventId) in order), CsvRegistrationRepository.cpp (read and write waitlisted), src/Cli.cpp (messages, waitlist command)modify5 h
      TestsRegistrationServiceTest +6 (full → waitlisted, promotion on cancel, FIFO order, no duplicate on the list, cancel while waitlisted, list disabled); CsvRegistrationRepositoryTest +2 round trips; CliTest +1add3 h
      DocsREADME.md usage, help text, CHANGELOG.md "Added"update0.5 h
      Releaseversion 0.1.0 → 0.2.0 (MINOR: new, backward-compatible feature)bump, tag0.5 h
      Subtotal · plus 30 % for uncertainty (first time the team touches the promotion logic)11 h → 14 h
      Risks found by the analysis
      1. Data files written by v0.1.0 must still load: waitlisted is a new value in an existing column, so old files stay valid.
      2. Promotion and cancellation must not both fill the last seat: one function, one test for the race-free order.
      3. The reminder job must skip waitlisted students.
      The enum value RegistrationStatus::Waitlisted already exists in the Lab-06 model, so the class diagram change is small. Good models make impact analysis cheaper.

      Output of the analysis: the affected list, an estimate with its uncertainty, the risks, the version decision, and a recommendation (approve for Sprint 2) that goes back into CR-14.

      Topic 9 · Release management

      Release management: planned releases and a polite deprecation

      %%{init: {'themeVariables': {'cScale0': '#3d8bfd', 'cScale1': '#20c997', 'cScale2': '#fd7e14', 'cScale3': '#6f42c1', 'cScaleLabel0': '#ffffff', 'cScaleLabel1': '#ffffff', 'cScaleLabel2': '#ffffff', 'cScaleLabel3': '#ffffff', 'fontSize': '16px'}}}%%
      timeline
        title ITC Club Hub release line
        Week 10 : v0.1.0 Sprint 1 increment
        Week 12 : v0.2.0 waiting list and 24 h fix : listEvents(bool) deprecated
        Week 14 : v1.0.0 project submission : listEvents(bool) removed
        Next term : v1.0.x bug fixes only : v1.1.0 attendance CSV export
      
      // v0.2.0: the old overload still works, but warns
      [[deprecated("use listEvents(EventFilter); removed in 1.0.0")]]
      std::vector<Event> listEvents(bool onlyUpcoming) const;
      
      std::vector<Event> listEvents(const EventFilter& filter) const;

      A caller now sees a compiler warning such as 'listEvents' is deprecated: use listEvents(EventFilter), with the version that removes it.

      • Release management decides what goes into a release, builds and tests it from a baseline, versions and tags it, writes the notes, distributes it, and supports it afterwards.
      • Cadence: time-based (every sprint, every term) or feature-based (when CR-14 is done). Student teams do best with time-based releases at the end of each sprint.
      • Deprecation is how you remove something without surprising users: announce it (changelog, release notes), warn (compiler warning, runtime message), keep it working for at least one MINOR release, and remove it only in the next MAJOR release.
      • Support window: say which versions still get fixes. For Club Hub after the course: only the latest 1.0.x.

      Topic 9 · Changelogs

      Worked example: CHANGELOG.md and a tag-triggered release

      # Changelog
      All notable changes to ITC Club Hub are documented here.
      The format is based on Keep a Changelog 1.1.0, and this
      project adheres to Semantic Versioning.
      
      ## [Unreleased]
      
      ## [0.2.0] - 2026-11-20
      ### Added
      - Waiting list for full events; a cancellation promotes
        the first waitlisted student (CR-14).
      - `waitlist <event>` command shows the queue.
      ### Changed
      - `handleCommand` uses a command map (no behaviour change).
      ### Deprecated
      - `listEvents(bool)`: use `listEvents(EventFilter)`.
        Will be removed in 1.0.0.
      ### Fixed
      - Cancelling exactly 24 h before the start was rejected
        (CR-13).
      
      ## [0.1.0] - 2026-11-06
      ### Added
      - Register, cancel and check in from the command line.

      Six section types: Added, Changed, Deprecated, Removed, Fixed, Security. Write the entry in the same pull request as the change, under Unreleased.

      # .github/workflows/release.yml
      name: release
      on:
        push:
          tags: ['v*.*.*']
      permissions:
        contents: write
      jobs:
        release:
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4
            - name: Tag must match the CMake project version
              run: grep -q "VERSION ${GITHUB_REF_NAME#v}" CMakeLists.txt
            - run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
            - run: cmake --build build
            - run: ctest --test-dir build --output-on-failure
            - name: Release notes = this version's CHANGELOG section
              run: |
                awk -v v="${GITHUB_REF_NAME#v}" \
                  '$0 ~ "^## \\[" v "\\]" {p=1; next} /^## \[/ {p=0} p' \
                  CHANGELOG.md > notes.md
            - run: gh release create "$GITHUB_REF_NAME" --notes-file notes.md
              env:
                GH_TOKEN: ${{ github.token }}
      The release job refuses a tag that does not match project(... VERSION ...) and never publishes a red build. Release notes come from the changelog, so there is one source of truth.

      Topic 10 · Maintainability metrics

      Worked example: cyclomatic complexity of handleCommand, counted by hand

      Cyclomatic complexity (McCabe, 1976) = number of independent paths through a function. By hand: start at 1 and add 1 for each if, else if, for, while, case, catch, &&, || and ?:. It is also the minimum number of test cases for full branch coverage of the function.
      FunctionDecision pointsCC
      handleCommand (before)5 if/else if on cmd + 1 for + 3 usage if + 1 deadline if = 1011
      handleCommand (after)1 ?: + 1 if = 23
      cancel, studentAndEvent2 if each3
      list, registerStudent, checkIn1 each2
      help, isCancellationOpennone1
      $ lizard src/Cli.cpp                 # before
        NLOC    CCN   token  PARAM  length  location
          41     11    283      1      41 Cli::handleCommand@16-56@src/Cli.cpp
      $ lizard src/Cli.cpp                 # after (largest function)
          10      3     88      1      10 Cli::handleCommand@71-80@src/Cli.cpp
      • The maximum per function dropped from 11 to 3; the total grew slightly (16 over seven functions). Refactoring does not remove the decisions, it spreads them into small, named, separately testable pieces.
      • A common threshold is CC ≤ 10 per function (McCabe's original advice); lizard warns above 15 by default.
      Maintainability (ISO/IEC 25010:2023)Example metricClub Hub target
      Modularityfiles touched per change requesttypically 3 or fewer
      Reusabilitycode used by both CLI and testsdomain has no I/O
      AnalysabilityCC and length per functionCC ≤ 10, ≤ 40 lines
      Modifiabilitylead time from CR approval to releasewithin one sprint
      Testabilityline coverage with gcovr (Lab-09)≥ 80 % of src/
      A metric is a question, not a verdict. Watch trends per release, and never reward people for a number: CC is easy to lower by making code worse.

      Topic 10 · Maintenance planning

      A maintenance plan: who keeps Club Hub alive after week 14?

      # ITC Club Hub · Maintenance plan (v1.0, week 12)
      
      ## Scope and support
      - Supported release: the latest 1.x; fixes ship as 1.x.PATCH.
      - Compilers: GCC 13+, Clang 17+, MSVC 2022+ (C++20).
      - Platforms: Windows 11, Ubuntu 24.04, macOS 14.
      
      ## Roles (after the course)
      - Maintainer: the clubs office student assistant, named per term.
      - Bugs: GitHub issue with the bug template; triage every Monday.
      - Response: blocker 2 days · major 1 week · minor next release.
      
      ## Routine work
      - Monthly: update the GoogleTest tag, run CI, read release notes.
      - Each term: build with the newest lab compilers (adaptive work).
      - 20 % of maintenance time goes to the technical-debt register.
      
      ## Effort and end of life
      - ACT 20 % x 300 development hours = 60 h per year (5 h/month).
      - Review every year; retire when the institute adopts a new system.
      • ISO/IEC/IEEE 14764:2022 asks for a maintenance plan before delivery: scope of support, who maintains, resources, the process for requests, and how the handover works.
      • Estimating effort with annual change traffic (Boehm, COCOMO, 1981): annual maintenance effort = ACT × development effort, where ACT is the fraction of the code expected to change each year.

      Club Hub estimate

      Development effort: 5 students × 60 h = 300 h

      Expected ACT: about 20 % (new clubs' requests, compiler updates)

      Maintenance: 0.20 × 300 h = 60 h per year, about 5 h per month

      Plan the handover. Without a named owner, a README that explains how to build and test, and green CI, a student project dies the week after the demo.

      Best practices · Common mistakes

      Keeping a system healthy: do this, avoid that

      Pin behaviour first

      Before changing code you do not fully understand, write tests (or characterization tests) that record what it does today.

      Small commits, two hats

      One refactoring per commit, tests green each time; behaviour changes and bug fixes go in their own commits.

      Every change is a request

      Log it, classify it, analyse its impact and estimate it before approving it. Keep rejected requests with their reason.

      One version, one changelog

      The version lives in CMakeLists.txt, tags follow SemVer, and each pull request adds its line under Unreleased.

      Pay debt every sprint

      Keep a register with principal and interest, and reserve a fixed share of each sprint for the highest-interest items.

      Plan for the next maintainer

      A named owner, a README that builds from scratch, supported compilers written down, CI that stays green.

      The big-bang rewrite

      Throwing away working legacy code and rebuilding it all at once. Prefer wrap, refactor and strangler-fig replacement.

      "Refactoring" without tests

      Without a safety net it is just editing and hoping. A red test during a refactoring means undo, not "fix the test".

      Silent breaking changes

      Removing a command or changing a file format in a MINOR or PATCH release, without deprecation or a changelog entry.

      Check your understanding

      Chapter quiz 10 questions

      Play this quiz live with the Realtime Quiz app: download the questions, use Import JSON on the instructor dashboard, then open a session. Open Realtime Quiz

      Wrap-up

      Summary

      • Maintenance is every change after delivery, and it is most of the life-cycle cost.
      • ISO/IEC/IEEE 14764: corrective and adaptive are reactive; preventive and perfective are proactive.
      • Lehman: a used system must keep changing, and its complexity grows unless you work against it.
      • Legacy options run from retain and wrap to refactor, replatform and replace; prefer incremental change.
      • Technical debt has principal and interest: register it and pay the highest interest first.
      • Refactor in small steps behind green tests, never mixed with behaviour changes.
      • Reverse engineering recovers models from code; characterization tests pin what it does today.
      • Baselines, SemVer and a changelog make every release traceable and every change honest.
      • Change requests go through impact analysis; plan maintenance with metrics and a named owner.
      Open Lab-10: 5 tasks + 1 challenge

      Next chapter: 11 · Emerging Trends in Software Engineering

      Slides