Swift refactoring

Swift, like any other programming language, can become messy and difficult to maintain without proper organization and structure. In this article, we’ll look at how you can professionally refactor your code, improving not only your own productivity, but also that of your team.

Understanding Code Refactoring

Code refactoring is not about writing new functions, but about improving existing code. The benefits are numerous, from making code easier to read and maintain, finding and fixing errors, to speeding up the program. However, the most important aspect is that it should not change the behavior of the program.

Key Swift Code Refactoring Techniques

As a developer, improving code through refactoring is an essential skill. By reworking and improving the internal structure of your code without changing its external behavior, you get a more efficient, readable, and maintainable codebase. There are several methods you can use to refactor your Swift code. Let’s dive into some key refactoring techniques with clear before and after examples.

Extraction method

If you find a method that has become too large or notice a piece of code that is repeated in multiple places, it’s time to consider refactoring with an extraction method.

This method involves taking a piece of code that can be grouped, moving it into a separate method, and replacing the old code with a method call. This helps make code more readable and reusable, reduces code redundancy, and allows the developer to decompose complex methods into smaller, more manageable ones.

func parseJSONAndCreateUser(data: Data) throws -> User {
   let decoder = JSONDecoder()
   let json = try decoder.decode([String: Any].self, from: data)
   guard let name = json["name"] as? String,
         let email = json["email"] as? String,
         let addressDict = json["address"] as? [String: Any],
         let street = addressDict["street"] as? String,
         let city = addressDict["city"] as? String else {
       throw NSError(domain: "", code: -1, userInfo: nil)
   }
   let address = Address(street: street, city: city)
   return User(name: name, email: email, address: address)
}

Code refactoring:

func parseJSONAndCreateUser(data: Data) throws -> User {
    let json = try decodeJSON(data: data)
    let (name, email, address) = try parseUserDetails(from: json)
    return User(name: name, email: email, address: address)
}

func decodeJSON(data: Data) throws -> [String: Any] {
    let decoder = JSONDecoder()
    let json = try decoder.decode([String: Any].self, from: data)
    return json
}

func parseUserDetails(from json: [String: Any]) throws -> (String, String, Address) {
    guard let name = json["name"] as? String,
          let email = json["email"] as? String,
          let addressDict = json["address"] as? [String: Any],
          let street = addressDict["street"] as? String,
          let city = addressDict["city"] as? String else {
        throw NSError(domain: "", code: -1, userInfo: nil)
    }
    let address = Address(street: street, city: city)
    return (name, email, address)
}

Built-in method

Used when the body of the method is as clear as its name. In this case, the method does not provide additional clarity and may be removed altogether. By replacing the method call with the contents of the method itself, we can reduce unnecessary indirection.

Bad example:

class Order {
    var items: [Item]
    
    func hasDiscountableItems() -> Bool {
        return items.contains { $0.isDiscountable }
    }
    
    func calculateTotal() -> Double {
        if hasDiscountableItems() {
            // сложный расчет скидки
        } else {
            // обычный расчет
        }
    }
}

Code refactoring:

class Order {
    var items: [Item]
    
    func calculateTotal() -> Double {
        if items.contains { $0.isDiscountable } {
            // сложный расчет скидки
        } else {
            // обычный расчет
        }
    }
}

Renaming method

As the code base evolves and new features are added, some method names may no longer accurately reflect what the method does. This is where the renaming technique comes in handy. A method involves changing the name of a method to a new name that more accurately reflects its functionality or behavior. The main goal is to improve code clarity and ensure that other developers can understand the purpose of a method simply by reading its name.

In more complex cases, method names may be clear in context, but not clear in isolation.

Bad example:

class DataProcessor {
    func process() {
        // ...
    }
}

Code refactoring:

class DataProcessor {
    func processRawDataIntoModel() {
        // ...
    }
}

Replace the conditions with guard

Nested conditional statements can make code difficult to read and understand. By replacing them with a guard statement, we can make the code cleaner and highlight the condition being tested and its consequences.

Bad example:

func process(data: Data?) {
    if let data = data {
        // данные обработки
    } else {
        // необрабатываемые данные
    }
}

Code refactoring:

func process(data: Data?) {
    guard let data = data else {
        // данные обработки
        return
    }
    // необрабатываемые данные
}

Replacing a temporary variable with a query

This technique is often used when you have a temporary variable that stores the result of an expression or query. The problem with such variables is that they can make methods longer, harder to understand, and harder to maintain. The essence of the method is to take the expression that initializes such a variable, encapsulate it in a new method, and then replace all references to the temporary variable with this new method. The main benefits are improved code clarity, and in some cases, elimination of side effects, making the code more understandable and logical.

Consider a scenario where you use a temporary variable to store the result of an expression.

Bad example:

class Order {
    var items: [Item]
    
    func calculateTotal() -> Double {
        let basePrice = items.reduce(0, { $0 + $1.price })
        // сложный расчет с basePrice
    }
}

Code refactoring:

class Order {
    var items: [Item]
    
    var basePrice: Double {
        return items.reduce(0, { $0 + $1.price })
    }

    func calculateTotal() -> Double {
        // сложный расчет с basePrice
    }
}

Conclusion

In your developer journey, refactoring plays an important role. It’s not just about code cleanup—it’s about durability, scalability, and continuous improvement. The techniques we’ve discussed serve as valuable tools in your refactoring toolbox.

Each method has its benefits, and by mastering these refactoring techniques, you can ensure that your code remains reliable, readable, and maintainable even as your projects grow in size and complexity. Remember that refactoring is not a one-time event, but an ongoing commitment to code quality and professionalism.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *