[Refactoring] Switch Statements code smell

Khi bạn có một toán tử Switch phức tạp hoặc là một danh sách các câu lệnh if

Nguyên Nhân

Hiếm khi sử dụng toán tử switch và case là một trong những dấu hiệu của lập trình hướng đối tượng

Thường thì code cho một switch có thể nằm rải rác khắp nơi trong chương trình. Khi có một điều kiện mới được thêm vào, bạn sẽ phải tìm đến tất cả các switch và thay thế nó.

Có một quy luật, khi bạn nhìn thấy switch bạn nên nghĩ tới khái niệm đa hình (polymorphism)

Cách khắc phục

1. Cô lập switch

Để cô lập switch và đặt nó vào đúng class, bạn phải cần tới 2 khái niệm là Extract MethodMove Method

Đoạn logic switch có thể move ra method mới (Extract method)

class Order
{
  // ...

  public double calculateTotal()
  {
    double total = 0;
    foreach (Product product in getProducts())
    {
      total += product.quantity * product.price;
    }

    // Apply regional discounts.
    switch (user.getCountry())
    {
      case "US": total *= 0.85; break;
      case "RU": total *= 0.75; break;
      case "CN": total *= 0.9; break;
      // ...
    }

    return total;
  }

After

class Order
{
  // ...

  public double calculateTotal()
  {
    double total = 0;
    foreach (Product product in getProducts())
    {
      total += product.quantity * product.price;
    }
    total = applyRegionalDiscounts(total);
    return total;
  }

  public double applyRegionalDiscounts(double total)
  {
    double result = total;
    switch (user.getCountry())
    {
      case "US": result *= 0.85; break;
      case "RU": result *= 0.75; break;
      case "CN": result *= 0.9; break;
      // ...
    }
    return result;
  }

Sau đó chúng ta áp dụng thêm kỹ thuật Move method, để move ra class mới đúng với nhiệm vụ của method đó.

Problem

class Order
{
  // ...

  public double calculateTotal()
  {
    double total = 0;
    foreach (Product product in getProducts())
    {
      total += product.quantity * product.price;
    }
    total = applyRegionalDiscounts(total);
    return total;
  }

  public double applyRegionalDiscounts(double total)
  {
    double result = total;
    switch (user.getCountry())
    {
      case "US": result *= 0.85; break;
      case "RU": result *= 0.75; break;
      case "CN": result *= 0.9; break;
      // ...
    }
    return result;
  }

Solution

class Order {
  // ...

  public double calculateTotal()
  {
    // ...
    total = Discounts.applyRegionalDiscounts(total, user.getCountry());
    total = Discounts.applyCoupons(total);
    // ...
  }


class Discounts {
  // ...

  public static double applyRegionalDiscounts(double total, string country)
  {
    double result = total;
    switch (country)
    {
      case "US": result *= 0.85; break;
      case "RU": result *= 0.75; break;
      case "CN": result *= 0.9; break;
      // ...
    }
    return result;
  }

  public static double applyCoupons(double total) {
      // ...
  }

2. Thay thế điều kiện switch bằng đa hình

Sau khi xác định được cấu trúc kế thừa, ta sử dụng nguyên tắc đa hình để thay thế điều kiện switch

Problem: Bạn có câu switch thực hiện một số logic dựa vào loại điều kiện đầu vào

public class Bird 
{
  // ...
  public double GetSpeed() 
  {
    switch (type) 
    {
      case EUROPEAN:
        return GetBaseSpeed();
      case AFRICAN:
        return GetBaseSpeed() - GetLoadFactor() * numberOfCoconuts;
      case NORWEGIAN_BLUE:
        return isNailed ? 0 : GetBaseSpeed(voltage);
      default:
        throw new Exception("Should be unreachable");
    }
  }
}

Solution: Tạo những Subclass tương ứng với từng nhánh của case. Trong mỗi Subclass khởi tạo một phương thức tương ứng với một điều kiện đó.

public abstract class Bird 
{
  // ...
  public abstract double GetSpeed();
}

class European: Bird 
{
  public override double GetSpeed() 
  {
    return GetBaseSpeed();
  }
}
class African: Bird 
{
  public override double GetSpeed() 
  {
    return GetBaseSpeed() - GetLoadFactor() * numberOfCoconuts;
  }
}
class NorwegianBlue: Bird
{
  public override double GetSpeed() 
  {
    return isNailed ? 0 : GetBaseSpeed(voltage);
  }
}

// Somewhere in client code
speed = bird.GetSpeed();

3. Thay thế parametter thành những method rõ ràng

Problem: bạn truyền vào param và kiểm tra điều kiện dựa trên param đó

void SetValue(string name, int value) 
{
  if (name.Equals("height")) 
  {
    height = value;
    return;
  }
  if (name.Equals("width")) 
  {
    width = value;
    return;
  }
  Assert.Fail();
}

Solution: Tách ra method riêng để gán giá trị

void SetHeight(int arg) 
{
  height = arg;
}
void SetWidth(int arg) 
{
  width = arg;
}

4. Null Object pattern

Probem: Khi thấy một số phương thức return null thay vì object đó, bạn phải check null cho object đó

if (customer == null) 
{
  plan = BillingPlan.Basic();
}
else 
{
  plan = customer.GetPlan();
}

Solution: thay vì null, bạn return một kế thừa null object của đối tượng đó

public sealed class NullCustomer: Customer 
{
  public override bool IsNull 
  {
    get { return true; }
  }
  
  public override Plan GetPlan() 
  {
    return new NullPlan();
  }
  // Some other NULL functionality.
}

// Replace null values with Null-object.
customer = order.customer ?? new NullCustomer();

// Use Null-object as if it's normal subclass.
plan = customer.GetPlan();

Mình sẽ có bài viết chi tiết về vấn đề này sau

Kết luận

Tác dụng là cải thiện tổ chức code của bạn

Khi nào không nên dùng?

  • Khi swtich xử lý một hành động đơn giản, thì không có lý do gì phải change code
  • Thường thường switch hay được sử dụng bởi Factory design pattern (Factory Method hoặc Abstract Factory) để select một class cụ thể nào đó

F G+ T

tuandph

Khởi đầu với .NET từ năm 2013 đến nay. Hiện tại mình đang làm full-stack developer. Yêu thích lập trình & chia sẽ kiến thức. Thời gian rảnh thường làm những tool vui vui và viết lách kể lệ sự đời.