Chapter 02: Refactoring

From Monolith to Modules

Software Engineering - Winter Semester 2025/26

A practical guide to refactoring the Road Profile Viewer from a 390-line monolith to modular, testable code.

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Where We Are Now

Congratulations! You've achieved:

✅ Chapter 02 (Code Quality in Practice): PEP8 compliance with Ruff
✅ Chapter 02 (Feature Branch Development): Feature branches & Pull Requests
✅ Chapter 02 (Automation and CI/CD): Automated CI/CD checks

Your CI is green! 🎉

But... your entire app lives in a single 390-line main.py file.

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

The Problem: Real Pain Points

Scenario 1: The Bug Hunt

You: "There's a bug in intersection calculation."
You: *Scrolls through 390 lines*
You: *Passes generate_road_profile(), create_dash_app()...*
You: "Found it at line 267! Wait, what does this depend on?"

Scenario 2: The Merge Conflict

Student A: Edits lines 50-120 (road generation)
Student B: Edits lines 200-350 (UI styling)
Result: MERGE CONFLICT (despite unrelated changes!)
Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

The Problem: Testing Nightmare

Scenario 3: Testing (Preview of Chapter 03 (Testing Fundamentals))

# You want to test find_intersection()
from main import find_intersection

# What actually happens:
# - Imports entire Dash framework
# - Starts web server
# - Takes 5 seconds per test
# - 20 functions to test = 100 seconds!

Result: You give up on testing. 😞

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Today's Learning Objectives

By the end of this lecture, you will:

  1. ✅ Understand why monolithic code becomes unmaintainable
  2. ✅ Identify natural boundaries in your code
  3. ✅ Refactor a monolith into focused modules
  4. ✅ Organize imports to avoid circular dependencies
  5. ✅ Experience the benefits of modular code
  6. ✅ Prepare your codebase for testing (Chapter 03 (Testing Fundamentals))
Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

What You WON'T Learn Today

Not covered (yet):

  • ❌ Design patterns by name (emerge naturally from practice)
  • ❌ Advanced architecture patterns (come later)
  • ❌ Testing (that's Chapter 03 (Testing Fundamentals)!)

Today's Focus: Practical refactoring with real code.

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Applying Feature Development Workflow

Remember Chapter 02 (Feature Development)'s workflow?

  1. Create a feature branch
  2. Implement your changes
  3. Push to GitHub
  4. Open a Pull Request
  1. Automated CI checks run
  2. Code review
  3. Merge when approved
  4. Delete the branch

Today's refactoring IS a feature! We'll use this workflow.

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Step 1: Create Feature Branch

# Make sure you're on main and up to date
$ git checkout main
$ git pull origin main

# Create a new feature branch
$ git checkout -b feature/refactor-to-modules
Switched to a new branch 'feature/refactor-to-modules'

# Verify you're on the new branch
$ git branch
  main
* feature/refactor-to-modules

Why? Main stays stable while we experiment safely.

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

The Plan: Iterative Refactoring

Small, safe steps with commits after each:

Step 1: Extract geometry.py → Commit → Test
Step 2: Extract road.py → Commit → Test
Step 3: Extract visualization.py → Commit → Test
Step 4: Simplify main.py → Commit → Test
Final: Push branch → Open PR → CI validates → Merge

Key Principle: After each step, the application still works perfectly.

Why small steps? Easy to identify what broke, easy to undo.

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Understanding the Monolith

What is a monolith?

Code where everything lives in one place, tightly coupled together.

Your current main.py (390 lines):

  • Road profile generation (lines 20-60)
  • Geometry calculations (lines 60-180)
  • Dash application UI (lines 190-390)
  • Application entry point

Everything is mixed together: Math, UI, data generation, app logic.

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Why Monoliths Become Problems

1. Hard to Navigate

  • Finding specific functionality requires scrolling through unrelated code
  • Line 111? Was that geometry or road generation?

2. Hard to Test

  • Must import everything to test one function
  • Dash starts, slow imports, side effects

3. Hard to Collaborate

  • Two developers editing same file = merge conflicts
  • Even when changes are completely independent!
Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Why Monoliths Become Problems (2)

4. High Coupling

def find_intersection(...):
    # Accidentally uses a global variable from UI section
    camera_x = CAMERA_POSITION  # Where is this defined??
    # Now geometry depends on UI - hard to test!

5. Violates "One File, One Purpose"

  • What does main.py do?
  • Current answer: "Everything!"
  • Better answer: "Starts the application. That's it."
Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Identifying Natural Boundaries

The "3-Word Description" Test

If you can describe a module in 3 words or less, it's focused enough.

✅ Good Examples:

  • geometry.py → "Ray intersection math"
  • road.py → "Road profile generation"
  • visualization.py → "Dash UI components"
  • main.py → "Application entry point"
Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

The "3-Word Test" Continued

❌ Bad Examples:

  • utilities.py → "Miscellaneous useful stuff" (too vague!)
  • helpers.py → "Helper functions" (which kind??)
  • functions.py → "Various functions" (meaningless!)

Key Principle: Specific, focused naming reveals clear purpose.

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Analyzing Your Current Code

Category 1: Pure Math Functions

  • calculate_ray_line() - Only depends on numpy
  • find_intersection() - Only depends on numpy
  • Characteristic: Same inputs = same outputs (pure)
  • Destination: geometry.py

Category 2: Data Generation

  • generate_road_profile() - Only depends on numpy
  • Characteristic: Generates data, doesn't display it
  • Destination: road.py
Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Analyzing Your Current Code (2)

Category 3: UI and Application Logic

  • create_dash_app() - Depends on Dash, geometry, road
  • update_graph() callback - Orchestrates everything
  • Characteristic: Creates UI, handles user input
  • Destination: visualization.py

Category 4: Entry Point

  • main() - Just starts the application
  • Characteristic: Minimal logic
  • Destination: main.py (keep simple!)
Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

The Dependency Hierarchy

Natural dependency flow:

main.py
    ↓ depends on
visualization.py
    ↓ depends on
geometry.py, road.py
    ↓ depends on
numpy (external library)

Key Insight: Dependencies flow ONE DIRECTION ONLY.

Why? Prevents circular dependencies!

✅ visualization.py can import from geometry.py

❌ geometry.py should NOT import from visualization.py

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Target Structure

After refactoring:

road-profile-viewer/
├── main.py                 # Entry point only (~15 lines)
├── geometry.py            # Pure math functions (~80 lines)
├── road.py                # Road generation (~60 lines)
├── visualization.py       # Dash app and UI (~200 lines)
└── (other files remain the same)

From 1 file (390 lines) → 4 focused modules (465 lines total)

Extra lines are documentation - that's good! 📝

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Hands-On: Step 1 - Create Module Files

# In your road-profile-viewer directory
$ touch geometry.py
$ touch road.py
$ touch visualization.py

# Verify the structure
$ ls *.py
geometry.py  main.py  road.py  visualization.py

Now we have our module files ready!

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Hands-On: Step 2 - Extract Geometry

Create geometry.py with:

  • calculate_ray_line() function
  • find_intersection() function
  • Comprehensive docstrings
  • Type hints for IDE support

Update main.py:

from geometry import calculate_ray_line, find_intersection

Delete the geometry function definitions from main.py.

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Hands-On: Step 2 - Test & Commit

Test that it works:

$ uv run road-profile-viewer
# Application should start normally
# Verify ray intersection still works in browser

✅ Checkpoint: Application works!

Commit your progress:

$ git add geometry.py main.py
$ git commit -m "Extract geometry functions to geometry.py

- Move calculate_ray_line() and find_intersection()
- Add comprehensive docstrings and type hints
- Update main.py imports
- Application still works identically"
Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Hands-On: Step 3 - Extract Road

Create road.py with:

  • generate_road_profile() function
  • Docstrings explaining clothoid approximation

Update main.py:

from road import generate_road_profile

Delete the road generation function from main.py.

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Hands-On: Step 3 - Test & Commit

Test again:

$ uv run road-profile-viewer
# Verify road generation still works

✅ Checkpoint: Still working!

Commit this step:

$ git add road.py main.py
$ git commit -m "Extract road generation to road.py

- Move generate_road_profile()
- Add docstrings
- Update imports
- Application still works identically"
Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Hands-On: Step 4 - Extract Visualization

Create visualization.py with:

  • create_dash_app() function
  • All UI layout code
  • All callbacks
  • Imports from geometry and road modules

Simplify main.py:

from visualization import create_dash_app

def main():
    app = create_dash_app()
    print("Starting Road Profile Viewer...")
    app.run(debug=True)
Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Hands-On: Step 4 - Test & Commit

Test the complete refactoring:

$ uv run road-profile-viewer
# Verify EVERYTHING still works:
# - Road renders correctly
# - Ray angle adjusts
# - Intersection point updates
# - Distance displays

✅ Checkpoint: Everything works! 🎉

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Hands-On: Step 4 - Final Commit

$ git add visualization.py main.py
$ git commit -m "Extract UI layer to visualization.py

- Move create_dash_app() and all UI code
- Simplify main.py to just entry point (~20 lines)
- visualization.py imports from geometry and road
- Application works identically to before

Completes modular refactoring:
main → visualization → geometry/road"

Now we have a clean commit history showing the refactoring progression!

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Verifying the Refactoring

Check your module structure:

$ ls *.py
geometry.py  main.py  road.py  visualization.py

Check line counts:

$ wc -l *.py
   175 geometry.py     # Pure geometry calculations
    67 road.py         # Road generation
   203 visualization.py # All UI code
    20 main.py         # Just the entry point
   ---

<!-- _header: '<div class="header-info"><span>Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules</span><span></span></div><div class="header-logo" role="img" aria-label="Hochschule Aalen"></div>' -->

   465 total

main.py went from 390 lines to 20 lines! 📉

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Verify Dependency Flow

# main.py imports:
from visualization import create_dash_app

# visualization.py imports:
from geometry import calculate_ray_line, find_intersection
from road import generate_road_profile

# geometry.py imports:
import numpy as np  # Only external dependency

# road.py imports:
import numpy as np  # Only external dependency

✅ One-directional dependencies - no cycles!

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Benefits Gained: Easy Navigation

Before:

# "Where's the intersection calculation?"
# *Scrolls through 390 lines*
# *Gets lost in unrelated code*

After:

# "Where's the intersection calculation?"
# Opens geometry.py → find_intersection() is right there!

Win: Find code in seconds instead of minutes.

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Benefits Gained: Focused Changes

Before:

# Changing road generation algorithm
# File: main.py (390 lines)
# Risk: Might accidentally break UI code

After:

# Changing road generation algorithm
# File: road.py (67 lines)
# Safety: Can't accidentally touch UI code!

Win: Changes are safer and more focused.

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Benefits Gained: Better Collaboration

Before:

Student A: Editing main.py lines 50-120 (road generation)
Student B: Editing main.py lines 200-350 (UI styling)
Result: MERGE CONFLICT! 😡
Time wasted: 30 minutes resolving conflicts

After:

Student A: Editing road.py
Student B: Editing visualization.py
Result: No conflicts! 😊
Time wasted: 0 minutes

Win: Parallel development without conflicts!

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Benefits Gained: Testability

Before:

# To test find_intersection()
from main import find_intersection
# Imports EVERYTHING (Dash, UI, etc.)
# Slow, heavy, side effects

After:

# To test find_intersection()
from geometry import find_intersection
# Only imports geometry module
# Fast, lightweight, no side effects

Win: Fast, focused tests (Chapter 03 (Testing Fundamentals)!)

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Benefits Gained: Reusability

Before:

# Want to use find_intersection() in another project?
# Copy-paste from main.py
# → Brings along unnecessary Dash code 😞

After:

# Want to use find_intersection() in another project?
# Just copy geometry.py
# → Clean, standalone module! 😊

Win: Pure functions are reusable everywhere.

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Common Pitfall #1: Circular Imports

The Error:

ImportError: cannot import name 'generate_road_profile'
from partially initialized module 'road'
(most likely due to a circular import)

The Cause:

# visualization.py
from road import generate_road_profile

# road.py (BAD!)
from visualization import create_dash_app
# Why would road.py need UI code??
Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Common Pitfall #1: Solution

The Solution:

Rule: Lower-level modules (geometry, road) should NEVER import from higher-level modules (visualization, main).

Dependencies flow one direction:

main → visualization → geometry/road

Not:

geometry ← visualization ← main (❌ WRONG!)
Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Common Pitfall #2: Unclear Boundaries

The Confusion:

# Where do camera_x and camera_y belong?
# - geometry.py? (used in calculations)
# - visualization.py? (it's a UI parameter)
# - A new config.py file?

The Solution:

  • If hardcoded: Keep as parameter default in function signature
  • If user-configurable: Keep in visualization.py (UI decides)
  • Don't create "config.py" prematurely! You'll know when needed.
Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Common Pitfall #3: Over-Refactoring

The Temptation:

Should I create:
- geometry_helpers.py?
- geometry_utils.py?
- ray_calculations.py?
- intersection_math.py?
- angle_converter.py?

The Solution: YAGNI Principle

  • "You Ain't Gonna Need It"
  • Start with 3-4 focused modules
  • Only split when module becomes too large (>300 lines)
  • Avoid "utility/helper" modules - they become dumping grounds!
Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Common Pitfall #4: Forgetting Imports

The Error:

# In main.py
def main():
    x, y = generate_road_profile()
# NameError: name 'generate_road_profile' is not defined

The Fix:

# Add the import at the top
from road import generate_road_profile

Pro Tip: Use IDE auto-import (Ctrl+. in VS Code).

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Running CI Checks

Your refactoring should pass all CI checks:

# Run Ruff (style check)
$ uv run ruff check .
All checks passed! ✅

# Run Ruff format check
$ uv run ruff format --check .
All files formatted correctly! ✅

# Run Pyright (type check)
$ uv run pyright
0 errors, 0 warnings ✅

If you get errors: Fix them before pushing!

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Push Your Feature Branch

Review your commits:

$ git log --oneline
7h8i9j0 Extract UI layer to visualization.py
d4e5f6g Extract road generation to road.py
a1b2c3d Extract geometry functions to geometry.py

Clean, logical progression! ✨

Push to GitHub:

$ git push -u origin feature/refactor-to-modules
Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Create Pull Request

$ gh pr create \
  --title "Refactor: Split monolithic main.py into focused modules" \
  --body "Refactors monolithic main.py (390 lines) into 4 modules:

- geometry.py: Pure math functions
- road.py: Road generation logic
- visualization.py: Dash UI layer
- main.py: Entry point only (20 lines)

Benefits: Easier navigation, testing, reduced merge conflicts

✅ All Ruff/Pyright checks pass
✅ Application works identically"

Or use GitHub web interface!

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Wait for CI Checks

GitHub Actions will automatically run:

✅ Ruff check (style)
✅ Ruff format check
✅ Pyright (types)

All should pass! (You've been running them locally.)

Once green: Ready to merge! 🚀

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Leveraging AI Coding Assistants

Important Disclaimer:

Before using AI for refactoring, you MUST understand:

  • ✅ What makes good module boundaries
  • ✅ How to avoid circular dependencies
  • ✅ The "3-word description" test
  • ✅ One-directional dependency flow

Why? Without these principles, you can't:

  • Write effective prompts
  • Evaluate AI's suggestions
  • Catch subtle mistakes

AI is a tool, not a replacement for your knowledge!

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

What AI Can and Cannot Do

AI Can Help With:

  • ✅ Mechanical code movement (copy/paste between files)
  • ✅ Updating import statements
  • ✅ Writing boilerplate (docstrings, type hints)
  • ✅ Maintaining consistency

AI Cannot:

  • ❌ Decide WHERE to split your code (requires judgment)
  • ❌ Understand your project's architecture needs
  • ❌ Evaluate trade-offs
  • ❌ Ensure alignment with team conventions
Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Effective AI Prompt

You've planned the refactoring (manually!), now AI helps execute.

You will refactor monolithic main.py (390 lines) into:
- geometry.py - Pure math functions
- road.py - Road profile generation
- visualization.py - Dash UI layer
- main.py - Entry point (~20 lines)

Create feature branch feature/refactor-to-modules
Make 4 incremental commits (one per step).

Step 1: Extract Geometry Module
- Copy calculate_ray_line() and find_intersection()
- Add imports: import numpy as np
- Keep all docstrings and type hints
- Commit: "refactor: extract geometry to separate module"

[Continue with Steps 2, 3, 4...]
Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Why This Prompt Works

Clear Structure:

  • ✅ Explicit file names and locations
  • ✅ Step-by-step breakdown
  • ✅ Specific requirements
  • ✅ Commit message format

Includes Context:

  • ✅ Explains WHY (absolute imports, etc.)
  • ✅ What to preserve (docstrings)
  • ✅ Shows desired end state

Enables Verification:

  • ✅ Each step independently testable
  • ✅ Clear success criteria
Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Your Responsibilities After AI Generates Code

Never blindly accept AI-generated code!

Always:

  1. ✅ Read every line - Understand what changed
  2. ✅ Test after each step - Run uv run road-profile-viewer
  3. ✅ Verify imports - Check for circular dependencies
  4. ✅ Run CI checks - Ensure Ruff, Pyright pass
  5. ✅ Review commits - Check git diff
  6. ✅ Validate structure - Match your plan?
Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Red Flags to Watch For

Common AI Mistakes:

  • ❌ Circular imports (geometry imports from visualization)
  • ❌ Missing or wrong import paths
  • ❌ Lost functionality (deleted instead of moved)
  • ❌ Changed logic (AI "improved" something unnecessarily)
  • ❌ Inconsistent naming or style

You are the quality gate!

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

The Human-AI Collaboration Model

Human: Strategic Thinking
├── Identify monolith problems
├── Decide module boundaries
├── Plan dependency hierarchy
└── Define success criteria
        ↓
AI: Tactical Execution
├── Move code between files
├── Write boilerplate code
└── Format commit messages
        ↓
Human: Verification & Quality
├── Test functionality
├── Review architecture
├── Check for subtle bugs
└── Validate against CI

You are the architect. AI is the construction crew.

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Learning Exercise Recommendation

1. First time: Do it manually (today's exercise)

  • Deeply understand the challenges
  • Build intuition for problems
  • Learn to spot circular dependencies

2. Second time: Use AI assistance (future projects)

  • Now you can evaluate AI's suggestions
  • You'll catch its mistakes
  • You'll write better prompts

Why this order? You can't effectively use a tool you don't understand.

Learn principles first, then leverage automation.

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Summary: Before vs. After

Before Refactoring:

main.py (390 lines)
└── Everything mixed: Math, UI, data, startup
  • ❌ Hard to find code
  • ❌ Hard to test
  • ❌ Merge conflicts
  • ❌ Tight coupling

After Refactoring:

├── geometry.py (175 lines)
├── road.py (67 lines)
├── visualization.py (203 lines)
└── main.py (20 lines)
Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Summary: Benefits

✅ Easy navigation - Clear module names
✅ Testable - Import geometry without Dash
✅ Collaborative - Edit different files simultaneously
✅ Reusable - Pure functions in standalone modules
✅ Maintainable - Single-responsibility modules

Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Key Principles Learned

  1. Separation of Concerns

    • Each module does one thing well
  2. Dependency Direction

    • Lower-level modules don't import from higher-level
  3. The "3-Word Description" Test

    • Can you describe the module concisely?
  4. YAGNI ("You Ain't Gonna Need It")

    • Don't create abstractions until needed
Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Key Takeaways

Remember:

  1. Monoliths grow organically - Fine initially, problems later
  2. Refactoring ≠ Rewriting - Same functionality, better structure
  3. Modules need clear boundaries - Pure functions vs. UI
  4. Dependencies flow one direction - Prevents circular imports
  5. Testability emerges from modularity - Can't test what you can't import
  6. The "3-Word Test" - Describe module purpose concisely
  7. YAGNI - Start simple, refactor when needed
  8. CI should pass - Refactoring doesn't change functionality
Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

Further Reading

On Refactoring:

  • Martin Fowler's "Refactoring: Improving the Design of Existing Code"
  • Quote: "Any fool can write code that a computer can understand. Good programmers write code that humans can understand."

On Module Design:

  • Robert C. Martin's "Clean Code"
  • SOLID Principles (you're applying Single Responsibility!)

On Python Modules:

  • Real Python: Python Modules and Packages
  • PEP 8: Package and Module Names
Software Engineering | WiSe 2025 | Refactoring: Monolith to Modules

🎯 Practice Time: Hands-On Exercise!

Now it's your turn to apply what you've learned!

What you'll do:

  • ✅ Refactor a real monolithic codebase (390 lines → 4 modules)
  • ✅ Follow the exact steps from today's lecture
  • ✅ Get automated feedback on your refactoring
  • ✅ See your score improve as you complete each step

Accept the assignment:
👉 https://classroom.github.com/a/swPrBybO

Experience the full workflow: Feature branch → Commits → Tests → CI/CD