[Design Pattern and Paradigm: Behavioral] 62 |책임 사슬 패턴(1부): 알고리즘을 유연하게 확장할 수 있는 민감 정보 필터링 프레임워크를 구현하는 방법은?

이전 레슨에서 템플릿 모드와 전략 모드를 배웠다면 오늘은 책임 모드의 사슬을 배울 것입니다. 이 세 가지 모드는 동일한 기능을 가지고 있습니다: 재사용 및 확장, 실제 프로젝트 개발, 특히 프레임워크 개발에서 더 일반적으로 사용됩니다. 이를 사용하여 프레임워크의 확장 지점을 제공하여 프레임워크 사용자가 수정하지 않고 사용할 수 있습니다. 프레임워크 소스 코드의 경우 확장점을 기반으로 프레임워크의 기능을 커스터마이즈한다.

오늘은 주로 책임 사슬 모델의 원리와 구현에 대해 설명합니다. 또한 책임 사슬 모델을 사용하여 알고리즘을 유연하게 확장할 수 있는 민감한 단어 필터링 프레임워크를 구현하도록 안내합니다. 다음 강의에서는 실제 전투에 좀 더 가까이 다가갈 예정이며, Servlet Filter와 Spring Interceptor를 분석하여 프레임워크에서 일반적으로 사용되는 필터와 인터셉터를 구현하기 위해 Chain of Responsibility 패턴을 사용하는 방법을 알아봅니다.

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

책임 사슬 패턴의 원칙 및 구현

책임 체인 모델의 영어 번역은 Chain Of Responsibility Design Pattern입니다. GoF의 "디자인 패턴"에서는 다음과 같이 정의됩니다.

둘 이상의 개체에 요청을 처리할 기회를 주어 요청 발신자와 수신자를 연결하지 않도록 합니다. 수신 개체를 연결하고 개체가 처리할 때까지 연결을 따라 요청을 전달합니다.

중국어로 번역하면 여러 수신 개체가 요청을 처리할 수 있도록 요청의 전송 및 수신을 분리합니다. 이러한 수신 개체를 체인으로 묶고 체인의 수신 개체 중 하나가 처리할 수 있을 때까지 체인을 따라 요청을 전달합니다.

이것은 다소 추상적이므로 이해하기 쉬운 용어로 더 자세히 설명하겠습니다.

책임 사슬 패턴에서는 여러 프로세서(즉, 정의에서 방금 언급한 "수신 개체")가 동일한 요청을 순서대로 처리합니다. 요청은 프로세서 A에서 먼저 처리된 다음 프로세서 B로 전달되고 프로세서 B에서 처리된 후 프로세서 C로 전달되는 식으로 체인을 형성합니다. 체인의 각 프로세서는 자체 처리 책임을 가정하므로 책임 체인 모드라고 합니다.

책임 체인 모델과 관련하여 코드 구현을 살펴보겠습니다. 코드 구현과 결합하면 해당 정의를 더 쉽게 이해할 수 있습니다. 책임 체인 모드를 구현하는 방법에는 여러 가지가 있으며 여기서는 일반적으로 사용되는 두 가지 방법을 소개합니다.

첫 번째 구현은 다음과 같습니다. 그 중 Handler는 모든 프로세서 클래스의 추상 부모 클래스이고 handle()은 추상 메서드입니다. 각각의 특정 핸들러 클래스(HandlerA, HandlerB)의 handle() 함수 코드 구조는 비슷하여 요청을 처리할 수 있으면 계속 전달하지 않고, 처리할 수 없으면 요청을 처리합니다. 후속 프로세서(즉, successor.handle() 호출). HandlerChain은 프로세서 체인이며, 데이터 구조의 관점에서 체인 헤드와 체인 테일을 기록하는 연결 목록입니다. 그 중 레코드 체인 꼬리는 프로세서 추가의 편의를 위한 것입니다.

public abstract class Handler {
  protected Handler successor = null;
  public void setSuccessor(Handler successor) {
    this.successor = successor;
  }
  public abstract void handle();
}
public class HandlerA extends Handler {
  @Override
  public boolean handle() {
    boolean handled = false;
    //...
    if (!handled && successor != null) {
      successor.handle();
    }
  }
}
public class HandlerB extends Handler {
  @Override
  public void handle() {
    boolean handled = false;
    //...
    if (!handled && successor != null) {
      successor.handle();
    } 
  }
}
public class HandlerChain {
  private Handler head = null;
  private Handler tail = null;
  public void addHandler(Handler handler) {
    handler.setSuccessor(null);
    if (head == null) {
      head = handler;
      tail = handler;
      return;
    }
    tail.setSuccessor(handler);
    tail = handler;
  }
  public void handle() {
    if (head != null) {
      head.handle();
    }
  }
}
// 使用举例
public class Application {
  public static void main(String[] args) {
    HandlerChain chain = new HandlerChain();
    chain.addHandler(new HandlerA());
    chain.addHandler(new HandlerB());
    chain.handle();
  }
}

사실 위의 코드 구현은 충분히 우아하지 않습니다. 프로세서 클래스의 handle() 함수는 자체 비즈니스 로직을 포함할 뿐만 아니라 다음 프로세서에 대한 호출, 즉 코드의 successor.handle()도 포함합니다. 이 코드 구조에 익숙하지 않은 프로그래머는 새로운 핸들러 클래스를 추가할 때 handle() 함수에서 successor.handle()을 호출하는 것을 잊을 수 있으며, 이로 인해 코드에 버그가 발생할 수 있습니다.

이 문제에 대응하여 코드를 리팩토링하고 템플릿 패턴을 사용하여 successor.handle() 호출 논리를 특정 핸들러 클래스에서 분리하고 추상 상위 클래스에 넣었습니다. 이러한 방식으로 특정 프로세서 클래스는 자체 비즈니스 로직만 구현하면 됩니다. 리팩토링 후 코드는 다음과 같습니다.

public abstract class Handler {
  protected Handler successor = null;
  public void setSuccessor(Handler successor) {
    this.successor = successor;
  }
  public final void handle() {
    boolean handled = doHandle();
    if (successor != null && !handled) {
      successor.handle();
    }
  }
  protected abstract boolean doHandle();
}
public class HandlerA extends Handler {
  @Override
  protected boolean doHandle() {
    boolean handled = false;
    //...
    return handled;
  }
}
public class HandlerB extends Handler {
  @Override
  protected boolean doHandle() {
    boolean handled = false;
    //...
    return handled;
  }
}
// HandlerChain和Application代码不变

두 번째 구현을 살펴보겠습니다. 코드는 다음과 같습니다. 이 구현은 더 간단합니다. HandlerChain 클래스는 연결된 목록 대신 배열을 사용하여 모든 프로세서를 저장하고 HandlerChain의 handle() 함수에서 각 프로세서의 handle() 함수를 차례로 호출해야 합니다.

public interface IHandler {
  boolean handle();
}
public class HandlerA implements IHandler {
  @Override
  public boolean handle() {
    boolean handled = false;
    //...
    return handled;
  }
}
public class HandlerB implements IHandler {
  @Override
  public boolean handle() {
    boolean handled = false;
    //...
    return handled;
  }
}
public class HandlerChain {
  private List<IHandler> handlers = new ArrayList<>();
  public void addHandler(IHandler handler) {
    this.handlers.add(handler);
  }
  public void handle() {
    for (IHandler handler : handlers) {
      boolean handled = handler.handle();
      if (handled) {
        break;
      }
    }
  }
}
// 使用举例
public class Application {
  public static void main(String[] args) {
    HandlerChain chain = new HandlerChain();
    chain.addHandler(new HandlerA());
    chain.addHandler(new HandlerB());
    chain.handle();
  }
}

GoF의 정의에 따르면 프로세서 체인의 프로세서가 요청을 처리할 수 있으면 요청을 계속 전달하지 않습니다. 실제로 책임 체인 모드의 또 다른 변형이 있습니다. 즉, 모든 프로세서에서 요청을 처리하고 중간 종료가 없습니다. 이 변형에는 프로세서를 저장하기 위해 연결된 목록을 사용하고 프로세서를 저장하기 위해 배열을 사용하는 두 가지 구현이 있습니다. 이는 위의 두 구현과 유사하며 약간만 수정하면 됩니다.

여기서는 아래와 같이 구현 방법 중 하나만 제공합니다. 다른 방법으로 위의 구현에 따라 직접 수정할 수 있습니다.

public abstract class Handler {
  protected Handler successor = null;
  public void setSuccessor(Handler successor) {
    this.successor = successor;
  }
  public final void handle() {
    doHandle();
    if (successor != null) {
      successor.handle();
    }
  }
  protected abstract void doHandle();
}
public class HandlerA extends Handler {
  @Override
  protected void doHandle() {
    //...
  }
}
public class HandlerB extends Handler {
  @Override
  protected void doHandle() {
    //...
  }
}
public class HandlerChain {
  private Handler head = null;
  private Handler tail = null;
  public void addHandler(Handler handler) {
    handler.setSuccessor(null);
    if (head == null) {
      head = handler;
      tail = handler;
      return;
    }
    tail.setSuccessor(handler);
    tail = handler;
  }
  public void handle() {
    if (head != null) {
      head.handle();
    }
  }
}
// 使用举例
public class Application {
  public static void main(String[] args) {
    HandlerChain chain = new HandlerChain();
    chain.addHandler(new HandlerA());
    chain.addHandler(new HandlerB());
    chain.handle();
  }
}

책임 사슬 패턴 적용 시나리오의 예

책임 사슬 패턴의 원리와 구현은 끝났으니 이제 실례를 통해 책임 사슬 패턴의 적용 시나리오에 대해 알아보자.

UGC(사용자 생성 콘텐츠, 사용자 생성 콘텐츠)를 지원하는 애플리케이션(포럼 등)의 경우 사용자 생성 콘텐츠(포럼에 게시된 게시물 등)에는 일부 민감한 단어(예: 음란물, 광고, 반응성 등)가 포함될 수 있습니다. ). 이 애플리케이션 시나리오의 경우 책임 체인 패턴을 사용하여 이러한 민감한 단어를 필터링할 수 있습니다.

민감한 단어가 포함된 콘텐츠의 경우 게시를 직접 금지하는 방법과 게시 전에 민감한 단어를 모자이크 처리(예: 민감한 단어를 ***로 대체)하는 두 가지 처리 방법이 있습니다. 첫 번째 처리 방법은 GoF가 제공한 책임 사슬 모델의 정의를 따르고 두 번째 처리 방법은 책임 사슬 모델의 변형입니다.

여기서는 첫 번째 구현의 코드 예제만 제공합니다. 아래와 같이 코드 구현의 골격만 제공하고 특정 민감한 단어 필터링 알고리즘은 제공하지 않습니다. 제 다른 컬럼 "의 관련 장을 참조하십시오. 데이터 구조 및 알고리즘 다중 패턴 문자열 일치는 자체 구현됩니다.

public interface SensitiveWordFilter {
  boolean doFilter(Content content);
}
public class SexyWordFilter implements SensitiveWordFilter {
  @Override
  public boolean doFilter(Content content) {
    boolean legal = true;
    //...
    return legal;
  }
}
// PoliticalWordFilter、AdsWordFilter类代码结构与SexyWordFilter类似
public class SensitiveWordFilterChain {
  private List<SensitiveWordFilter> filters = new ArrayList<>();
  public void addFilter(SensitiveWordFilter filter) {
    this.filters.add(filter);
  }
  // return true if content doesn't contain sensitive words.
  public boolean filter(Content content) {
    for (SensitiveWordFilter filter : filters) {
      if (!filter.doFilter(content)) {
        return false;
      }
    }
    return true;
  }
}
public class ApplicationDemo {
  public static void main(String[] args) {
    SensitiveWordFilterChain filterChain = new SensitiveWordFilterChain();
    filterChain.addFilter(new AdsWordFilter());
    filterChain.addFilter(new SexyWordFilter());
    filterChain.addFilter(new PoliticalWordFilter());
    boolean legal = filterChain.filter(new Content());
    if (!legal) {
      // 不发表
    } else {
      // 发表
    }
  }
}

위의 구현을 읽은 후 다음과 같이 민감한 단어 필터링 기능도 구현할 수 있고 코드가 더 간단한데 왜 책임 체인 모드를 사용해야 합니까? 오버엔지니어링인가요?

public class SensitiveWordFilter {
  // return true if content doesn't contain sensitive words.
  public boolean filter(Content content) {
    if (!filterSexyWord(content)) {
      return false;
    }
    if (!filterAdsWord(content)) {
      return false;
    }
    if (!filterPoliticalWord(content)) {
      return false;
    }
    return true;
  }
  private boolean filterSexyWord(Content content) {
    //....
  }
  private boolean filterAdsWord(Content content) {
    //...
  }
  private boolean filterPoliticalWord(Content content) {
    //...
  }
}

애플리케이션 디자인 패턴은 주로 코드의 복잡성을 처리하고 개폐 원칙을 충족하며 코드의 확장성을 향상시키는 것이라고 이전에 여러 번 말했습니다. 여기에 책임 사슬 패턴을 적용하는 것도 예외는 아닙니다. 사실 우리가 전략 패턴을 설명할 때 유사한 질문에 대해서도 이야기했습니다. 예를 들어 전략 패턴을 사용하는 이유는 무엇입니까? 당시 제시된 이유는 현재 책임 사슬 모델을 적용한 이유와 거의 동일하므로 당시 설명과 함께 살펴보시면 됩니다.

먼저 책임 사슬 패턴이 코드 복잡성을 어떻게 처리하는지 살펴보겠습니다.

큰 코드 논리 블록을 함수로 분할하고 큰 클래스를 작은 클래스로 분할하는 것은 코드 복잡성을 처리하는 일반적인 방법입니다. 책임 체인 모델을 적용하여 각 민감한 단어 필터 기능을 계속 분할하고 독립 클래스로 설계하여 SensitiveWordFilter 클래스를 더욱 단순화하여 SensitiveWordFilter 클래스의 코드가 너무 많거나 복잡하지 않도록 합니다.

둘째, 책임 체인 모델이 어떻게 코드가 개폐 원칙을 충족하고 코드의 확장성을 향상시킬 수 있는지 살펴보겠습니다.

예를 들어 새로운 필터링 알고리즘을 확장하려면 특수 기호도 필터링해야 합니다.무책임 체인 모드의 코드 구현에 따라 SensitiveWordFilter의 코드를 수정해야 하며 개방 및 개방 원칙을 위반합니다. 폐쇄. 그러나 이러한 수정은 상대적으로 집중되어 있으며 수용 가능합니다. 책임 사슬 모드의 구현은 더 우아합니다. 새로운 Filter 클래스를 추가하고 addFilter() 함수를 통해 FilterChain에 추가하기만 하면 됩니다. 다른 코드는 전혀 수정할 필요가 없습니다.
다만 이를 구현하기 위해 Chain of Responsibility 패턴을 사용하더라도 새로운 필터링 알고리즘을 추가할 때 여전히 클라이언트 코드(ApplicationDemo)를 수정해야 하므로 개폐 원칙을 완전히 준수하지 못한다고 할 수 있습니다. .

사실 우리가 그것을 정제하면 위의 코드를 프레임워크 코드와 클라이언트 코드의 두 가지 범주로 나눌 수 있습니다. 그 중 ApplicationDemo는 클라이언트 코드, 즉 프레임워크를 사용하는 코드에 속합니다. ApplicationDemo 이외의 코드는 민감한 단어 필터링 프레임워크 코드에 속합니다.

민감한 단어 필터링 프레임워크가 우리가 개발하고 유지하는 것이 아니라 우리가 도입한 타사 프레임워크라고 가정하면 새로운 필터링 알고리즘을 확장하고 싶고 프레임워크의 소스 코드를 직접 수정하는 것은 불가능합니다. 이때 책임 사슬 모델을 사용하여 처음에 언급한 것을 달성할 수 있으며, 프레임워크의 소스 코드를 수정하지 않고 책임 사슬 모델에서 제공하는 확장점을 기반으로 새로운 기능을 확장할 수 있습니다. 즉, 프레임워크의 코드 범위 내에서 개방-폐쇄 원칙을 구현합니다.

또한 책임 체인 모드를 사용하면 책임 체인이 없는 구현 방법에 비해 또 다른 이점이 있습니다. 즉, 필터링 알고리즘의 구성이 더 유연하고 특정 필터링 알고리즘만 사용하도록 선택할 수 있습니다.

주요 검토

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

책임 사슬 패턴에서는 여러 프로세서가 동일한 요청을 순차적으로 처리합니다. 요청은 프로세서 A에서 먼저 처리된 다음 프로세서 B로 전달되고 프로세서 B에서 처리된 후 프로세서 C로 전달되는 식으로 체인을 형성합니다. 체인의 각 프로세서는 자체 처리 책임을 가정하므로 책임 체인 모드라고 합니다.

GoF의 정의에서 프로세서가 요청을 처리할 수 있게 되면 요청을 후속 프로세서로 계속 전달하지 않습니다. 물론 실제 개발에서는 이 모드의 변형도 있습니다. 즉, 요청이 중간에 종료되지 않고 모든 프로세서에서 처리됩니다.

책임 사슬 패턴에는 두 가지 일반적인 구현이 있습니다. 하나는 연결된 목록을 사용하여 프로세서를 저장하는 것이고 다른 하나는 배열을 사용하여 프로세서를 저장하는 것입니다. 후자의 구현이 더 간단합니다.

수업 토론

오늘 우리는 책임 사슬 모델을 사용하는 것에 대해 이야기했습니다. 우리는 프레임워크 코드가 개방 및 폐쇄 원칙을 충족하도록 만들 수 있습니다. 새 핸들러를 추가하려면 클라이언트 코드만 수정하면 됩니다. 코드를 수정하지 않고 클라이언트 코드도 개폐 원칙을 충족시키려면 어떻게 해야 합니까?

Supongo que te gusta

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