diff --git a/legacy-code/01-sprout-wrap-worksheet.md b/legacy-code/01-sprout-wrap-worksheet.md new file mode 100644 index 0000000..3c5cc2a --- /dev/null +++ b/legacy-code/01-sprout-wrap-worksheet.md @@ -0,0 +1,85 @@ +### Exercise 1: Sprout a Simple Class + +Topic 1: When refactoring legacy code, how do you decide whether to extract a Sprout Method versus +creating a Sprout Class? What are the trade-offs in terms of testability, cohesion, +and future maintenance? + +Topic 2: How do you recognize when legacy behavior should remain within the current class versus +when it’s time to sprout a new class for better separation of concerns? + +Topic 3: In a large legacy system with high coupling, how can Sprout Method and Sprout Class be +part of a broader strategy to gradually modernize the codebase? + +- **Objective:** Implement the Sprout Method on a simple class. +- **Task:** + - Create a `Car` class with a `drive()` method that prints `"The car is driving!"`. + - Use the **Sprout Method** to add new functionality (e.g., adding a `fuel()` method) to the `Car` + class, **without modifying** the original `drive()` method. + - Add a `fuel()` method that prints `"Refueling the car!"`. + - Extend the `Car` class to call both `drive()` and `fuel()` in the new method + `drive_with_fuel()`. + +- **Hint:** + - Focus on isolating the changes to the `drive_with_fuel()` method, while keeping the original + `drive()` method intact. + +- **Time Limit:** 5-10 minutes + +--- + +### Exercise 2: Implement a Wrap Method + +- **Objective:** Apply the **Wrap Method** to enhance functionality. +- **Task:** + - Create a `Coffee` class with a `cost()` method that prints `"Basic coffee: $5"`. + - Implement a **Wrap Method** where a new class, `MilkDecorator`, wraps the `Coffee` class to + modify its behavior by adding a cost for milk. + - The `MilkDecorator` class should call the original `cost()` method of `Coffee` and then add an + additional cost for milk. + - After that, create a `MilkDecorator` instance and call the `cost()` method to see the new + behavior. + +- **Time Limit:** 5-10 minutes + +--- + +### Exercise 3: Wrapping Multiple Behaviors + +- **Objective:** Apply the **Wrap Method** to chain multiple decorators. +- **Task:** + - Building on the previous exercise, extend the `MilkDecorator` to create a `SugarDecorator`. + - The `SugarDecorator` should add an additional cost for sugar, while still keeping the + `MilkDecorator` functionality. + - Create a `Coffee` object and wrap it with both `MilkDecorator` and `SugarDecorator`. + - Use the `cost()` method to see the total cost after both decorators are applied. + +- **Time Limit:** 5-10 minutes + +--- + +### Exercise 4: Python Decorator for Wrap Method + +- **Objective:** Implement a **decorator function** in Python to wrap a method. +- **Task:** + - Create a `Car` class with a `drive()` method that prints `"The car is driving!"`. + - Write a **Python decorator function** that adds functionality to the `drive()` method. + For instance, before calling the `drive()` method, the decorator could print + `"Starting the car..."`. + - Apply the decorator function to the `drive()` method and call it. + +- **Time Limit:** 5-10 minutes + +--- + +### Exercise 5: Refactor with Decorators + +- **Objective:** Refactor an existing method to use a decorator for extra functionality. +- **Task:** + - Given a simple `User` class with a `login()` method that prints `"User logged in"`, write + a decorator that logs an additional message such as `"Tracking login time..."` whenever the + `login()` method is called. + - Apply the decorator to the `login()` method and test it. + +- **Time Limit:** 5-10 minutes + + diff --git a/legacy-code/01-sprout-wrap.md b/legacy-code/01-sprout-wrap.md new file mode 100644 index 0000000..792a78d --- /dev/null +++ b/legacy-code/01-sprout-wrap.md @@ -0,0 +1,270 @@ +--- +title: Working Effectively with Legacy Code +subtitle: Chapter 6 - I Dont Have Much Time and I Have to Change It +... + +# Pre-work + +- Book: *Working Effectively with Legacy Code* by Michael Feathers +- Focus: Chapter 6: I Dont Have Much Time and I Have to Change It + +# Timetable + +| Activity | Time | +|-----------------------|--------| +| Greetings, Warmup | 5 min | +| Sprout Method & Class | 15 min | +| Exercise 1 | 15 min | +| Wrap Method & Class | 15 min | +| Exercise 2 | 15 min | +| Summary | 10 min | +| Closing | 5 min | + +# Warmup + +- What’s your current strategy when you need to change legacy code? +- Ever added a wrapper instead of modifying directly? +- Share in chat: "Sprout" or "Wrap" — which sounds safer to you? + +\note{ +Use this to establish mental models and prepare learners to explore change-based techniques. +} + +# Sprout Method + +- Add new logic in a new method +- Call the new method from existing one +- No edits to original logic + +# Legacy Code Example + +```python +class VehicleLogger: + def log_trip(self, vehicle_type, distance_km, fuel_used_liters): + print(f"Trip: {distance_km} km") + if vehicle_type == "gasoline": + efficiency = distance_km / fuel_used_liters + print(f"Fuel efficiency: {efficiency:.2f} km/l") + elif vehicle_type == "electric": + efficiency = distance_km / fuel_used_liters # misuse of field + print(f"Energy efficiency: {efficiency:.2f} km/kWh") +``` + +# Improved Code Example + +```python +class VehicleLogger: + def log_trip(self, vehicle_type, distance_km, fuel_or_energy_used): + print(f"Trip: {distance_km} km") + self._log_efficiency(vehicle_type, distance_km, fuel_or_energy_used) + + def _log_efficiency(self, vehicle_type, distance_km, amount_used): + if vehicle_type == "gasoline": + efficiency = distance_km / amount_used + print(f"Fuel efficiency: {efficiency:.2f} km/l") + elif vehicle_type == "electric": + efficiency = distance_km / amount_used + print(f"Energy efficiency: {efficiency:.2f} km/kWh") +``` +\note{ +Benefits: + Isolated new behavior (_log_efficiency) → easier to test + Minimal change to existing method → reduced risk + Future changes (e.g., hybrid logic) can be added safely to the new method +} + +# Sprout Class + +- Create a new class for new behavior +- Keeps old logic intact +- Ideal for complex changes needing state + +# Legacy code Example + +```python +class VehicleLogger: + def log_trip(self, vehicle_type, distance_km, fuel_used_liters): + print(f"Trip: {distance_km} km") + if vehicle_type == "gasoline": + efficiency = distance_km / fuel_used_liters + print(f"Fuel efficiency: {efficiency:.2f} km/l") + elif vehicle_type == "electric": + efficiency = distance_km / fuel_used_liters + print(f"Energy efficiency: {efficiency:.2f} km/kWh") +``` + +# Improved code Example + +```python +class VehicleLogger: + def log_trip(self, vehicle_type, distance_km, fuel_or_energy_used): + print(f"Trip: {distance_km} km") + if vehicle_type == "electric": + EVLogger().log_efficiency(distance_km, fuel_or_energy_used) + elif vehicle_type == "gasoline": + efficiency = distance_km / fuel_or_energy_used + print(f"Fuel efficiency: {efficiency:.2f} km/l") + +class EVLogger: + def log_efficiency(self, distance_km, energy_kwh): + efficiency = distance_km / energy_kwh + print(f"Energy efficiency: {efficiency:.2f} km/kWh") +``` +\note{ +Benefits: + New behavior is isolated in EVLogger + We avoid changing or bloating the legacy method with EV-specific logic + EVLogger is easy to test independently +} +# Exercise 1 + +- Prompt + - When refactoring legacy code, how do you decide whether to extract a Sprout Method versus +creating a Sprout Class? What are the trade-offs in terms of testability, cohesion, +and future maintenance? +- Time limit: 15 minutes + +# Wrap Method + +- The original method is renamed +- A new method with the same name as the original wraps it +- New logic (e.g. logging, validation) is inserted safely +- No change to existing callers — they still call same method + +## Legacy code Example + +```python +class Engine: + def calculate_torque(self, rpm, throttle): + return (rpm * throttle) / 100.0 +``` +# Improved code Example + +```python +class Engine: + def _calculate_torque_original(self, rpm, throttle): + return (rpm * throttle) / 100.0 + + def calculate_torque(self, rpm, throttle): + print(f"[LOG] Inputs: rpm={rpm}, throttle={throttle}") + torque = self._calculate_torque_original(rpm, throttle) + print(f"[LOG] Output: torque={torque}") + return torque +``` +\note{ +Benefits: + Add behavior without modifying original logic + Keep method name unchanged for existing callers + Enable logging, validation, or instrumentation + Safe and reversible change + Supports incremental refactoring +} + +# Wrap Class + +- Create a new class that wraps the legacy class +- Delegates calls while injecting new logic +- Helps isolate change when subclassing is risky + +## Legacy code Example + +```python +class Engine: + def calculate_torque(self, rpm, throttle): + return (rpm * throttle) / 100.0 +``` + +# Improved code Example + +```python +class LoggingEngine(Engine): + def calculate_torque(self, rpm, throttle): + print(f"[LOG] Calculating torque: rpm={rpm}, throttle={throttle}") + torque = super().calculate_torque(rpm, throttle) + print(f"[LOG] Torque result: {torque}") + return torque +``` +\note{ +Benefits: + Extend behavior via subclassing + Override methods to add new logic + Avoid modifying the original class + Useful for testing or temporary changes + Keeps legacy code stable while injecting behavior +} + +# Usage +```python +engine = LoggingEngine() +engine.calculate_torque(3000, 70) +``` + +# Exercise 2 + +- Prompt + - How do you recognize when legacy behavior should remain within the current class versus +when it’s time to sprout a new class for better separation of concerns? +- Time limit: 15 minutes + +# Decorators and the Wrap Method + +- Decorators are a special form of wrapping that allows for dynamic extension. +- In the context of the Wrap Method, decorators provide an elegant solution for layering additional behavior. + +### Python Decorator Pattern Example + +```python +class Vehicle: + def drive(self): + return "Driving" + +class VehicleDecorator(Vehicle): + def __init__(self, vehicle): + self._vehicle = vehicle + + def drive(self): + return self._vehicle.drive() + +class LoggingDecorator(VehicleDecorator): + def drive(self): + action = self._vehicle.drive() + print(f"[LOG] Action: {action}") + return action + +# Usage +car = LoggingDecorator(Vehicle()) +car.drive() +``` + +\note{ +This decorates a Vehicle object to add logging behavior without modifying the original class. +} + + +# Comparison & Benefits + +| Technique | Location
of Change | Scope | Purpose | Risk | Code Impact | +|---------------|------------|-----------------|-------------------------------|------|---------------------------| +| Sprout Method | Same class | One method | Isolate new logic | Low | Add method,
call it | +| Sprout Class | New class | Functionality | Extract cohesive
behavior | Low | New class,
inject it | +| Wrap Method | Same class | One method | Insert logic around
method| Low | Rename + wrap
method | +| Wrap Class | Subclass | Multiple methods| Modify/extend
behavior | Med | New subclass
created | + + +\note{ +Summarize the differences in the implementation of decorators in C++ and Python, emphasizing the trade-offs between the two languages in terms of flexibility and performance. +} + + +# Summary + +- **Sprout** = add new code that old code calls +- **Wrap** = write code that calls into the old code +- Use *Method* for simple logic, *Class* for complex or stateful logic +- These give you *safe entry points* into legacy code + +# Final Thought + +> "You don't need to clean up the whole kitchen to make a cup of tea. Just clear a spot." +> +> — Inspired by Feathers’ philosophy