함수형언어의 디자인패턴
함수형 프로그래밍에서는 전통적인 디자인 패턴들이 다음 세가지로 나타난다고 함.
- 패턴이 언어에 흡수 (일급함수로 패턴처리)
- 패턴 해법이 함수형 패러다임에도 존재하지만, 구체적인 구현 방식은 다름 ( 팩토링 = 커링 )
- 패턴 해법이 패러다임에 없는 기능으로 구현 ( 매타 프로그래밍을 사용한 해법 )
함수 수준의 재사용
디자인 패턴으로 해결할 수 있는 문제들은 아주 특정된 문제에만 적용되기때문에 사용범위는 좁을 수밖에 없음.
저자가 생각하는 디자인 패턴의 존재 목적은 자바 가지고 있는 언어의 결함을 메꾸기 위해서 필요한 것이며
뼈대뿐인 클래스에 래핑하지 않고서는 행동을 전달할 수 없는 한계가 있기 때문임.
함수형 언어는 객체지향 언어들보다 더 큰 단위로 재사용함.
함수형 프로그래머들은 위의 그림처럼 큰단위의 재사용 매커니즘을 추출하려고 함.
템플릿(Template) 메서드 패턴
- 탬플릿은 하나의 틀을 의미, 하나의 틀에서 만들어진 형태는 같은 것으로 간주.
- 일반적으로 상위클래스에서 추상메서드를 통해 기능의 골격을 제공, 하위 클래스는 세부처리를 구체화함.
- 추상 메서드의 정의는 일종의 가이드 문서역할을 함. ( 팅빈 메서드를 구현할 수 있지만 )
일급함수를 사용하면 불필요한 클래스를 없앨 수 있기 때문에 템플릿 메서드 디자인 패턴을 구현하기 쉬워짐.
그루비로 작성한 전형적인 템플릿 메서드
abstract class Customer {
def plan
def Customer() {
plan = []
}
def abstract checkCredit()
def abstract checkInventory()
def abstract ship()
def process() {
checkCredit()
checkInventory()
ship()
}
}
일급함수를 사용한 템플릿 메서드
class Customer {
def plan, checkCredit, checkInventtory, ship
def CustomerBlocks() {
plan = []
}
// 특별보호 접근자(?.) : 객체가 널인지 확인한 후에 메서드를 실행
def process() {
checkCredit?.call()
checkInventory?.call()
ship?.call()
}
}
// 일급함수를 이용하면 아래와 같이 심플하게 구현가능
class UsCustomerBlocks extends Customer {
def UsCustomerBlocks() {
checkCredit = { plan.add "checking US customer credit" }
checkInventory = { plan.add "checking US warehouses" }
ship = { plan.add "Shipping to US address" }
}
}
전략(Strategy) 패턴
- strategy는 전략, 전술로 알고리즘을 뜻함.
- 알고리즘을 사용하는 곳과 알고리즘을 제공하는 곳을 분리시킨 구조로 알고리즘을 동적으로 교체 수 있는 장점을 지님
- 일급함수를 사용하면 사용이 간편하게 구현이 가능
그루비로 작성한 전통적인 전략 패턴
interface Calc {
def product(n, m)
}
class CalcMult implements Calc {
def product(n, m) { n * m }
}
class CalcAdds implements Calc {
def product(n, m) {
def result = 0
n.times { result += m }
result
}
}
class StrategyTest {
def listOfStrategies = [new CalcMult(), new CalcAdds()]
@Test
public void product_verifier() {
listOfStrategies.each { s -> assertEquals(10, s.product(5, 2))}
}
}
개선된 함수형프로그래밍 전략패턴
@Test
public void product_verifier() {
def listOfExp = [
{n, m -> n * m },
{n, m ->
def result = 0
n.times { result += m }
result }]
listOfExp.each { e -> assertEquals(10, e(5, 2)) }
}
전통적인 방법은 같은 클래스나 인터페이스를 상속해야하는등 각 전략에 이름과 구조를 정해야되는 제약사항이 존재함
코드블록을 일급함수로 사용하여, 이전 예제에서의 보일러플레이트 코드 대부분을 제거할 수 있음.
플라이웨이트(flyweight) 패턴
- flyweight는 '(권투·레슬링 등의) 플라이급 선수(보통 체중 48~51kg사이)'를 말함.
- 인스턴스를 많이 생성한다면 new가 많아지고 이는 메모리 사용량이 많아짐을 의미.
- 메모리 사용량을 줄이기 위한 방법으로, 인스턴스를 필요한 대로 다 만들어 쓰지 말고, 동일한 것은 가능하면 공유해서 객체생성을 줄이자는 것
그루비로 작성한 플라이웨이트 패턴
class Computer {
def type
def cpu
def memory
def hardDrive
def cd
}
class Desktop extends Computer {
def driveBays
def fanWattage
def videoCard
}
class Laptop extends Computer {
def usbPorts
def dockingBay
}
// 컴퓨터와 사용자연계해주는 클래스
class AssignedComputer {
def computerType
def userId
public AssignedComputer(computerType, userId) {
this.computerType = computerType
this.userId = userId
}
}
@Singleton(strict=false)
class ComputerFactory {
def types = [:]
// 오브젝트를 미리만들어서 캐시
private ComputerFactory() {
def laptop = new Laptop()
def tower = new Desktop()
types.put("MacBookPro6_2", laptop)
types.put("SunTower", tower)
}
def ofType(computer) {
types[computer]
}
}
@Test
public void flyweight_computers() {
def bob = new AssignedComputer(ComputerFactory.instance.ofType("MacBookPro6_2"), "Bob")
def steve = new AssignedComputer(ComputerFactory.instance.ofType("MacBookPro6_2"),"Steve")
assertTrue(bob.computerType == steve.computerType)
}
그루비에서 메모이제이션 (memoization) 상세 예제
@Field
boolean incrementChange = false
@Memoized
int increment(int value) {
println("++ increment(" + incrementChange + ") value : " + value)
incrementChange = true
value + 1
}
def square = { value ->
println("++ square() value : " + value)
value * value
}.memoize()
println("---------------------------------------------------")
println("\t increment >> " + increment(10))
println("\t incrementChange >> " + incrementChange)
println("---------------------------------------------------")
incrementChange = false
println("\t increment >> " + increment(10))
println("\t incrementChange >> " + incrementChange)
println("---------------------------------------------------")
println("\t increment >> " + increment(11))
println("\t incrementChange >> " + incrementChange)
println("---------------------------------------------------")
println("\t square >> " + square(10))
println("\t square >> " + square(10))
println("\t square >> " + square(11))
/**
[ 결과값 ]
---------------------------------------------------
++ increment(false) value : 10
increment >> 11
incrementChange >> true
---------------------------------------------------
increment >> 11
incrementChange >> false
---------------------------------------------------
++ increment(false) value : 11
increment >> 12
incrementChange >> true
---------------------------------------------------
++ square() value : 10
square >> 100
square >> 100
++ square() value : 11
square >> 121
*/
그루비에서 플라이웨이트를 Momoize한 함수정의한 버전
class Computer {
def type
def cpu
def memory
def hardDrive
def cd
}
class Desktop extends Computer {
def driveBays
def fanWattage
def videoCard
}
class Laptop extends Computer {
def usbPorts
def dockingBay
}
class AssignedComputer {
def computerType
def userId
public AssignedComputer(computerType, userId) {
this.computerType = computerType
this.userId = userId
}
}
def computerOf = { type ->
def of = [ MacBookPro6_2: new Laptop(), SunTower: new Desktop()]
return of[type]
}
def computerOfType = computerOf.memoize()
@Test
public void flyweight_computers() {
def sally = new AssignedComputer(computerOfType("MacBookPro6_2"), "Sally")
def betty = new AssignedComputer(computerOfType("MacBookPro6_2"), "Betty")
assertTrue sally.computerType == betty.computerType
}
Memozies된 함수는 런타임 값을 캐시할 수 있게 해주는 함수임.
전통적인 플라이웨이트 패턴에서는 새클래스를 펙토리로 생성하여 사용하는데 함수형버전에서는 하나의 메소드를 구현한 후 Memoize한 버전을 리턴하면 플라이웨이트 패턴의 의미를 보존하면서 아주 간단하게 구현을 할 수 있음.
팩토리 패턴 ( 커링 )
객체를 생성하기 위해 인터페이스를 정의하지만, 어떤 클래스의 객체를 생성할 지에 대해서는 하위 클래스에서 결정
[ 팩토리 패턴 활용 예 ]
- 생성할 객체 타입을 예측할 수 없을 때 ==> 부모 클래스 타입을 이용
- 생성할 객체의 명세를 하위 클래스에서 정의하고자 하는 경우
- 객체 생성의 책임을 하위 클래스에 위임시키고 어느 하위 클래스가 위임 했는지에 대한 정보를 은닉하고자 하는 경우
커링(currying)
고차함수를 응용하는 방법의 하나로 파라미터 여러 개를 가진 함수를 한 개의 파라미터만 받는 함수의 조합으로 표현하는 기법
- 디자인 패턴 차원에서 보면, 커링은 함수의 팩토리처럼 사용됨.
- 함수형 언어에서 보편적인 기능은 함수를 여느 자료구조처럼 사용할 수 있게 해주는 일급함수 덕분에 주어진 조건에 따라 다른 함수들을 리턴하는 함수를 만들 수 있음.
그루비에서의 커링
그루비는 커링을 Closure 클래스의 curry() 함수를 사용하여 구현
def adder = {x, y -> x + y}
def incrementer = adder.curry(1)
println "increment : " + incrementer(5)
println "increment : " + incrementer(6)
println "increment : " + incrementer(7)
println "increment : " + incrementer(8)
println "increment : " + incrementer(9)
// 출력
increment : 6
increment : 7
increment : 8
increment : 9
increment : 10
스칼라에서의 커링
def modN(n : Int)(x : Int) = x % n
def multiply(f : Int => Int, x : Int, y : Int) = f(x) + y
println(multiply(modN(2), 9, 4))
// 출력
5
커링이 언어나 런타임에 내장되어 있기 때문에, 함수 팩토리 개념이 이미 녹아들어있어 다른 구조물이 필요없다.