'Kotlin 핵심 프로그래밍' 참고: 디자인 패턴

창조 패턴

주류 생성 패턴에는 팩토리 메소드 패턴, 추상 팩토리 패턴, 빌더 패턴이 포함됩니다.

동반 객체는 팩토리 패턴을 향상시킵니다.

어떤 곳에서는 팩토리 패턴이 단순 팩토리, 팩토리 메소드 패턴, 추상 팩토리로 세분화됩니다.

여기서는 주로 간단한 팩토리 패턴을 소개하는데, 그 핵심 기능은 클라이언트에 노출시키지 않고 팩토리 클래스를 통해 객체 인스턴스의 생성 로직을 숨기는 것입니다. 일반적인 사용 시나리오는 상위 클래스와 여러 하위 클래스가 있는 경우 이 모드를 사용하여 하위 클래스 개체를 만들 수 있는 것입니다.

개인용 컴퓨터와 서버 호스트를 모두 생산하는 컴퓨터 처리 공장이 있다고 가정해 보겠습니다. 우리는 친숙한 팩토리 패턴 디자인을 사용하여 비즈니스 로직을 설명합니다.

interface Computer {
    
    
    val cpu: String
}

class PC(override val cpu: String = "Core") : Computer
class Server(override val cpu: String = "Xeon") : Computer

enum class ComputerType {
    
     PC, Server }

class ComputerFactory {
    
    
    fun produce(type: ComputerType): Computer {
    
    
        return when (type) {
    
    
            ComputerType.PC -> PC()
            ComputerType.Server -> Server()
        }
    }
}
fun main() {
    
    
    val compter = ComputerFactory().produce(ComputerType.PC)
    println(compter.cpu)
}

위 코드는 ComputerFactory 클래스의 produce 메소드를 호출하여 다양한 Computer 하위 클래스 객체를 생성합니다. 클라이언트에서 인스턴스 생성 로직을 분리합니다. 객체 생성 로직이 변경되면(예: 구성 매개변수 수가 변경됨) 이 모드는 생성 메소드 내부의 코드만 수정하면 됩니다. 객체를 직접 생성하는 것과 비교하면 방법이 더 많습니다. 유지관리에 도움이 됩니다.

팩토리 클래스 대신 싱글턴 사용

우리가 이미 알고 있는 것은 Kotlin이 Java에서 싱글톤 패턴을 구현하기 위해 object 사용을 지원한다는 것입니다. 따라서 팩토리 클래스 대신 ComputerFactory 싱글톤을 구현할 수 있습니다.

object ComputerFactory {
    
     // 用 object 代替 class
    fun produce(type: ComputerType): Computer {
    
    
        return when (type) {
    
    
            ComputerType.PC -> PC()
            ComputerType.Server -> Server()
        }
    }
}
fun main() {
    
    
    val compter = ComputerFactory.produce(ComputerType.PC)
    println(compter.cpu)
}

operator 연산자 오버로딩 invoke 메소드로 대체하여 표현식을 더욱 단순화할 수 있습니다. produce

object ComputerFactory {
    
       
    operator fun invoke(type: ComputerType): Computer {
    
    
        return when (type) {
    
    
            ComputerType.PC -> PC()
            ComputerType.Server -> Server()
        }
    }
}
fun main() {
    
    
    val compter = ComputerFactory(ComputerType.PC)
    println(compter.cpu)
}

동반 객체는 정적 팩토리 메소드를 생성합니다.

대신 Computer()을 통해 직접 인스턴스를 생성할 수 있나요? ComputerFactory()

생성자 대신 정적 팩토리 메소드 사용을 고려해보세요. Java의 static을 대체하고 기능과 표현 면에서 더 강력한 기능을 갖춘 Kotlin의 동반 객체에 대해 생각해 보셨을 것입니다. Computer 인터페이스에서 컴패니언 개체를 정의하면 위의 요구 사항을 충족할 수 있습니다. 코드는 다음과 같습니다.

interface Computer {
    
    
    val cpu: String
    companion object {
    
    
        operator fun invoke(type: ComputerType): Computer {
    
    
            return when (type) {
    
    
                ComputerType.PC -> PC()
                ComputerType.Server -> Server()
            }
        }
    }
}

class PC(override val cpu: String = "Core") : Computer
class Server(override val cpu: String = "Xeon") : Computer

enum class ComputerType {
    
     PC, Server }

fun main() {
    
    
    val compter = Computer(ComputerType.PC)
    println(compter.cpu)
}

컴패니언 객체의 이름을 지정하지 않고 Computer을 통해 컴패니언 객체의 메소드를 직접 호출할 수 있습니다. 물론, 동반 개체에 이름을 지정하고 싶다면 다음과 같이 동반 개체에 Computer의 이름을 지정할 수 있습니다. Factory을 사용하여 이름을 지정합니다.

interface Computer {
    
    
    val cpu: String
    companion object Factory {
    
    
        operator fun invoke(type: ComputerType): Computer {
    
    
            return when (type) {
    
    
                ComputerType.PC -> PC()
                ComputerType.Server -> Server()
            }
        }
    }
}

fun main() {
    
    
    val compter = Computer.Factory(ComputerType.PC)
    println(compter.cpu)
}

참고: 컴패니언 개체에 이름이 있더라도 호출 시 지정된 이름 표시를 생략할 수 있습니다.

컴패니언 개체 메서드 확장

실제 비즈니스에서 우리가Computer 인터페이스의 사용자라고 가정합니다. 예를 들어 프로젝트에 도입된 타사 클래스 라이브러리이고 모든 클래스의 구현 세부정보는 다음과 같습니다. 잘 숨겨져 있어요. 따라서 로직을 추가로 변환하려면 Kotlin의 동반 객체 메서드도 확장 기능의 특성을 활용하여 이 요구 사항을 잘 충족할 수 있습니다.

예를 들어, CPU 모델을 기반으로 컴퓨터 유형을 결정하기 위해Computer에 함수를 추가하려는 경우 다음과 같이 구현할 수 있습니다.

fun Computer.Companion.fromCPU(cpu: String): ComputerType? = when(cpu) {
    
    
    "Core" -> ComputerType.PC
    "Xeon" -> ComputerType.Server
    else -> null
}

컴패니언 개체의 이름을 Factory로 지정하면 다음과 같이 구현할 수 있습니다.

fun Computer.Factory.fromCPU(cpu: String): ComputerType? = when(cpu) {
    
    
    "Core" -> ComputerType.PC
    "Xeon" -> ComputerType.Server
    else -> null
}

옮기다:

fun main() {
    
    
    val type = Computer.fromCPU("Core")
    println(type)
}

인라인 함수는 추상 팩토리를 단순화합니다.

Kotlin의 인라인 함수는 매개변수 유형을 지정할 수 있다는 점에서 큰 효과를 발휘합니다. 이 기능을 활용하면 Abstract Factory라는 더 복잡한 팩토리 패턴을 개선할 수도 있습니다.

공장 모델은 이미 제품 계층 문제를 매우 잘 처리할 수 있으며, 이전 섹션에서는 서버와 PC를 생산하는 컴퓨터 제조업체의 문제를 해결하는 데 이를 사용했습니다. 더 생각해보면, 문제가 여러 제품 계층으로 발생하는 경우, 예를 들어 이제 브랜드 소유자 개념이 도입되었으므로 Dell, Asus, Acer와 같은 여러 컴퓨터 브랜드가 있으므로 다른 팩토리 클래스를 추가해야 합니다. . 하지만 모델별로 팩토리를 구축하면 코드 유지 관리가 어려워지기 때문에 이때는 추상 팩토리 패턴을 도입해야 합니다.

추상 공장 패턴

구체적인 클래스를 지정하지 않고 관련되거나 상호 의존적인 개체 집합을 만들기 위한 인터페이스를 제공합니다.

추상 팩토리 정의에서는 "관련되거나 상호 의존적인 객체 그룹"을 "제품군"으로 부를 수도 있습니다. 위의 예에서는 다양한 컴퓨터 브랜드를 대표하는 3가지 제품군을 언급했습니다.

다음으로 추상 팩토리를 사용하여 특정 요구 사항을 충족하겠습니다.

class Dell: Computer {
    
     }
class Asus: Computer {
    
     }
class Acer: Computer {
    
     }

class DellFactory: AbstractFactory() {
    
    
    override fun produce() = Dell()
}
class AsusFactory: AbstractFactory() {
    
    
    override fun produce() = Asus()
}
class AcerFactory: AbstractFactory() {
    
    
    override fun produce() = Acer()
}

abstract class AbstractFactory {
    
    

    abstract fun produce(): Computer
    
    companion object {
    
    
        operator fun invoke(factory: AbstractFactory): AbstractFactory {
    
    
            return factory
        }
    }
} 

fun main() {
    
     
    val dellFactory = AbstractFactory(DellFactory())
    val dell = dellFactory.produce()
    println(dell)
}

위 코드에서는 특정 팩토리 클래스를 생성할 때마다 특정 팩토리 객체를 생성용 매개변수로 전달해야 하는데, 이는 분명히 구문이 그다지 우아하지 않습니다.

다음으로 Kotlin의 인라인 함수를 사용하여 이러한 상황을 개선할 수 있습니다. 우리가 해야 할 일은 AbstractFactory 클래스에서 invoke 메소드를 다시 구현하는 것뿐입니다.

abstract class AbstractFactory {
    
    

    abstract fun produce(): Computer
    
    companion object {
    
    
        inline operator fun <reified T : Computer> invoke(): AbstractFactory = when(T::class) {
    
    
            Dell::class -> DellFactory()
            Asus::class -> AsusFactory()
            Acer::class -> AcerFactory()
            else -> throw IllegalArgumentException()
        }
    }
}

fun main() {
    
     
    val dellFactory = AbstractFactory<Dell>()
    val dell = dellFactory.produce()
    println(dell)
}
  • 1) 를 인라인 함수로 사용하여 invoke 메소드를 정의함으로써 키워드를 도입할 수 있습니다. 구체화된 매개변수 유형의 구문 기능을 사용합니다.inlinereified
  • 2) 구현하려는 매개변수 타입은 Computer 이며, invoke 메소드에서는 특정 타입을 판단하여 해당 팩토리 클래스를 반환합니다. .

빌더 패턴 대신 명명된 선택적 매개변수 사용

Java 개발에서 다음과 같이 뱀만큼 긴 생성자를 작성한 적이 있습니까?

// Boolean 类型的参数表示 Robot 是否含有对应固件
Robot robot = new Robot(1, true, true, false, false, false, false, false, false)

처음 작성을 마쳤을 때 되돌아보면 여전히 이해할 수 있지만, 하루가 지나면 대부분을 잊어버릴 수도 있고, 일주일이 지나면 더 이상 그것이 무엇인지 알지 못할 수도 있습니다. 이러한 비즈니스 시나리오에 직면할 때 우리의 일반적인 접근 방식은 Builder(빌더) 패턴을 통해 이를 해결하는 것입니다.

빌더 패턴

빌더 패턴이 수행하는 주요 작업은복잡한 객체의 구성과 표현을 분리하여 동일한 구성 프로세스가 다른 표현을 생성할 수 있도록 하는 것입니다.

팩토리 패턴과 생성자 모두 동일한 문제를 안고 있습니다. 즉, 많은 수의 선택적 매개변수에 맞게 확장되지 않는다는 것입니다. 이제 코드 이름, 이름, 배터리, 무게, 높이, 속도, 부피 등 여러 속성을 포함하는 로봇 클래스가 있다고 가정합니다. 많은 제품에는 걷지 못하거나 소리를 내지 못하는 등 이러한 특성 중 일부가 없으며 일부 로봇에는 배터리가 필요하지도 않습니다.

나쁜 접근 방식은 모든 속성을 생성자 매개변수로 사용하여 위에 표시된 클래스를 설계Robot하는 것입니다. 또는 필요한 매개변수만 포함된 생성자를 제공한 다음 다양한 상황에 대한 선택적 속성을 포함하는 추가 생성자를 제공하는 텔레스코핑 생성자 패턴을 채택했을 수도 있습니다. 이 모델은 통화 시 많은 개선이 이루어졌지만 분명한 단점도 있습니다. 생성자 매개변수의 수가 증가하면 제어력이 빨리 상실되고 코드 유지 관리가 어려워지기 때문입니다.

빌더 패턴은 위의 문제를 피할 수 있습니다. 우리는 Kotlin을 사용하여 Java로 빌더 패턴을 구현합니다.

class Robot private constructor(
    val code: String,
    val battery: String?,
    val height: Int?,
    val weight: Int?) {
    
    
    
    class Builder(val code: String) {
    
    
        private var battery: String? = null
        private var height: Int? = null
        private var weight: Int? = null
        
        fun setBattery(battery: String?): Builder {
    
    
            this.battery = battery
            return this
        }
        fun setHeight(height: Int): Builder {
    
    
            this.height = height
            return this
        }
        fun setWeight(weight: Int): Builder {
    
    
            this.weight = weight
            return this
        }
        fun build(): Robot {
    
    
            return Robot(code, battery, height, weight)
        }
    }
}


fun main() {
    
    
    val robot = Robot.Builder("007")
        .setBattery("R6")
        .setHeight(100)
        .setWeight(80)
        .build()
}

의 체인 호출 디자인은 훨씬 더 우아해 보이지만 동시에 선택적 매개변수 설정도 더 의미론적으로 보입니다. 이는 카레 구문과 다소 유사합니다. 또한 빌더 패턴의 또 다른 이점은 다중 선택적 매개변수 문제를 해결한다는 것입니다.객체 인스턴스를 생성할 때 set 메소드를 사용하여 필수 매개변수를 할당하기만 하면 됩니다.

그러나 빌더 패턴에는 몇 가지 단점도 있습니다.

  • 1) 비즈니스에 필요한 매개변수가 많은 경우 코드는 여전히 길게 표시됩니다.
  • 2) Builder; 사용 시 마지막에 build 메소드를 호출하는 것을 잊어버릴 수도 있습니다.
  • 3) 객체를 생성할 때 생성자를 먼저 생성해야 하기 때문에 오버헤드가 추가되며 성능이 매우 중요한 경우에는 특정 문제가 발생할 수 있습니다.

실제로 Kotlin으로 프로그램을 설계할 때 대부분의 경우 빌더 패턴 사용을 피할 수 있습니다. "Effective Java"는 빌더 패턴을 소개할 때 다음과 같이 설명합니다. 기본적으로 빌더 패턴은 Ada 및 Python에서와 마찬가지로 명명된 선택적 매개변수를 시뮬레이션합니다. 다행히 Kotlin은 명명된 선택적 매개변수가 있는 프로그래밍 언어이기도 합니다.

명명된 선택적 매개변수

Kotlin의 함수와 생성자 모두 이 기능을 지원합니다. 이제 살펴보겠습니다. 이는 주로 다음 두 가지 점에서 나타납니다.

  • 1) 매개변수의 값을 지정할 때, 모든 매개변수 중 위치가 아닌 매개변수 이름을 가져와서 결정할 수 있습니다.
  • 2) 매개변수를 기본값으로 설정할 수 있으므로 반드시 모든 매개변수가 아닌 일부 매개변수에 대한 값만 제공할 수 있습니다.

따라서 Kotlin의 기본 구문 기능을 직접 사용하여 빌더 패턴의 효과를 얻을 수 있습니다. 이제 위의 Robot 예를 다시 디자인해 보세요.

class Robot(
    val code: String,
    val battery: String? = null,
    val height: Int? = null,
    val weight: Int? = null
)

val robot1 = Robot(code = "007")
val robot2 = Robot(code = "007", battery = "R6")
val robot3 = Robot(code = "007", height = 100, weight = 80)

빌더 패턴과 비교하여 이름이 지정된 선택적 매개변수를 통해 클래스를 구성하면 다음과 같은 많은 이점이 있음을 알 수 있습니다.

  • 1) 코드가 매우 단순해졌습니다. 이는 Robot 클래스의 구조 코드 양뿐만 아니라 <를 선언할 때의 구문에도 반영됩니다. a i=2> 개체. 더 간결하게 작성하세요.Robot
  • 2) 객체를 선언할 때 각 매개변수 이름을 명시적으로 지정할 수 있으며 순서대로 작성할 필요가 없어 매우 편리하고 유연합니다.
  • 3) Robot 클래스의 각 객체는 val에 의해 선언되므로 var

또한 클래스의 기능이 충분히 단순하다면 data class을 사용하여 데이터 클래스를 직접 선언하는 것이 더 좋습니다. 아시다시피 데이터 클래스는 위의 모든 기능도 지원합니다.

require 메소드는 매개변수를 제한합니다.

빌더 패턴의 또 다른 기능은 build 메소드의 매개변수에 제약조건을 추가할 수 있다는 것입니다.

예를 들어 로봇의 무게가 배터리 모델을 기반으로 결정되어야 한다고 가정하면 배터리 모델을 전달하기 전에 weight 속성에 값을 할당할 수 없습니다. 예외가 발생합니다.

fun build(): Robot {
    
    
    if (weight != null && battery == null) {
    
    
        throw IllegalArgumentException("Battery should be determined when setting weight")
    } else {
    
    
        return Robot(code, battery, height, weight)
    }
}

특정 테스트 사례를 실행합니다.

val robot = Robot.Builder("007")
    .setWeight(100)
    .build()

그러면 다음과 같은 예외 정보를 찾을 수 있습니다.

Exception in thread "main" java.lang.IllegalArgumentException:Battery should be determined when setting weight

메소드에서 매개변수를 제한하는 이 방법build은 비즈니스를 더욱 안전하게 만들 수 있습니다. 그렇다면 명명된 선택적 매개변수를 통해 클래스를 구성하는 방식을 구현하는 방법은 무엇입니까?

분명히 Robot 클래스의 init 메소드에 위의 인증 코드를 추가할 수도 있습니다. 하지만 Kotlin에서는 클래스나 함수에 require 키워드를 사용하여 함수 매개변수를 제한할 수도 있습니다. 기본적으로 이는 Java의 와 다소 유사한 인라인 메서드입니다. assert.

class Robot(
    val code: String,
    val battery: String? = null,
    val height: Int? = null,
    val weight: Int? = null
) {
    
    
    init {
    
    
        require(weight == null || battery != null) {
    
    
            "Battery should be determined when setting weight."
        }
    }
}

객체를 생성할 때 조건을 충족하지 않는 동작이 있는 경우 Robot 개체가 발생하면 예외가 발생합니다. require

val robot = Robot(code="007", weight = 100)
>>> java.lang.IllegalArgumentException: Battery should be determined when setting weight

Kotlin의 require 메소드를 사용하면 매개변수 제약 조건 코드를 의미상 더 친숙하게 만들 수 있다는 것을 알 수 있습니다.

일반적으로 Kotlin에서는 명명된 선택적 매개변수를 지원하므로 Kotlin에서 빌더 패턴을 사용하지 않는 것이 좋습니다. 이를 통해 여러 선택적 매개변수로 클래스를 구성할 때 더 간결하고 도움이 되는 디자인을 디자인할 수 있습니다.

행동 패턴

주류 행동 패턴에는 관찰자 패턴, 전략 패턴, 템플릿 메서드 패턴, 반복자 패턴, 책임 사슬 패턴 및 상태 패턴이 포함됩니다.

Kotlin의 관찰자 패턴

관찰자 패턴은 일대다 종속 관계를 정의하여 하나 이상의 관찰자 객체가 주체 객체를 모니터링할 수 있도록 합니다. 이런 방식으로 관찰된 객체의 상태가 변경되면 해당 관찰자에게 이를 알려 해당 관찰자 객체가 자동으로 업데이트될 수 있도록 해야 합니다.

간단히 말해서 관찰자 패턴은 두 가지 작업을 수행합니다.

  • 구독자(관찰자)상태 모니터링 추가 또는 삭제 게시자용 (관찰자) a>;
  • 게시자의 상태가 변경되면 해당 이벤트를 듣고 있는 모든 관찰자에게 이벤트가 통보되고, 관찰자는 응답 로직을 실행합니다.

주목할 만한

Java의 자체 표준 라이브러리는 관찰자 패턴 구현을 돕기 위해 java.util.Observable 클래스와 java.util.Observer 인터페이스를 제공합니다.

이를 사용하여 주가를 동적으로 업데이트하는 예를 구현해 보겠습니다.

import java.util.*

class StockUpdate: Observable() {
    
    
    val observers = mutableSetOf<Observer>();
    fun setStockChanged(price: Int) {
    
    
        this.observers.forEach {
    
     it.update(this, price) }
    }
}

class StockDisplay: Observer {
    
    
    override fun update(o: Observable, price: Any) {
    
    
        if (o is StockUpdate) {
    
    
            println("The latest stock price is ${
      
      price}.")
        }
    }
}
fun main() {
    
    
    val su = StockUpdate()
    val sd = StockDisplay()
    su.observers.add(sd)
    su.setStockChanged(100)
}

위의 예에서는 관찰 가능한 게시자 클래스가 생성됩니다StockUpdate. 이 클래스는 변경 사항을 모니터링하는 관찰자 개체를 유지하며observers 를 통해 관리됩니다. a> 메소드는 실행 응답 로직을 실행합니다. 메소드를 실행하면 업데이트된 주가가 관찰자에게 전달되고 해당 클래스 객체가 메소드. addremoveStockUpdatesetStockChangedupdate

Delegate.Observable

실제로 Kotlin의 표준 라이브러리에는 동일한 시나리오를 구현하는 데에도 사용할 수 있는 관찰 가능한 추가 대리자 속성이 도입되었습니다.

import kotlin.properties.Delegates

interface StockUpdateListener {
    
    
    fun onRise(price: Int)
    fun onFall(price: Int)
}
class StockDisplay: StockUpdateListener {
    
    
    override fun onRise(price: Int) {
    
    
        println("The latest stock price has risen to ${
      
      price}.")
    }
    override fun onFall(price: Int) {
    
    
        println("The latest stock price has fell to ${
      
      price}.")
    }
}
class StockUpdate {
    
    
    var listeners = mutableSetOf<StockUpdateListener>()
    var price: Int by Delegates.observable(0) {
    
     _, old, new ->
        listeners.forEach {
    
    
            if (new > old) it.onRise(price) else it.onFall(price)
        }
    }
}

fun main() {
    
    
    val su = StockUpdate()
    val sd = StockDisplay()
    su.listeners.add(sd)
    su.price = 100
    su.price = 98
}

이 버전에서는 보다 구체적인 요구 사항을 구현했습니다. 즉, 주가가 오르거나 내릴 때 다른 맞춤형 견적 사본을 인쇄하십시오.

자세히 생각해 보면 java.util.Observer 인터페이스를 구현하는 클래스는 응답 논리를 작성하기 위해 update 메서드만 재정의할 수 있다는 것을 알 수 있습니다. 즉, 서로 다른 논리적 응답에 대해 여러 개가 있는 경우 이 방법에서도 이를 구별해야 합니다. 분명히 이렇게 하면 구독자의 코드가 부풀어오르게 보일 것입니다.

다른 관점에서 보면 게시자의 이벤트 푸시를 제3자 서비스로 간주하면 하나의 API 인터페이스만 제공하고 API 호출자는 더 많은 기능을 수행해야 합니다.

분명히 Delegates.observable()을 사용하는 솔루션이 더 유연합니다. 이는 메타데이터KProperty객체, 이전 값, 위임된 속성의 새 값을 나타내는 3개의 매개변수를 제공합니다.

추가 StockUpdateListener 인터페이스를 정의함으로써 상승 및 하강의 다양한 응답 로직을 인터페이스 메소드로 캡슐화하여 StockDisplay에서 인터페이스를 구현할 수 있습니다. onRiseonFall 메소드는 분리를 달성합니다.

대표자.거부권

모니터링된 값이 마음대로 수정되는 것을 원하지 않는 경우도 있습니다. observable 대리자 속성 외에도 Kotlin의 표준 라이브러리는 vetoable 속성도 제공합니다. 이름에서 알 수 있듯이 veto은 "거부"를 나타냅니다. "는 vetoable가 새 값이 적용되기 전에 이를 가로채고 이를 수락할지 여부를 결정하는 기능을 제공한다는 의미입니다.

예를 들어:

import kotlin.properties.Delegates

var value: Int by Delegates.vetoable(0) {
    
     prop, old, new ->
    new > 0
}

value = 1
println(value)
>>> 1

value = -1
println(value)
>>> 1

변경 가능한Int 객체가 여기에서 생성됩니다value, by 키워드를 사용하여 추가합니다 입니다. 로 변경하려고 하면 인쇄된 결과는 여전히 이전 값 이며 양의 정수 할당만 허용됩니다. 따라서 Delegates.vetoable위임 속성. 초기화 값은 0value-11

고차 함수 단순화 전략 모드 및 템플릿 방법 모드

개방-폐쇄 원칙 따르기: 전략 패턴

수영선수를 나타내는 추상 클래스Swimmer와 수영 메소드swim가 다음과 같이 표현된다고 가정합니다.

class Swimmer {
    
    
    fun swim() {
    
    
        println("I am swimming...")
    }
}

fun main() {
    
    
    val shaw = Swimmer()
    shaw.swim()
}

shaw은 수영에 재능이 있기 때문에 평영, 배영, 자유형 영법을 빠르게 마스터했습니다. 따라서 swim 메소드를 3가지 다른 수영 자세를 나타내는 메소드로 변환해야 합니다.

class Swimmer {
    
    
    fun breaststroke() {
    
    
        println("I am breaststroking...")
    }
    fun backstroke() {
    
    
        println("I am backstroke...")
    }
    fun freestyle() {
    
    
        println("I am freestyling...")
    }
}

그러나 이것은 그다지 좋은 디자인은 아닙니다. 우선, 모든 수영선수가 이 세 가지 수영 자세를 마스터한 것은 아닙니다. 모든 Swimmer 클래스 객체가 모든 메소드를 호출할 수 있다면 더 위험할 것입니다. 둘째, 향후 새로운 행동 방식이 필연적으로 추가될 예정이며, Swimmer 클래스를 수정하는 것은 개방성과 폐쇄성의 원칙을 위반하는 것입니다.

따라서 더 나은 접근 방식은 수영 동작을 인터페이스로 캡슐화하는 것입니다. 다양한 시나리오에 따라 다양한 수영 방법을 호출할 수 있습니다. 이 시나리오를 해결하려면 전략 패턴이 좋은 아이디어입니다.

전략 패턴은 일련의 알고리즘을 정의하고 이를 서로 대체할 수 있도록 별도로 캡슐화합니다. 이 패턴을 사용하면 알고리즘을 사용하는 고객과 독립적으로 알고리즘 변경이 가능합니다.

기본적으로 전략 패턴의 역할은 다양한 행동 전략(Strategy)을 독립적으로 캡슐화하고 이를 클래스에서 논리적으로 분리하는 것입니다. 그런 다음 다양한 컨텍스트(Context)에 따라 다양한 전략을 선택하도록 전환한 다음 클래스 개체를 사용하여 호출합니다. 아래에서는 친숙한 방식으로 수영을 다시 구현합니다.

interface SwimStrategy {
    
    
    fun swim()
}
class Breaststroke: SwimStrategy {
    
    
    override fun swim() {
    
    
        println("I am breaststroking...")
    }
}
class Backstroke: SwimStrategy {
    
    
    override fun swim() {
    
    
        println("I am backstroke...")
    }
}
class Freestyle: SwimStrategy {
    
    
    override fun swim() {
    
    
        println("I am freestyling...")
    }
}
class Swimmer(private val strategy: SwimStrategy) {
    
    
    fun swim() {
    
    
        strategy.swim()
    }
}
fun main() {
    
    
    // tom会自由泳
    val tom = Swimmer(Freestyle())
    tom.swim()
    // jack会蛙泳
    val jack = Swimmer(Breaststroke())
    jack.swim()
}

이 솔루션은 분리 및 재사용 목적을 달성하며 다양한 시나리오에서 다양한 전략으로 쉽게 전환할 수 있습니다. 그러나 이 버전에는 이전보다 훨씬 많은 코드가 포함되어 있습니다.

고차함수 추상 알고리즘

우리는 고차 함수의 아이디어를 사용하여 전략 클래스를 다시 생각합니다. 분명히 전략을 함수로 캡슐화한 다음 이를 매개변수로 Swimmer 클래스.

전략 클래스의 목적은 매우 명확하므로 행동 알고리즘에 대한 추상화일 뿐이므로 고차 함수 표현은 좋은 대안 아이디어입니다.

이제 고차 함수를 사용하여 위의 예를 다시 구현합니다.

fun breaststroke() {
    
     println("I am breaststroking...") }
fun backstroke() {
    
     println("I am backstroking...") }
fun freestyle() {
    
     println("I am freestyling...") }

class Swimmer(val swimming: () -> Unit) {
    
    
    fun swim() {
    
    
        swimming()
    }
}

fun main() {
    
    
    // tom会自由泳
    val tom = Swimmer(::freestyle)
    tom.swim()
    // jack会蛙泳
    val jack = Swimmer(::breaststroke)
    jack.swim()
}

보시다시피 코드의 양이 획기적으로 줄어들었고, 구조가 명확하고 읽기 쉽습니다. 전략 알고리즘은 함수로 캡슐화되어 있으므로 Swimmer 클래스 객체를 초기화할 때 함수를 사용하여 구성 매개변수 전달을 위한 구문. 물론 valLambda 표현식으로 사용하여 함수를 선언할 수도 있습니다. 그러면 매개변수를 전달할 때 더욱 간결하고 직관적이게 됩니다.

템플릿 메서드 패턴: 상속 대신 고차 함수

고차 함수로 개선할 수 있는 또 다른 디자인 패턴은 템플릿 메서드 패턴입니다.

템플릿 메소드 패턴과 전략 패턴이 해결해야 할 문제는 어느 정도 유사하며, 일반 알고리즘과 특정 컨텍스트를 분리할 수 있습니다. 그러나 전략 패턴이 ​​알고리즘을 위임하는 아이디어를 채택하는 경우 그러면 전통적인 템플릿 메서드 패턴상속에 더 기반을 두게 됩니다. .

템플릿 메서드 패턴의 정의를 살펴보겠습니다.

  • 알고리즘에서 작업 프레임워크를 정의하고 일부 단계를 하위 클래스로 연기하여 하위 클래스가 알고리즘의 구조를 변경하지 않고도 알고리즘의 특정 특정 단계를 재정의할 수 있도록 합니다.

전략 패턴과 달리 템플릿 메소드 패턴의 행동 알고리즘은 더 명확한 개요 구조를 갖고 있으며, 정확히 동일한 단계가 추상 클래스에서 구현되고 개인화할 수 있는 특정 단계가 하위 클래스에 정의됩니다.

예를 들어, 우리가 문제를 처리하기 위해 시민센터에 갈 때 일반적으로 다음과 같은 구체적인 단계를 따릅니다.

  • 1) 번호를 받기 위해 줄을 서서 기다리세요.
  • 2) 사회 보장 목록 취득, 시민권 신청, 부동산 증명서 신청 등 귀하의 필요에 따라 맞춤형 서비스를 처리합니다.
  • 3) 서비스 직원의 태도를 평가하십시오.

이는 템플릿 방식 모델을 적용할 수 있는 일반적인 시나리오로 전체 서비스 단계는 알고리즘 개요로, 1)단계와 3)단계는 동일한 알고리즘이지만 2)단계는 실제 필요에 따라 맞춤화할 수 있습니다. 다음으로 코드를 사용하여 이 예제의 작업 프레임워크를 정의하는 추상 클래스를 구현합니다.

abstract class CivicCenterTask {
    
    
    fun execute() {
    
    
        this.lineUp()
        this.askForHelp()
        this.evaluate()
    }
    private fun lineUp() {
    
    
        println("line up to take a number");
    }
    private fun evaluate() {
    
    
        println("evaluaten service attitude");
    }
    abstract fun askForHelp()
}

여기서 askForHelp 메소드는 추상 메소드입니다. 다음으로,CivicCenterTask 클래스를 상속할 특정 하위 클래스를 정의한 다음 추상 단계를 구현합니다.

class PullSocialSecurity: CivicCenterTask() {
    
    
    override fun askForHelp() {
    
    
        println("ask for pulling the social security")
    }
}
class ApplyForCitizenCard: CivicCenterTask() {
    
    
    override fun askForHelp() {
    
    
        println("apply for a citizen card")
    }
}

옮기다:

fun main() {
    
    
    val task = PullSocialSecurity()
    task.execute()
    
    val task2 = ApplyForCitizenCard()
    task2.execute()
}

Kotlin에서는 유사한 아이디어를 사용하여 전략 패턴을 변환하여 템플릿 메서드 패턴을 단순화할 수도 있습니다. 고차 함수를 사용하여 추상 부분을 전달합니다.

class CivicCenterTask {
    
    
    fun execute(askForHelp: () -> Unit) {
    
    
        this.lineUp()
        askForHelp()
        this.evaluate()
    }
    private fun lineUp() {
    
    
        println("line up to take a number");
    }
    private fun evaluate() {
    
    
        println("evaluaten service attitude");
    }
}

fun pullSocialSecurity() {
    
    
    println("ask for pulling the social security")
}
fun applyForCitizenCard() {
    
    
    println("apply for a citizen card")
}

fun main() {
    
    
    val task1 = CivicCenterTask()
    task1.execute(::pullSocialSecurity)

    val task2 = CivicCenterTask()
    task2.execute(::applyForCitizenCard)
}

고차 함수의 도움으로 템플릿 모드 패턴을 더 쉽게 구현할 수 있음을 알 수 있습니다.

연산자 오버로딩 및 반복자 패턴

때때로 우리는 동일한 유형의 많은 개체를 포함하는 특정 컨테이너 클래스를 정의합니다. hasNext, next, first 등과 같은 이 컨테이너 클래스의 객체에 대해 반복 메서드를 직접 제공하려는 경우, 그런 다음 반복자를 사용자 정의할 수 있습니다. 그러나 일반적으로 Java 표준 라이브러리는 java.util.Iterator 인터페이스를 제공하므로 반복자를 직접 구현할 필요는 없으며 컨테이너 클래스를 사용하여 인터페이스를 구현한 다음 필요한 반복자를 구현할 수 있습니다. 행동 양식.

이 디자인 패턴은 반복자 패턴으로, 핵심 기능은 순회와 구현을 분리하는 것이며 순회 중에 객체의 내부 표현을 노출할 필요가 없습니다.

구현 Iterator 인터페이스의 간단한 예:

data class Book(val name:String)

class Bookcase(books: List<Book>): Iterator<Book> {
    
    
    private val iterator: Iterator<Book> = books.iterator()
    override fun hasNext() = this.iterator.hasNext()
    override fun next() = this.iterator.next()
}

fun main() {
    
    
    val bookcase = Bookcase(
        listOf(
            Book("DiveintoKotlin"),
            Book("ThinkinginJava")
        )
    )
    while (bookcase.hasNext()) {
    
    
        println("Thebooknameis${
      
      bookcase.next().name}")
    }
}

Bookcase 객체는 List<Book> 인스턴스와 동일한 반복자를 가지므로 후자 반복자의 모든 메소드를 직접 호출할 수 있습니다.

물론 우리는 일반적으로 다음과 같이 보다 간결한 순회 인쇄 방법을 사용합니다.

for (book in bookcase) {
    
    
    println("The book name is ${
      
      book.name}")
}

반복자 메서드 오버로드

Kotlin에는 더 나은 솔루션이 있습니다. Kotlin에는 operator 키워드를 사용하여 많은 연산자 오버로딩 기능이 내장되어 있다는 매우 강력한 언어 기능이 있습니다.

Bookcase 클래스의 iterator 메소드를 오버로드하여 보다 효율적인 구문 버전을 구현할 수 있습니다.

data class Book(val name:String)

class Bookcase(val books: List<Book>) {
    
    
    operator fun iterator(): Iterator<Book> = this.books.iterator()
}

우리는 한 줄의 코드로 위의 모든 효과를 달성합니다. 아직 끝나지 않았습니다. Kotlin은 확장 기능도 지원하므로 모든 객체에 대해 내장된 반복자를 가질 수 있습니다.

확장 함수를 통한 반복자 메서드 오버로드

현재 Bookcase 클래스가 가져온 클래스이고 해당 소스 코드를 수정할 수 없다고 가정합니다. 아래에서는 확장 구문을 사용하여 Bookcase 클래스 객체는 반복 기능을 추가합니다:

data class Book(val name: String)
class Bookcase(val books: List<Book>) {
    
    }
operator fun Bookcase.iterator(): Iterator<Book> = books.iterator()

코드는 여전히 매우 간결합니다. 반복자의 논리를 더 효과적으로 제어하려면 object표현식을 통해 제어할 수도 있습니다.

operator fun Bookcase.iterator(): Iterator<Book> = object : Iterator<Book> {
    
    
    val iterator = books.iterator()
    override fun hasNext() = iterator.hasNext()
    override fun next() = iterator.next()
}

일반적으로 반복자 패턴은 흔히 사용되는 디자인 패턴은 아니지만 이를 통해 Kotlin의 확장 기능 적용과 연산자 오버로딩의 힘을 더 깊이 이해할 수 있습니다.

부분 기능을 사용하여 책임 사슬 모델 구현

간단히 말해서, 책임 체인 패턴의 목적은 요청의 송신자와 수신자 사이의 결합 관계를 피하고, 객체를 체인에 연결하고, 객체가 이를 처리할 때까지 이 체인을 따라 요청을 전달하는 것입니다.

이제 좀 더 구체적인 예를 들어보겠습니다. 전산학부 학생회는 학생회기금을 관리하며, 이는 각종 활동과 조직인력에 사용됩니다. 지출이 100위안 이내이면 각 지부의 승인을 받고, 100위안을 초과하면 총장의 동의가 필요하며, 500위안을 초과하는 경우에는 총장의 동의를 받아야 한다. 위안화를 취득하려면 대학 상담사인 Mr. Chen의 승인이 필요합니다. 또한, 대학 내에는 지원금 상한선이 1,000위안이라는 미고지 규정이 있는데, 이를 초과할 경우 기본적으로 지원이 거절된다.

물론 가장 간단한 방법을 사용if-else하여 자금 승인 요건을 충족할 수 있습니다. 그러나 열고 닫는 원리에 따라 논리를 분리해야 합니다. 다음으로, 책임 사슬 모델과 결합된 객체 지향 아이디어를 사용하여 프로그램을 설계하겠습니다.

data class ApplyEvent(val money: Int, val title: String)

interface ApplyHandler {
    
    
    val successor: ApplyHandler?
    fun handleEvent(event: ApplyEvent)
}

class GroupLeader(override val successor: ApplyHandler?): ApplyHandler {
    
    
    override fun handleEvent(event: ApplyEvent) {
    
    
        when {
    
    
            event.money <= 100 -> println("Group Leader handled application: ${
      
      event.title}")
            else -> when(successor) {
    
    
                    is ApplyHandler -> successor.handleEvent(event)
                    else -> println("Group Leader: This application cannot be handdle.")
                }
        }
    }
}

class President(override val successor: ApplyHandler?): ApplyHandler {
    
    
    override fun handleEvent(event: ApplyEvent) {
    
    
        when {
    
    
            event.money <= 500 -> println("President handled application: ${
      
      event.title}")
            else -> when(successor) {
    
    
                    is ApplyHandler -> successor.handleEvent(event)
                    else -> println("President: This application cannot be handdle.")
                }
        }
    }
}

class College(override val successor: ApplyHandler?): ApplyHandler {
    
    
    override fun handleEvent(event: ApplyEvent) {
    
    
        when {
    
    
            event.money > 1000 -> println("College: This application is refused.")
            else -> println("College handled application: ${
      
      event.title}.")
        }
    }
}

fun main() {
    
    
    val college = College(null)
    val president = President(college)
    val groupLeader = GroupLeader(president)
    groupLeader.handleEvent(ApplyEvent(10, "buy a pen"))
    groupLeader.handleEvent(ApplyEvent(200, "team building"))
    groupLeader.handleEvent(ApplyEvent(600, "hold a debate match"))
    groupLeader.handleEvent(ApplyEvent(1200, "annual meeting of the college"))
}

작업 결과:

Group Leader handled application: buy a pen.
President handled application: team building.
College handled application: hold a debate match.
College: This application is refused.

이 예에서는 학생회 장관을 대표하기 위해 GroupLeader, PresidentCollege의 세 가지 클래스를 선언합니다. , 지부장, 총장, 대학 모두ApplyHandler인터페이스를 구현합니다. 인터페이스에는 nullable 후속 객체successor와 애플리케이션 이벤트 처리를 위한 메서드handleEvent가 포함되어 있습니다. 처리를 위해 자금 신청 이벤트를 GroupLeader 객체에 전달하면 특정 자금 금액을 기준으로 신청을 successor 객체로 전송할지 여부를 판단하고, 또한 President 클래스에 의해 처리됩니다. 비유하자면 책임 메커니즘의 사슬이 최종적으로 형성됩니다.

이제 책임 체인의 메커니즘을 다시 생각해 보겠습니다. 전체 체인의 각 처리 링크에는 입력 매개변수에 대한 검증 표준이 있다는 것을 알 수 있습니다. 위의 예에서는 주로 자금 조달 이벤트 금액을 확인합니다. 요구 . 입력 매개변수가 책임 체인의 특정 링크의 유효 수신 범위 내에 있으면 해당 링크는 이에 대해 정상적인 처리 작업을 수행할 수 있습니다. 프로그래밍 언어에는 이러한 상황을 설명하는 특별한 용어인 "부분 함수"가 있습니다.

부분 함수 유형 구현: PartialFunction

부분 함수란 무엇입니까?

부분함수란 수학에서의 개념으로, 정의 영역에는 값 영역에 해당 값이 없는 일부 값이 정의 영역에 있을 수 있음을 의미합니다X /span>Y .

이해를 돕기 위해 부분 함수와 일반 함수를 비교할 수 있습니다. 일반 함수에서는 이 유형의 모든 값을 (Int) -> Unit과 같은 지정된 유형의 매개변수에 전달할 수 있으며 모든 Int 값을 받을 수 있습니다. 부분 함수에서는 입력 유형의 매개변수 값을 반드시 수신할 필요는 없습니다. 예:

fun mustGreaterThan5(x: Int): Boolean {
    
    
    if (x > 5) return true
    else throw Exception("x must be greator than 5")
}

mustGreaterThan5(6)
>>> true

mustGreaterThan5(1)
>>> java.lang.Exception: x must be greator than 5 at Line57.mustGreaterThan5(Unknown Source) // 必须传入大于5的值

부분 함수가 언급되는 이유는 Scala와 같은 일부 함수형 프로그래밍 언어에는 책임 사슬 구현을 단순화하는 데 사용할 수 있는PartialFunction 유형이 있기 때문입니다. 무늬. Kotlin의 언어 기능은 유연하고 강력하기 때문에 표준 라이브러리는 PartialFunction를 지원하지 않지만 일부 오픈 소스 라이브러리에서는 이 기능을 구현했습니다. PartialFunction유형을 정의하는 방법을 살펴보겠습니다.

class PartialFunction<in P1, out R>(
    private val defineAt : (P1) -> Boolean,
    private val f : (P1) -> R
) : (P1) -> R {
    
    

    override fun invoke(p1 : P1) : R {
    
    
        if (defineAt(p1)) {
    
    
            return f(p1)
        } else {
    
    
            throw IllegalArgumentException("Value: ($p1) isn't supported by this function")
        }
    }

    fun isDefinedAt(p1: P1) = defineAt(p1)
}

PartialFunction수업의 구체적인 역할:

  • 이 클래스 객체를 선언할 때 두 개의 구성 매개변수를 수신해야 하며, 그 중 definetAt은 확인 기능이고 f는 처리 기능입니다. < /span>
  • PartialFunction 클래스 객체가 invoke 메소드를 실행할 때 definetAt는 입력 매개변수를 수정합니다< a i= 4>타당성 검증을 수행합니다.p1
  • 검증 결과가 통과되면 f 함수를 실행하고 p1를 매개변수로 전달하고, 그렇지 않으면 예외가 발생합니다.

PartialFunction클래스는 책임 체인 모델의 각 링크에서 입력 확인 및 처리 로직 문제를 해결할 수 있지만 여전히 해결해야 할 문제가 있습니다. 즉, 요청을 체인 전체에 전달하는 방법입니다.

다음으로 Kotlin의 확장 기능을 사용하여 메소드를 PartialFunction 클래스에 추가합니다. 그 전에 이 클래스의 메소드에 주목해보자. 사실 특별한 것은 없고 단지 내부적으로 복사하는 메소드일 뿐이다. 메소드에서 호출됩니다. orElseisDefinedAtdefinetAtorElse

infix fun <P1, R> PartialFunction<P1, R>.orElse(that: PartialFunction<P1, R>): PartialFunction<P1, R> {
    
    
    return PartialFunction({
    
     this.isDefinedAt(it) || that.isDefinedAt(it) }) {
    
    
        when {
    
    
            this.isDefinedAt(it) -> this(it)
            else -> that(it)
        }
    }
}

orElse 메소드에서는 책임 체인인 또 다른PartialFunction 클래스 객체that를 전달할 수 있습니다. 패턴.의 후계자입니다. isDefinedAt 메소드 실행 결과가 false이면 that 객체가 호출되어 애플리케이션 처리를 계속합니다.

여기서는 키워드 infix를 사용하여 orElse를 중위 함수로 만들어 체인 호출 구문을 더욱 직관적으로 만듭니다.

orElse로 책임 사슬을 구축하세요

다음으로 설계된 PartialFunction 클래스와 확장된 orElse 메소드를 사용하여 초기 예제를 다시 구현하겠습니다.

val groupLeader = run {
    
    
    val definetAt: (ApplyEvent) -> Boolean = {
    
     it.money <= 200 }
    val handler: (ApplyEvent) -> Unit = {
    
     println(" groupLeader ... ") }
    PartialFunction(definetAt, handler)
}

val president = run {
    
    
    val definetAt: (ApplyEvent) -> Boolean = {
    
     it.money <= 500 }
    val handler: (ApplyEvent) -> Unit = {
    
     println(" president ... ") }
    PartialFunction(definetAt, handler)
}

val college = run {
    
    
    val definetAt: (ApplyEvent) -> Boolean = {
    
     true }
    val handler: (ApplyEvent) -> Unit = {
    
     println(" college ... ") }
    PartialFunction(definetAt, handler)
}

그런 다음 다음과 같이 호출하십시오.

fun main() {
    
    
    val applyChain = groupLeader orElse president orElse college
    applyChain(ApplyEvent(10, "buy a pen"))
    applyChain(ApplyEvent(200, "team building"))
    applyChain(ApplyEvent(600, "hold a debate match"))
    applyChain(ApplyEvent(1200, "annual meeting of the college"))
}

더 나은 문법적 표현을 위해 여기를 사용orElse했습니다.

ADT는 상태 패턴을 구현합니다.

상태 모드와 전략 모드 사이에는 몇 가지 유사점이 있는데 둘 다 특정 알고리즘과 비즈니스 로직의 전환을 실현할 수 있습니다. 다음은 상태 모드의 정의입니다.

  • 상태 패턴을 사용하면 내부 상태가 변경될 때 객체의 동작을 변경하여 객체가 해당 클래스를 수정하는 것처럼 보이게 할 수 있습니다.

상태 모드는 구체적으로 다음에 반영됩니다.

  • 상태는 동작을 결정하고 객체의 동작은 내부 상태에 따라 결정됩니다.
  • 런타임 중에 객체의 상태가 변경되면 그에 따라 동작도 변경됩니다. 표면적으로는 클래스가 수정된 것처럼 동일한 객체가 실행 시간에 따라 다르게 동작합니다.

전략 모드와 다시 비교해 보면 두 모드의 차이점도 알 수 있습니다.

  • 전략 모드는 클라이언트 측의 다양한 전략 구현 간을 전환하여 알고리즘을 변경합니다.
  • 상태 패턴에서 객체는 내부 상태를 수정하여 다양한 동작 방법 간에 전환합니다.

정수기의 예를 살펴보겠습니다. 정수기에는 시작되지 않음, 냉각 모드, 가열 모드의 세 가지 작동 상태가 있다고 가정해 보겠습니다. Sealed 클래스를 사용하여 다양한 정수기 상태를 나타내는 ADT를 캡슐화할 수 있습니다.

class WaterMachine {
    
    
    var state : WaterMachineState = WaterMachineState.Off(this)
    fun turnHeating() {
    
    
        this.state.turnHeating()
    }
    fun turnCooling() {
    
    
        this.state.turnCooling()
    }
    fun turnOff() {
    
    
        this.state.turnOff()
    }
}

sealed class WaterMachineState(open val machine: WaterMachine) {
    
    
    class Off(override val machine: WaterMachine): WaterMachineState(machine)
    class Heating(override val machine: WaterMachine): WaterMachineState(machine)
    class Cooling(override val machine: WaterMachine): WaterMachineState(machine)

    fun turnHeating() {
    
    
        if (this !is Heating) {
    
     
            machine.state = Heating(machine)
            println("turn heating")
        } else {
    
    
            println("The state is already heating mode.")
        }
    }
    fun turnCooling() {
    
    
        if (this !is Cooling) {
    
     
            machine.state = Cooling(machine)
            println("turn cooling")
        } else {
    
    
            println("The state is already cooling mode.")
        }
    }
    fun turnOff() {
    
    
        if (this !is Off) {
    
     
            machine.state = Off(machine)
            println("turn off")
        } else {
    
    
            println("The state is already off.")
        }
    }
}

위의 ADT 데이터 구조를 사용하여 다음과 같은 요구 사항을 실현하십시오. Shaw는 아침에 출근할 때 정수기를 냉각 모드로 조정하고, 인스턴트 국수를 만들고 싶을 때 정수기를 가열 모드로 변경하므로 매일 라면을 먹을 시간 다음으로 물을 마시는 다음 동료는 다시 냉장실로 전환해야 합니다. 드디어 퇴근 시간이 되자 김씨는 정수기를 켠다.

enum class Moment{
    
    
    EARLY_MORNING,   // 早上上班
    DRINKING_WATER,  // 日常饮水
    INSTANCE_NOODLES,// Shaw吃泡面
    AFTER_WORK       // 下班
}

fun waterMachineOps(machine: WaterMachine, moment: Moment){
    
    
    when(moment){
    
    
        Moment.EARLY_MORNING,
        Moment.DRINKING_WATER -> machine.turnCooling()
        Moment.INSTANCE_NOODLES -> machine.turnHeating()
        Moment.AFTER_WORK -> machine.turnOff()
    }
}

fun main() {
    
    
    val machine = WaterMachine()
    waterMachineOps(machine, Moment.DRINKING_WATER)
    waterMachineOps(machine, Moment.INSTANCE_NOODLES)
    waterMachineOps(machine, Moment.DRINKING_WATER)
    waterMachineOps(machine, Moment.AFTER_WORK)
}

결과:

turn cooling
turn heating
turn cooling
turn off

구조적 패턴

데코레이터 패턴: 인터페이스 위임으로 상용구 코드 줄이기

Java에서는 동작을 클래스로 확장하려는 경우 일반적으로 두 가지 옵션이 있습니다.

  • 이를 상속받는 하위 클래스를 디자인합니다.
  • 데코레이터 패턴을 사용하여 클래스를 장식한 다음 기능을 확장합니다.

모든 상황이 클래스 확장 요구 사항을 충족하기 위해 상속을 사용하는 데 적합한 것은 아니므로("리히터 대체 원칙"을 따라야 함) 많은 경우 데코레이터 패턴이 이러한 문제를 해결하는 더 나은 방법이 됩니다.

데코레이터 패턴: 원본 클래스 파일을 변경하거나 상속을 사용하지 않고 객체의 기능을 동적으로 확장합니다. 이 패턴은 래퍼 객체를 생성하여 실제 객체를 래핑합니다.

요약하면 데코레이터 패턴은 다음 작업을 수행합니다.

  • 장식할 클래스의 인스턴스를 포함하는 장식 클래스를 만듭니다.
  • 장식된 클래스는 장식된 클래스의 모든 메서드를 재정의합니다.
  • 데코레이션 클래스에서 강화해야 할 기능을 확장합니다.

데코레이터 패턴의 가장 큰 장점은"구성이 상속보다 낫다" 특정 시나리오를 피하고 상속으로 인해 발생하는 문제. 그러나 모든 데코레이터 메서드를 재정의해야 하므로 상용구 코드가 많아질 수 있으므로 때로는 장황할 수 있습니다.

Kotlin에서는 by 키워드 위임 기능을 사용하여 장식된 클래스의 모든 메소드를 장식된 클래스 객체에 위임한 다음 필요한 메소드만 재정의하면 됩니다. 장식했습니다. 이제 데코레이터 패턴의 구현이 더욱 우아해졌습니다.

interface MacBook {
    
    
    fun getCost(): Int
    fun getDesc(): String
    fun getProdDate(): String
}
class MacBookPro: MacBook {
    
    
    override fun getCost() = 10000
    override fun getDesc() = "Macbook Pro"
    override fun getProdDate() = "Late 2011"
}
// 装饰类
class MacBookUpgrade(val macBook: MacBook) : MacBook by macBook {
    
    
    override fun getCost() = macBook.getCost() + 219
    override fun getDesc() = macBook.getDesc() + ", + 1G Memory"
}

fun main() {
    
    
    val macBookPro = MacBookPro()
    val macBookUpgrade = MacBookUpgrade(macBookPro)
    println(macBookUpgrade.getCost())
    println(macBookUpgrade.getDesc())
}

코드에 표시된 대로 MacBook Pro을 나타내는 클래스를 생성합니다. 이 클래스는 각각 예산을 나타내는 MacBook 인터페이스의 세 가지 메서드를 구현합니다. , 모델 정보 및 생산 연도.

본래 메모리 구성MacBook이 부족하다고 생각되면 메모리 1G를 추가하는 등 업그레이드가 필요합니다. 이때 구성 정보와 예산 방법은 영향을 받다. .

따라서 Kotlin의 클래스 위임 구문을 통해 인터페이스의 모든 메소드를 구성 매개변수 객체에 위임하는 MacBookUpgrade 클래스를 구현합니다.. MacBookmacbook

따라서 재정의 구문을 통해 변경해야 하는 costgetDesc 메서드만 재정의하면 됩니다. 생산 연도는 변경되지 않으므로 다시 작성할 필요가 없습니다. MacBookUpgrade 클래스는 장식된 개체의 getProdDate 메서드를 자동으로 호출합니다.

일반적으로 Kotlin은 클래스 위임을 통해 데코레이터 패턴의 상용구 코드를 줄입니다. 그렇지 않으면 Macbook 클래스를 상속하지 않고 데코레이션 클래스와 공개 부모 추상 클래스를 만들어야 합니다. 장식된 수업의.

데코레이터를 확장 기능으로 대체

class Printer {
    
    
    fun drawLine() {
    
    
        println("————————")
    }
    fun drawDottedLine() {
    
    
        println("- - - - -")
    }
    fun drawStars() {
    
    
        println("********")
    }
}

여기서 실선, 점선, 별표를 각각 그릴 수 있는 세 가지 그리기 방법이 있는 그리기 클래스Printer를 정의합니다.

이제 우리는 전체 드로잉 프로세스를 표시하기 위해 각 드로잉의 시작과 끝 부분에 텍스트 설명을 포함하는 새로운 요구 사항이 있습니다.

한 가지 아이디어는 각 그리기 방법을 새로운 기능으로 장식하는 것입니다. 그러나 이것은 확실히 중복되어 보일 것입니다. 특히 앞으로는Printer 클래스가 다른 그리기 방법을 추가할 수도 있습니다. 이는 우아한 디자인 아이디어가 아닙니다. .

더 나은 솔루션을 제공하기 위해 클래스를 장식하는 대신 확장을 사용하는 방법을 살펴보겠습니다.

fun Printer.startDraw(decorated: Printer.() -> Unit) {
    
    
    println("+++ start drawing +++")
    this.decorated()
    println("+++ end drawing +++")
}

fun main() {
    
    
    Printer().run {
    
    
        startDraw {
    
     drawLine() }
        startDraw {
    
     drawDottedLine() }
        startDraw {
    
     drawStars() }
    }
}

이전에 소개된 방법run을 기억하시나요? lambda 함수를 매개변수로 받아서 클로저 형태로 반환하며, 반환값은 마지막 행의 값 또는 지정된 return 표현식의 값입니다. run 구문과 결합하면 우리의 요구 사항을 더욱 우아하게 달성할 수 있습니다.

요약하다

디자인 패턴 Kotlin의 솔루션 주목
팩토리 메소드 패턴 비례 object 类 + invoke 重载
반생 우상 companion object + invoke 重载
반생애 상보전 방법
창조 패턴
추상 공장 패턴 내부함수inline+reified 창조 패턴
빌더 패턴 명명된 선택적 매개변수 + require 메소드 제약조건 창조 패턴
관찰자 패턴 Delegates.Observable 위임 구문
Delegates.Vetoable 위임 구문
행동 패턴
전략 패턴 고차 함수(::함수 참조 구문) 행동 패턴
템플릿 메소드 패턴 고차 함수(::함수 참조 구문) 행동 패턴
반복자 패턴 연산자 오버로딩 iterator
확장 함수 오버로딩 iterator
행동 패턴
책임 사슬 모델 모방 부분 기능 행동 패턴
상태 모드 ADT(대수적 데이터 유형) 활용 행동 패턴
데코레이터 패턴 인터페이스 위임 by 구문
확장 기능
구조적 패턴

Guess you like

Origin blog.csdn.net/lyabc123456/article/details/135039868