code and design smells. are they a real threat?
TRANSCRIPT
Code and design smells. Are they a real threat?
Antoni Rasul
Lehman's laws of software evolution
• Originally formulated by Manny Lehman in 1974 and later refined, as a result of empirical study within IBM
• Aimed at finding scientific properties of evolution of different classes of software
• System types:
• S-type (specified) Systems
• P-type (problem solving) Systems
• E-type (evolutionary) Systems
E-type Systems laws of evolution (since 1996)
I. Continuing Change
II. Increasing Complexity
III. Self Regulation
IV. Conservation of Organizational Stability
V. Conservation of Familiarity
VI. Continuing Growth
VII. Declining Quality
VIII.Feedback System
Technical debt
• Monetary metaphor
• Impossible to measure precisely
• If not repaid regularly – accumulates interest
Time
Effort
Interest payment
Feature
Warning signs of rising debt
• The quality of your software is degrading
• The bug backlog is increasing
• The bug threshold in release is rising
• Your productivity is slipping
• Fictional deadlines
• The team members don't care
Effective ways to reduce the debt
• Define the debt
• Revise the Definition of Done
• Testing tasks part of story
• Done means code cleaned, refactored, deployed and tested
• Use tools like Sonar to visualise (also for management)
• Refactor
• Identify code smells and plan for change
• Automate
Refactoring
• Refactoring: a change made to the internal structure of software to make it easier to understand and cheaper to modify without changing its observable behavior.
• Refactor when:
• Adding a function
• Fixing a bug
• Doing a code review
• Perform larger refactorings in small increments
Code smells
• Duplicated Code
• Shotgun surgery
• Feature envy
• Switch statements
• Refused bequest
• Telescoping constructors
• Big Ball of Mud
Techniques
• Extract method/class
• Substitute algorithm
• Introduce parameter object
• Replace type conditional with Polymorphism/Strategy
• Fluent interface with Essence pattern
Duplicated Code or Structure
• In same class
Extract Method
• In sibling classes
Extract Method
Pull method Up
• In unrelated classes
Substitute algorithm
Extract Class
Use extracted class in other places
Shotgun surgery
• Adding simple feature requires small changes in gazillion classes
• Difficult to eliminate in big systems
• Removing duplicates first – reduces the smell
• Code generation tools help reduce the problem further
Feature envy
• One class’s method is extensively using data and methods
of another class
• Usually easy to spot and fix – move the method to the class
where it belongs
• If method uses several classes – move it to the one with
most used data
• Use Move Method, possibly with Extract Method
Switch by type in multiple places - problem
public class PriceCalculator { public double calculatePrice(Customer customer, Car car, Period period) { double tax = 0; double discount = 0; switch(customer.getType()) { case REGULAR: discount = 0; tax = 0.2; case BUSINESS: discount = 0.5; tax = 0.2; case BIG_BUSINESS: discount = 0.2; tax = 0.1; } return period.getDays() * car.getDayPrice() * (1 - discount * tax); } }
Switch by type - Replace type code by polymorphism
public interface Customer { CustomerType getType(); } public interface CustomerType { double getDiscount(); double getTax(); } public class RegularCustomer implements CustomerType { ... } public class BusinessCustomer implements CustomerType { ... } public class BigBusinessCustomer implements CustomerType { ... }
Switch by type - refactored
public class PriceCalculator { public double calculatePrice(Customer customer, Car car, Period period) { final double tax = customer.getType().getTax(); final double discount = customer.getType().getDiscount(); return period.getDays() * car.getDayPrice() * (1 - discount * tax); } }
• If price calculation algorithm would depend on Customer type – use Strategy
Refused bequest
public interface Car { String getModel(); double getDayPrice(); } public class Truck implements Car { public String getMaxCargoVolume(){...}; } public class CarFleet implements Car { private List<Car> cars; public String getModel() { return null; // Hmmmmm Let's return null here } public double getDayPrice() { return this.cars.stream().mapToDouble(c -> c.getDayPrice()).sum(); } }
• When subclass inherits data/methods that it doesn’t want
• When sublcass refuses to meaningfully implement all interface methods
Refused bequest - ideas
• Hierarchy is wrong
• Reshuffle hierarchy, so that parent contains only what’s common
• Another solution – pull the violating class out of the hierarchy and use a delegate to the instance of (former) parent class
Refused bequest – example refactored
public interface RentableItem { double getDayPrice(); } public interface Car extends RentableItem { String getModel(); } public interface CarFleet extends RentableItem { double getFleetDiscount(); } public class StandardFleet implements CarFleet { private List<Car> cars; public double getDayPrice() { return this.cars.stream().mapToDouble(c -> c.getDayPrice()).sum(); } public double getFleetDiscount() { return 0.1; } }
Telescoping constructors
public class RentalContract { ... public RentalContract(Customer cust, List<Car> cars, Date startDate) {
this(cust, car, startDate, null); } public RentalContract(Customer cust, List<Car> cars, Date startDate, Date endDate) {
this(cust, cars, null, null, startDate, endDate); } public RentalContract(Customer customer, List<Car> cars, BigDecimal extraDiscount, String description, Date startDate, Date endDate) {
this.customer = customer; this.cars = cars; this.extraDiscount = extraDiscount; this.description = description; this.startDate = startDate; this.endDate = endDate;
} ...
}
Telescoping constructors – required fields
public RentalContract( Customer customer, List<Car> cars, BigDecimal extraDiscount, String description, Date startDate, Date endDate) {...}
• Instantiating is cumbersome, error-prone
• Optional fields are mixed with required ones
Telescoping constructors – refactoring ideas
• Introduce parameter object from startDate and endDate
public class DateRange { private Date start; private Date end; ... public DateRange withStart(Date start) {
this.start = start; return this;
} public DateRange withEnd(Date end) {
this.end = end; return this;
} }
• Use Essence pattern composed of: • Builder • Fluent interface • Validation of required fields before object construction
Telescoping constructors – fluent builder public class RentalContractBuilder { ... public RentalContractBuilder withCustomer(Customer customer) { this.customer = customer; return this; } public RentalContractBuilder addCar(Car car) { cars.add(car); return this; } public RentalContractBuilder withCars(List<Car> cars) { this.cars = cars; return this; } public RentalContractBuilder withDateRange(DateRange dateRange) { this.dateRange = dateRange; return this; } public void validate() { customerValidator.validate(customer); carValidator.validate(cars); datesValidator.validate(dateRange); } public RentalContract build() { validate(); return new RentalContract(this); } }
Telescoping constructors – end result
public class RentalContract { ... RentalContract(RentalContractBuilder contractBuilder) { this.customer = contractBuilder.getCustomer(); this.cars = contractBuilder.getCars(); this.dateRange = contractBuilder.getDateRange(); } }
RentalContract rentalContract = new RentalContractBuilder() .withCustomer(customer) .addCar(mustang) .addCar(camaro) .withDateRange(new DateRange().withStart(new Date())) .build();
• Constructor with default access
• We are sure that rentalContract is a valid instance after build()
Big Ball of Mud – The Mikado Method
• Allows for systematic reshaping of the system
• Easy to use
• Provides stability to the codebase while changing it
• Fits in an incremental process
• Scalable, the whole team can participate
The Mikado Method
• Set a goal
• Experiment
• Visualize
• Undo
The Mikado Method - flow
Draw goal
Implement goal naively
Errors?
Come up with immediate solutions and draw them as
sub-goals
Revert changes
Select next goal
Yes
No Change makes sense?
Check in !
All goals completed?
Done!
Yes
Yes
Yes
No
No
Mikado graph
Mikado Goal
Prerequisite: immediate
solution Another
prerequisite
More prerequisites ..linked to
previous
Can have common
dependencies
Summary
• Forgive the predecessors
• Take small steps
• Work as a team
• Convince management
• Willingness to change