---
title: 'Class Diagrams'
source: 'https://academia.sh/en/courses/modeling-and-representation/class-diagrams'
course: 'Modeling and Representation'
language: en
updated: '2026-08-17T18:08:16+00:00'
license: 'CC BY-SA 4.0'
---

# Class Diagrams

The share of the ambiguity that relationship-type and multiplicity symbols take: 20 symbols leave 2048 systems, 25 leave 64, 31 leave 1; a binary field's symbol takes exactly one bit, while a three-valued field's symbol takes 1.58.

The previous lesson left a single number: the twenty-symbol structural
representation collapses 2048 systems onto a single diagram. The number alone says
little. Adding symbols to the diagram lowers it, but by how much has not been
measured — does a symbol halve the ambiguity, take more, or are there symbols that
take none at all.

The class representation is well suited to answering this question, because each
relationship symbol writes a specific fact, and that fact is dropped when it is not
written. What a class, inheritance, and an interface are was established in the
Programming Fundamentals course; this lesson does not define them. The only thing it
measures is: by how many times do the **relationship-type** and **multiplicity**
symbols lower the number of systems collapsing onto the same representation.

## One Bit per Symbol

The shared definition defines three levels of detail. At level zero, only each
association's source, target, direction, and multiplicity are written. At level
one, the **lifecycle** field is also written: whether the association carries a
lifecycle ownership. At level two, the **ordered** and **unique** fields of
associations that carry a collection are also added as bracketed notes.

- **SD8** — The three levels are measured on the same system; the only thing that
  changes is the set of fields written.
- **SD9** — The bit equivalent of the ambiguity is the binary logarithm of the
  number of systems collapsing onto the same representation. If a single system
  remains, the bit count is zero and the representation has become reversible.

```python
"""M01/K06 shared definition (excerpt): the structural view at three levels of detail
and the number of systems each level collapses onto the same view. The count is
exhaustive."""
from itertools import product
from math import log2

ASSOCIATIONS = [
    {"name": "a1", "source": "WorkOrder", "target": "Technician", "direction": "one-way", "multiplicity": "N:1",
     "lifecycle": "independent", "ordered": False, "unique": True},
    {"name": "a2", "source": "WorkOrder", "target": "LineItem", "direction": "one-way", "multiplicity": "1:N",
     "lifecycle": "dependent",  "ordered": True,  "unique": False},
    {"name": "a3", "source": "LineItem",  "target": "Part", "direction": "one-way", "multiplicity": "N:1",
     "lifecycle": "independent", "ordered": False, "unique": True},
    {"name": "a4", "source": "Technician",   "target": "Machine", "direction": "two-way", "multiplicity": "N:M",
     "lifecycle": "independent", "ordered": False, "unique": True},
    {"name": "a5", "source": "WorkOrder", "target": "Document", "direction": "one-way", "multiplicity": "1:N",
     "lifecycle": "dependent",  "ordered": True,  "unique": True},
]


def hidden_fields(association):
    fields = ["lifecycle"]
    if association["multiplicity"] in ("1:N", "N:M"):
        fields += ["ordered", "unique"]
    return fields


def structural_view(associations, detail=0):
    symbols = []
    for assoc in associations:
        s = {"source": assoc["source"], "target": assoc["target"], "direction": assoc["direction"], "multiplicity": assoc["multiplicity"]}
        if detail >= 1:
            s["lifecycle"] = assoc["lifecycle"]
        if detail >= 2:
            for field in hidden_fields(assoc):
                s[field] = assoc[field]
        symbols.append(s)
    return symbols


def symbol_count(view):
    return sum(len(s) for s in view)


def same_view_count(associations, detail=0):
    options = {"lifecycle": ("independent", "dependent"), "ordered": (False, True),
               "unique": (False, True)}
    goal_view = structural_view(associations, detail)
    spaces = []
    for assoc in associations:
        fields = hidden_fields(assoc)
        spaces.append([dict(zip(fields, d)) for d in product(*[options[f] for f in fields])])
    count = 0
    for choice in product(*spaces):
        candidate = [dict(assoc, **c) for assoc, c in zip(associations, choice)]
        if structural_view(candidate, detail) == goal_view:
            count += 1
    return count


WRITTEN = {0: "source/target/dir/mult", 1: "+ lifecycle", 2: "+ ordered, unique"}
measurements = []
print("detail  fields written           symbols  same view    bits")
for level in (0, 1, 2):
    view = structural_view(ASSOCIATIONS, level)
    symbols, drop = symbol_count(view), same_view_count(ASSOCIATIONS, level)
    measurements.append((symbols, drop))
    print(f"  {level:5d}  {WRITTEN[level]:23s}{symbols:6d}{drop:14d}{log2(drop):7.2f}")
print()
print("transition        symbols added  bits gained  bits per symbol")
for (s0, d0), (s1, d1) in zip(measurements, measurements[1:]):
    print(f"  {s0:2d} -> {s1:2d} symbols{s1 - s0:11d}{log2(d0) - log2(d1):12.2f}"
          f"{(log2(d0) - log2(d1)) / (s1 - s0):18.2f}")
```

```
detail  fields written           symbols  same view    bits
      0  source/target/dir/mult     20          2048  11.00
      1  + lifecycle                25            64   6.00
      2  + ordered, unique          31             1   0.00

transition        symbols added  bits gained  bits per symbol
  20 -> 25 symbols          5        5.00              1.00
  25 -> 31 symbols          6        6.00              1.00
```

Three numbers side by side. **System:** five associations, 31 facts.
**Representation:** 20, 25, or 31 symbols. **Cost:** 2048, 64, and 1 system,
respectively.

The lower table gives the lesson's main result: **every added symbol takes exactly
one bit.** Five lifecycle symbols take five bits, six ordering and uniqueness notes
take six bits; neither more nor less. In the last row, the ambiguity drops to zero
bits, meaning the 31-symbol representation becomes reversible — there is a single
system that produces that representation, and the reader is not forced to make a
choice.

This exact match is not a coincidence. Every hidden field is binary: lifecycle has
two values, ordered has two values, unique has two values. Writing a binary field
cuts the option space exactly in half, and a space that is cut in half loses exactly
one bit. The next section will measure the case where this condition no longer
holds.

The measure's scope is also bounded here. The model only counts association lines;
class names, attributes, and operations are outside the model, so the number 2048 is
**only the relationship ambiguity**. A real class representation carries more than
this, and every new field it carries brings its own hidden fields with it; the
number here is a lower bound, not a total.

## Lifecycle Ownership

The bit taken by the lifecycle symbol distinguishes two kinds of relationship on the
representation. **Composition** states a lifecycle ownership: when the owning
object is deleted, the part object is deleted too, because the part has no
existence independent of its owner. **Aggregation** does not assert ownership; the
part continues to exist after the whole is deleted. The concept of composition
itself was established in the Programming Fundamentals course; what is added here
is **which fact** the distinction between the two kinds carries.

The distinction is a single symbol on the representation; at the instance level, it
is the count of deleted objects.

- **SD10** — Example instance: one work order, three line items, two documents, one
  technician, two machines, three parts — twelve objects in total.
- **SD11** — Deletion starts from the root and propagates **transitively** only
  through dependent associations. If an association's target was not deleted while
  its source was, the target is left **unlinked**.
- **SD12** — For the valid content count of a collection, a four-part catalog and a
  three-line-item work order are taken.

```python
"""The instance-level counterpart of the unwritten fields. The lifecycle field
determines deletion propagation; the ordered and unique fields determine a
collection's valid content count. Example instance: 1 work order, 3 line items,
2 documents, 1 technician, 2 machines, 3 parts."""
from itertools import product

OBJECTS = {"WorkOrder": 1, "LineItem": 3, "Part": 3, "Technician": 1, "Machine": 2, "Document": 2}
PAIRS = [("WorkOrder", "Technician"), ("WorkOrder", "LineItem"), ("LineItem", "Part"),
         ("Technician", "Machine"), ("WorkOrder", "Document")]     # shared definition's a1..a5 order
ACTUAL = ("independent", "dependent", "independent", "independent", "dependent")


def delete(lifecycles, root="WorkOrder"):
    """Composition is lifecycle ownership: when the owner is deleted, the part is deleted too."""
    deleted, changed = {root}, True
    while changed:
        changed = False
        for (source, target), lifecycle in zip(PAIRS, lifecycles):
            if lifecycle == "dependent" and source in deleted and target not in deleted:
                deleted.add(target)
                changed = True
    unlinked = {target for (source, target), lifecycle in zip(PAIRS, lifecycles)
                if lifecycle == "independent" and source in deleted and target not in deleted}
    return sum(OBJECTS[t] for t in deleted), sum(OBJECTS[t] for t in unlinked), sorted(deleted)


distribution = {}
for lifecycles in product(("independent", "dependent"), repeat=5):
    n = delete(lifecycles)[0]
    distribution[n] = distribution.get(n, 0) + 1
print("total objects:", sum(OBJECTS.values()), "| lifecycle choices:", 2 ** 5,
      "| distinct deleted-object values:", len(distribution))
print("  deleted objects:", sorted(distribution), "-> at least", min(distribution), ", at most", max(distribution))
n, u, deleted_set = delete(ACTUAL)
print("with lifecycle written, a single value:", n, "objects deleted", deleted_set,
      "|", u, "objects left unlinked")
print()
print("a2 (WorkOrder -> LineItem, 1:N): a 4-part catalog, a 3-line-item work order")
print("  ordered  unique  valid content count")
for ordered in (False, True):
    for unique in (False, True):
        content = set()
        for d in product(range(4), repeat=3):
            if unique and len(set(d)) != len(d):
                continue
            content.add(d if ordered else tuple(sorted(d)))
        print(f"  {str(ordered):8s}{str(unique):9s}{len(content):10d}")
```

```
total objects: 12 | lifecycle choices: 32 | distinct deleted-object values: 11
  deleted objects: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12] -> at least 1 , at most 12
with lifecycle written, a single value: 6 objects deleted ['Document', 'LineItem', 'WorkOrder'] | 4 objects left unlinked

a2 (WorkOrder -> LineItem, 1:N): a 4-part catalog, a 3-line-item work order
  ordered  unique  valid content count
  False   False            20
  False   True              4
  True    False            64
  True    True             24
```

Someone looking at the twenty-symbol representation cannot know how many objects
will go when a work order is deleted. Thirty-two lifecycle choices produce **eleven
distinct** deleted-object values, ranging from 1 to 12: if every association is
independent, only the work order is deleted; if all are dependent, every object in
the system goes. The diagram is consistent with all eleven of these outcomes.

There is a gap in the range, and its cause is instructive: of the twelve values,
only **11 does not appear**. Objects go in groups, not one at a time — three line
items together, three parts together, two documents together — and no combination
of these groups adds up to exactly eleven objects. Deletion behavior is not a
continuous range but a discrete set of values that the association structure
permits.

When the lifecycle symbols are written, the result drops to a single value: **6
objects** are deleted — the work order, three line items, two documents — and **4
objects are left unlinked**: the three parts the line items point to, and the
assigned technician. The five bits taken by five symbols collapse an
instance-level range to a single number. The corresponding consequence is concrete:
whoever writes the deletion procedure makes the decision themselves if that symbol
is absent from the representation, and the decision they make is not written on the
diagram.

The four objects left unlinked are a separate item, and they show the cost of
aggregation. The parts are not deleted, but there is no longer a line item pointing
to them; they sit unreachable in the system. Composition removes this situation and
replaces it with something else: every object whose owner is deleted goes, even if
it is being used from somewhere else.

The multiplicity symbol is incomplete in the same way. The representation writes
1:N between `WorkOrder` and `LineItem`, and its meaning is limited to the sentence
"a work order has more than one line item." How many distinct valid contents a
three-line-item work order can have, from a four-part catalog, depends on two
unwritten fields: **64** in an ordered list that accepts repeats, **4** in an
unordered, unique set. The sixteenfold gap between them sits as a single `1:N` on
the representation.

## A Symbol Does Not Always Take One Bit

The result of exactly one bit per symbol came from the hidden fields being binary.
Per the shared definition's resolution rule, this condition is tested with a second
configuration: what would happen if the lifecycle field took **three** values.

- **SD13** — A three-valued lifecycle field distinguishes: a plain **association**
  that asserts no ownership, **aggregation** that shares the part, and
  **composition** that carries the part along with its lifecycle. The number of
  symbols written does not change; only the number of values the symbol can
  distinguish increases.

```python
"""Second configuration: what if the lifecycle field were three-valued instead of
binary. Three values: plain association, aggregation, composition. The number of
symbols written does not change; what changes is the number of bits a symbol takes.
The count is exhaustive again."""
from itertools import product
from math import log2

MULTIPLICITIES = ["N:1", "1:N", "N:1", "N:M", "1:N"]          # shared definition's five associations


def hidden_fields(multiplicity):
    return ["lifecycle", "ordered", "unique"] if multiplicity in ("1:N", "N:M") else ["lifecycle"]


def exhaustive_count(lifecycle_values, lifecycle_written):
    spaces = []
    for mult in MULTIPLICITIES:
        options = [range(1 if lifecycle_written else lifecycle_values) if field == "lifecycle" else range(2)
                   for field in hidden_fields(mult)]
        spaces.append(list(product(*options)))
    return sum(1 for _ in product(*spaces))


print("lifecycle values  written  symbols  same view    bits")
for values in (2, 3):
    for written, symbols in ((False, 20), (True, 25)):
        s = exhaustive_count(values, written)
        print(f"{values:17d}  {str(written):8s}{symbols:6d}{s:12d}{log2(s):8.2f}")
print()
for values in (2, 3):
    gained = log2(exhaustive_count(values, False)) - log2(exhaustive_count(values, True))
    print(f"{values}-valued lifecycle field: 5 symbols take {gained:.2f} bits"
          f", {gained / 5:.2f} per symbol")
```

```
lifecycle values  written  symbols  same view    bits
                2  False       20        2048   11.00
                2  True        25          64    6.00
                3  False       20       15552   13.92
                3  True        25          64    6.00

2-valued lifecycle field: 5 symbols take 5.00 bits, 1.00 per symbol
3-valued lifecycle field: 5 symbols take 7.92 bits, 1.58 per symbol
```

The representation is the same in both cases: 20 symbols to 25. The bits gained,
however, rise from 5.00 to 7.92, 1.58 per symbol. The rule is corrected as follows:
**a symbol takes bits equal to the binary logarithm of the number of values it
distinguishes.** For a binary field this is exactly one bit; for a three-valued
field it is 1.58, for a four-valued field it is two bits.

Reading the result in reverse is more useful. The information lost when the
lifecycle symbol is not written depends on **how many things that symbol can
distinguish**. In a representation that does not distinguish plain association from
aggregation, the unwritten symbol costs 1.00 bit; in a representation that
distinguishes all three, the same unwritten symbol costs 1.58 bits. Enriching a
representation also **makes leaving it incomplete more expensive** — and until
these two effects appear in the same table, enrichment cannot be counted as a gain.

## Summary

- The low-detail structural representation has 20 symbols and 2048 systems; once
  lifecycle symbols are added, 25 symbols and 64 systems; once ordering and
  uniqueness notes are also added, 31 symbols and a single system remain.
- Measured in bits, every added symbol takes **exactly one bit**: 5 symbols take
  5.00 bits, 6 symbols take 6.00 bits. In the thirty-one-symbol representation, the
  ambiguity is zero bits, meaning the representation has become reversible.
- Composition states lifecycle ownership, aggregation does not. When lifecycle
  symbols are not written, deleting a work order is consistent with 11 distinct
  outcomes in the 12-object system (between 1 and 12); once written, the result
  drops to a single value: 6 objects deleted, 4 left unlinked.
- The multiplicity symbol states that a collection exists, not its content: in a
  four-part catalog, a three-line-item work order's valid content count is 64 if
  ordered and repeating, 4 if unordered and unique — a sixteenfold gap sitting as a
  single `1:N` on the representation.
- One bit per symbol holds only for binary fields. If the lifecycle field were
  three-valued, the system count collapsing onto the twenty-symbol representation
  would be 15552, and the same five symbols would take 7.92 bits, that is, 1.58
  bits per symbol.

## Next Step

Everything counted in this lesson was static: which type is linked to what, how
many, who carries whose lifecycle. Into how many separate parts the system is split
while running was never asked — the six classes of the five associations could run
in a single unit or in six separate units, and the thirty-one-symbol representation
does not distinguish any of these.

The next lesson splits the same set of classes into runtime units and counts the
partition: into how many distinct component partitions the six classes fall, how
many of these a component representation collapses onto the same drawing, and which
associations become remote links once the components are placed onto nodes.
