TDD와 디자인 패턴이 서로 어떻게 영향을 주는지 크게 두가지로 구분할 수 있다.
첫 번째는 TDD 사이클의 리팩토링 시점에 문제가 도출되어 이를 해결하는 과정에서 나오는, 자연스러운 디자인 패턴의 적용.
두 번째는 디자인 패턴을 적용한 설계를 TDD를 통해 구현하면서 쓸데없는 디자인을 빠르게 제거하거나 수정하는 것이다.
비슷한 일을 하는 애들인데.. 일괄적으로 만들어 생성 할 수 없을 까?
프로그래밍할 때 가장 많이 고민하는 것 중 하나가 인스턴스 생성이다. 요구사항이 늘어날수록 비슷한 일을 하는 클래스가 점점 늘어나는데, 이들을 따로 만들어 처리하는 연쇄적인 상황이 발생하기 때문이다.
다음에 제시된 요구 사항을 살펴보자
고객은 SMS로 사용자들에게 메시지 발송을 원했다. 상용화 후 몇달이 지나서 MMS도 추가하길 원했다 ..
요구 사항 반영한 코드
class SMSmessage:
def dispatchSMS(self):
print("dispatchSMS SMS")
class PUSHmessage:
def dispatchPUSH(self):
print("dispatchPUSH PUSH")
class MessageSender:
def __init__(self, type):
self.type = type
# 각 메시지 타입별로 분기
def send(self, message):
if (self.type == "sms"):
smsMessage = SMSmessage()
smsMessage.dispatchSMS()
else:
pushMessage = PUSHmessage()
pushMessage.dispatchPUSH()
messageSender = MessageSender("sms")
messageSender.send("HIHIHI")
팩토리 메서드 패턴을 적용한 예와 단위 테스트
import pytest
from main import MessageSender, MessageType, MessageFactory
def test_send_message():
# Given
messageSender: MessageSender = MessageSender()
messageType: MessageType = MessageType.SMS
text: str = "TESTEST"
# When
result = messageSender.send(messageType, text)
# Then
assert result is True
def test_MessageFactory():
# Given
messageFactory = MessageFactory()
messageType: MessageType = MessageType.SMS
# When
message = MessageFactory.generateMessage(messageType)
# Then
assert message is not None
팩토리 메서드를 적용한 코드
class SMSmessage:
def dispatchSMS(self):
print("dispatchSMS SMS")
class PUSHmessage:
def dispatchPUSH(self):
print("dispatchPUSH PUSH")
from enum import Enum
class MessageType(Enum):
SMS = 1,
PUSH = 2
class MessageFactory:
@staticmethod
def generateMessage(type: MessageType):
if type == MessageType.SMS:
return SMSmessage()
else:
return PUSHmessage()
class MessageSender:
# 각 메시지 타입별로 분기
def send(self, type: MessageType, message: str):
message = MessageFactory.generateMessage(type)
return self.dispatch(message)
def dispatch(self, message):
#TODO 메시지 발송 로직
return True
messageSender = MessageSender()
messageSender.send(MessageType.SMS, "HIHIHI")
이렇게 팩토리 메서드를 이용하여 메시지 인스턴스를 생성하는 리팩토링을 진행해보았다.
큰 흐름은 똑같은거 같은데 이걸 각각 다 구현해야 할까?
발송할 때 만들어지는 메시지는 각 특성에 맞게 Header와 tail 기반으로 생성된다고 하자. 비슷한 흐름이긴 하지만 두 요소에는 서로 다른 부분이 있을 것이다. 그렇다고 각각을 모두 분기해서 로직을 정의해야 할까? 이럴 때 이용하는 방법 중 하나가 '템플릿 메서드' 패턴이다.
from abc import ABCMeta, abstractmethod
class Message(metaclass=ABCMeta):
def makeDocument(self):
makeHeader = self.makeHeader()
makeTail = self.makeTail()
return "make " + makeHeader + " " + makeTail
@abstractmethod
def makeHeader(self):
pass
@abstractmethod
def makeTail(self):
pass
class SMSmessage(Message):
def dispatchSMS(self):
print("dispatchSMS SMS")
def makeHeader(self):
return "sms header"
def makeTail(self):
return "sms tail"
class PUSHmessage(Message):
def dispatchPUSH(self):
print("dispatchPUSH PUSH")
def makeHeader(self):
return "push header"
def makeTail(self):
return "push tail"
from enum import Enum
class MessageType(Enum):
SMS = 1,
PUSH = 2
class MessageFactory:
@staticmethod
def generateMessage(type: MessageType):
if type == MessageType.SMS:
return SMSmessage()
else:
return PUSHmessage()
class MessageSender:
# 각 메시지 타입별로 분기
def send(self, type: MessageType):
messageInstance = MessageFactory.generateMessage(type)
return self.dispatch(messageInstance)
def dispatch(self, message: Message):
#TODO 메시지 발송 로직
ms = message.makeDocument()
print(ms)
return ms
messageSender = MessageSender()
messageSender.send(MessageType.SMS)
템플릿 메서드 적용한 단위 테스트코드
import pytest
from main import MessageSender, MessageType, MessageFactory
def test_send_message():
# Given
messageSender: MessageSender = MessageSender()
messageType: MessageType = MessageType.SMS
# When
result = messageSender.send(messageType)
# Then
assert result == "make sms header sms tail"
def test_MessageFactory():
# Given
messageFactory = MessageFactory()
messageType: MessageType = MessageType.SMS
# When
message = MessageFactory.generateMessage(messageType)
# Then
assert message is not None
템플릿 메서드 패턴을 이용하면 공통된 흐름을 유지하면서 각 클래스에 필요한 로직 정의에 집중할수 있는 장점이 있다. 하지만 결합도가 크고 쓸모없는 기능이 부여된다는 단점이 있어 심사숙고를 해야한다.
알고리즘 부분만 컴포넌트화 해서 필요할 때마다 끼워넣어 사용할 수 있지 않을까??
응답데이터 파싱
from enum import Enum
class ResponseDataType(Enum):
JSON = 1,
XML = 2,
class ResponseWorker:
def __init__(
self,
responseData: str,
responseDataType:
ResponseDataType):
self.responseData = responseData
self.responseDataType = responseDataType
def parse(self) -> object :
if self.responseDataType == ResponseDataType.JSON:
return self.doParsingAsJson(self.responseData)
if self.responseDataType == ResponseDataType.XML:
return self.doParsingAsXML(self.responseData)
def doParsingAsJson(self, data: str) -> object:
print("doParsingAsJson");
pass
def doParsingAsXML(self, data: str) -> object:
print("doParsingAsXML");
pass
응답받은 데이터와 파싱할 문서 타입을 받아, 이를 생성자가 입력한 타입을 비교하여 각 타입으로 파싱한다. 이런게 하면 문서타입이 추가할 때마다 비대해질것이다. 하지만 연동 시점에 알고리즘이 구현되어 있는 인스턴스를 제공한다면, 기존 코드를 건드리지도 않고 여러 알고리즘을 전략적으로 사용 할 수 있다.
전략 패턴을 적용해 리팩토링한 응답 데이터 파싱
from abc import ABCMeta, abstractmethod
class Parser(metaclass=ABCMeta):
@abstractmethod
def parsing(self, data: str) -> object:
pass
class JSONParser(Parser):
def parsing(self, data: str) -> str:
return "JSONParser"
class XMLParser(Parser):
def parsing(self, data: str) -> str:
return "XMLParser"
class ResponseWorker:
def responseParse(self, data: str) -> None :
self.responseData = data
def parse(self, parser: Parser) -> str:
return parser.parsing(self.responseData)
테스트코드
def test_response_worker(capsys):
# Given
responseData = "XML data"
responseParser: ResponseWorker = ResponseWorker()
responseParser.responseParse(responseData)
# When
res = responseParser.parse(XMLParser());
# Then
assert res == "XMLParser"
# assert captured == "hello\n"
파싱 로직은 Parser 인터페이스를 구현한 각 클래스에서 담당하게 된다.
어떻게 하면 상태에 따른 행동들을 깔끔하게 처리할 수 있을 까?
현업에서 클래스를 구현하다 보면, 처음ㅇ에는 한 가지 일을 했지만 점점 더 많은 행동을 하게 되는 것을 경험한다. 그리고 책임져야 할 케이스가 점차 많아지면 결국 위와 같이 고민을 하게 되는데, 흔히 인스턴스를 생성할 때 상태를 나타내는 값을 전역변수에 넘겨주고 내부적으로 상태에 따른 조건문으로 구현한다. 아마 분기가 필요한 각 행동에서 비슷한 모양의 설정 값을 파단하는 조건문이 들어갈 것이다.
from enum import Enum
class ConnectionType(Enum):
TYPE_3G = 1,
TYPE_WIFI = 2,
class MessageSender:
def getConnectionType(self) -> ConnectionType :
return self.connectionType
def setConnectionType(self, ct: ConnectionType) -> None :
self.connectionType = ct
def sendText(self) -> str :
if self.connectionType == ConnectionType.TYPE_3G:
# 3g 상태일때 메시지 보내는 로직 (성공)
return 'text type_3g'
elif self.connectionType == ConnectionType.TYPE_WIFI:
# 와이파이 상태일 때 메시지 보내는 로직 (성공)
return 'text type_wifi'
def sendPhoto(self) -> str :
if self.connectionType == ConnectionType.TYPE_3G:
# 3g 상태일때 이미지 보내는 로직 (실패)
return 'fail photo type_3g'
elif self.connectionType == ConnectionType.TYPE_WIFI:
# 와이파이 상태일 때 이미진 보내는 로직 (성공)
return 'photo type_wifi'
위의 예제를 살펴보면 마의 종류에 따라서 전송 가능한 용량에 제한이 있다는 것을 알 수 있다. 그리고 각 메서드를 실행할 때마다 타입을 확인하는 분기문이 있다. 상태 패텬은 각 상태에 따른 행동을 캡슐화하는 것을 기본으로 시작한다.
상태패턴을 적용한 코드
from abc import ABCMeta, abstractmethod
class State(metaclass=ABCMeta):
@abstractmethod
def sendText(self) -> bool:
pass
@abstractmethod
def sendPhoto(self) -> bool:
pass
class State3g(State):
def sendText(self):
return True
def sendPhoto(self):
return False
class StateWifi(State):
def sendText(self):
return True
def sendPhoto(self):
return True
class MessageSender:
def __init__(self, state: State):
self.state = state
def sendText(self) -> bool:
return self.state.sendText()
def sendPhoto(self) -> bool:
return self.state.sendPhoto()
상태 패턴 테스트코드
def test_message_send_text():
# Given
state3g: State3g = State3g()
messageSenderOn3G: MessageSenderWithState = MessageSenderWithState(state3g)
stateWifi: StateWifi = StateWifi()
messageSenderOnWifi: MessageSenderWithState = MessageSenderWithState(stateWifi)
# When
result3GText = messageSenderOn3G.sendText()
result3GPhoto = messageSenderOn3G.sendPhoto()
resultWifi = messageSenderOnWifi.sendPhoto()
# Then
assert result3GText is True
assert resultWifi is True
assert result3GPhoto is False
상태 패턴을 이용하여 3G망일 때의 행동과 Wifi망에 연결되어 있을 때의 행동을 캡슐화 했다. 고려해야 할 케이스가 많아질수록 관리해야 할 클래스 역시 늘어난다는 단점이 있지만, 각 케이스에 대한 행동 정의를 명확히 할수 있는 점에서 이런 단점도 무시할 수 있다.