본문 바로가기

Design Pattern

[Design Pattern] 3. Chain of Responsibility Pattern(책임 연쇄 패턴) C#

728x90

요청이 들어 왔을 때 수행하는 객체에 들어오면 각각의 요청에 대해서 특정한 객체가 담당하는 것이 아닌 객체를 사슬처럼 연결해서 요청을 수행하지 못하는 객체라면 다음 객체에 넘기는 식의 패턴을 말합니다.

 

수행하는 객체가 여럿이고 특정되지 않은 상황에서 사용하는 패턴입니다

 

이 또한 행동 패턴(Behavioral Design Patterns)중 하나입니다.

 

장점

  • 단일 책임 원칙을 만족하며 작업을 호출하는 클래스들과 수행하는 클래스들과 분리할 수 있습니다.
  • 수행하는 클래스들의 내부 구조를 알 필요 없습니다.
  • 개방/폐쇄 원칙을 만족하며 기존 코드를 변경하지 않고 새 핸들러들을 도입할 수 있습니다.

단점

  • 일부 요청들은 처리되지 않을 수 있습니다.

 

C#으로 Console 프로젝트를 하나 생성해서 할 것입니다.

 

이번에도 간단하게 만들겠습니다.

목표는 입력한 수가 홀, 짝, 0을 구별하는 결과를 출력하는 것입니다.

 

프로젝트의 tree구조는 아래와 같습니다.

Chain (Project)

|- Program.cs (Main)

|- IHandler.cs

|- BaseHandler.cs

|- ZeroHandler.cs

|- OddHandler.cs

|- EvenHandler.cs

|- NumberClassifier.cs

 

그럼 구현해 보겠습니다.

 

IHandler.cs

namespace Chain
{
    public interface IHandler
    {
        IHandler SetNext(IHandler nextHandler);
        object Process(object request);
    }

}

수행할 수 없는 작업일 경우 책임을 넘길 클래스를 정할 "SetNext"메소드와 작업을 수행하는 "Process"메소드를 선언합니다.

 

BaseHandler.cs

namespace Chain
{
    public abstract class BaseHandler : IHandler
    {
        private IHandler nextHandler;

        public virtual object Process(object request)
        {
            if (nextHandler != null)
            {
                return nextHandler.Process(request);
            }
            else
            {
                return null;
            }
        }

        public IHandler SetNext(IHandler nextHandler)
        {
            this.nextHandler = nextHandler;
            return nextHandler;
        }
    }
}

IHandler를 구현하고 abstract클래스로 제작하여 책임을 전가할 수 있도록 제작해줍니다.

여기서 "Process"를 보시면 virtual로 되어있는데요. 이를 통해 상속받은 클래스에서 overide해서 작업을 재정의해서 사용하게 만듭니다. 또한 다음 작업을 수행할 클래스가 없는 경우 null을 리턴해주게 제작합니다.

 

ZeroHandler.cs

using System;

namespace Chain
{
    public class ZeroHandler : BaseHandler
    {
        public override object Process(object request)
        {
            if (Int32.Parse(request.ToString()) == 0)
            {
                return "Zero";
            }
            else
            {
                return base.Process(request);
            }
        }
    }
}

Process를 재정의 하여 0인지 판별하고 0이 아니라면 부모 클래스인 BaseHandler에 넘겨 책임을 전가합니다.

 

EvenHandler.cs

using System;

namespace Chain
{
    public class EvenHandler : BaseHandler
    {
        public override object Process(object request)
        {
            if (Int32.Parse(request.ToString()) % 2 == 0)
            {
                return "Even Number";
            }
            else
            {
                return base.Process(request);
            }
        }
    }
}

Process를 재정의 하여 짝수인지 판별하고 아니라면 부모 클래스인 BaseHandler에 넘겨 책임을 전가합니다.

 

OddHandler.cs

using System;

namespace Chain
{
    public class OddHandler : BaseHandler
    {
        public override object Process(object request)
        {
            if ( Int32.Parse(request.ToString())%2 == 1 )
            {
                return "Odd Number";
            }
            else
            {
                return base.Process(request);
            }
        }
    }
}

Process를 재정의 하여 홀수인지 판별하고 아니라면 부모 클래스인 BaseHandler에 넘겨 책임을 전가합니다.

 

NumberClassifier.cs

using System;

namespace Chain
{
    public class NumberClassifier
    {
        private BaseHandler handler;

        public void SetHandler(BaseHandler handler)
        {
            this.handler = handler;
        }

        public object Classify(int number)
        {
            if (handler == null) throw new NullReferenceException();

            return handler.Process(number);
        }
    }
}

구현하지 않고 Program.cs에서 처음에 오는 Handler에 직접 사용해도 되나 그렇게 되면 이름에 의한 혼동이나 프로그램의 부피가 커질수록 관리 하기 힘들어질 우려가 있어 만들었습니다.

 

지정해둔 handler를 유지하고 싶다면 해당 클래스를 static이나 싱글톤 패턴을 적용하여 사용하면 될 것 같습니다.

 

Program.cs

using System;

namespace Chain
{
    class Program
    {
        static void Main(string[] args)
        {
            NumberClassifier classifier = new NumberClassifier();

            ZeroHandler zeroHandler = new ZeroHandler();
            OddHandler oddHandler = new OddHandler();
            EvenHandler evenHandler = new EvenHandler();

            zeroHandler.SetNext(oddHandler).SetNext(evenHandler);

            classifier.SetHandler(zeroHandler);

            for (int i = 0; i <= 10; i++)
            {
                Console.WriteLine($"{i} is {classifier.Classify(i)}");
            }

        }
    }
}

Process를 실행 시킬 NumberClassifier를 먼저 만들어주고 각 Handler를 만듭니다.

여기서는 zeroHandler를 먼저 추가했는데 그 이유가 EvenHandler를 먼저 실행할 경우 짝수로 출력 되기 때문입니다.

 

for문으로 0~10까지 넣고 결과를 출력하게 만들었습니다.

 

실행 결과는 아래의 이미지와 같습니다.

실행 결과

 

알맞지 않은 값이 들어오면 계속 책임 전가하면서 끝에 결국 null을 리턴하게 되겠지만 본 예제 코드에서는 중간에 int로 변경하기 떄문에 int가 아닌 경우 에러가 나 null을 리턴하지 않습니다.

 

패턴을 적용하는 사람의 입맛대로 수정해서 사용하면 되는 부분이며 이를 읽는 분들이 패턴을 알아가시면 좋겠다 생각할 따름입니다.