ЁЯПл The SchoolтА║ЁЯПЫя╕П Software ArchitectureтА║ЁЯз▒ рдзрдбрд╛ 02 тАФ Modularity: рдХрдореА рдорд╛рд░реНрдЧрд┐рдХрд╛ рдЕрд╕рд▓реЗрд▓реНрдпрд╛ рдЦреЛрд▓реНрдпрд╛, рдореЛрдЬреВрди
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯз▒ рдзрдбрд╛ 02 тАФ Modularity: рдХрдореА рдорд╛рд░реНрдЧрд┐рдХрд╛ рдЕрд╕рд▓реЗрд▓реНрдпрд╛ рдЦреЛрд▓реНрдпрд╛, рдореЛрдЬреВрди

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 02 ┬╖ рдорд╛рдЧреЗ: lesson-01-what-is-architecture ┬╖ рдкреБрдвреЗ: lesson-03-layers


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбрд╛ 01, рдЖрдгрд┐ рдореЛрдЬрдорд╛рдк. Cohesion (рдПрдХрд╛ рдЦреЛрд▓реАрддрд▓реНрдпрд╛ рдЧреЛрд╖реНрдЯреА рдПрдХрддреНрд░ рдЕрд╕рд╛рдпрд▓рд╛ рд╣рд╡реНрдпрд╛рдд рдХрд╛?) рдЖрдгрд┐ coupling (рдХрд┐рддреА рдорд╛рд░реНрдЧрд┐рдХрд╛ рдЖрдд рдпреЗрддрд╛рдд рдЖрдгрд┐ рдмрд╛рд╣реЗрд░ рдЬрд╛рддрд╛рдд?), рдпрд╛рдВрдЪреЗ рддреБрдореНрд╣реА рд▓рдХреНрд╖ рдареЗрд╡реВ рд╢рдХрд╛рд▓ рдЕрд╢рд╛ рдЖрдХрдбреНрдпрд╛рдВрдд рд░реВрдкрд╛рдВрддрд░: afferent coupling Ca, efferent coupling Ce, instability I, abstractness A рдЖрдгрд┐ main sequence рдкрд╛рд╕реВрдирдЪреЗ рдЕрдВрддрд░ D тАФ Robert C. Martin рдпрд╛рдВрдЪреЗ package metrics тАФ рдЖрдгрд┐ рддреНрдпрд╛рд╕реЛрдмрдд рдПрдХ рд╕реЛрдкрд╛ relational-cohesion рдЖрдХрдбрд╛ H. arch/analyze.py рдордзреАрд▓ coupling() рдЖрдгрд┐ arch/demo.py рдордзреАрд▓ modularity().

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рд╢рд╛рд│реЗрдЪреНрдпрд╛ рдЪрд╛рдВрдЧрд▓реНрдпрд╛ рдЗрдорд╛рд░рддреАрдд рдкреНрд░рддреНрдпреЗрдХ рдЦреЛрд▓реАрдЪреЗ рдПрдХрдЪ рдХрд╛рдо рдЕрд╕рддреЗ. Science lab рдордзреНрдпреЗ test tubes рдЕрд╕рддрд╛рдд. рдЧреНрд░рдВрдерд╛рд▓рдпрд╛рдд рдкреБрд╕реНрддрдХреЗ рдЕрд╕рддрд╛рдд. рдЕрд░реНрдзреНрдпрд╛ test tubes рдЧреНрд░рдВрдерд╛рд▓рдпрд╛рдд рдареЗрд╡рдд рдирд╛рд╣реАрдд. рд╣реЗрдЪ cohesion: рдПрдХрддреНрд░ рд╡рд╛рдкрд░рд▓реНрдпрд╛ рдЬрд╛рдгрд╛рд▒реНрдпрд╛ рдЧреЛрд╖реНрдЯреА рдПрдХрддреНрд░ рд░рд╛рд╣рддрд╛рдд.

рдЖрдгрд┐ рдЪрд╛рдВрдЧрд▓реНрдпрд╛ рдЦреЛрд▓реАрд▓рд╛ рджрд╣рд╛ рджрд╛рд░реЗ рдирд╕рддрд╛рдд. Science lab рд▓рд╛ рдЗрддрд░ рдкреНрд░рддреНрдпреЗрдХ рдЦреЛрд▓реАрдХрдбреЗ рджрд╛рд░ рдЕрд╕рддреЗ, рддрд░ рдореБрд▓реЗ рджрд┐рд╡рд╕рднрд░ рддрд┐рдЪреНрдпрд╛рддреВрди рдпреЗ-рдЬрд╛ рдХрд░рдд рд░рд╛рд╣рд┐рд▓реА рдЕрд╕рддреА, рдЖрдгрд┐ рдХреЛрдгрд╛рд▓рд╛рд╣реА рдзрдХреНрдХрд╛ рди рд▓рд╛рд╡рддрд╛ рдЖрдд рдХрд╛рд╣реАрдЪ рдмрджрд▓рддрд╛ рдЖрд▓реЗ рдирд╕рддреЗ. рд╣реЗрдЪ coupling: рдЦреЛрд▓реАрд▓рд╛ рдЬрд┐рддрдХреНрдпрд╛ рдХрдореА рдорд╛рд░реНрдЧрд┐рдХрд╛ рд▓рд╛рдЧрддрд╛рдд, рддрд┐рддрдХреА рддреА рдмрджрд▓рд╛рдпрд▓рд╛ рд╕реЛрдкреА.

рджреАрдкрд┐рдХрд╛ рдкреНрд░рддреНрдпреЗрдХ рдордЬрд▓реНрдпрд╛рдЪреА рджрд╛рд░реЗ рдореЛрдЬрддреЗ. рдЖрдд рдпреЗрдгрд╛рд░реА рджрд╛рд░реЗ (рдЬреНрдпрд╛рдВрдирд╛ рдпрд╛ рдордЬрд▓реНрдпрд╛рдЪреА рдЧрд░рдЬ рдЖрд╣реЗ рдЕрд╕реЗ рд▓реЛрдХ) рдордЬрд▓рд╛ рдмрджрд▓рд╛рдпрд▓рд╛ рдЕрд╡рдШрдб рдХрд░рддрд╛рдд тАФ рдЕрдиреЗрдХ рдЬрдг рддреНрдпрд╛рд╡рд░ рдЕрд╡рд▓рдВрдмреВрди рдЕрд╕рддрд╛рдд. рдмрд╛рд╣реЗрд░ рдЬрд╛рдгрд╛рд░реА рджрд╛рд░реЗ (рдпрд╛ рдордЬрд▓реНрдпрд╛рд▓рд╛ рд▓рд╛рдЧрдгрд╛рд▒реНрдпрд╛ рдЧреЛрд╖реНрдЯреА) рддреНрдпрд╛рд▓рд╛ рд╕рд╣рдЬ рдбрд│рдорд│реАрдд рдХрд░рддрд╛рдд тАФ рджреБрд╕рд░рд╛ рдордЬрд▓рд╛ рдмрджрд▓рд▓рд╛ рдХреА рдпрд╛рд▓рд╛ рдЬрд╛рдгрд╡рддреЗ.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    app["ЁЯзСтАНЁЯПл app floor<br/>Ca 2 ┬╖ Ce 3 ┬╖ I 0.60"]
    domain["ЁЯУЪ domain floor<br/>Ca 5 ┬╖ Ce 2 ┬╖ I 0.29 ┬╖ A 0.33"]
    infra["ЁЯФМ infra floor<br/>Ca 3 ┬╖ Ce 2 ┬╖ I 0.40"]
    web["ЁЯЦея╕П web floor<br/>Ca 2 ┬╖ Ce 1 ┬╖ I 0.33"]
    main["ЁЯФС main<br/>Ca 0 ┬╖ Ce 1 ┬╖ I 1.00"]
    main --> app
    main --> web
    main --> infra
    web --> app
    app --> domain
    app --> infra
    domain --> infra
    domain --> web
    infra --> domain

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрд╡реГрддреНрддреА + рдПрдХ lab: https://school-edh.pages.dev/software-architecture/lesson-diagrams.html#l02

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг "coupling рдХрдореА, cohesion рдЬрд╛рд╕реНрдд рдареЗрд╡рд╛" рд╣рд╛ рд╕рд▓реНрд▓рд╛ рд╕рдЧрд│реНрдпрд╛рдВрдирд╛ рдорд╛рдиреНрдп рдЕрд╕рддреЛ рдЖрдгрд┐ рдХреЛрдгреАрдЪ рддрдкрд╛рд╕реВ рд╢рдХрдд рдирд╛рд╣реА. рдЖрдХрдбреЗ рддреЛ рддрдкрд╛рд╕рдгреНрдпрд╛рдЬреЛрдЧрд╛ рдХрд░рддрд╛рдд. рдПрдЦрд╛рджрд╛ рд╕реНрдерд┐рд░ component (рдХрдореА I) рдЕрд╕реНрдерд┐рд░ component import рдХрд░рдд рдЕрд╕реЗрд▓ рддрд░ рддреЛ Stable Dependencies Principle рдореЛрдбрддреЛ: рдЬреНрдпрд╛ рдЧреЛрд╖реНрдЯреАрд╡рд░ рд╕рдЧрд│реЗ рдЯреЗрдХрддрд╛рдд, рддреА рдЖрддрд╛ рд╡рд╛рд░рдВрд╡рд╛рд░ рдмрджрд▓рдгрд╛рд▒реНрдпрд╛ рдЧреЛрд╖реНрдЯреАрд╡рд░ рдЯреЗрдХрддреЗ. рддреБрдореНрд╣реА рддреЗ рдкрд╛рд╣реВ рд╢рдХрддрд╛, рддреНрдпрд╛рдЪрд╛ рдХрд▓ рдкрд╛рд╣реВ рд╢рдХрддрд╛, рдЖрдгрд┐ тАФ рдзрдбрд╛ 09 рдордзреНрдпреЗ тАФ рддреНрдпрд╛рд╡рд░ build fail рдХрд░реВ рд╢рдХрддрд╛.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

arch/analyze.py рдордзреАрд▓ coupling(g, classes) рдЦреЛрд▓реНрдпрд╛рдВрдирд╛ floor(m) (рдирд╛рд╡рд╛рдЪрд╛ рдкрд╣рд┐рд▓рд╛ рднрд╛рдЧ) рдиреБрд╕рд╛рд░ рдЧрдЯрд╛рдд рдЯрд╛рдХрддреЛ рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ рдордЬрд▓реНрдпрд╛рд╕рд╛рдареА рдореЛрдЬрддреЛ: N, R, H, Ca, Ce, I, A рдЖрдгрд┐ D. scan() рдкреНрд░рддреНрдпреЗрдХ module рдЪреЗ classes рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ class abstract рдЖрд╣реЗ рдХрд╛ (рддреНрдпрд╛рдЪреЗ bases рдЖрдгрд┐ decorators, ast рдиреЗ рд╡рд╛рдЪреВрди) рдиреЛрдВрджрд╡рддреЛ. apply(g, remove=тАж) рдХрд╛рд╣реА рдорд╛рд░реНрдЧрд┐рдХрд╛ рдХрд╛рдвреВрди graph рдЪреА copy рдмрдирд╡рддреЛ, рдореНрд╣рдгрдЬреЗ files рд▓рд╛ рд╣рд╛рдд рди рд▓рд╛рд╡рддрд╛ рддреБрдореНрд╣реА рдПрдЦрд╛рджрд╛ fix рдХрд░реВрди рдкрд╛рд╣реВ рд╢рдХрддрд╛.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 arch/demo.py modularity
python3 - <<'EOF'
import sys; sys.path.insert(0, "arch"); from analyze import scan, edges, floor, coupling, apply
cb = scan(); g = cb.imports
ca = sorted({a for a, b in edges(g) if floor(a) != "domain" and floor(b) == "domain"})
ce = sorted({a for a, b in edges(g) if floor(a) == "domain" and floor(b) != "domain"})
print("Ca (outside rooms that import domain):", ca)
print("Ce (domain rooms that import outside):", ce)
print(f"I = {len(ce)} / ({len(ca)} + {len(ce)}) = {len(ce) / (len(ca) + len(ce)):.2f}")
clean = apply(g, remove=[("domain.grades", "infra.db"), ("domain.timetable", "web.format")])
m = coupling(clean, cb.classes)["domain"]
print(f"without the 2 outward corridors: Ce={m['ce']} I={m['i']:.2f} A={m['a']:.2f} D={m['d']:.2f}")
EOF

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

modularity рд╣реЗ рдЫрд╛рдкрддреЛ:

тФАтФА per floor (component): N rooms, R corridors inside, H = (R+1)/N, Ca in, Ce out, I = Ce/(Ca+Ce), A abstract, D = |A+I-1|
   app     N=3 R=0 H=0.33 ┬╖ Ca=2 Ce=3 I=0.60 ┬╖ A=0.00 D=0.40
   domain  N=5 R=2 H=0.60 ┬╖ Ca=5 Ce=2 I=0.29 ┬╖ A=0.33 D=0.38
   infra   N=2 R=0 H=0.50 ┬╖ Ca=3 Ce=2 I=0.40 ┬╖ A=0.00 D=0.60
   main    N=1 R=0 H=1.00 ┬╖ Ca=0 Ce=1 I=1.00 ┬╖ A=0.00 D=0.00
   web     N=2 R=1 H=1.00 ┬╖ Ca=2 Ce=1 I=0.33 ┬╖ A=0.00 D=0.67

рддреБрдордЪрд╛ snippet рд╣реЗ рдЫрд╛рдкрддреЛ:

Ca (outside rooms that import domain): ['app.enrol', 'app.fees_service', 'app.report_card', 'infra.db', 'infra.email']
Ce (domain rooms that import outside): ['domain.grades', 'domain.timetable']
I = 2 / (5 + 2) = 0.29
without the 2 outward corridors: Ce=0 I=0.00 A=0.33 D=0.67

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

Domain рдордЬрд▓реНрдпрд╛рд╡рд░ рд╕рдЧрд│реЗ рдЯреЗрдХрддрд╛рдд (Ca = 5) тАФ рдореНрд╣рдгреВрди рддреЛ рд╕рдЧрд│реНрдпрд╛рдд рд╕реНрдерд┐рд░ рдЕрд╕рд╛рдпрд▓рд╛ рд╣рд╡рд╛. рджреЛрди рдорд╛рд░реНрдЧрд┐рдХрд╛ рд╣реЗ рдмрд┐рдШрдбрд╡рддрд╛рдд: domain.grades тЖТ infra.db рдЖрдгрд┐ domain.timetable тЖТ web.format. рддреНрдпрд╛ рдХрд╛рдврд▓реНрдпрд╛ рдХреА domain рдЪреА instability 0.29 рд╡рд░реВрди 0.00 рд╡рд░ рдпреЗрддреЗ. D рдЪреЗ рдХрд╛рдп рдЭрд╛рд▓реЗ рддреЗ рдкрд╛рд╣рд╛: рддреЛ 0.38 рд╡рд░реВрди 0.67 рд╡рд░, рд╡рд░ рдЧреЗрд▓рд╛, рдХрд╛рд░рдг domain рдЖрддрд╛ рдкреВрд░реНрдг рд╕реНрдерд┐рд░ рдЖрд╣реЗ рдкрдг рдлрдХреНрдд рдПрдХ рддреГрддреАрдпрд╛рдВрд╢ abstract. рдореНрд╣рдгреВрдирдЪ D рд╣рд╛ рдЗрд╢рд╛рд░рд╛ рдЖрд╣реЗ: рдЗрдереЗ fix рдмрд░реЛрдмрд░ рдЖрд╣реЗ, рдЖрдгрд┐ D "рд╕реНрдерд┐рд░ рдЖрдгрд┐ concrete" рдореНрд╣рдгрддреЛ тАФ рдЬреЗ рдлрдХреНрдд рд╢рд╛рд│реЗрдЪреЗ рдирд┐рдпрдо рдмрджрд▓рд▓реЗ рддрд░рдЪ рдмрджрд▓рдгрд╛рд▒реНрдпрд╛ business rules рд╕рд╛рдареА рд╕рд╛рдорд╛рдиреНрдп рдЖрд╣реЗ. app рдордЬрд▓реНрдпрд╛рдЪрд╛ H = 0.33 рдЖрд╣реЗ: рддреНрдпрд╛рдЪреНрдпрд╛ рддреАрди рдЦреЛрд▓реНрдпрд╛ рдПрдХрдореЗрдХрд╛рдВрдирд╛ рдХрдзреАрдЪ import рдХрд░рдд рдирд╛рд╣реАрдд. рдордЬрд▓рд╛ рдЦреЛрд▓реНрдпрд╛рдВрдирд╛ рдкреНрд░рдХрд╛рд░рд╛рдиреБрд╕рд╛рд░ рдЧрдЯрд╛рдд рдЯрд╛рдХрддреЛ, рдХрд╛рдорд╛рдиреБрд╕рд╛рд░ рдирд╡реНрд╣реЗ тАФ рдзрдбрд╛ 05 рддреНрдпрд╛рдВрдирд╛ рддреНрдпрд╛рдРрд╡рдЬреА рд╡рд┐рднрд╛рдЧрд╛рдиреБрд╕рд╛рд░ рдЧрдЯрд╛рдд рдЯрд╛рдХрддреЛ.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

On a real project тАФ рд╣реЗ рдЖрдХрдбреЗ рдХрд╛рдврдгрд╛рд░реА tools:

рдлрдХреНрдд standard library рд╡рд╛рдкрд░реВрди рдХреЛрдгрддреНрдпрд╛рд╣реА Python package рдЪрд╛ рдЭрдЯрдкрдЯ fan-in / fan-out count тАФ arch/analyze.py рдиреЗрдордХреЗ рд╣реЗрдЪ рдХрд░рддреЛ тАФ scan() рддреБрдордЪреНрдпрд╛ рд╕реНрд╡рддрдГрдЪреНрдпрд╛ code рдХрдбреЗ рд╡рд│рд╡рд╛:

python3 -c 'import sys; sys.path.insert(0, "arch"); from analyze import scan, coupling
cb = scan("src", "yourapp"); print(coupling(cb.imports, cb.classes))'

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рдкреНрд░рддреНрдпреЗрдХ component рдЪреЗ I рдЖрдгрд┐ D CI рдордзреНрдпреЗ рдиреЛрдВрджрд╡рд╛ рдЖрдгрд┐ рддреНрдпрд╛рдВрдЪрд╛ рдЖрд▓реЗрдЦ рдХрд╛рдврд╛. рдЬреНрдпрд╛ component рдЪрд╛ fan-in рд╡рд╛рдврдд рдЕрд╕рддрд╛рдирд╛ instability рд╣рд│реВрд╣рд│реВ рд╡рд░ рдЪрдврддреЗ, рддреЛ рддреБрдордЪреНрдпрд╛ codebase рдЪрд╛ рдкреБрдврдЪрд╛ "рдХреЛрдгреА рд╣рд╛рдд рд▓рд╛рд╡рд╛рдпрдЪреА рд╣рд┐рдВрдордд рдХрд░рдд рдирд╛рд╣реА" рдЕрд╕рд╛ рднрд╛рдЧ рдЖрд╣реЗ.

тПня╕П рдкреБрдвреЗ

рдкреНрд░рддреНрдпреЗрдХ рдордЬрд▓реНрдпрд╛рдЪреЗ рдЖрдХрдбреЗ рдЙрдкрдпреЛрдЧреА рдЖрд╣реЗрдд. рдордЬрд▓реНрдпрд╛рдВрдмрджреНрджрд▓рдЪрд╛ рдирд┐рдпрдо рдЕрдзрд┐рдХ рдЪрд╛рдВрдЧрд▓рд╛: рдХреЛрдгрддрд╛ рдордЬрд▓рд╛ рдХреЛрдгрд╛рд▓рд╛ call рдХрд░реВ рд╢рдХрддреЛ? Layers рдЖрдгрд┐ dependency rule.

git checkout lesson-03-layers

ЁЯз▒ Lesson 02 тАФ Modularity: rooms with few corridors, measured

ЁЯУН You are here: Lesson 02 of 12 ┬╖ Previous: lesson-01-what-is-architecture ┬╖ Next: lesson-03-layers


ЁЯУж What's in this branch

Lesson 01, plus measuring. Cohesion (do the things in one room belong together?) and coupling (how many corridors lead in and out?), turned into numbers you can track: afferent coupling Ca, efferent coupling Ce, instability I, abstractness A and distance from the main sequence D тАФ Robert C. Martin's package metrics тАФ plus a simple relational-cohesion number H. coupling() in arch/analyze.py and modularity() in arch/demo.py.

ЁЯзТ Explain like I'm 5

In a good school building, each room has one job. The science lab has the test tubes. The library has the books. You do not keep half the test tubes in the library. That is cohesion: things that are used together live together.

And a good room does not have ten doors. If the science lab had a door to every other room, children would walk through it all day, and nobody could change anything inside without bumping into someone. That is coupling: the fewer corridors a room needs, the easier it is to change.

Dipika counts the doors of each floor. Doors coming in (people who need this floor) make the floor hard to change тАФ many people rely on it. Doors going out (things this floor needs) make it easy to disturb тАФ when another floor changes, this one feels it.

ЁЯЧ║я╕П Diagram

flowchart LR
    app["ЁЯзСтАНЁЯПл app floor<br/>Ca 2 ┬╖ Ce 3 ┬╖ I 0.60"]
    domain["ЁЯУЪ domain floor<br/>Ca 5 ┬╖ Ce 2 ┬╖ I 0.29 ┬╖ A 0.33"]
    infra["ЁЯФМ infra floor<br/>Ca 3 ┬╖ Ce 2 ┬╖ I 0.40"]
    web["ЁЯЦея╕П web floor<br/>Ca 2 ┬╖ Ce 1 ┬╖ I 0.33"]
    main["ЁЯФС main<br/>Ca 0 ┬╖ Ce 1 ┬╖ I 1.00"]
    main --> app
    main --> web
    main --> infra
    web --> app
    app --> domain
    app --> infra
    domain --> infra
    domain --> web
    infra --> domain

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/software-architecture/lesson-diagrams.html#l02

тЭУ What

ЁЯдФ Why

Because "keep coupling low, cohesion high" is advice everyone agrees with and nobody can check. Numbers make it checkable. A stable component (low I) that imports an unstable one breaks the Stable Dependencies Principle: the thing everyone leans on now leans on something that changes often. You can see it, trend it, and тАФ in lesson 09 тАФ fail the build on it.

ЁЯФз How (in this repo)

coupling(g, classes) in arch/analyze.py groups rooms by floor(m) (the first part of the name) and counts, for each floor: N, R, H, Ca, Ce, I, A and D. scan() records each module's classes and whether each one is abstract (by its bases and decorators, read with ast). apply(g, remove=тАж) makes a copy of the graph with some corridors removed, so you can try a fix without touching the files.

ЁЯзк Try it

python3 arch/demo.py modularity
python3 - <<'EOF'
import sys; sys.path.insert(0, "arch"); from analyze import scan, edges, floor, coupling, apply
cb = scan(); g = cb.imports
ca = sorted({a for a, b in edges(g) if floor(a) != "domain" and floor(b) == "domain"})
ce = sorted({a for a, b in edges(g) if floor(a) == "domain" and floor(b) != "domain"})
print("Ca (outside rooms that import domain):", ca)
print("Ce (domain rooms that import outside):", ce)
print(f"I = {len(ce)} / ({len(ca)} + {len(ce)}) = {len(ce) / (len(ca) + len(ce)):.2f}")
clean = apply(g, remove=[("domain.grades", "infra.db"), ("domain.timetable", "web.format")])
m = coupling(clean, cb.classes)["domain"]
print(f"without the 2 outward corridors: Ce={m['ce']} I={m['i']:.2f} A={m['a']:.2f} D={m['d']:.2f}")
EOF

тЬЕ Verify тАФ what you should see

modularity prints:

тФАтФА per floor (component): N rooms, R corridors inside, H = (R+1)/N, Ca in, Ce out, I = Ce/(Ca+Ce), A abstract, D = |A+I-1|
   app     N=3 R=0 H=0.33 ┬╖ Ca=2 Ce=3 I=0.60 ┬╖ A=0.00 D=0.40
   domain  N=5 R=2 H=0.60 ┬╖ Ca=5 Ce=2 I=0.29 ┬╖ A=0.33 D=0.38
   infra   N=2 R=0 H=0.50 ┬╖ Ca=3 Ce=2 I=0.40 ┬╖ A=0.00 D=0.60
   main    N=1 R=0 H=1.00 ┬╖ Ca=0 Ce=1 I=1.00 ┬╖ A=0.00 D=0.00
   web     N=2 R=1 H=1.00 ┬╖ Ca=2 Ce=1 I=0.33 ┬╖ A=0.00 D=0.67

Your snippet prints:

Ca (outside rooms that import domain): ['app.enrol', 'app.fees_service', 'app.report_card', 'infra.db', 'infra.email']
Ce (domain rooms that import outside): ['domain.grades', 'domain.timetable']
I = 2 / (5 + 2) = 0.29
without the 2 outward corridors: Ce=0 I=0.00 A=0.33 D=0.67

ЁЯПБ What you just proved

The domain floor is the one everyone leans on (Ca = 5) тАФ so it should be the most stable. Two corridors spoil that: domain.grades тЖТ infra.db and domain.timetable тЖТ web.format. Remove them and the domain's instability drops from 0.29 to 0.00. Notice what happened to D: it went up, from 0.38 to 0.67, because the domain is now perfectly stable but only one third abstract. That is why D is a hint: here the fix is right, and D says "stable and concrete" тАФ which is normal for business rules that change only when the rules of the school change. The app floor has H = 0.33: its three rooms never import each other. A floor groups rooms by kind, not by job тАФ lesson 05 groups them by department instead.

тЪая╕П Common mistakes

ЁЯПн In production

On a real project тАФ tools that compute these numbers:

A quick fan-in / fan-out count for any Python package, with only the standard library, is exactly what arch/analyze.py does тАФ point scan() at your own code:

python3 -c 'import sys; sys.path.insert(0, "arch"); from analyze import scan, coupling
cb = scan("src", "yourapp"); print(coupling(cb.imports, cb.classes))'

ЁЯПн Why this matters in production: record I and D per component in CI and plot them. A component whose instability creeps up while its fan-in grows is the next "nobody dares to touch it" part of your codebase.

тПня╕П Next

Numbers per floor are useful. A rule about floors is better: which floor may call which? Layers and the dependency rule.

git checkout lesson-03-layers
тЖР Previouswhat is architectureNext тЖТlayers

This page is the lesson's README from the lesson-02-modularity branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.