코트린 전략 모델

의 목적

Strategy 디자인 모델은 일련의 알고리즘을 정의하고 서로 바꾸어 사용할 수 있도록 한다.여기서 내가 말한 algorithm은 정렬, 검색, 데이터에서 값을 계산하는 모든 논리를 가리킨다.괜찮습니다.어떤 의미에서 보면 Template Method 모델의 연장이지만 반대로 Strategy는 계승이 아니라 조합을 좋아한다.정책은 특정한 종류로부터 계승되지 않고 하나의 공공 인터페이스만 실현된다.이것은 비용을 계승하지 않고 코드 봉인과 알고리즘 교체를 쉽게 할 수 있다.

문제.

Strategy 해결할 수 있는 문제의 한 예는 판촉 종류의 가격 계산 방법을 고려하는 것일 수 있다.
data class Item(val name: String, val price: Double) // product on the bill

enum class Promotion { // enum with promotion kinds
    NoPromotion, SpecialPromotion, ChristmasPromotion
}

class Bill {
    private val items = mutableListOf<Item>() // list of producsts on the bill
    fun addItem(item: Item): Bill {
        items.add(item)
        return this
    }

    // method calculating final price from list of items and selected promotion
    fun calculateFinalPrice(promotion: Promotion): Double {
        val initialSum = items.sumOf { it.price }

        // checking promotion and using right algorythm to calculate price
        return when (promotion) { 
            Promotion.NoPromotion -> initialSum
            Promotion.SpecialPromotion -> when {
                initialSum > 20 -> initialSum * 0.95
                initialSum > 30 -> initialSum * 0.85
                initialSum > 40 -> initialSum * 0.75
                else -> initialSum
            }
            Promotion.ChristmasPromotion -> initialSum * 0.80
        }
    }
}

I'm aware that Double is not the best type to use for money operations, but for the ease of use in this post examples I decided to use it.


우리는 이곳에서 세 차례의 판촉을 했는데, 그 중 한 번NoPromotion은 아무런 변화가 없었다.판촉을 계산하는 방법은 Bill류에서 이루어졌기 때문에 제품을 수집하는 것 외에 이 종류는 최종 가격을 계산하는데 우리는 여기에 저장하지 않았다Single Responsibility Principle.
만약 공교롭게도 새로운 업그레이드 요청이 있다면, enum 클래스에 Bill 하나를 추가하고 실행하기만 하면 됩니다.잊어버리면 IDE에서 새 케이스가 처리되지 않았다고 보고합니다.SRP가 부족한 것 외에 이것은 결코 나쁘게 보이지 않는다.
현재, 우리는 표준 영수증을 제외하고, 당신은 같은 판촉을 고려한 영수증을 발급할 수 있기를 희망합니다.
class Invoice {
    ...
}
당신은 판촉 코드를 복제할 수도 있고 판촉을 통해 인공 추출InvoiceBill이 계승할 추상적인 영수증류를 실현할 수도 있습니다.물론, 그것들이 아직 다른 종류로부터 계승되지 않은 경우를 제외하고는.
그리고 일부 유형이 이 차원 구조에 적합하지 않을 수도 있고 이것도 업그레이드를 실행해야 한다...

구현


그리고'전략'이 나왔는데 모두 흰색1이었다.개별 승진을 계산하는 방법을 공공 인터페이스가 있는 단독 클래스로 쉽게 옮길 수 있습니다.이렇게 되면 고객들은 심지어 그들이 어떤 판촉 방식을 사용하고 있는지조차 알 필요가 없다.

요약


추상적인 실현에서 시작하여 이 모델의 모든 부분을 이해하자.
// the client class, strategy is provided in the constructor
class Context(private val strategy: Strategy) {
    // using generic strategy interface
    fun useStrategy() = strategy.use() 
}

interface Strategy { // using interface instead of class is very important
    // abstract or concrete class would limit using the strategy only to its hierarchy
    fun use() // strategies usually have single public method
}

class StrategyA : Strategy { // first strategy
    override fun use() { // concrete algorithm implementation
        println("using strategy A")
    }
}

class StrategyB : Strategy { // second strategy
    override fun use() {
        println("using strategy B")
    }
}

fun main() {
    // using either strategy is identical
    // strategies are transparent for the client
    val contextA = Context(StrategyA())
    contextA.useStrategy()
    val contextB = Context(StrategyB())
    contextB.useStrategy()
}
여기에는 Context 클래스가 있기 때문에 클라이언트가 이 정책을 사용합니다.그것은 전략적 인터페이스만 알고 구체적인 종류는 모른다.이를 통해 새로운 전략으로 전략 시리즈를 쉽게 확장할 수 있고 고객을 업데이트할 필요가 없습니다.Strategy류가 아니라 인터페이스를 사용하기 때문에 특정한 전략은 서로 느슨한 관련이 있고 클라이언트가 통용되는 API를 확보한다.
그것은 이렇게 상징적으로 나타낼 수 있다.

솔루션


프로모션은 특정 클래스에 포함될 수 있습니다.
interface Promotion {
    fun calculate(sum: Double): Double
    val name: String
}

// singleton, because this strategy doesn't need to keep its state - but it could
object ChristmasPromotion : Promotion {
    override val name = "Christmas Promotion"
    override fun calculate(sum: Double): Double {
        return sum * 0.8
    }
}

// this is a NullObject, a special case of Strategy not performing any actions
object NoPromotion : Promotion { 
    override val name = "No Promotion"
    override fun calculate(sum: Double): Double {
        return sum
    }
}

object SpecialPromotion : Promotion {
    override val name = "Special Promotion"
    override fun calculate(sum: Double): Double {
        return when {
            sum > 20 -> sum * 0.95
            sum > 30 -> sum * 0.85
            sum > 40 -> sum * 0.75
            else -> sum
        }
    }
}
이러한 프로모션으로 인해 Bill클래스는 다음과 같이 간소화되었습니다.
class Bill {
    ...
    // strategies can be passed in the constructor, or in the method that uses them
    fun calculateFinalPrice(promotion: Promotion): Double {
        println("applying ${promotion.name}")
        val initialSum = items.sumOf { it.price }
        return promotion.calculate(initialSum) // the promotion object calculates the price
    }
}
아마도 이것은 가장 좋은 예가 아닐 것이다. 왜냐하면 우리는 여전히 SRPBill가 최종 수량을 계산하지 않았지만, 지금은 적어도 그것을 Promotion 대상에게 위탁하고 있기 때문이다.
새 유형의 업그레이드를 추가하면 클래스의 업데이트가 발생하지 않습니다.Bill류나 그 어떠한 다른 요구 사항이 포함된 클래스에서 같은 승급류를 사용할 수 있습니다.
단독 클래스에서 논리를 테스트하는 것은 Invoice 조건의 초기 예시보다 훨씬 쉽다.다른 업그레이드를 추가하면 기존의 테스트를 복구해야 하지 않고, 새로운 클래스에 새로운 테스트만 추가합니다.
이러한 테스트의 놀라움을 파괴하지 않기 위해서, when 일부 설정에서 신기하게 추출하는 것이 아니라, 공공 방법이나 구조 함수에서 필요한 모든 값을 얻었는지 확인하십시오.
일반적으로 정책은 상태를 저장할 필요가 없고 제공된 데이터에 대해 몇 가지 작업만 수행할 수 있다.이것은 사용Strategy의 의미 있는 소수의 상황 중의 하나다.

다양한 전략


좋아, 단일 전략은 멋있지만, 우리가 Singleton 다양한 전략을 세우는 것을 막을 수 있는 것은 아무것도 없다.최종 가격은 세금이나 충성도 계획에 따라 다를 수 있습니다.
interface Tax {
    fun applyTaxes(sum: Double): Double
}

interface Promotion {
    fun applyPromotion(sum: Double): Double
}

interface LoyaltyProgram {
    fun applyPolicy(sum: Double): Double
}
그것들도 하나의 대상의 매개 변수로 클라이언트에게 전달할 수 있다.다음은 기본값이 있는 예시입니다. (Kotlin에서 가장 간단한 Builder. 기본값과 실제 다른 정책만 덮어쓸 수 있습니다.
class Bill( 
    // all strategies here are `object`s, 
    // their implementation is an irrelevant detail
    val tax: Tax = DefaultTax,
    val promotion: Promotion = NoPromotion,
    val clientPolicy: LoyaltyProgram = NewClient
)

val newClientAnarchist = Bill(
        tax = NoTax, // well it's not how it works in real life...
        clientPolicy = NewClient
)

val returningClientWithSpecialPromotionBill = Bill(
        clientPolicy = ReturningClient,
        promotion = SpecialPromotion
)
AStatic Factory도 편해질 수 있다
class Bill private constructor (
        private val tax: Tax,
        private val promotion: Promotion,
        private val clientPolicy: ReturningClientPolicy
) {
    ...
    companion object Factory{
        val defaultTax = DefaultTax
        val defaultPromotion = NoPromotion
        val defaultClientPolicy = NewClient

        fun returningClient(): Bill = Bill(
                defaultTax, defaultPromotion, ReturningClient
        )

        fun returningClientWithSpecialPromotion() = Bill(
                defaultTax, SpecialPromotion, ReturningClient
        )

        fun newClientAnarchist() = Bill(
                NoTax, defaultPromotion, NewClient
        )
    }
}
그러나 현재류Bill는 적어도 Bill의 구체적인 실현을 알고 있다
최종 금액의 각 구성 부분의 계산은 반드시 고정된 순서에 따라 진행해야 하며 판촉 등에서 세금을 징수할 수 없다. Template Method 방법과 유사하지만 Strategy류는 정확한 절차 순서를 책임지지만 전략 모델은 이러한 절차의 실현을 대체할 수 있다.
fun calculateFinalPrice(): Double {
    val initialSum = items.sumOf { it.price }
    return initialSum.run { 
        promotion.applyPromotion(this) // `this` is the initial sum
    }.run {
        clientPolicy.applyPolicy(this) // now it's the amount after applying the promotion
    }.run {
        tax.applyTaxes(this) // and after loyalty policy
    } // returning final amount after all the modifiers
}
서로 다른 세금은 전체 법안이 아닌 특정 유형의 제품에 적용될 것이다.그러나 세법이 끊임없이 변화한다고 가정하면 Bill 전략을 사용하면 새로운 법규에 신속하게 응답할 수 있고 전략 고객을 갱신할 필요가 없다.
나는 Tax 방법으로 줄을 서는 이런 조작을 좋아하지 않는다는 것을 인정한다.다행히도 확장 기능을 사용하여 다음과 같은 기능을 개선할 수 있습니다.
fun Double.applyPromotion(promotion: Promotion): Double {
    return promotion.applyPromotion(this)
}

fun Double.applyPolicy(policy: LoyaltyProgram): Double {
    return policy.applyPolicy(this)
}

fun Double.applyTaxes(tax: Tax): Double {
    return tax.applyTaxes(this)
}
그리고 우리는 다음과 같은 것이 있다.
fun calculateFinalPrice(): Double {
    val initialSum = items.sumOf { it.price }
    return initialSum
            .applyPromotion(promotion)
            .applyPolicy(clientPolicy)
            .applyTaxes(tax)
    // nice :)
}

인용하다

run ()는 왕왕 하나의 공공 방법만 있기 때문에 Strategy 연산자를 고려할 수 있다.또한 invoke() 정책의 기본 구현이 아닌 불곱하기annonmous class:
interface Tax {
    // having this allows to use object as a method
    operator fun invoke(sum: Double): Double
}
...
class Bill(
        val tax: Tax = object : Tax {
            override fun invoke(sum: Double): Double {
                return sum
            }
        },
        ...
) {
    ...
    fun calculateFinalPrice(): Double {
        val initialSum = items.sumOf { it.price }
        return initialSum.run{
            promotion(this) // calling the `invoke()`
        }.run {
            clientPolicy(this)
        }.run {
            tax(this)
        }
    }
}
// using annonmous classes allows you to create new strategies on the fly
// but not having them as a concrete class kills a lot of benefits of the pattern
val customBill = Bill(promotion = object : Promotion {
    override fun applyPromotion(sum: Double): Double {
        return sum * 0.123512
    }
})

명명


이름 지정 규칙은 팀 또는 프로젝트에 따라 다를 수 있습니다.나는 개인적으로 패턴 함수 부분의 수식자를 사용하지 않고 의미 있는 도메인 이름을 더 좋아한다.
// I like this more
class SpecialPromotion: Promotion{
    fun calculate(initialPrice: Double): Double{
        ...
    }
}
// than this
class SpecialPromotionStrategy: PromotionStrategy{
    fun use(initialPrice: Double): Double{
        ...
    }
}
첫 번째 스타일을 사용하면 나는 심지어 사용 여부를 고려할 필요가 없다NullObjects.내가 사용하는domainStrategy Pattern류는 내부 규칙에 따라 최종 가격을 계산하는 방법을 안다.Promotion를 이름에 쓰면 나에게 어떤 정보를 제공할 수 있습니까?우리는 각 KotlinStrategy의 이름에 Singleton를 더해야 합니까?
이 기본 설정은 모든 디자인 모드를 나타냅니다.안타깝게도 흔히 볼 수 있는 방법(특히 오래된 항목에서)은 통상적으로 불문율적인 규칙을 엄격히 준수하는 것이다.따라서 이 종류가 특정 모델의 일부라는 것을 즉시 알 수 있습니다.누군가(예를 들어 20년 경력의 초급 개발자)가 모델에 관한 책을 기억하고 엄격하게 틀에 따라만 그것을 실현할 수 있기 때문이다.
만약 당신이 이 화제에 흥미가 있다면, 나는 Kevlin Henney - Seven Ineffective Codding Habbits of Many Programmers의 멋진 강연을 추천할 수 있다. 그는 강연에서 명칭을 언급했다.

요약

object 모델은 일련의 알고리즘을 만들어 서로 다른 논리를 서로 다른 종류에 봉하고 인터페이스 뒤의 클라이언트를 숨긴다.그것은 실현된 교환 사용을 지원한다.이 전략의 사용은 고객 코드를 간소화하고 코드의 중복과 조건 문장을 피했다.클라이언트 테스트와 전략 알고리즘을 분리함으로써 테스트를 크게 간소화시켰다.
이런 모델은 최초의 전체 전략인'가족'이 하나의 유형으로 구성되어도 자주 사용해야 한다.봉인 코드의 장점은 인터페이스와 새로운 종류를 추가하는 것보다 크다.통상적으로 주어진 알고리즘은 다른 곳에서 사용하거나 다른 알고리즘을 추가해야 한다.
하지만 지나치게 하지 마세요.하나의 알고리즘이 한 곳에서 한 줄의 코드를 사용하고 전파될 전망이 없을 때, 어떤 곳에서 전략을 만드는 것은 무의미하다.Strategy 사용Kotlin 연산자와 invoke () 연산자는 이 전략을 사용하는 흥미로운 가능성을 제공했다. 이것은 명명 파라미터 덕분이다.그러나 문법상의 당분이 테스트나 다른 곳에서 코드를 사용하는 것을 어렵게 할 수 있는지 주의해야 한다.

찬성 의견


  • 알고리즘 봉인 - 전체 알고리즘은 하나의 단독 클래스에서 시스템의 어느 곳에서든 사용할 수 있다.

  • 조합은 계승보다 중요하다. 알고리즘과 클라이언트 사이에는 긴밀한 관계가 없다.

  • 만약 정책의 정책만 데이터를 어떻게 처리하는지 알고 있다면 클라이언트는 일반적인 인터페이스를 사용하기 때문에 조건 표현식은 사라진다.

  • 호환성 실현 - 정렬과 같은 서로 다른 실현은 어떤 상황에서는 더욱 잘 일할 수 있다.이 정책은 클라이언트를 변경하지 않고 수정 가능한 알고리즘을 신속하게 제공할 수 있도록 합니다.

  • 테스트하기 쉽습니다. 독립된 클라이언트와 정책 테스트입니다.정책 변경으로 인해 클라이언트 테스트가 수정되지는 않습니다.
  • 기만하다


  • 더 많은 대상 실례를 가지고 있다. extension functions so Singleton을 사용하면 이 문제를 해결하는 데 도움이 된다.

  • 과도하게 사용될 수 있습니다. 만약 이 알고리즘이 다른 어느 곳에서도 사용할 수 없거나 대체 버전이 필요하다면, 정책을 추가하는 것은 불필요할 수도 있습니다.그러나 어쨌든 테스트를 더욱 쉽게 할 것이다.
  • 죄송합니다. 이것은 매우 우아한 내부 우스갯소리입니다. 그러나 너무 적합합니다.

    좋은 웹페이지 즐겨찾기