템플릿 메소드와 스트레티지 패턴

Template Method와 Strategy Pattern은 비슷한 문제를 해결하고, 보통 호환되어 쓰인다. 그러나 , 탬플릿 메소드는 문제를 해결하기 위해 상속을 사용하는 반면, 스트레티지는 위임을 사용한다. 탬플릿 메소드와 스트레티지는 둘 다 구체적인 내용으로부터 일반적인 알고리즘을 분리하는 문제를 해결하는 패턴이다. 의존 관계 역전을 따르기 위해서는 이 일반적인 알고리즘이 구체적 구현에 의존하지 않도록 해야 하며, 일반적인 알고리즘과 구체적인 구현이 추상화에 의존하게 해야한다.

탬플릿 메소드 패턴

기본적인 메인 루프 구조

initialize();

while(!done())
{
    Idle();
}

cleanUp();

먼저, 어플리케이션을 초기화 한다. 그리고 메인 루프에 들어 간다. 메인 루프에서 프로그램이 요구하는 어떤 일을 한다. 예를 들어 GUI 이벤트를 처리하거나, 데이터 베이스 레코드를 처리할 수도 있을 것이다. 마지막으로, 모든 일이 끝나면 메인 루프를 나가면서 정리를 한다.

이 구조는 아주 평범하므로 Application이라는 이름의 클래스에 집어 넣는다. 그러고 나면 작성하려는 모든 새 프로그램에서 이 클래스를 재사용할 수 있다.

화씨 온도를 섭씨 온도로 변환하는 프로그램(초기 버전)

def main():
    done = False

    while not done:
        print("Input Fahrenheit:")
        fahrenheit = number_from_input_stream()
        if fahrenheit is None:
            done = True
        else:
            celsius = 5.0 / 9.0 * (fahrenheit - 32)
            print(f"{fahrenheit:.2f}℉ = {celsius:.2f}℃")

    print("Fahrenheit to Celsius Finished!")
		
def is_number(s):
    try:
        float(s)
        return True
    except ValueError:
        return False

def number_from_input_stream():
    parsed_number = [float(s) for s in sys.stdin.readline().split() if is_number(s)]
    if len(parsed_number) == 1:
        return parsed_number[0]
    else:
        return None
	

Application 추상 클래스

class Application(metaclass=ABCMeta):
    def __init__(self):
        self.done = False

    @abstractmethod
    def init(self):
        raise NotImplementedError  # pragma: no cover

    @abstractmethod
    def idle(self):
        raise NotImplementedError

    @abstractmethod
    def cleanup(self):
        raise NotImplementedError

    def set_done(self):
        self.done = True

    def run(self):
        self.init()
        while not self.done:
            self.idle()
        self.cleanup()

이 클래스는 일반적인 메인 루프 애플리케이션을 묘사하고 있다. 구현된 run 함수에서 메인 루프를 볼 수 있다. 모든 구체적인 작업이 추상 메소드인 init, idle, cleanup에 맡겨진 것 또한 볼 수 있다. Application 을 상속하여 구현한 결과는 아래와 같다.

class TemperatureConverter(Application):
    def __init__(self):
        super().__init__()

    def init(self):
        self.done = False

    def idle(self):
        print("Input Fahrenheit:")
        fahrenheit = number_from_input_stream()
        if fahrenheit is None:
            self.set_done()
        else:
            celsius = 5.0 / 9.0 * (fahrenheit - 32)
            print(f"{fahrenheit:.2f}℉ = {celsius:.2f}℃")

    def cleanup(self):
        print("Fahrenheit to Celsius Finished!")


if __name__ == '__main__':
    converter = TemperatureConverter()
    converter.run()

이렇게 특정 애플리케이션에 탬플릿 메소드를 사용하는 것은 바람직하지 않다. 프로그램이 복잡해지고 내용만 더 늘어날 뿐이다.

템플릿 메소드 패턴은 객체 지향 프로그래밍에서 고전적인 재사용 형태 중의 하나를 보여준다. 일반적인 알고리즘은 기반 클래스에 있고, 다른 구체적인 내용에서 상속된다. 상속은 아주 강한 관계여서, 파생 클래스는 필연적으로 기반 클래스에 묶이게 된다.

스트래티지 패턴

스트래티지 패턴은 일반적인 알고리즘과 구체적인 구현 사이의 의존성 반전 문제를 완전히 다른 방식으로 풀어낸다. 앞의 탬플릿 패턴과 같이 일반적인 알고리즘을 추상 기반 클래스에 넣는 대신, ApplicationRunner라는 이름의 구체 클래스에 넣는다. Application이란 이름의 인터페이스 안에서 일반적 알고리즘이 호출해야할 추상 메소드를 정의한다. 이 인터페이스에서 ConverterStrategy를 파생시켜 ApplicationRunner에게 넘겨 준다. 그러면 ApplicationRunner는 이 인터페이스에 위임한다.

스트래티지

ApplicationRunner Class

class ApplicationRunner:
    def __init__(self, application):
        self.application = application

    def run(self):
        self.application.init()
        while not self.application.done:
            self.application.idle()
        self.application.cleanup()

Application Interface

class Application(metaclass=ABCMeta):
    @abstractmethod
    def init(self):
        raise NotImplementedError

    @abstractmethod
    def idle(self):
        raise NotImplementedError

    @abstractmethod
    def cleanup(self):
        raise NotImplementedError

    @abstractmethod
    def set_done(self):
        raise NotImplementedError

ConverterStrategy Class

class ConverterStrategy(Application):
    def __init__(self):
        self.done = False

    def init(self):
        self.done = False

    def idle(self):
        print("Input Fahrenheit:")
        fahrenheit = number_from_input_stream()
        if fahrenheit is None:
            self.set_done()
        else:
            celsius = 5.0 / 9.0 * (fahrenheit - 32)
            print(f"{fahrenheit:.2f}℉ = {celsius:.2f}℃")

    def cleanup(self):
        print("Fahrenheit to Celsius Finished!")

    def set_done(self):
        self.done = True

main 함수

runner = ApplicationRunner(ConverterStrategy())
runner.run()

이 구조는 이익과 비용 면에서 템플릿 메소드 구조에 비해 더 낫다는 사실이 명백하다. 스트래티지에는 탬플릿 메소드보다 더 많은 전체 클래스 개수와 더 많은 간접 지정이 있다. ApplicationRunner 내부의 위임 포인트는 실행 시간과 데이터 공간 면에서 상속의 경우보다 좀 더 많은 비용을 초래한다. 반면, 서로 다른 많은 애플리케이션을 실행한다면, ApplicationRunner 인스턴트를 재사용하여 Application의 다른 많은 구현에 이것을 넘겨 줄 수 있을 테고, 그럼으로써 일반적인 알고리즘과 그성이 제어하는 구체적인 부분 사이의 결합 정도를 감소 시킬 수 있다.

템플릿 메소드 패턴이 일반적인 알고리즘으로 많은 구체적인 구현을 조작할 수 있게 해주는 반면, DIP를 준수하는 스트래티지 패턴은 각각의 구체적인 구현이 다른 많은 일반적인 알고리즘에 의해 조작될 수 있게 해준다.

Active Object Pattern

ActiveObject 객체는 Command 객체의 리스트를 유지한다. 사용자는 ActiveObject에 새로운 명령을 추가할 수도 있고, run()을 호출할 수 도 있다. run()은 단순히 각 명령을 실행하고 제거하면서 리스트를 훑어 나가는 함수다.

  • ActiveObject
class ActiveObject:
    def __init__(self):
        self.command_queue = []

    def add_command(self, command):
        self.command_queue.append(command)

    def run(self):
        while len(self.command_queue):
            command = self.command_queue.pop(0)
            command.execute()						
  • Command Interface
class Command(metaclass=ABCMeta):
    @abstractmethod
    def execute(self):
        raise NotImplementedError				

이것은 그다지 특별해 보이지 않는다. 하지만 리스트에 있는 Command 객체 중 하나가 자기를 복제하여 그 복제본을 리스트에 다시 넣는다면 어떤 일이 일어날까? 이 리스트는 절대 비지 않게되고 run()함수는 절대로 return하지 않을 것이다.

  • SleepCommand
class SleepCommand(Command):
    def __init__(self, milliseconds, active_object, wakeup_command):
        self.start_time = 0
        self.sleep_time = milliseconds
        self.active_object = active_object
        self.wakeup_command = wakeup_command
        self.started = False

    def execute(self):
        current_time = current_time_milliseconds()
        if not self.started:
            self.started = True
            self.start_time = current_time
            self.active_object.add_command(self)
        elif (current_time - self.start_time) < self.sleep_time:
            self.active_object.add_command(self)
        else:
            self.active_object.add_command(self.wakeup_command)
  • WakeupCommand
class WakeupCommand(Command):
    def execute(self):
        print("Command Executed!")
  • TestSleepCommand

SleepCommand의 생성자는 3개의 인자를 갖는다. 첫 번째는 ms 단위의 지연 시간이다. 두 번째는 이 명령이 실행될 장소인 ActiveObject이다. 마지막 인자는 wakeup이라는 또 다른 명령 객체이다. 이 테스트가 기대하는 동작은 SleepCommand가 일정 시간만큼 기다렸다가 wakeup 명령을 실행하는 것이다.

class TestSleepCommand(TestCase):
    def setUp(self) -> None:
        self.active_object = ActiveObject()
        self.sleep_command = SleepCommand(1000, self.active_object, WakeupCommand())
        self.active_object.add_command(self.sleep_command)

    def test_execute(self):
        expected = "Command Executed!\n"
        buf = io.StringIO()

        start_time = current_time_milliseconds()
        with contextlib.redirect_stdout(buf):
            self.active_object.run()
        sleep_time = current_time_milliseconds() - start_time

        self.assertTrue(sleep_time >= 1000)
        self.assertTrue(sleep_time < 1100)
        self.assertEqual(expected, buf.getvalue())

이 기법의 다양한 변형을 사용하여 멀티스레드 시스템을 구축하는 것은, 지금까지도 그래왔고 앞으로도 계속될 아주 일반적인 실천 방법이다. 이런 종류의 스레드는 각 Command 인스턴트가 다음 Command 인스턴트 실행이 가능해지기 전에 완료되기 때문에, RTC(run-to-completion) 테스크라는 이름으로 알려져 있다. Command인스턴트가 모두 완료될 때까지 실행된다는 사실은 RTC 스레드에 모두 같은 런타임 스택을 공유한다는 흥미로운 이점을 부여한다. 전통적인 멀티스레드 시스템에서의 스레드와 달리 여기서는 각 RTC 스레드에 대해 별도의 런타임 스택을 정의하거나 할당할 필요가 없다. 이것은 많은 스레드가 실행되고 메모리가 제한된 시스템에서 강력한 이점이 될 수 있다.

Command Pattern

단순한 커맨드 적용

복사기 소프트웨어를 위한 단순한 커맨드 패턴

복사기 소프트웨어를 위한 단순한 커맨드 패턴

이 구조를 제대로 만들면 Command 객체를 시스템에 차례로 넘겨 줄 수 있고, 그것이 정확히 어떤 종류의 Command를 표현하는지 알 필요 없이 do()를 실행할 수 있다. 이 것은 흥미로운 단순화로 이어진다. 이 시스템은 이벤트 주도적이다. 시스템에서 일어나는 특정 이벤트에 따라 릴레이가 열리거나 닫히고, 모터는 움직이거나 멈추고, 클러치는 접속되거나 차단된다. 이 이벤트 대부분은 센서에 의해 탐지된다. 예를 들어, 종이 한 장이 용지 통로의 일정한 위치에 이르렀음을 광센서가 감지하면 특정 클러치를 접속시켜야 한다. 이를 위해 그냥 해당하는 ClutchOnCommand를 그 광센서를 제어하는 객체와 묶어 구현하면 된다.

센서 주도 Command

센서는 자신이 하는 일을 모른다. 그저 어떤 이벤트를 탐자할 때마다 묶여 있는 Command에서 do()를 호출할 뿐이다. Sensor가 개별적인 클러치나 릴레이에 대해 알 필요가 없다는 뜻이다. 따라서 Sensor의 함수는 단순해질 수 있다.

Command Pattern은 명령의 개념을 캡슐화함으로써 연결된 장치에서 시스템의 논리적인 상호 연결을 분리해 낼 수 있게 한다.

트랜젝션

커맨드 패턴의 또 다른 일반적인 사용법이자 급여 관리 문제에서 유용하게 쓸 수 있는 방법은 트랜젝션의 생성과 실행에 관련되어 있다. 예를 들어 직원들의 데이터 베이스를 관리하는 시스템을 작성하고 있다고 생각해 보자. 사용자들은 이 데이터 베이스를 이용하여 새 직원을 추가하고 기존 직원을 삭제하고 직원의 속성을 변경하는 등의 작업을 할 수 있다.

직원 데이터 베이스

Command 객체는 검증되지 않은 데이터를 위한 저장소 역할을 하고, 검증 메소드를 구현하여, 마지막으로 트랜젝션을 실행하는 메소드를 구현한다. AddEmployeeTransaction은 Employee가 포함하고 있는 것과 똑같은 데이터 필드를 갖고 있다. 또한 PayClassification객체에 대한 포인터도 저장한다. 이 필드와 객체는 시스템에 새로운 직원을 추가하려는 지시가 있을 때, 사용자가 지정한 날짜에 의해 생성된다.

트랜젝션

물리적 분리

사용자에게서 데이터를 받는 코드와 그 데이터를 검증하고 그것으로 작업을 하는 코드, 그리고 업무 객체 그 자체를 극적으로 분리한다.

시간적 분리

검증과 실행 코드를 분리할 수 있다. 이 트랜젝션 객체는 우선 목록에 저장되어 검증되고나서 나중에 실행될 수 있다.

Weather Station 설계 (옵저버 패턴)

기상 스테이션 설계

클래스 다이어 그램

weather station

기상 스테이션 구현

public interface Subject {
 public void registerObserver(Observer o);
 public void removeObserver(Observer o);
 public void notifyObserver(Observer o);
}

public interface Observer {
 public void update(float temp, float humidity, float pressure);
}

public interface DisplayElement {
 public void display();
}

상태를 갱신하기 위해 측정치를 직접 전달하는 것이 좋은 방법일까? 갱신된 상태를 옵저버에 전달하는 다른 좋은 방법을 고려해 보자.

WeatherData에서 Subject Interface 구현하기

public class WeatherData implements Subject {
 private ArrayList observers;
 private float temperature;
 private float humidity;
 private float pressure;
 
 public WeatherData() {
  observers = new ArrayList();
 }
	
public void registerObserver(Observer o) {
 observers.add(o);
}
		
public void removeObserver(Observer o) {
 int i = observers.indexOf(o);
 if(i >= 0) {
  observers.remove(i);
 }
}
		
public void notifyObservers() {
 for(int i = 0; i < observers.size(); i++) {
  Observer observer = (Observer)observers.get(i);
	observer.update(temperature, humidity, pressure);
 }
}

 public void measurementChanged() {
  notifyObservers();
 }
		
public void setMeasurements(float temperature, float humidity, float pressure) {
 this.temperature = temperature;
 this.humidity = humidity;
 this.pressure = pressure;
 }
	// 기타 WeatherData method
}

Display 항목 구현하기

public class CurrentConditionDisplay implements Observer, DisplayElement {
	private float temperature;
	private float humidity;
	private float pressure;
		
	public CurrentCoditionDisplay (Subject weatherData) {
		this.weatherData = weatherData;
		weatherData.registerObserver(this);
	}
		
	public void update(float temperature, float humidity, float pressure) {
		this.temperature = temperature;
		this.humidity = humidity;
		display();
	}
		
	public void display() {
		System.out.println("Current coditions: " + temperature + "F degrees and " + humidity + "% humidity");
	}
}

기상 스테이션 실행

public class WeathreStation {
 public static void main(String[] args) {
  WeatherData weatherData = new WeatherData();
	CurrentConditionDisplay currentDisplay = new CurrentConditionDisplay(weatherData);				
	StatisticsDisplay statisticsDisplay = new StatisticsDisply(weatherData);
	ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);
	
	weatherData.setMeasurements(80, 65, 30.4f);
	weatherData.setMeasurements(82, 70, 29.2f);
	weatherData.setMeasurements(78, 90, 29.2f);
 }
}

Observer Pattern의 정의

정의

옵저버 패턴에서는 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의합니다.

클래스 다이어그램

observer [그림1 ]

옵저버 패턴에서 상태를 저장하고 지배하는 것은 주제 객체 입니다.
따라서 상태가 들어 있는 객체는 하나만 있을 수 있습니다.
옵저버는 여러 개가 있을 수 있으며, 주제 객체에서 상태가 바뀌었다는 것을 알려 주기를 기다리는, 주제에 의존적인 성질을 가지게 된다.
그러므로 하나의 주제와 여러 개의 옵저버가 연과된, 일대다 관계가 성립한다.
데이터 주인은 주제입니다.
옵저버는 데이터가 변경되었을 때 주제에서 갱신해 주기를 기다리는 입장이기 때문에 의존성을 가진다고 할 수 있습니다.
이런 방법을 사용하면 여러 객체에서 동일한 데이터를 제어하도록 하는 것에 비해 더 깔끔한 객체지향 디자인을 만들 수 있습니다.

느슨한 결합의 위력

  • 두 객체가 느슨하게 결합되어 있다는 것은, 그 둘이 상호작용을 하긴 하지만 서로에 대해 잘 모른다는 것을 의미합니다.
  • 옵저버 패턴에서는 주제와 옵저버가 느슨하게 결합되어 있는 객체 디자인을 제공합니다.

주제가 옵저버에 대해서 아는 것은 옵저버가 특정 인터페이스를 구현한다는 것 뿐입니다.

옵저버의 Concrete Class가 무엇인지, 옵저버가 무엇을 하는지 등에 대해서는 알 필요가 없습니다.

옵저버는 언제든지 새로 추가할 수 있습니다.

주제는 옵저버 인터페이스를 구현하는 객체의 목록에만 의존하기 때문에 언제든지 새로운 옵저버를 추가할 수 있다. 실행 중에 한 옵저버를 다른 옵저버로 바뀌도 되고, 그렇게 해도 주제 객체는 계속해서 데이터를 보낼 수 있다. 마찬가지로 옵저버를 아무 때나 제거해도 된다.

새로운 형식의 옵저버를 추가하려고 할 때도 주제를 전혀 변경할 필요가 없다.

옵저버가 되어야 하는 새로운 구상 클래스가 생겼다고 가정해 보자. 이 때도 새로운 클래스 형식을 받아 들일 수 있도록 주제를 바꿔야 할 필요는 없다. 새로운 클래스에서 Observer 인터페이스를 구현하고 옵저버로 등록하기만 하면 된다. 주제 객체는 전혀 신경도 쓰지 않는다.

주제와 옵저버는 서로 독립적으로 재 사용할 수 있다.

주제나 옵저버를 다른 용도로 활용할 일이 있다고 해도 손쉽게 재 사용할 수 있다. 그 둘이 서로 단단하게 결합되어 있지 않기 때문이다.

주제나 옵저버가 바뀌더라도 서로한테 영향을 미치지 않는다.

둘이 서로 느슨하게 결합되어 있기 때문에 주제 혹은 옵저버 인터페이스를 구현한다는 조건만 만족 된다면 어떻게 바꿔도 문제가 되지 않는다.

디자인 원칙

서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.