본문 바로가기

IT/헤드퍼스트 디자인패턴

[디자인패턴] 스트래티지 패턴

안녕하세요 남갯입니다 


오늘은 스트래티지 패턴에 대해 포스팅 해보려고합니다.


해드퍼스트 디자인패턴의 내용을 참조했습니다.


스트래티지 패턴이란?

스트래티지 패턴은 알고리즘 군을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만든다. 스트래티지를 활용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다. 알고리즘의 군이란 비슷한 Behavior(동작)을 하는 기능을 뜻하는것이다. 비슷한 알고리즘군을 Composition을 이용하므로서 알고리즘군은 별도의 클래스를 만들어 캡슐화를 시켜 시스템의 유연성을 향상시킵니다. 



오리게임의 예시

오리 게임 회사에서 어플리케이션을 만든다고 했을때, 기존에 날지못하는 오리들에게 하늘을 나는 기능을 추가해달라는 요구를 받았습니다.




요구사항 1 : fly기능을 추가해줘!


Duck을 상속받은 오리들(사진엔 두개지만 더 많은 오리들) 이 오리들의 기능중에 fly()라는 나는 기능을 추가한다고 해보자.


fly()라는 함수를 추가해서 만들었지만? 이 많은 오리들중에 날지못하는 장난감오리가 있었는데 그 오리에게도 fly하는 기능을 줘서 의도치 않게 장난감오리도 하늘을 날게 되었다.


이렇게 된다면 원래 하려고 했던 의도에 벗어나 일부 서브클래스에 오류가 생기게 된다.


코드를 한부분을 바꿨지만 모든 오리가 날게 되는 부작용이 생긴다.


여기서 날지못하는 오리를 아무것도 하지 않도록 override를 시켰다고 치자!



abstract class BeforeDuck(){
fun swim() = println("수영가능합니다.")
open fun fly() = println("나는 날 수 있어!")
abstract fun display()
}

class NotFlyDuck() : BeforeDuck(){
override fun fly() = println("나는 날지 못해!!!!!")
override fun display() {
//러버덕
}
}



요구사항 2 : 삑삑거리는 고무오리를 추가!



기본적으로 오리는 꿱꿱거리지만 삑삑거리는 오리가 추가됐다고 생각해보자.

위와같이 quack()을 override를 통해 꿱꿱을 처리했다.


abstract class BeforeDuck(){
open fun quack() = println("꿱꿱")
fun swim() = println("수영가능합니다.")
abstract fun display()
}

class RubberDuck() : BeforeDuck(){
override fun quack() {
println("삑삑")
}
override fun display() {
//러버덕
}
}


위의 두 방법대로 진행한 상속을 이용한 코드는 일일이 관리를 하게되면서 변경이 일어나게 됐을때 헷갈릴뿐더러 자주 바뀌는 프로그램에서 매번 fly 혹은 quack을 확인해야하고

오버라이드를 시켜 바꿔야하는데, 그러다보면 오류가 나기 쉬울뿐더러 엄청 많은 양의 오리를 추가된다면 헬파티가 될것이다.





그렇다면 우리는 어떻게 수정을 해야할까?

아! 그래 그러면 동작이 달라지는 부분을 인터페이스로 구현해서 개별적으로 상속을 시키자!!


interface Flyable{
fun fly()
}

interface Quackable{
fun quack()
}

class MyDuck() : BeforeDuck() , Flyable , Quackable{
override fun display() {
//
}

override fun fly() {
//하늘을 난다.
}

override fun quack() {
//꿱꿱
}
}



이렇게 인터페이스를 구현해 오리에게 상속을 시키게 되면 코드의 중복 즉 boilerplate 코드가 증가하면서 재사용성을 기대하기 어렵고 관리하는 측면에서 문제점이 있습니다. 또한 단순히 하늘을 나는게 아니라 방식이 다르다면? 더욱 더 난감해지게 됩니다.



여기서 필요한건?

여기서 필요한 패턴이 스태리티지 패턴입니다. 코드에 대한 새로운 요구사항이 있을때마다 바뀌는 부분이 있다면 바뀌지 않는부분에서 골라내어 분리시켜서 캡슐화를 시킵니다. 그렇게되면 바뀌지 않는 부분은 영향을 받지 않고 바뀌는부분만 고치거나 확장가능합니다. 저번에 포스팅한 객체지향설계 패턴인  OCP (open closed principle)의 확장에는 열려있고 수정에는 닫혀있는 원리와 비슷한것 같습니다. 




바뀌는 기능을 분리해봅시다!


자주 바뀌는 기능에서는 fly와 quack의 기능이 있었습니다.


fly를 이제 duck 클래스에 넣지 말고 인터페이스로 구현합니다.





/*fly 에 대한 정의*/
interface flyBehavior {
fun fly()
}

class FlyWithWings() : flyBehavior {
override fun fly() = println("난 날수 있어")
}

class FlyNoWay() : flyBehavior {
override fun fly() = println("난 날수 없어")
}

/*quack에 대한 정의*/
interface QuackBehavior {
fun Quack()
}

class Quack : QuackBehavior {
override fun Quack() = println("꿱꿱!!")
}

class Squeak : QuackBehavior {
override fun Quack() = println("삑삑!!")
}

class MuteQuack() : QuackBehavior {
override fun Quack() = println("나는 소리를 못내!")
}


일단 두가지정도의 기능이 있었기 때문에 날수 있는 기능과 날지 못하는 기능 두가지를 FlyBehavior,QuackBehavior 인터페이스로 상속받아 구현합니다.  이렇구 구현하게되면 행동에대한 재사용성도 가능해지고 Duck클래스를 수정하지 않아도 인터페이스를 상속하는 클래스를 추가함으로써 새로운 행동을 추가가 가능합니다.



다음 해야할일?

abstract class Duck {
lateinit var quackBehavior: QuackBehavior
lateinit var flyBehavior: flyBehavior
fun performQuack() = quackBehavior.Quack()
fun performFly() = flyBehavior.fly()
fun swim() = println("모든 오리는 물에 뜹니다.")
abstract fun display()
}

이렇게 Duck 클래스를 만들고 quackBehavior와 FlyBehavior를 만들어 넣고

그 행동을 수행할 performQuack(), performFly() 를 이용해 동작을 구현합니다.


이렇게 구현한 Duck클래스를 상속받아


MallardDuck에게 꿱꿱의 소리를 내게하고 날게만들라고 하게되면

class MallardDuck : Duck() {
init {
quackBehavior = Quack() // 꿱꿱!
flyBehavior = FlyWithWings() //오리 날다.
}

override fun display() = println("나는 MallardDuck 이다.")
}


이렇게 MallarDuck을 구현하고 이걸 사용하는 곳에서 사용하게 되면


val mallardDuck = MallardDuck()
mallardDuck.performQuack() // print 꿱꿱
mallardDuck.performFly() // 오리날다.


여기서 추가적으로


오리 게임회사니까 오리가 날다가 못날도록 동적으로 변경한다고 하게됐을때는 


mallardDuck.flyBehavior = FlyNoWay()
mallardDuck.quackBehavior = Squeak()

이런 코드를 이용해 set을 시켜줌으로서 현재의 오리의 동작을 동적으로 변경이 가능합니다.


행동의 클래스와 오리의 클래스를 이런식으로 합치는것을 구성(Composition)을 이용한다라고 부르는데, 오리클래스에서 상속을 받는대신 행동의 객체로 구성됨으로써 행동을 부여받았습니다. 


이 디자인원칙은 상속보다는 구성을 활용해서 만들어야한다는 디자인원칙입니다.


저도 유연한 코드를 잘 만들지 못하는 편이였는데, 스트래티지 패턴을 통해 유연한 코드를 만들어보세요!


- 틀린부분이 있다면 비방말고 충고 부탁드립니다 감사합니다.





 ---추가----






class CharacterTest : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
main()
}

private fun main() {
val warrior = Warrior()
warrior.weaponBehavior = SwordWeapon()
warrior.move()
warrior.attack()
}
}

abstract class Character {
open lateinit var weaponBehavior: WeaponBehavior
fun move() = println("적을 향해 이동중")
fun attack() = println(weaponBehavior.attackWeapon())
abstract fun display()
}

class Warrior : Character() {
override fun display() {
}
}

class Anchor : Character() {
override fun display() {
}
}

class Magician : Character() {
override fun display() {
}
}

class Cleric : Character() {
override fun display() {
}
}

interface WeaponBehavior {
fun getWeaponName(): String
fun attackWeapon()
}

class SwordWeapon : WeaponBehavior {
override fun getWeaponName(): String = "검"
override fun attackWeapon() = println("${getWeaponName()}으로 공격.")
}

class LongSwordWeapon : WeaponBehavior {
override fun getWeaponName(): String = "롱소드"
override fun attackWeapon() = println("${getWeaponName()}로 공격.")
}

class CaneWeapon : WeaponBehavior {
override fun getWeaponName(): String = "지팡이"
override fun attackWeapon() = println("${getWeaponName()}로 공격.")
}

class BowWeapon : WeaponBehavior {
override fun getWeaponName(): String = "활"
override fun attackWeapon() = println("${getWeaponName()}로 공격.")
}

class CrossWeapon : WeaponBehavior {
override fun getWeaponName(): String = "십자가"
override fun attackWeapon() = println("${getWeaponName()}로 공격.")
}