How to identify and fix code smells?

akhilani

Akhil Kadangode

Posted on November 16, 2023

How to identify and fix code smells?

Understanding Code Smell and Its Types with Examples

In software development, maintaining a clean and efficient codebase is crucial. Recognizing code smells, which are warning signs indicating potential problems in your code, is an essential skill. While they don't directly affect the functioning of the program, they can make the code harder to maintain and extend. Let's delve into the first five common code smells, understand their implications, and explore ways to resolve them, with examples for better clarity.

1. Long Method

The 'Long Method' smell occurs when a method in a program becomes too lengthy, containing too many lines of code. This often happens when the method is handling too many responsibilities.

Problem: Long methods are hard to read, understand, and debug. They often contain duplicate code and make it challenging to identify and fix bugs.

Example:

function processOrder($order) {
    // Check inventory
    // Update order status
    // Calculate shipping cost
    // Apply discounts
    // Validate payment information
    // Send notification
    // Dozens of lines handling various aspects of order processing
}
Enter fullscreen mode Exit fullscreen mode

In this example, processOrder does multiple things, making it a complex method to understand and modify.

Solution: The solution is to break the method into smaller, more focused methods. This practice is known as method decomposition.

function checkInventory($order) { /* ... */ }
function updateOrderStatus($order, $status) { /* ... */ }
function calculateShippingCost($order) { /* ... */ }
// More such focused methods
Enter fullscreen mode Exit fullscreen mode

By breaking the long method into smaller methods, each with a single responsibility, the code becomes cleaner, easier to maintain, and less prone to errors.

2. Large Class

A 'Large Class' is a class that tries to do too much. It often contains too many fields, methods, and lines of code.

Problem: Large classes are challenging to understand and maintain. They often violate the Single Responsibility Principle (SRP), leading to a higher chance of bugs and difficulties in testing.

Example:

class OrderManager {
    // Fields for order processing, customer details, payment, notifications
    // Methods for each of these aspects
    // Hundreds of lines of code
}
Enter fullscreen mode Exit fullscreen mode

In this case, OrderManager is doing more than managing orders; it's handling multiple unrelated responsibilities.

Solution: The key is to apply SRP and break the class into smaller classes, each handling a single responsibility.

class OrderProcessor { /* ... */ }
class CustomerManager { /* ... */ }
class PaymentHandler { /* ... */ }
class NotificationSender { /* ... */ }
Enter fullscreen mode Exit fullscreen mode

Refactoring the large class into several smaller classes makes the code more modular, easier to understand, and simplifies future modifications.

3. Primitive Obsession

Primitive Obsession refers to the overuse of primitive data types instead of small objects for simple tasks.

Problem: It can lead to duplication, lack of expressiveness, and increased error potential. For instance, using raw strings or numbers for complex data like currency, phone numbers, or date ranges can be problematic.

Example:

function createInvoice($date, $amount, $currency) {
    // Uses primitives for date and currency
}
Enter fullscreen mode Exit fullscreen mode

Here, $date and $currency are likely to be strings or numbers, which are not expressive and prone to errors.

Solution: Replace primitives with small objects that have a specific role.

class Date {
    private $date;
    // Constructor and methods
}

class Currency {
    private $currency;
    // Constructor and methods
}

function createInvoice(Date $date, Amount $amount, Currency $currency) { /* ... */ }
Enter fullscreen mode Exit fullscreen mode

By using specific classes, the code becomes more readable, maintainable, and less prone to common mistakes.

4. Long Parameter List

Methods with long lists of parameters can be confusing and hard to use.

Problem: They make method calls difficult to read and understand, especially when multiple parameters are of the same type. This can also lead to errors, such as parameters being passed in the wrong order.

Example:

function createReport($startDate, $endDate, $user, $format, $includeImages, $pageSize, $orientation) {
    // Complex method with many parameters
}
Enter fullscreen mode Exit fullscreen mode

In this example, understanding what each parameter represents is challenging, and calling this method can be error-prone.

Solution: Encapsulate the parameters in an object.

class ReportConfig {
    private $startDate;
    private $endDate;
    // Other fields

    // Constructor and methods
}

function createReport(ReportConfig $config) { /* ... */ }
Enter fullscreen mode Exit fullscreen mode

Using a parameter object improves readability and makes the method call simpler and less error-prone.

5. Data Clumps

Data clumps occur when the same group of data fields (like parameters, variables) repeatedly appear together in multiple classes or methods.

Problem: This repetition is a sign that the data belongs together and should be encapsulated in its own object. It makes the code less maintainable and increases the risk of inconsistencies.

Example:

function calculateShippingCost($width, $height, $depth, $weight) { /* ... */ }
function calculatePackageVolume($width, $height, $depth) { /* ... */ }
Enter fullscreen mode Exit fullscreen mode

In these functions, the parameters $width, $height, and $depth repeatedly appear together.

Solution: Group these fields into a class.

class Package {
    private $width;
    private $height;
    private $depth;
    private $weight;

    // Constructor and methods
}

function calculateShippingCost(Package $package) { /* ... */ }
function calculatePackageVolume(Package $package) { /* ... */ }
Enter fullscreen mode Exit fullscreen mode

Encapsulating the related data into a Package class reduces duplication and makes the functions cleaner and more consistent.

6. Switch Statements

Using excessive switch or if statements, especially when dealing with different types or actions, can be a sign of trouble.

Problem: Such statements are hard to manage and extend. They often violate the Open-Closed Principle, as adding a new type or action requires modifying the existing switch statement.

Example:

function calculatePay($employee) {
    switch ($employee->getType()) {
        case 'Manager':
            // Calculate pay for Manager
            break;
        case 'Worker':
            // Calculate pay for Worker
            break;
        // More cases
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, adding a new employee type requires changes to the calculatePay function.

Solution: Use polymorphism, allowing each type to have its own implementation of a method.

abstract class Employee {
    abstract function calculatePay();
}

class Manager extends Employee {
    function calculatePay() { /* ... */ }
}

class Worker extends Employee {
    function calculatePay() { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

By using polymorphism, the code becomes more manageable and adheres to the Open-Closed Principle.

7. Temporary Field

Temporary fields are instance variables that are only set in certain situations.

Problem: They make the class's state inconsistent and the code difficult to understand, as the fields are not always relevant.

Example:

class ReportGenerator {
    private $data;
    private $temporaryResult; // Set only in specific conditions

    function generateReport() {
        // Uses temporaryResult in some scenarios
    }
}
Enter fullscreen mode Exit fullscreen mode

In this scenario, temporaryResult is not always used, making the class's state unpredictable.

Solution: Restructure the class to avoid temporary fields.

class ReportGenerator {
    private $data;

    function generateReport() {
        $temporaryResult = $this->calculateTemporaryResult();
        // Use $temporaryResult
    }

    private function calculateTemporaryResult() {
        // Calculate and return the result
    }
}
Enter fullscreen mode Exit fullscreen mode

This approach ensures the state of the class is consistent and clear.

8. Refused Bequest

This smell occurs when a subclass does not use inherited methods and properties from its parent class.

Problem: It indicates inappropriate inheritance, where the subclass and the superclass are not truly in a 'is-a' relationship.

Example:

class Bird {
    function fly() { /* ... */ }
}

class Ostrich extends Bird {
    // Ostrich doesn't fly
}
Enter fullscreen mode Exit fullscreen mode

Here, Ostrich inherits fly but doesn't use it, which is misleading.

Solution: Rethink the inheritance structure.

class Bird { /* Common bird properties */ }

class FlyingBird extends Bird {
    function fly() { /* ... */ }
}

class Ostrich extends Bird { /* ... */ }
Enter fullscreen mode Exit fullscreen mode

This approach correctly models the relationship between the classes.

9. Alternative Classes with Different Interfaces

This smell occurs when two classes do the same thing but with different method names.

Problem: It makes the code harder to understand and maintain, as the same concept is represented in different ways.

Example:

class CSVReader {
    function readCSV() { /* ... */ }
}

class XMLReader {
    function loadXML() { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

Both classes perform similar operations but with different method names.

Solution: Standardize the interface or create a common interface.

interface FileReader {
    function read();
}

class CSVReader implements FileReader {
    function read() { /* ... */ }
}

class XMLReader implements FileReader {
    function read() { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

By creating a common interface, the code becomes more uniform and easier to use.

10. Parallel Inheritance Hierarchies

This happens when you create a subclass in one class hierarchy, and as a result, you need to create a subclass in another.

Problem: It leads to tight coupling between the hierarchies, making the code harder to maintain.

Example:

class User {}
class UserValidator {}

class Admin extends User {}
class AdminValidator extends UserValidator {}
Enter fullscreen mode Exit fullscreen mode

Here, for every new User type, a new Validator type is needed.

Solution: Combine the hierarchies or use delegation.

class User {
    private $validator;

    function __construct(UserValidator $validator) {
        $this->validator = $validator;
    }
}

class Admin extends User { /* ... */ }
Enter fullscreen mode Exit fullscreen mode

With this approach, you avoid parallel inheritance and reduce coupling.

11. Lazy Class

A 'Lazy Class' refers to a class that does very little and doesn't justify its existence. It’s like having a tool that you hardly ever use.

Problem: These classes add unnecessary complexity to the codebase, making it harder to maintain.

Example:

class Name {
    private $name;

    function getName() {
        return $this->name;
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, the Name class is overly simplistic and doesn't add much value.

Solution: Consider eliminating the class and using simpler data structures.

// Instead of using a Name class, use a simple string variable
$name = "John Doe";
Enter fullscreen mode Exit fullscreen mode

By replacing the class with a basic data type, the code becomes more straightforward and less bloated.

12. Speculative Generality

'Speculative Generality' occurs when code is added to support anticipated future features that never get used.

Problem: This leads to unnecessary complexity and code that is harder to understand and maintain.

Example:

class FileManager {
    function read() { /* ... */ }
    function write() { /* ... */ }
    function compress() { /* Never used */ }
}
Enter fullscreen mode Exit fullscreen mode

In this case, the compress method is never used and adds to the complexity.

Solution: Remove or comment out the unused code.

class FileManager {
    function read() { /* ... */ }
    function write() { /* ... */ }
    // Removed unused compress method
}
Enter fullscreen mode Exit fullscreen mode

Removing speculative features simplifies the code and focuses on current functionality.

13. Feature Envy

A method suffering from 'Feature Envy' is more interested in a class other than the one it resides in.

Problem: This smell indicates poor distribution of responsibilities and often leads to tightly coupled classes.

Example:

class Order {
    private $total;

    function getTotal() {
        return $this->total;
    }
}

class Report {
    function generateOrderReport(Order $order) {
        $total = $order->getTotal();
        // Extensively uses Order's data
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, generateOrderReport in Report is more concerned with Order than Report.

Solution: Move the envious method to the class it envies.

class Order {
    private $total;

    function getTotal() {
        return $this->total;
    }

    function generateReport() {
        // Generate report based on Order's data
    }
}
Enter fullscreen mode Exit fullscreen mode

By relocating the method, the responsibilities are more logically distributed.

14. Inappropriate Intimacy

When one class knows too much about the internals of another class, it's considered 'Inappropriately Intimate'.

Problem: This breaks encapsulation and makes classes tightly coupled, leading to a fragile code structure.

Example:

class Order {
    public $orderNumber;
    // Other public fields
}

class OrderProcessor {
    function process(Order $order) {
        echo $order->orderNumber;
        // Directly accesses Order's fields
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, OrderProcessor directly accesses Order's internal fields.

Solution: Use getter methods or rethink the design to reduce coupling.

class Order {
    private $orderNumber;

    function getOrderNumber() {
        return $this->orderNumber;
    }
}

class OrderProcessor {
    function process(Order $order) {
        echo $order->getOrderNumber();
    }
}
Enter fullscreen mode Exit fullscreen mode

Using getters and setters maintains encapsulation and reduces dependencies between classes.

15. Message Chains

A 'Message Chain' is a long chain of method calls, often navigating an object graph to perform operations.

Problem: These chains create tight coupling between the client and the intermediate objects. A change in any intermediate link can break the entire chain.

Example:

echo $order->getCustomer()->getAccount()->getBillingAddress()->getZipCode();
Enter fullscreen mode Exit fullscreen mode

Here, a simple operation requires navigating through multiple objects.

Solution: Apply the Law of Demeter to refactor the code, reducing dependencies.

class Order {
    function getBillingZipCode() {
        return $this->getCustomer()->getAccount()->getBillingAddress()->getZipCode();
    }
}

echo $order->getBillingZipCode();
Enter fullscreen mode Exit fullscreen mode

By providing a method in Order that hides the chain, the code becomes more robust and less prone to breakage from changes in the object graph.

16. Middle Man

A 'Middle Man' is a class that does little else than delegating work to other classes.

Problem: These classes increase complexity without adding significant value, making the code harder to navigate.

Example:

class OrderController {
    private $orderService;

    function processOrder($data) {
        return $this->orderService->processOrder($data);
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, OrderController merely delegates the call to OrderService, adding an unnecessary layer.

Solution: Remove the middle man and interact directly with the service.

// Directly use OrderService in the client code
$orderService = new OrderService();
$orderService->processOrder($data);
Enter fullscreen mode Exit fullscreen mode

By eliminating the intermediary, the code becomes more straightforward and easier to understand.

17. Insider Trading

This smell occurs when classes have an intimate knowledge of each other’s internals, breaking encapsulation.

Problem: It creates a tightly coupled system, where changes in one class can have far-reaching effects on others.

Example:

class Order {
    private $data;

    function processData() {
        // Process data
    }
}

class OrderProcessor {
    function process(Order $order) {
        $order->data = // Modify data directly
        $order->processData();
    }
}
Enter fullscreen mode Exit fullscreen mode

OrderProcessor directly manipulates Order's private data, indicating a high level of coupling.

Solution: Use public methods for interaction and maintain encapsulation.

class Order {
    private $data;

    function setData($data) {
        $this->data = $data;
    }

    function processData() {
        // Process data
    }
}

class OrderProcessor {
    function process(Order $order) {
        $order->setData(/* new data */);
        $order->processData();
    }
}
Enter fullscreen mode Exit fullscreen mode

Encapsulation is preserved, reducing the risk of unintended side-effects from external manipulation.

18. Large Class (God Object)

A 'Large Class', often called a 'God Object', knows too much or does too much, centralizing the functionality of a system.

Problem: It violates the Single Responsibility Principle, making the class difficult to understand, maintain, and extend.

Example:

class Application {
    // Methods for user management, order processing, logging, notifications, etc.
}
Enter fullscreen mode Exit fullscreen mode

This Application class is overloaded with responsibilities.

Solution: Break the class into smaller, focused classes.

class UserManager { /* ... */ }
class OrderProcessor { /* ... */ }
class Logger { /* ... */ }
class NotificationService { /* ... */ }
Enter fullscreen mode Exit fullscreen mode

By distributing responsibilities, the system becomes more modular and maintainable.

19. Divergent Change

Divergent Change occurs when one class is commonly changed in different ways for different reasons.

Problem: A single class undergoing changes for various unrelated reasons becomes hard to maintain and understand.

Example:

class ReportGenerator {
    function generateCSVReport() { /* ... */ }
    function generatePDFReport() { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

Modifications in report generation (CSV or PDF) involve changing the ReportGenerator class.

Solution: Separate responsibilities into different classes.

class CSVReportGenerator { function generate() { /* ... */ } }
class PDFReportGenerator { function generate() { /* ... */ } }
Enter fullscreen mode Exit fullscreen mode

Separating concerns reduces the complexity and makes each class more focused and easier to maintain.

20. Shotgun Surgery

'Shotgun Surgery' is similar to divergent change but in reverse. It occurs when a change in one part of the system results in many small changes in various other parts.

Problem: The codebase becomes fragile, as changes are scattered across multiple locations.

Example:

// Changing database schema affects multiple classes
class UserManager { /* ... */ }
class OrderManager { /* ... */ }
class InventoryManager { /* ... */ }
Enter fullscreen mode Exit fullscreen mode

A database schema change requires modifications in several manager classes.

Solution: Centralize the change-prone parts.

class Database {
    // Centralized database handling
}

class UserManager {
    private Database $db;
    /* ... */
}
Enter fullscreen mode Exit fullscreen mode

Centralization makes the system more resilient to changes, as modifications are more localized.

21. Data Class

A 'Data Class' is a class that only contains fields and methods for accessing them (like getters and setters) without any additional functionality.

Problem: These classes do not encapsulate any behavior, which leads to code that is procedural rather than object-oriented, and it can lead to logic being scattered across the codebase.

Example:

class User {
    public $name;
    public $email;

    function getName() {
        return $this->name;
    }

    function getEmail() {
        return $this->email;
    }
}
Enter fullscreen mode Exit fullscreen mode

User here is a simple container of data without any behavior.

Solution: Add behavior to the class that is relevant to the data it holds.

class User {
    private $name;
    private $email;

    function getName() {
        return $this->name;
    }

    function getEmail() {
        return $this->email;
    }

    function sendNotification($message) {
        // Send a notification to the user
    }
}
Enter fullscreen mode Exit fullscreen mode

Adding methods that act on the data makes the class more encapsulated and object-oriented.

22. Comments

Reliance on comments to explain complex or unclear code is a code smell. Good code should mostly be self-explanatory.

Problem: Excessive use of comments often masks bad code that should be refactored for clarity. Comments can become outdated or misleading if not maintained alongside the code.

Example:

// Check if the user is eligible for a discount and apply it
function applyDiscount($user, $order) {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

The function's purpose and implementation should be clear without needing comments.

Solution: Refactor the code to be more self-explanatory.

function applyDiscountIfEligible($user, $order) {
    if ($this->isUserEligibleForDiscount($user)) {
        $this->applyDiscount($order);
    }
}
Enter fullscreen mode Exit fullscreen mode

Refactoring the code into well-named methods reduces the need for comments and makes the code's intent clear.

23. Duplicate Code

'Duplicate Code' is one of the most common code smells where the same code structure is repeated in more than one place.

Problem: It makes the codebase larger and more complex than necessary, and if a bug is found in one place, it likely needs to be fixed in multiple places.

Example:

function calculateTotalOrder($order) {
    // ...
}

function calculateTotalInvoice($invoice) {
    // Similar code to calculateTotalOrder
}
Enter fullscreen mode Exit fullscreen mode

The calculation logic is duplicated in two different functions.

Solution: Abstract the duplicated code into a single method or class.

function calculateTotal($items) {
    // Unified calculation logic
}

// Now use calculateTotal in both order and invoice calculations
Enter fullscreen mode Exit fullscreen mode

Centralizing the common code reduces duplication and the potential for bugs.

Wrapping Up

Addressing code smells is a continuous and essential process in software development. It's not just about fixing immediate problems but about improving the overall health and maintainability of the codebase. By regularly refactoring to address these issues, developers can ensure their code is clean, efficient, and adaptable for future changes. This practice not only enhances the quality of the current project but also sets a high standard for any future work on the codebase. Remember, clean code leads to a more robust, scalable, and enjoyable development experience.

💖 💪 🙅 🚩
akhilani
Akhil Kadangode

Posted on November 16, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

How to identify and fix code smells?
refactoring How to identify and fix code smells?

November 16, 2023