Skip to main content

48433 - Summary

1. Introduction to Software Architecture

1.1 Lecture

What Is Software Architecture?

Definition: the set of structures needed to reason about a system — its elements, their relations, and their properties (Bass, Clements & Kazman, 2022).

In short: it's the system's blueprint — not the code itself, but the map of what parts exist and how they connect, so you can understand the system without reading every line of code.

  • Not vague decisions: it's not just "early decisions" or "major decisions" — those labels are too fuzzy to define.
  • An abstraction: shows the public interface of each part, hides the private implementation.
  • Multiple views, one system: like the human body — skeletal, muscular, nervous, and circulatory systems are different diagrams, but all describe the same body.

Three Kinds of Architectural Structures

Every system needs three different diagram types, each answering a different question:

StructureShowsAnswers
Component-and-ConnectorWhat's running, and how the pieces talk to each otherWhat talks to what? What's shared/replicated?
ModuleHow the code is split into classes/packagesWho owns what code? What depends on what?
AllocationHow software maps onto hardware/teams/filesWhere does it run? Who builds it?

Component-and-Connector

Module elements

Deployment structure

Why Architecture Matters

Architecture is cheap to change on paper, and very expensive to change once built — so getting it right early matters more than almost anything else in a project.

  • Cheap to explore, expensive to change — test ideas on the model before building the real thing.
  • Shared language — lets business owners, architects, and developers discuss the same system.
  • Reusable abstraction — a good architecture can guide future, similar systems.

Three Key Effects

  1. Lasts a long time — hard to change once built.
  2. Determines what's possible later — constraints lock in early.
  3. Determines quality (performance, security, maintainability) — can't easily bolt these on afterward.

Trade-off: systems built for speed are hard to maintain; systems built for easy maintenance are hard to speed up. You usually can't optimise for both.

SOLID Principles

Five rules for designing classes so a codebase stays easy to change as it grows. Each one follows the same pattern: Rule → Problem it prevents → Fix.

S — Single Responsibility
  • Rule: One class = one job.
  • Problem: A class that both formats a report and saves it to disk has two reasons to change — a formatting update and a storage update both touch the same class.
  • Fix: Split it into two classes — one for content, one for output — so each only changes for one reason.
O — Open/Closed
  • Rule: Add new behaviour without editing existing, working code.
  • Problem: A LogOn function is hard-coded per modem type (DialHayes, DialCourrier...), so it needs editing every time a new modem is released.
  • Fix: Introduce an abstract Modem interface. New modems just implement it — LogOn itself never changes again.
L — Liskov Substitution
  • Rule: If code expects a parent type, you must be able to swap in a child type and nothing breaks.
  • Plain English: A child class is only allowed if it can do everything the parent promised, the same way.

LSP does not "auto-fix" your code. It is a design rule. When inheritance breaks callers, LSP tells you: stop inheriting that way — redesign so the child keeps the parent's promises.


Easy example: Birds

Imagine a function written for any Bird:

def make_it_fly(bird: Bird):
bird.fly() # promise: every Bird can fly
print("Flying!")

Bad design (breaks LSP):

class Bird:
def fly(self):
print("flap flap")

class Penguin(Bird): # ❌ real life: penguin is a bird
def fly(self): # ❌ code life: cannot keep the fly() promise
raise Exception("Can't fly!")

make_it_fly(Penguin()) # 💥 crashes

Real-world "is-a" lied. Callers expected fly() to work. Penguin broke that.

How LSP tells you to fix it:

Don't put fly() on a parent that not all children can honour. Split the types:

class Bird:                    # shared stuff only (eat, sleep, ...)
def eat(self):
print("eating")

class FlyingBird(Bird): # only birds that CAN fly
def fly(self):
print("flap flap")

class Sparrow(FlyingBird):
pass

class Penguin(Bird): # penguin is still a Bird — just not a FlyingBird
def swim(self):
print("swim swim")

def make_it_fly(bird: FlyingBird): # now only flying birds are accepted
bird.fly()
print("Flying!")

make_it_fly(Sparrow()) # ✅ works
# make_it_fly(Penguin()) # ✅ type system / design stops this mistake

What changed?

  • Before: parent promised something some children can't do → crash.
  • After (LSP fix): parent only promises what all children can do. Flying is moved to FlyingBird.

Same idea: Circle / Ellipse (lecture example)

  • Problem: Circle extends Ellipse looks right in maths, but Ellipse promises "set width and height separately." Circle can't keep that promise → wrong results.
  • LSP fix: Don't make Circle inherit Ellipse. Give both a shared parent like Shape with only shared behaviour (e.g. area()).

Remember: LSP = swap the child in, and old code still works. If it doesn't, inheritance is wrong — redesign the hierarchy.

I — Interface Segregation
  • Rule: Many small, client-specific interfaces beat one big one.
  • Problem: One large Service interface serves every client — a change made for Client A risks breaking Clients B and C too, even though they never use that part.
  • Fix: Split it into small, client-specific interfaces so each client only depends on what it actually uses.
D — Dependency Inversion
  • Rule: Depend on abstractions, never on concrete classes.
  • Problem: In procedural code, high-level modules depend directly on low-level details — a change to a detail can ripple all the way up.
  • Fix: Both high-level and low-level code depend on a shared abstract interface instead, so details can change freely without touching the high-level logic.

Quick Reference

PrincipleCore Idea
SSingle ResponsibilityOne class = one job.
OOpen/ClosedExtend without modifying.
LLiskov SubstitutionSubclass must be a drop-in replacement.
IInterface SegregationSmall, focused interfaces.
DDependency InversionDepend on abstractions, not concretions.

Common Challenges Today

  • Systems outgrew simple diagrams — boxes-and-arrows aren't precise enough anymore.
  • More legacy/packaged systems need integrating, not building from scratch.
  • Loosely-coupled components (e.g. microservices) are now the norm.
  • Modelling is essential — too costly to experiment on real systems directly, so solutions are tested on models first.

TL;DR: Architecture = structures for reasoning about a system, seen through C&C / module / allocation views. It matters because it's cheap to explore early but locks in quality and flexibility later. SOLID gives concrete rules for keeping class design flexible. Modern systems demand more rigorous modelling due to scale and complexity.