GoF의 디자인 패턴, 해석자 패턴에 대해 알아본다.
해당 글은, 다음의 코드를 기반으로 이해하는 것이 편리합니다.
Script: BEGIN FRONT LOOP 2 BACK RIGHT END BACK END
쉽게 보기
BEGIN // 스크립트 시작
FRONT // [명령] 앞으로 가기
LOOP 2 // 반복문 시작, 반복 횟수
BACK // [명령] 뒤로 가기
RIGHT // [명령] 오른쪽으로 가기
END // 스크립트 끝
BACK // [명령] 뒤로 가기
END // 스크립트 끝
Context
: 스크립트에서 결과를 가져옴Expression
: 스크립트를 구성하는 각 구문을 처리BeginExpression
: BEGIN
구문을 처리하는 ExpressionCommandListExpression
: 여러개의 CommandExpression
을 가질 수 있음CommandExpression
: 실제 실행할 수 있는 명령에 대한 구문 (LOOP
, BACK
etc)을 나타내는 인터페이스LoopCommandExpression
: 반복문 루프를 처리하는 구문ActionCommandExpression
: FRONT
, BACK
, RIGHT
, LEFT
의 동작을 처리하는 구문import Foundation
internal func main() {
let script = "BEGIN FRONT LOOP 2 BACK RIGHT END BACK LOOP 4 BACK FRONT LEFT END LEFT END"
let context = Context(script: script)
let expression = BeginExpression()
if expression.parse(context: context) {
print(expression.description)
}
}
main()
import Foundation
internal class Context {
private(set) var currentKeyword: String?
internal init(script: String) {
self.tokenizer = Tokenizer(script: script)
self.readNextKeyword()
}
internal func readNextKeyword() {
self.currentKeyword = self.tokenizer.nextToken
}
private let tokenizer: Tokenizer
}
internal class Tokenizer {
internal init(script: String) {
self.tokens = script.components(separatedBy: .whitespaces)
}
internal var nextToken: String? {
guard self.tokens.isEmpty == false else {
return nil
}
return self.tokens.removeFirst()
}
private var tokens: [String]
}
Context
는 실제 문자열을 토큰으로 나눠주는 토크나이저를 갖는다.Context
는 외부에서 쉽게 다음 토큰을 얻기 위한 Wrapping 클래스라 생각하면 되겠다.import Foundation
internal protocol Expression: Loggable {
func parse(context: Context) -> Bool
func run() -> Bool
}
internal protocol KeywordAcceptable {
static func isValid(keyword: String) -> Bool
}
internal protocol Loggable {
var description: String { get }
}
parse
는 Context
를 받아 자신이 처리할 수 있는지 확인하고,run
은 만들어진 다음 expression들에 대해 동작을 실행하고 전파하는 역할을 한다.KeywordAcceptable
은 파서 기능중에 구문과 즉각 대응되는 Expression에 대해 이를 정의해주기 위해 만들었다.import Foundation
internal class BeginExpression: Expression {
internal func parse(context: Context) -> Bool {
// 내 키워드가 맞는지 확인
guard let keyword = context.currentKeyword,
Self.isValid(keyword: keyword) else {
return false
}
// 하위 Expression 생성
self.expression = CommandListExpression()
// 다음으로 넘기기 전 Context 후처리
context.readNextKeyword()
guard let expression else {
return false
}
return expression.parse(context: context);
}
internal func run() -> Bool {
guard let expression else {
return false
}
return expression.run()
}
private var expression: CommandListExpression?
}
extension BeginExpression: KeywordAcceptable {
internal static func isValid(keyword: String) -> Bool {
keyword == "BEGIN"
}
}
extension BeginExpression: Loggable {
internal var description: String {
"BEGIN " + "[" + (self.expression?.description ?? "") + "]"
}
}
import Foundation
internal class CommandListExpression: Expression {
internal func parse(context: Context) -> Bool {
var result: Bool = true
while true {
guard let keyword = context.currentKeyword else {
result = false
break
}
guard keyword != "END" else {
context.readNextKeyword()
break
}
guard let command = self.determineCommand(with: keyword),
command.parse(context: context) else {
result = false
break
}
self.commands.append(command)
}
return result
}
internal func run() -> Bool {
for command in self.commands {
guard command.run() else {
return false
}
}
return true
}
// 원래는 다른 방식으로 하는게 좋은데 그냥 대충 함
private func determineCommand(with keyword: String) -> CommandExpression? {
let command: CommandExpression?
if LoopCommandExpression.isValid(keyword: keyword) {
command = LoopCommandExpression(keyword: keyword)
} else if ActionCommandExpression.isValid(keyword: keyword) {
command = ActionCommandExpression(keyword: keyword)
} else {
command = nil
}
return command
}
private var commands = [CommandExpression]()
}
extension CommandListExpression: Loggable {
internal var description: String {
self.commands.map { $0.description }.joined(separator: " ")
}
}
import Foundation
internal protocol CommandExpression: Expression, KeywordAcceptable {
var keyword: String { get }
}
import Foundation
internal class LoopCommandExpression: CommandExpression {
internal let keyword: String
internal var count: Int?
internal init(keyword: String) {
self.keyword = keyword
}
internal func parse(context: Context) -> Bool {
guard Self.isValid(keyword: self.keyword) else {
return false
}
context.readNextKeyword()
guard let count = context.currentKeyword else {
return false
}
self.count = Int(count)
context.readNextKeyword()
guard context.currentKeyword != nil else {
return false
}
self.expression = CommandListExpression()
guard let expression else {
return false
}
return expression.parse(context: context)
}
internal func run() -> Bool {
guard let count, let expression else {
return false
}
for _ in (0..<count) {
guard expression.run() else {
return false
}
}
return true
}
private var expression: CommandListExpression?
}
extension LoopCommandExpression: KeywordAcceptable {
internal static func isValid(keyword: String) -> Bool {
keyword == "LOOP"
}
}
extension LoopCommandExpression: Loggable {
internal var description: String {
"LOOP(\(self.count ?? 0))" + "{" + (self.expression?.description ?? "") + "}"
}
}
import Foundation
internal class ActionCommandExpression: CommandExpression {
internal let keyword: String
internal init(keyword: String) {
self.keyword = keyword
}
internal func parse(context: Context) -> Bool {
guard Self.isValid(keyword: self.keyword) else {
return false
}
context.readNextKeyword()
guard context.currentKeyword != nil else {
return false
}
return true
}
internal func run() -> Bool {
print("cmd: \(self.keyword)")
return true
}
}
extension ActionCommandExpression: KeywordAcceptable {
internal static func isValid(keyword: String) -> Bool {
["FRONT", "BACK", "LEFT", "RIGHT"].contains(keyword)
}
}
extension ActionCommandExpression: Loggable {
internal var description: String {
self.keyword
}
}
BEGIN [FRONT LOOP(2){BACK RIGHT} BACK LOOP(4){BACK FRONT LEFT} LEFT]
cmd: FRONT
cmd: BACK
cmd: RIGHT
cmd: BACK
cmd: RIGHT
cmd: BACK
cmd: BACK
cmd: FRONT
cmd: LEFT
cmd: BACK
cmd: FRONT
cmd: LEFT
cmd: BACK
cmd: FRONT
cmd: LEFT
cmd: BACK
cmd: FRONT
cmd: LEFT
cmd: LEFT