Refactoring and code smells

BCS1430

Dr. Ashish Sai

đź“… Week 3 Lecture 1

đź’» BCS1430.ashish.nl

📍 EPD150 MSM Conference Hall

// This is a disgusting hack but I can't think of any other way to do this.
// TODO - SERIOUSLY FIX THIS BEFORE IT ENDS IN TEARS
// Date - twelve years ago
// Author - some guy who left nine years ago and 
//          has since become a Buddhist monk that has taken a vow of silence

Introduction to Refactoring

Introduction to Refactoring

  • Refactoring: The process of improving the internal structure of code without changing its external behavior.

Motivations for Refactoring

  • Enhance Code Clarity
  • Mitigate Technical Debt 1
  • Ease Future Modifications
  • Spot and Resolve Defects

Refactoring Challenges

  • Legacy Code: Navigating complex, undocumented systems.
  • Time Management: Juggling refactoring and new features.
  • Bug Risk: Ensuring thorough testing.
  • Stakeholder Buy-In: Advocating the benefits of refactoring.

When to Refactor

  • Before Adding a New Feature: Tidy code before adding new features.
  • When Fixing a Bug: Simplify surrounding code for clarity and easier bug resolution.
  • During Code Review: Implement identified enhancements.
  • As You Go: Continually refine code during development.

Refactoring Example

Refactoring

  • Small, Behavior-Preserving Transformations: Minor yet cumulative modifications for major restructuring.

  • Systematic Process: Ensures the external behavior remains consistent while improving internal architecture.

The Starting Code

The Starting Code

Consider a video store’s calculation logic:

class Customer {
    private List<Rental> rentals;

    public double calculateTotalAmount() {
        double totalAmount = 0;
        for (int i = 0; i < rentals.size(); i++) {
            Rental rental = rentals.get(i);
            double amount = 0;

            switch (rental.getMovie().getPriceCode()) {
                case Movie.REGULAR:
                    amount = 2;
                    break;
                case Movie.NEW_RELEASE:
                    amount = 3;
                    break;
                case Movie.CHILDRENS:
                    amount = 1.5;
                    break;
                default:
                    amount = 0;
                    break;
            }

            totalAmount += amount;
        }
        return totalAmount;
    }
}

Identifying the First Refactor - Extract Method

Goal: Simplify the calculateTotalAmount method by creating smaller, more readable chunks of code.

Extract the switch statement into amountFor in the Rental class:

 public double amountFor() {
        switch (getMovie().getPriceCode()) {
            case Movie.REGULAR:
                return 2;
            case Movie.NEW_RELEASE:
                return 3;
            case Movie.CHILDRENS:
                return 1.5;
            default:
                return 0;
        }
    }

Refactoring Step by Step

  1. Identify a section to extract: Target a self-contained code chunk.
  2. Create a new method: Move the code into a new method in the appropriate class.
  3. Replace old code: Substitute the original code with the new method call.
  4. Test: Ensure behavior remains consistent.

Benefits of Extract Method

  • Improved Readability: The main method becomes concise.
  • Reusability: Other system parts can now use the new method.
  • Separation of Concerns: Enhances encapsulation.

Continuing the Refactoring

Look for opportunities to:

  • Break down long methods.

  • Move operations closer to data.

  • Replace conditionals with polymorphism.

Polymorphism Over Conditionals

Before: Switch statements based on movie type.

After: Use polymorphism with subclasses like RegularMovie, NewReleaseMovie, and ChildrensMovie.

Implementing Polymorphism

  1. Define a common interface: All movie types should respond to getCharge.
  2. Create subclasses: Each represents a different movie type.
  3. Move the logic: Shift charge calculation to the respective subclass.

Example:

public abstract class Movie {
    private String title;

    public Movie(String title) {
        this.title = title;
    }

    public String getTitle() {
        return title;
    }

    abstract double getCharge();
}

public class RegularMovie extends Movie {
    public RegularMovie(String title) {
        super(title);
    }

    @Override
    double getCharge() {
        return 2; 
    }
}

public class NewReleaseMovie extends Movie {
    public NewReleaseMovie(String title) {
        super(title);
    }

    @Override
    double getCharge() {
        return 3; 
    }
}

public class ChildrensMovie extends Movie {
    public ChildrensMovie(String title) {
        super(title);
    }

    @Override
    double getCharge() {
        return 1.5; 
    }
}

Example:

class Rental {
    private Movie movie;

    public Rental(Movie movie) {
        this.movie = movie;
    }

    public double amountFor() {
        return movie.getCharge();
    }
}

Example:

Benefits of Polymorphism

  • Flexibility: Adding a new movie type is straightforward.

  • Adherence to Open/Closed Principle: System is open for extension but closed for modification.

So far

  • Encapsulation Achieved: Broke down a complex method into smaller, manageable parts, enhancing readability and flexibility.

  • Refactoring Impact: Small, progressive enhancements yielding substantial overall improvements.

Principles of Refactoring

  • Core Principles:
    • Do not change the observable behavior of the code.

    • Make small, incremental changes.

    • Test after each change.

    • Focus on simplicity and clarity.

Introduction to Code Smells

Introduction to Code Smells

  • Code Smells: Indicators of potential issues in your code that may require refactoring.

  • Importance: Recognizing smells is the first step towards improving code quality.

Introduction to Code Smells

  • Common Smells: Include Duplicated Code, Long Method, Large Class, and more.

  • Use smells as a guide, not a strict rule, to identify areas for improvement.

1. Duplicated Code

  • What It Is: The same code structure appearing in more than one place.

  • Problems: Increases the likelihood of errors and makes the code harder to maintain.

Example

// Example of Duplicated Code
public void processOrder() {
    // ... some code ...
    double orderTotal = price * quantity;
    double tax = orderTotal * 0.05;
    double finalPrice = orderTotal + tax;
    // ... more code ...
}

public void calculateBill() {
    // ... some code ...
    double billTotal = itemPrice * itemCount;
    double tax = billTotal * 0.05;
    double finalAmount = billTotal + tax;
    // ... more code ...
}

1. Refactoring Duplicated Code

// Extracted method to handle tax and final price calculation
public double calculateTotalWithTax(double total) {
    double tax = total * 0.05;
    return total + tax;
}

public void processOrder() {
    // ... some code ...
    double orderTotal = price * quantity;
    double finalPrice = calculateTotalWithTax(orderTotal);
    // ... more code ...
}

public void calculateBill() {
    // ... some code ...
    double billTotal = itemPrice * itemCount;
    double finalAmount = calculateTotalWithTax(billTotal);
    // ... more code ...
}

By extracting the common logic into a method, we reduce duplication and improve maintainability.

2. Long Method - Problem

  • What It Is: Methods with too many lines of code.

  • Problems: Hard to understand and maintain. Often contain hidden bugs.

2. Long Method - Problem

    public class ReportGenerator {
        public void generateReport(DataSet data) {
            // Initialization and setup
            String report = "";
            // Complex data processing
            for (DataPoint point : data) {
                // Detailed data analysis and report generation
                report += analyzeDataPoint(point);
            }
            // More processing and formatting
            // Final report compilation
            System.out.println(report);
        }
    }

2. Long Method - Solution

  • Break down into smaller methods, each with a clear purpose.
    public class ReportGenerator {
        public void generateReport(DataSet data) {
            String report = processDataForReport(data);
            outputReport(report);
        }

        private String processDataForReport(DataSet data) {
            String report = "";
            for (DataPoint point : data) {
                report += analyzeDataPoint(point);
            }
            return report;
        }

        private void outputReport(String report) {
            // Final report compilation and output
            System.out.println(report);
        }

        private String analyzeDataPoint(DataPoint point) {
            // Detailed data analysis
            // Return analysis result as String
        }
    }
  • The generateReport method is now concise, with clear sub-methods handling specific parts of the report generation process.

3. Large Class: Problem

  • What It Is: Classes with too many responsibilities.

  • Problems: Difficult to understand, maintain, and modify.

3. Large Class: Problem

    public class Employee {
        private String name;
        private String address;
        private String phoneNumber;
        private double salary;
        // ... many more attributes

        public void calculatePay() {
            // Method to calculate pay
        }

        public void save() {
            // Method to save employee details
        }

        // ... many more methods
    }
  • This Employee class is handling personal details, pay calculations, and data storage, among other things.

3. Large Class: Solution

  • Solution: Use techniques like Extract Class to divide into smaller, more focused classes.
    public class Employee {
        private String name;
        private EmployeeDetails details;
        private PayCalculator payCalculator;

        // Employee class now delegates responsibilities
    }

    public class EmployeeDetails {
        private String address;
        private String phoneNumber;
        // ... other personal details
    }

    public class PayCalculator {
        private double salary;

        public void calculatePay() {
            // Method to calculate pay
        }
    }

4. Long Parameter List

  • What It Is: Methods with a high number of parameters.
  • Problems: Hard to understand and use. Increases the risk of errors.

4. Long Parameter List

public void processOrder(String customerName, String customerEmail, String shippingAddress,
                         String billingAddress, String orderItem, int quantity, 
                         String paymentMethod, String cardNumber, String expiryDate, 
                         String cvv) {
    // Method logic for processing the order...
}

4. Solution to Long Parameter List

  • Solution: Use objects to group parameters or replace parameters with method calls.
public class CustomerInfo {
    private String name;
    private String email;
    // Constructors, getters, and setters...
}

public class OrderDetails {
    private String orderItem;
    private int quantity;
    // Constructors, getters, and setters...
}

public class PaymentInfo {
    private String paymentMethod;
    private String cardNumber;
    private String expiryDate;
    private String cvv;
    // Constructors, getters, and setters...
}

// The method now takes these objects as parameters:
public void processOrder(CustomerInfo customerInfo, OrderDetails orderDetails, PaymentInfo paymentInfo) {
    // Method logic using the provided objects...
}

4. Long Parameter List: Alternative Solution

  • Alternative: Replace parameters with method calls to retrieve the needed information.

Example: Method Calls Instead of Parameters

// Assuming there are methods to retrieve customer and order details:
CustomerInfo customerInfo = getCustomerInfo(customerId);
OrderDetails orderDetails = getOrderDetails(orderId);
PaymentInfo paymentInfo = getPaymentInfo(paymentId);

// The method can be simplified further:
public void processOrder(CustomerInfo customerInfo, OrderDetails orderDetails, PaymentInfo paymentInfo) {
    // Method logic...
}

5. Switch Statements

  • What It Is: Excessive use of switch or complex if-else chains.
  • Problems: Hard to modify and extend. Often violates Open/Closed Principle.

5. Switch Statements

public class AnimalSound {
    public String makeSound(String animalType) {
        switch (animalType) {
            case "dog":
                return "Bark";
            case "cat":
                return "Meow";
            case "cow":
                return "Moo";
            // More cases for different animals
            default:
                throw new IllegalArgumentException("Unknown animal type");
        }
    }
}
  • Issues: Each new animal type requires modifying the makeSound method.

5. Switch Statements - Solution

  • Solution: Use polymorphism and other design patterns to handle varying behavior more gracefully.
interface Animal {
    String makeSound();
}

class Dog implements Animal {
    public String makeSound() { return "Bark"; }
}

class Cat implements Animal {
    public String makeSound() { return "Meow"; }
}

class Cow implements Animal {
    public String makeSound() { return "Moo"; }
}

// Using the Animal interface
public class AnimalSound {
    public String makeSound(Animal animal) {
        return animal.makeSound();
    }
}
  • Benefits: Easily extendable by adding new classes. No need to modify existing code to add new animal types, adhering to the Open/Closed Principle.

Strategies for Detecting Smells

Strategies for Detecting Smells

  • Regular Reviews: Peer reviews and pair programming can help identify smells.

  • Refactoring Tools: Many IDEs and tools can point out common smells.

Identifying Duplicated Code

  • Identification: Look for similar code segments across different methods or classes.

  • Examples:

    • Copy-pasted loops or conditionals.

    • Repeated code blocks in different parts of the application.

Extract Method

  • Technique: Isolate repeated code into a single method.

Problem:

class ReportGenerator {
    void generateReport() {
        // ... some code ...
        System.out.println("Calculating...");
        int sum = 0;
        for(int i = 0; i < data.size(); i++) { sum += data.get(i); }
        System.out.println("Sum: " + sum);
        // ... more code ...
        // Repeated sum calculation in another method
    }
}

Extract Method

Refactored Code:

class ReportGenerator {
    private int calculateSum(List<Integer> data) {
        int sum = 0;
        for(Integer value : data) { sum += value; }
        return sum;
    }
    
    void generateReport() {
        // ... some code ...
        System.out.println("Sum: " + calculateSum(data));
        // ... more code ...
    }
}
  • Benefits: Reduces redundancy, centralizes changes, and improves code readability.

Pull Up Method/Field

  • Technique: Move duplicate code to a common superclass.

Problem:

class Dog {
    void eat() { System.out.println("Eating..."); }
}

class Cat {
    void eat() { System.out.println("Eating..."); }
}

Pull Up Method/Field

Refactored Code:

class Animal {
    void eat() { System.out.println("Eating..."); }
}

class Dog extends Animal {}

class Cat extends Animal {}
  • Benefits: Eliminates duplicate code across subclasses, centralizes behavior for easier updates and maintenance.

Long Method

  • Identification: Methods that span dozens of lines, often doing more than one thing.

  • Example:

public void processOrder(Order order) {
    // Initialization code
    initializeOrderProcessing(order);
    // Validation code
    validateOrder(order);
    // Processing code
    processOrderItems(order);
    // Summary generation
    generateOrderSummary(order);
}

Refactoring Techniques: Long Method

  • Extract Method: Break down into smaller methods with clear names indicating their purpose.

  • Replace Temp with Query: Replace temporary variables with queries to the method itself.

  • Introduce Parameter Object or Preserve Whole Object: Group parameters into objects.

Long Method: Extract Method

  • Technique: Break down a long method into smaller methods with clear names indicating their purpose.

Problem Code:

public void printReport(List<Report> reports) {
    for(Report report : reports) {
        // Print header
        // Print details
        // Print footer
    }
}

Long Method: Extract Method

Refactored Code:

public void printReport(List<Report> reports) {
    for(Report report : reports) {
        printHeader(report);
        printDetails(report);
        printFooter(report);
    }
}

private void printHeader(Report report) { /*...*/ }
private void printDetails(Report report) { /*...*/ }
private void printFooter(Report report) { /*...*/ }
  • Benefits: Each part of the code has a clear purpose, improving readability and maintainability.

Long Method: Replace Temp with Query

  • Technique: Replace temporary variables with queries to the method itself.

Problem Code:

public double calculateTotal() {
    double basePrice = quantity * itemPrice;
    if(basePrice > 1000) {
        return basePrice * 0.95;
    } else {
        return basePrice * 0.98;
    }
}

Long Method: Replace Temp with Query

Refactored Code:

public double calculateTotal() {
    if(basePrice() > 1000) {
        return basePrice() * 0.95;
    } else {
        return basePrice() * 0.98;
    }
}

private double basePrice() {
    return quantity * itemPrice;
}
  • Benefits: Reduces the clutter of temporary variables and ensures that the calculation is always up to date.

Long Method: Parameter or Whole Object

  • Technique: Group parameters into objects.

Problem Code:

public void createReservation(Date start, Date end, Guest guest, Room room) {
    // Logic using start, end, guest, and room
}

Long Method: Parameter or Whole Object

Refactored Code:

public class ReservationRequest {
    private Date start;
    private Date end;
    private Guest guest;
    private Room room;
    // Constructor and accessors
}

public void createReservation(ReservationRequest request) {
    // Logic using request object
}
  • Benefits: Simplifies method signatures, makes the code more readable, and groups related data together.

Large Class

  • Identification: Classes with an excessive number of fields, methods, or lines of code.

  • Example:

    class Order {
        // Dozens of fields and methods handling various aspects of orders
    }

Large Class

  • Refactoring Techniques:

    • Extract Class: Create new classes to handle parts of the functionality.

    • Extract Subclass or Extract Interface: Use when part of the behavior is used in only some instances.

Large Class: Extract Class

  • Technique: Create new classes to handle parts of the functionality.

Problem Code:

class Order {
    private Customer customer;
    private List<Item> items;
    private Address shippingAddress;
    private Address billingAddress;
    // ... many more fields and methods related to payment, shipping, etc.

    void processOrder() { /* ... */ }
    void calculateTotal() { /* ... */ }
    // ... many more methods
}

Large Class: Extract Class

Refactored Code:

class Order {
    private Customer customer;
    private List<Item> items;
    private PaymentDetails paymentDetails;
    private ShippingDetails shippingDetails;
    // Simplified Order class
}

class PaymentDetails {
    private Address billingAddress;
    // ... payment related fields and methods
}

class ShippingDetails {
    private Address shippingAddress;
    // ... shipping related fields and methods
}
  • Benefits: Reduces complexity by delegating responsibilities to new classes, improving readability and maintainability.

Large Class: Extract Subclass or Interface

  • Technique: Use when part of the behavior is used in only some instances.

Problem Code:

class Order {
    private boolean isPriorityOrder;
    // ... many fields and methods

    void processOrder() {
        if (isPriorityOrder) {
            // Priority order processing
        } else {
            // Normal order processing
        }
    }
}

Large Class: Extract Subclass or Interface

Refactored Code (Extract Subclass):

class Order {
    // ... common fields and methods
}

class PriorityOrder extends Order {
    // Priority order specific fields and methods
    @Override
    void processOrder() {
        // Priority order processing
    }
}

class NormalOrder extends Order {
    // Normal order specific fields and methods
    @Override
    void processOrder() {
        // Normal order processing
    }
}

Large Class: Extract Subclass or Interface

Refactored Code (Extract Interface):

interface Order {
    void processOrder();
}

class PriorityOrder implements Order {
    // Priority order specific fields and methods
    public void processOrder() {
        // Priority order processing
    }
}

class NormalOrder implements Order {
    // Normal order specific fields and methods
    public void processOrder() {
        // Normal order processing
    }
}
  • Benefits: Separates different behaviors into distinct classes or interfaces, enhancing the Single Responsibility Principle and making the system easier to understand and modify.

Long Parameter List

  • Identification: Methods with a long list of parameters, making them hard to understand and use.

  • Example:

    public void createReport(String title, String author, Date date, Data data, Format format, ...) {
        // Method body
    }

Long Parameter List

We already had a look at both the approaches.

Switch Statements

  • Identification: Excessive use of switch or complex if-else chains in the code.

  • Example:

    switch (employee.type) {
        case COMMISSIONED:
            return calculateCommissionedPay(employee);
        case HOURLY:
            return calculateHourlyPay(employee);
        // other cases
    }

Switch Statements

  • Refactoring Techniques:

    • Replace Conditional with Polymorphism: Use polymorphism to handle varying behavior based on type.

    • Replace Type Code with Subclasses: Create subclasses for each type code.

Switch Statements: Polymorphism

  • Technique: Use polymorphism to handle varying behavior based on an object’s type instead of a switch or complex if-else chains.

Problem Code:

class Employee {
    Type type;

    double calculatePay() {
        switch (type) {
            case COMMISSIONED:
                return calculateCommissionedPay();
            case HOURLY:
                return calculateHourlyPay();
            // other cases
        }
    }
}

Switch Statements: Polymorphism

Refactored Code:

abstract class Employee {
    abstract double calculatePay();
}

class CommissionedEmployee extends Employee {
    double calculatePay() {
        return calculateCommissionedPay();
    }
}

class HourlyEmployee extends Employee {
    double calculatePay() {
        return calculateHourlyPay();
    }
}

// Usage:
Employee employee = new CommissionedEmployee();
double pay = employee.calculatePay();
  • Benefits: Eliminates the switch statement by encapsulating the varying behavior within each subclass. This makes the code easier to extend and maintain, as new types can be added without modifying existing code.

Switch Statements: Subclasses

  • Technique: Create subclasses for each type code, moving the behavior dependent on the type into these subclasses.

Problem Code:

class Employee {
    enum Type { COMMISSIONED, HOURLY, SALARIED }
    Type type;

    double calculatePay() {
        switch (type) {
            case COMMISSIONED:
                return calculateCommissionedPay();
            case HOURLY:
                return calculateHourlyPay();
            // other cases
        }
    }
}

Switch Statements: Subclasses

Refactored Code:

abstract class Employee {
    abstract double calculatePay();
}

class CommissionedEmployee extends Employee {
    double calculatePay() {
        return calculateCommissionedPay();
    }
}

class HourlyEmployee extends Employee {
    double calculatePay() {
        return calculateHourlyPay();
    }
}

class SalariedEmployee extends Employee {
    double calculatePay() {
        return calculateSalariedPay();
    }
}

// Usage:
Employee employee = new HourlyEmployee();
double pay = employee.calculatePay();
  • Benefits: Each subclass clearly represents a specific type of employee and its corresponding calculation method. It adheres to the Open/Closed Principle, as new types can be added without affecting existing classes.

Feature Envy

  • Identification: A method that seems more interested in a class other than the one it actually is in.

  • Example:

    public class ReportGenerator {
        public void generateReport(Data data) {
            // Method body heavily using methods and fields from 'Data' class
        }
    }

Feature Envy

  • Refactoring Techniques:

    • Move Method: Move the method to the class it is most interested in.

    • Extract Method: If only part of the method suffers from feature envy, extract that part.

Feature Envy : Move Method

  • Technique: Move the method to the class it is most interested in.

Problem Code:

class ReportGenerator {
    // ... other methods ...
    public void generateReport(Data data) {
        System.out.println("Report Title: " + data.getTitle());
        System.out.println("Report Data: " + data.getFormattedData());
        // Several other lines interacting with 'Data' class
    }
}

class Data {
    String getTitle() { /* ... */ }
    String getFormattedData() { /* ... */ }
    // ... other methods ...
}

Feature Envy : Move Method

Refactored Code:

class ReportGenerator {
    // ... other methods ...
    public void generateReport(Data data) {
        data.printReportDetails();
    }
}

class Data {
    // ... other methods ...
    void printReportDetails() {
        System.out.println("Report Title: " + getTitle());
        System.out.println("Report Data: " + getFormattedData());
        // Moved method content here
    }
}
  • Benefits: Aligns the method with the data it primarily operates on, improving cohesion and making the code more logical and easier to understand.

Feature Envy: Extract Method

  • Technique: If only part of the method suffers from feature envy, extract that part.

Problem Code:

class ReportGenerator {
    public void generateReport(Data data) {
        // ... some code working with ReportGenerator's own data ...
        System.out.println("Report Data: " + data.getFormattedData());
        // ... more code working with ReportGenerator's own data ...
    }
}

class Data {
    String getFormattedData() { /* ... */ }
    // ... other methods ...
}

Feature Envy: Extract Method

Refactored Code:

class ReportGenerator {
    public void generateReport(Data data) {
        // ... some code working with ReportGenerator's own data ...
        printDataDetails(data);
        // ... more code working with ReportGenerator's own data ...
    }

    private void printDataDetails(Data data) {
        System.out.println("Report Data: " + data.getFormattedData());
        // Isolated the part that was showing feature envy
    }
}

class Data {
    // ... other methods ...
}
  • Benefits: Separates the responsibilities more clearly, addressing the feature envy within the method by isolating the envious segment, making the code more modular and maintainable.

Data Clumps

  • Identification: Groups of data that always appear together but aren’t organized into a structure.

  • Example:

    public void createCustomer(String firstName, String lastName, String street, String city, String zip) {
        // Method body
    }

Data Clumps

  • Refactoring Techniques:

    • Introduce Parameter Object or Class: Group the clumped data into a single object representing the entire concept.

Data Clumps: Parameter Object or Class

  • Technique: Group the clumped data into a single object representing the entire concept.

Problem Code:

class CustomerService {
    public void createCustomer(String firstName, String lastName, String street, String city, String zip) {
        // Logic to create a customer
    }
}
  • Issues: The createCustomer method takes multiple parameters related to customer and address, making it cumbersome and prone to errors.

Data Clumps: Parameter Object or Class

Refactored Code:

class Customer {
    String firstName;
    String lastName;
    Address address;
}

class Address {
    String street;
    String city;
    String zip;
}

class CustomerService {
    public void createCustomer(Customer customer) {
        // Logic to create a customer
    }
}
  • Benefits: Encapsulates related data into coherent structures, simplifying method signatures and promoting code reuse and clarity. This makes the code more maintainable and understandable, as well as easier to extend with new customer-related attributes or behaviors.

Primitive Obsession

  • Identification: Overuse of primitive types instead of small objects for simple tasks.

  • Example:

    public void processDate(String date) {
        // Complex manipulation using string
    }

Primitive Obsession

  • Refactoring Techniques:

    • Replace Data Value with Object: Create an object for the type of data you have.

    • Replace Array with Object: Use an object to represent a group of related data instead of an array.

Primitive Obsession: Data Value

  • Technique: Create an object for the type of data you have.

Problem Code:

class User {
    private String name; // Primitive type
    private String phone; // Primitive type

    public void displayUserInfo() {
        System.out.println("Name: " + name + ", Phone: " + phone);
    }
}

Primitive Obsession: Data Value

Refactored Code:

class Phone {
    private String number;

    public Phone(String number) {
        this.number = number;
    }

    public String formatNumber() {
        // Format number (e.g., add dashes)
        return number;
    }
}

class User {
    private String name; // Still primitive type, appropriate here
    private Phone phone; // Replaced with object

    public User(String name, String phoneNumber) {
        this.name = name;
        this.phone = new Phone(phoneNumber);
    }

    public void displayUserInfo() {
        System.out.println("Name: " + name + ", Phone: " + phone.formatNumber());
    }
}
  • Benefits: Encapsulates data and behavior related to phone numbers, making the code more understandable and maintainable.

Primitive Obsession: Array

  • Technique: Use an object to represent a group of related data instead of an array.

Problem Code:

class UserData {
    String[] userInfo; // [0] for name, [1] for phone, [2] for address

    public void displayUserInfo() {
        System.out.println("Name: " + userInfo[0] + ", Phone: " + userInfo[1] + ", Address: " + userInfo[2]);
    }
}

Primitive Obsession: Array

Refactored Code:

class User {
    private String name;
    private String phone;
    private String address;

    public User(String name, String phone, String address) {
        this.name = name;
        this.phone = phone;
        this.address = address;
    }

    public void displayUserInfo() {
        System.out.println("Name: " + name + ", Phone: " + phone + ", Address: " + address);
    }
}
  • Benefits: Each piece of data is now clearly defined, improving readability and reducing the risk of errors like incorrect array index access.

Testing and Refactoring

The Role of Testing in Refactoring

  • Purpose of Testing: Confirm unchanged behavior post-refactoring.

  • Continuous Testing: Implement continuous testing during refactoring.

  • Test Coverage: Extensive test coverage for thorough checks.

Writing Good Tests

  • Good Test Traits:
    • Readable: Easily comprehensible.
    • Reliable: Yield consistent results.
    • Fast: Quick execution for development efficiency.
    • Isolated: Focus on one aspect, independent of others.
  • Test Data: Use representative data that covers normal, boundary, and error conditions.

Example Test Case and Refactoring

  • Scenario: Refactoring a method to calculate discounts based on customer type.

  • Before Refactoring: Complex method with multiple conditionals.

  • Test Case Example:

    @Test
    public void testCalculateDiscount() {
        Customer customer = new Customer("Regular");
        double discount = order.calculateDiscount(customer);
        assertEquals(0.1, discount, "Regular customers should get a 10% discount.");
    }

    Example Test Case and Refactoring

  • Refactoring: Use polymorphism to handle different customer types.

  • After Refactoring: Cleaner method with customer type determining discount strategy.

Refactoring Catalog

Extract Method

  • Motivation: You have a code fragment that can be grouped together.

  • Mechanics: Identify the fragment, create a new method, and replace the old code with a call to the method.

  • Example:

    // Before
    public void printOwing() {
        printBanner();
        //print details
        System.out.println ("name: " + _name);
        System.out.println ("amount: " + getOutstanding());
    }

Extract Method

    // After
    public void printOwing() {
        printBanner();
        printDetails(getOutstanding());
    }
    private void printDetails(double outstanding) {
        System.out.println ("name: " + _name);
        System.out.println ("amount: " + outstanding);
    }
  • Benefits: Improves readability and reusability of code.

Rename Method

  • Motivation: The name of the method does not clearly describe what the method does.

  • Mechanics: Change the method name and update all references to it.

  • Example:

    // Before
    class Person {
        String getTelephoneNumber() {
            return officeTelephone.getTelephoneNumber();
        }
    }

Rename Method

    // After
    class Person {
        String getOfficePhoneNumber() {
            return officeTelephone.getTelephoneNumber();
        }
    }
  • Benefits: Improves the clarity and readability of the code.

Inline Method

  • Motivation: A method’s body is just as clear as its name, or it’s used only in a few places.

  • Mechanics: Replace calls to the method with the method’s content and delete the method.

  • Example:

    // Before
    class Person {
        int getRating() {
            return (moreThanFiveLateDeliveries()) ? 2 : 1;
        }
        boolean moreThanFiveLateDeliveries() {
            return _numberOfLateDeliveries > 5;
        }
    }

Inline Method

    // After
    class Person {
        int getRating() {
            return (_numberOfLateDeliveries > 5) ? 2 : 1;
        }
    }
  • Benefits: Simplifies the code when a method is only adding unnecessary indirection.

Move Method/Field

  • Motivation: A method or field is more related to another class than the one it currently is in.

  • Mechanics: Create a new method/field in the target class and adjust the code to reference the new location.

  • Example:

    // Before
    class Account {
        private AccountType _type;
        double overdraftCharge() {
            if (_type.isPremium()) {
                // calculation
            }
            else {
                // different calculation
            }
        }
    }

Move Method/Field

    // After
    class AccountType {
        double overdraftCharge() {
            if (this.isPremium()) {
                // calculation
            }
            else {
                // different calculation
            }
        }
    }
  • Benefits: Improves the organization of the code and enhances its clarity.

Legacy Code and Refactoring

Challenges of Legacy Code

  • Definition: Legacy code often refers to code that is inherited from others or older systems.

  • Common Issues:

    • Poorly documented or undocumented.

    • Tightly coupled and hard to understand.

    • Lacking tests, making changes risky.

Challenges of Legacy Code

  • Understanding Legacy Code: Often the first challenge is just understanding what the code does and why.

Strategies for Refactoring Legacy Code

  • Testing as a First Step: Begin with test writing for comprehension and documentation.
  • Refactor in Small Steps: Implement and test small changes regularly.
  • Focus on High-Impact Areas: Concentrate on frequently altered or problematic sections.

Incremental Improvement in Legacy Systems

  • Continuous Refactoring: Integrate code cleanup routinely.
  • Refactoring with Each Feature: Refactor with each new feature addition.
  • Documentation: Update or create documentation.

Netflix’s Evolution: Context & Hurdles

  • Background: Transition from DVD rental to streaming due to market evolution. Inadequate monolithic codebase for the shift.

  • Challenges:

    • Scalability: Limited capacity for growing subscribers and global expansion.
    • Reliability: System outages impacting user experience.

Netflix’s Transition: Key Strategies & Results

  • Strategies:
    • Microservices: Enhanced scalability and development speed.
    • Cloud Migration (AWS): Improved scalability and reliability.
    • Chaos Engineering: Boosted system resilience through intentional failures.
    • Continuous Delivery: Streamlined testing and deployment.

Clean Code vs. Performance

  • Trade-offs: Balancing readability and efficiency.
  • Prioritization: Deciding between maintainability and speed.
  • Iterative Strategy: Gradual adjustments with impact assessment.