[Design Pattern and Paradigm: Behavioral Type] 60 | Strategy Pattern (Part 1): 긴 if-else/switch 분기 판단 코드를 피하는 방법은?

지난 두 강의에서 우리는 템플릿 패턴에 대해 배웠습니다. 템플릿 모드는 주로 코드 재사용 및 확장 역할을 합니다. 또한 템플릿 모드와 유사하지만 더 유연하게 사용할 수 있는 콜백에 대해서도 이야기했습니다. 그들 사이의 주요 차이점은 코드 구현이며 템플릿 패턴은 상속을 기반으로 구현되며 콜백은 구성을 기반으로 구현됩니다.

오늘 우리는 또 다른 행동 패턴인 전략 패턴을 배우기 시작합니다. 실제 프로젝트 개발에서도 이 모드를 많이 사용합니다. 가장 일반적인 애플리케이션 시나리오는 긴 if-else를 피하거나 분기 판단을 전환하는 데 사용하는 것입니다. 그러나 그 이상을 수행합니다. 또한 템플릿 모드와 같은 프레임워크 확장 지점 등을 제공할 수 있습니다.

전략 패턴에 대해서는 두 가지 클래스로 나누어 설명하겠습니다. 오늘은 스트래티지 패턴의 원리와 구현, 분기판단 논리를 피하기 위해 사용하는 방법에 대해 설명합니다. 다음 강의에서는 구체적인 예를 사용하여 전략 패턴의 적용 시나리오와 실제 설계 의도를 자세히 설명하겠습니다.

더 이상 고민하지 않고 오늘의 공부를 공식적으로 시작합시다!

Strategy Pattern의 원리와 구현

전략 모드, 영어 전체 이름은 Strategy Design Pattern입니다. GoF 책 "Design Patterns"에서는 다음과 같이 정의되어 있습니다.

알고리즘 계열을 정의하고 각각을 캡슐화하고 상호 교환 가능하게 만드십시오. 전략을 사용하면 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있습니다.

중국어로 번역하면 다음과 같습니다. 알고리즘 클래스 계열을 정의하고 각 알고리즘을 개별적으로 캡슐화하여 서로 대체할 수 있도록 합니다. 전략 패턴은 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있습니다(여기서 클라이언트는 알고리즘을 사용하는 코드를 나타냄).

우리는 팩토리 패턴이 객체의 생성과 사용을 분리하는 것이고 관찰자 패턴은 관찰자와 관찰 대상을 분리한다는 것을 알고 있습니다. 전략 패턴은 둘과 유사하며 분리 역할을 할 수도 있지만 전략 정의, 생성 및 사용의 세 부분을 분리합니다. 다음으로 완전한 전략 패턴이 갖추어야 할 세 가지 부분에 대해 자세히 말씀드리겠습니다.

1. 정책의 정의

전략 클래스의 정의는 전략 인터페이스와 이 인터페이스를 구현하는 전략 클래스 그룹을 포함하여 비교적 간단합니다. 모든 전략 클래스는 동일한 인터페이스를 구현하기 때문에 클라이언트 코드는 구현 프로그래밍이 아닌 인터페이스를 기반으로 다양한 전략을 유연하게 대체할 수 있습니다. 샘플 코드는 다음과 같습니다.

public interface Strategy {
  void algorithmInterface();
}
public class ConcreteStrategyA implements Strategy {
  @Override
  public void  algorithmInterface() {
    //具体的算法...
  }
}
public class ConcreteStrategyB implements Strategy {
  @Override
  public void  algorithmInterface() {
    //具体的算法...
  }
}

2. 정책 수립

전략 모드에는 일련의 전략이 포함되므로 전략을 사용할 때 유형(type)은 일반적으로 사용할 전략을 결정하는 데 사용됩니다. 생성 논리를 캡슐화하려면 클라이언트 코드에서 생성 세부 정보를 보호해야 합니다. 유형을 기반으로 전략을 생성하는 논리를 추출하여 팩토리 클래스에 넣을 수 있습니다. 샘플 코드는 다음과 같습니다.

public class StrategyFactory {
  private static final Map<String, Strategy> strategies = new HashMap<>();
  static {
    strategies.put("A", new ConcreteStrategyA());
    strategies.put("B", new ConcreteStrategyB());
  }
  public static Strategy getStrategy(String type) {
    if (type == null || type.isEmpty()) {
      throw new IllegalArgumentException("type should not be empty.");
    }
    return strategies.get(type);
  }
}

일반적으로 전략 클래스가 상태 비저장이고 멤버 변수를 포함하지 않으며 순수 알고리즘 구현일 뿐이면 이러한 전략 개체를 공유하고 사용할 수 있으며 getStrategy()가 실행될 때마다 새 개체를 생성할 필요가 없습니다. 정책 객체의 호출. 이러한 상황에 대응하여 위의 팩토리 클래스 구현을 이용하여 각각의 정책 객체를 미리 생성하여 팩토리 클래스에 캐싱하고 사용할 때 바로 반환할 수 있습니다.

반대로 정책 클래스가 상태 저장인 경우 비즈니스 시나리오의 필요에 따라 공유 가능한 정책 개체를 캐싱하는 대신 팩토리 메서드에서 새로 생성된 정책 개체를 가져올 때마다 다음을 따라야 합니다. 정책 팩토리 클래스는 다음과 같이 구현됩니다.

public class StrategyFactory {
  public static Strategy getStrategy(String type) {
    if (type == null || type.isEmpty()) {
      throw new IllegalArgumentException("type should not be empty.");
    }
    if (type.equals("A")) {
      return new ConcreteStrategyA();
    } else if (type.equals("B")) {
      return new ConcreteStrategyB();
    }
    return null;
  }
}

3. 전략의 사용

방금 전략의 정의와 생성에 대해 이야기했습니다. 이제 전략의 사용에 대해 살펴보겠습니다.
우리는 전략 패턴에 선택적 전략 세트가 포함되어 있다는 것을 알고 있습니다. 클라이언트 코드는 일반적으로 사용할 전략을 어떻게 결정합니까? 가장 일반적인 것은 런타임에 사용할 전략을 동적으로 결정하는 것이며, 이는 전략 패턴의 가장 일반적인 애플리케이션 시나리오이기도 합니다.

여기서 "런타임 다이내믹"이란 사전에 어떤 전략을 사용할지 모르지만 프로그램 실행 중 구성, 사용자 입력 및 계산 결과와 같은 불확실한 요소에 따라 사용할 전략을 동적으로 결정한다는 의미입니다. 다음으로 예를 들어 설명하겠습니다.

// 策略接口:EvictionStrategy
// 策略类:LruEvictionStrategy、FifoEvictionStrategy、LfuEvictionStrategy...
// 策略工厂:EvictionStrategyFactory
public class UserCache {
  private Map<String, User> cacheData = new HashMap<>();
  private EvictionStrategy eviction;
  public UserCache(EvictionStrategy eviction) {
    this.eviction = eviction;
  }
  //...
}
// 运行时动态确定,根据配置文件的配置决定使用哪种策略
public class Application {
  public static void main(String[] args) throws Exception {
    EvictionStrategy evictionStrategy = null;
    Properties props = new Properties();
    props.load(new FileInputStream("./config.properties"));
    String type = props.getProperty("eviction_type");
    evictionStrategy = EvictionStrategyFactory.getEvictionStrategy(type);
    UserCache userCache = new UserCache(evictionStrategy);
    //...
  }
}
// 非运行时动态确定,在代码中指定使用哪种策略
public class Application {
  public static void main(String[] args) {
    //...
    EvictionStrategy evictionStrategy = new LruEvictionStrategy();
    UserCache userCache = new UserCache(evictionStrategy);
    //...
  }
}

위의 코드에서 "비실행 동적 결정", 즉 두 번째 응용 프로그램의 사용 방법은 전략 모드를 활용할 수 없음을 알 수 있습니다. 이 애플리케이션 시나리오에서 전략 패턴은 실제로 "객체 지향 다형성 기능" 또는 "구현보다는 인터페이스에 기반한 프로그래밍 원칙"으로 변질됩니다.

분기 판단을 피하기 위해 전략 모드를 사용하는 방법은 무엇입니까?

사실 분기판단 논리를 없앨 수 있는 모드는 전략 모드 뿐만 아니라 나중에 이야기할 상태 모드도 있습니다. 사용할 모드는 애플리케이션 시나리오에 따라 다릅니다. 전략 모드는 다양한 유형의 역학에 따라 전략이 사용되도록 결정되는 애플리케이션 시나리오에 적합합니다.

if-else 또는 switch-case 분기 판단 논리가 생성되는 방법을 확인하기 위해 먼저 예를 사용하겠습니다. 구체적인 코드는 다음과 같습니다. 이 예제에서는 전략 패턴을 사용하지 않고 전략의 정의, 생성 및 사용을 직접 연결했습니다.

public class OrderService {
  public double discount(Order order) {
    double discount = 0.0;
    OrderType type = order.getType();
    if (type.equals(OrderType.NORMAL)) { // 普通订单
      //...省略折扣计算算法代码
    } else if (type.equals(OrderType.GROUPON)) { // 团购订单
      //...省略折扣计算算法代码
    } else if (type.equals(OrderType.PROMOTION)) { // 促销订单
      //...省略折扣计算算法代码
    }
    return discount;
  }
}

분기 판단 논리를 제거하는 방법은 무엇입니까? 전략 패턴이 유용한 곳입니다. 전략 패턴을 사용하여 위의 코드를 리팩토링하고 다양한 주문 유형의 할인 전략을 전략 클래스로 설계하고 팩토리 클래스는 전략 객체 생성을 담당합니다. 구체적인 코드는 다음과 같습니다.

// 策略的定义
public interface DiscountStrategy {
  double calDiscount(Order order);
}
// 省略NormalDiscountStrategy、GrouponDiscountStrategy、PromotionDiscountStrategy类代码...
// 策略的创建
public class DiscountStrategyFactory {
  private static final Map<OrderType, DiscountStrategy> strategies = new HashMap<>();
  static {
    strategies.put(OrderType.NORMAL, new NormalDiscountStrategy());
    strategies.put(OrderType.GROUPON, new GrouponDiscountStrategy());
    strategies.put(OrderType.PROMOTION, new PromotionDiscountStrategy());
  }
  public static DiscountStrategy getDiscountStrategy(OrderType type) {
    return strategies.get(type);
  }
}
// 策略的使用
public class OrderService {
  public double discount(Order order) {
    OrderType type = order.getType();
    DiscountStrategy discountStrategy = DiscountStrategyFactory.getDiscountStrategy(type);
    return discountStrategy.calDiscount(order);
  }
}

리팩터링된 코드에는 if-else 분기 판단 문이 없습니다. 사실 이것은 정책 팩토리 클래스 덕분입니다. 팩토리 클래스에서 맵을 사용하여 전략을 캐시하고 유형에 따라 맵에서 직접 해당 전략을 얻어 if-else 분기 판단 논리를 피합니다. 나중에 분기 판단 논리를 피하기 위해 상태 패턴을 사용하는 것에 대해 이야기할 때 동일한 루틴을 사용한다는 것을 알게 될 것입니다. 본질적으로 "테이블 조회 방법"을 사용하여 유형 분기를 기반으로 판단하는 대신 유형(코드의 전략은 테이블)을 기반으로 테이블을 조회합니다.

그러나 비즈니스 시나리오에서 매번 다른 정책 개체를 생성해야 하는 경우 다른 팩터리 클래스 구현을 사용해야 합니다. 구체적인 코드는 다음과 같습니다.

public class DiscountStrategyFactory {
  public static DiscountStrategy getDiscountStrategy(OrderType type) {
    if (type == null) {
      throw new IllegalArgumentException("Type should not be null.");
    }
    if (type.equals(OrderType.NORMAL)) {
      return new NormalDiscountStrategy();
    } else if (type.equals(OrderType.GROUPON)) {
      return new GrouponDiscountStrategy();
    } else if (type.equals(OrderType.PROMOTION)) {
      return new PromotionDiscountStrategy();
    }
    return null;
  }
}

이 구현은 원래 if-else 분기 논리를 OrderService 클래스에서 팩토리 클래스로 전송하는 것과 동일하지만 실제로 제거되지는 않습니다. 이 문제를 해결하는 방법에 대해서는 오늘 일시적으로 종료하겠습니다. 메시지 영역에서 자신의 생각을 이야기할 수 있으며 다음 수업에서 설명하겠습니다.

주요 검토

자, 오늘의 내용은 여기까지입니다. 집중해야 할 부분을 함께 요약하고 복습해 봅시다.

전략 패턴은 알고리즘 클래스 계열을 정의하고 각 알고리즘을 개별적으로 캡슐화하여 서로 대체할 수 있도록 합니다. 전략 패턴은 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있습니다(여기서 클라이언트는 알고리즘을 사용하는 코드를 나타냄).

전략 패턴은 전략의 정의, 생성 및 사용을 분리하는 데 사용됩니다. 사실 완전한 전략 패턴은 이 세 부분으로 구성됩니다.

  • 전략 클래스의 정의는 전략 인터페이스와 이 인터페이스를 구현하는 전략 클래스 그룹을 포함하여 비교적 간단합니다.
  • 정책 생성은 정책 생성의 세부 사항을 캡슐화하는 팩토리 클래스에 의해 수행됩니다.
  • 전략 모드에는 일련의 선택적 전략이 포함되어 있습니다.클라이언트 코드가 사용할 전략을 선택하는 방법을 결정하는 두 가지 방법이 있습니다: 컴파일 시 정적 결정 및 런타임 시 동적 결정. 그 중 "실행시 동적 결정"은 전략 패턴의 가장 일반적인 응용 시나리오입니다.

또한 전략 모드를 통해 if-else 분기 판단을 제거할 수도 있습니다. 실제로 이것은 전략 팩토리 클래스의 이점을 제공하며, 더 본질적으로 "조회 테이블 방법"을 사용하여 유형 분기를 기반으로 한 판단을 유형 조회 테이블의 도움으로 대체합니다.

수업 토론

오늘 우리는 전략 팩토리 클래스에서 매번 새로운 전략 객체를 반환해야 하는 경우 팩토리 클래스에서 if-else 분기 판단 로직을 작성해야 한다고 말했습니다. 그렇다면 이 문제를 어떻게 해결해야 할까요?

Supongo que te gusta

Origin blog.csdn.net/qq_32907491/article/details/131218948
Recomendado
Clasificación