Github : https://github.com/junghun9102/DesignPatternTemplate

branch : feature/observer

 

 

junghun9102/DesignPatternTemplate

Contribute to junghun9102/DesignPatternTemplate development by creating an account on GitHub.

github.com

 

 

날씨 정보(WeatherData)의 변경에 따라

WeatherBroadcast에서 변경된 날씨를 예보하고

WeatherStatistics에서 데이터를 분석하기 위해 저장하고 누적데이터를 보여주려한다.

 

 


Commit 708033d9

 

WeatherBroadcast

 - update, broadcast

 

WeatherStatistics

 - update, collectData, showCumulativeData

 

WeatherData

 - setMeasurements, mesurementsChanged

 

전체 코드보다 위의 함수에만 집중하면 된다.

 

 

class WeatherBroadcast {

    private lateinit var weatherData: WeatherData

    fun update(weatherData: WeatherData) {
        this.weatherData = weatherData

        broadcast()
    }

    private fun broadcast() {
        StringBuilder()
            .append("Current Weather is ")
            .append("Temperature(").append(weatherData.temperature).append("), ")
            .append("Humidity(").append(weatherData.humidity).append("), ")
            .append("Pressure(").append(weatherData.pressure).append(")")
            .let(::println)
    }
}

 

class WeatherStatistics {

    private val temperatureData: HashMap<Long, Float> = HashMap()
    private val humidityData: HashMap<Long, Float> = HashMap()
    private val pressureData: HashMap<Long, Float> = HashMap()

    fun update(weatherData: WeatherData) {
        collectData(weatherData)
    }

    private fun collectData(weatherData: WeatherData) {
        val currentTime = System.currentTimeMillis()
        temperatureData[currentTime] = weatherData.temperature
        humidityData[currentTime] = weatherData.humidity
        pressureData[currentTime] = weatherData.pressure

        showCumulativeData()
    }

    private fun showCumulativeData() {
        StringBuilder()
            .append("========== Cumulative Weather Data ==========\n")
            .append("temperature : ").append(temperatureData).append('\n')
            .append("humidity : ").append(humidityData).append('\n')
            .append("pressure : ").append(pressureData).append('\n')
            .let(::println)
    }

    private fun getDateStr(time: Long) = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(Date(time))
}

 

 

WeatherBroadcast와 WeatherStatistics는 weatherData 변경에 따라 추가적인 일을 하려한다.

 

update가 되면

WeatherBroadcast는 방송하고(broadcast)

WeatherStatistics는 데이터를 누적하고(collectData)누적데이터를 보여준다(showCumulativeData).

 

 

 

 

class WeatherData(
    private val weatherBroadcast: WeatherBroadcast,
    private val weatherStatistics: WeatherStatistics
) {

    var temperature: Float = 0f
        private set
    var humidity: Float = 0f
        private set
    var pressure: Float = 0f
        private set

    fun setMeasurements(temperature: Float, humidity: Float, pressure: Float) {
        this.temperature = temperature
        this.humidity = humidity
        this.pressure = pressure

        measurementsChanged()
    }

    private fun measurementsChanged() {
        weatherBroadcast.update(this)
        weatherStatistics.update(this)
    }

    override fun toString() = "t $temperature, h $humidity, p $pressure"

}

 

 

WeatherData는 측정된 데이터를 받아 변경하고

WeatherBroadcast와 WeatherStatistics에게 변경된 데이터를 알려준다.

 

 

잘 작동하지만 WeatherData는 누구에게 데이터를 전해줘야할지 알고 있어야한다.

강한 의존성이 생겼다.

 

 

만약 설계상 추가적으로 데이터를 전달할 사용자가 생긴다면?

-> 계속해서 WeatherData에 손을 대야한다.

 

만약 동적으로 데이터를 받고 싶거나 받고 싶지 않은 사용자가 생긴다면?

-> 사용자를 nullable하게 만들어 처리할 수 있으나 사용자가 명확히 정해져야 한다.

 

 

 


Commit d1151ce0

 

interface Observer<T> {
    fun update(data: T)
}

 

WeatherForecast와 WeatherStatistics의 공통적인 코드를 Interface로 정의했다.

 

 

interface Subject<T> {
    val observers: MutableList<Observer<T>>

    fun registerObserver(observer: Observer<T>)
    fun removeObserver(observer: Observer<T>)
    fun notifyObservers()
}

 

WeatherData에서 사용자들을 각각 구현체로 가지고 있던 부분을 observers로 묶고

동적으로 새로운 사용자 등록하고 제거할 수 있는 함수를 만들었다.

또 기존에 사용자들에게 각각 update했던 부분을 notifyObservers로 만들었다.

 

 

 

날씨 정보를 사용하는 사용자를 Observer, 즉 관찰자라고 할 수 있다. WeatherBroadcast, WeatherStatistics

날씨 정보는 Subject, Observable(관찰할 수 있는) 등으로 부른다. WeatherData

 

JavaAPI에서 Observable과 Observer가 있었으나 deprecated 되었다.

 

 

 

 

class WeatherBroadcast(weatherData: Subject<WeatherData>) : Observer<WeatherData> {

    init {
        weatherData.registerObserver(this)
    }

    private lateinit var weatherData: WeatherData

    override fun update(data: WeatherData) {
        this.weatherData = data

        broadcast()
    }

    private fun broadcast() {
        StringBuilder()
            .append("Current Weather is ")
            .append("Temperature(").append(weatherData.temperature).append("), ")
            .append("Humidity(").append(weatherData.humidity).append("), ")
            .append("Pressure(").append(weatherData.pressure).append(")")
            .let(::println)
    }
}

 

class WeatherStatistics(weatherData: Subject<WeatherData>) : Observer<WeatherData> {

    init {
        weatherData.registerObserver(this)
    }

    private val temperatureData: HashMap<Long, Float> = HashMap()
    private val humidityData: HashMap<Long, Float> = HashMap()
    private val pressureData: HashMap<Long, Float> = HashMap()

    override fun update(data: WeatherData) {
        collectData(data)
    }

    private fun collectData(weatherData: WeatherData) {
        val currentTime = System.currentTimeMillis()
        temperatureData[currentTime] = weatherData.temperature
        humidityData[currentTime] = weatherData.humidity
        pressureData[currentTime] = weatherData.pressure

        showCumulativeData()
    }

    private fun showCumulativeData() {
        StringBuilder()
            .append("========== Cumulative Weather Data ==========\n")
            .append("temperature : ").append(temperatureData).append('\n')
            .append("humidity : ").append(humidityData).append('\n')
            .append("pressure : ").append(pressureData).append('\n')
            .let(::println)
    }

}

 

class WeatherData : Subject<WeatherData>{

    override val observers: MutableList<Observer<WeatherData>> = mutableListOf()

    var temperature: Float = 0f
        private set
    var humidity: Float = 0f
        private set
    var pressure: Float = 0f
        private set

    fun setMeasurements(temperature: Float, humidity: Float, pressure: Float) {
        this.temperature = temperature
        this.humidity = humidity
        this.pressure = pressure

        measurementsChanged()
    }

    private fun measurementsChanged() {
        notifyObservers()
    }

    override fun toString() = "t $temperature, h $humidity, p $pressure"

    override fun registerObserver(observer: Observer<WeatherData>) {
        observers.add(observer)
    }

    override fun removeObserver(observer: Observer<WeatherData>) {
        observers.remove(observer)
    }

    override fun notifyObservers() {
        observers.forEach {
            it.update(this)
        }
    }

}

 

 

WeatherForcast와 WeatherStatistics는 생성자에

Subject를 받아 등록하는 것 외에는 동일하게 동작한다.

 

 

WeatherData는 동적으로 사용자를 추가 제거할 수 있는 대가로 코드가 조금 늘었다.

하지만 더 이상 누가 사용하는지 알지 못한다.

특정 구현체를 알지 못하기 때문에 영향을 받지 않는다.

 

 

 


옵저버 패턴

한 객체가 바뀌면 그 객체에 의존하는 다른 객체들한테

연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.

 

 

 

'SoftwareDesign' 카테고리의 다른 글

SRP  (0) 2020.03.01
SOLID 개요  (0) 2020.03.01
2. Strategy  (0) 2020.01.07
1. Singleton  (0) 2019.12.25
DesignPattern 왜 알아야 할까?  (0) 2019.12.23

 

Github : https://github.com/junghun9102/DesignPatternTemplate

branch : feature/strategy

 

junghun9102/DesignPatternTemplate

Contribute to junghun9102/DesignPatternTemplate development by creating an account on GitHub.

github.com

 

이번에는 Strategy 패턴이다.

Strategy는 직역하면 전략인데 어떤 패턴일까?

 

다양한 오리들을 가지고 있는 시스템을 만들고 있다.

청둥오리, 고무오리, 미끼용 오리 등 다양한 오리들을 만들어보려고 한다.

 


Commit 4ee19cf6

 

abstract class Duck {

    open fun quack() {
        println("quack! quack!")
    }

    fun swim() {
        println("swimming")
    }

    open fun fly() {
        println("I believe i can fly")
    }

    abstract fun display()

}
class MallardDuck : Duck() {
    override fun display() {
        println("It seems like MallardDuck")
    }
}

 

대부분의 오리들은 "꿱"하고 소리를 낼 것이며 수영을 할 것이고 날 것이다.

단 모두 다르게 생겼기 때문에 display만 추상 메소드로 만들었다.

 

모든 오리들이 위의 형태를 만족하면 좋았겠지만 그렇지 않은 오리가 발생했다.

 

고무 오리는 소리를 낼 수 있고 수영할 수 있지만 날 수 없다.

미끼용 나무 오리(DecoyDuck)는 소리를 낼 수 없고 날 수 없다.

 

 

 

class RubberDuck : Duck() {
    override fun display() {
        println("It seems like RubberDuck")
    }

    override fun fly() {

    }
}
class DecoyDuck : Duck() {
    override fun display() {
        println("It seems like DecoyDuck")
    }

    override fun quack() {

    }

    override fun fly() {

    }
}

 

고무오리는 날 수 없기 때문에 fly 메소드를 아무행동을 하지 않도록 오버라이딩했고

미끼용 나무 오리는 quack, fly 메소드를 빈 몸통으로 오버라이딩했다.

일단은 원하는데로는 동작한다.

 

하지만 날지못하는 오리가 더 늘어난다면 날지 못하도록 모두 오버라이딩해야하고 중복코드가 발생한다. 

"날지못한다"처럼 몸통을 비워 처리해서 중복코드가 많이 없어 보이지만

많약 예외 케이스의 코드양이 많다면 중복코드 양이 늘어날 것이다.

 

또한 오버라이딩을 통해 많은 서브클래스에서 처리한다면 신경쓸 범위가 늘어나는 것이다.

 

변경은 어쩔 수 없이 일어날 수 밖에 없다.

어떻게 해야 그 변경을 잘 받아들일 수 없는 구조를 만들 수 있을까?

 

 

 


Commit cf0b9c56

 

 

문제를 해결하기 위해서는 변경이 일어나는 부분과 그렇지 않을 부분을 구분하는 것에서 시작한다.

 

변경이 일어나는 부분 : quack, fly

변경이 일어나지 않는 부분 : swim, display

 

1. 변경이 일어나는 코드를 각각 캡슐화 한다.

2. 인터페이스를 만들고 이를 상속해 클래스 집합을 만든다.

3. 그리고 구성을 통해 가지고 있으며 변경이 일어나는 메소드에서 사용한다.

4. 하위 클래스에서는 생성자에서 초기화하거나 중간에 동적으로 변경하거나 유연하게 사용할 수 있다.

 

Class Diagram

 

abstract class Duck {

    lateinit var quackBehavior: QuackBehavior
    lateinit var flyBehavior: FlyBehavior

    fun quack() {
        quackBehavior.quack()
    }

    fun fly() {
        flyBehavior.fly()
    }

    fun swim() {
        println("swimming")
    }

    abstract fun display()

}

부모 클래스는 이런식으로 수정이 되었다.

 

interface FlyBehavior {
    fun fly()
}
class FlyWithWings : FlyBehavior {
    override fun fly() {
        println("fly with wings")
    }
}
class FlyNoWay : FlyBehavior {
    override fun fly() {

    }
}

하나의 클래스 집합

 

class MallardDuck : Duck() {

    init {
        quackBehavior = Quack()
        flyBehavior = FlyWithWings()
    }

    override fun display() {
        println("It seems like MallardDuck")
    }
}
class DecoyDuck : Duck() {

    init {
        quackBehavior = MuteQuack()
        flyBehavior = FlyNoWay()
    }

    override fun display() {
        println("It seems like DecoyDuck")
    }
}

초기화하여 사용한다.

 

 

기능이 추가되어도 Interface를 상속받아 만들어 사용할 수 있다.

그 기능을 여러 곳에서 사용하여도 중복코드없이 구성할 수 있다.

 

또한 알고리즘이 변경이 되어도 사용하는 부분에 영향없이 수정이 가능한 장점이 있다.

 

 


스트래티지 패턴

알고리즘군을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만든다.

스트래티지를 활용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.

 

 

 

 

 

 

'SoftwareDesign' 카테고리의 다른 글

SRP  (0) 2020.03.01
SOLID 개요  (0) 2020.03.01
3. Observer  (0) 2020.01.19
1. Singleton  (0) 2019.12.25
DesignPattern 왜 알아야 할까?  (0) 2019.12.23

 

Github : https://github.com/junghun9102/DesignPatternTemplate

branch : feature/singleton

 

junghun9102/DesignPatternTemplate

Contribute to junghun9102/DesignPatternTemplate development by creating an account on GitHub.

github.com

 

싱글톤은 가장 쉽게 접할 수 있는 디자인 패턴이라고 생각한다.

 

1. 인스턴스를 하나만 가져야 할 때

2. 인스턴스를 하나를 초과해 만들 필요가 없을 때

사용한다.

 

예시 프로젝트로 세계관을 가진 게임을 만드려고 한다.

이 세계에는 유일신만이 존재한다.

그렇기 때문에 God이라는 클래스의 인스턴스는 하나만 존재해야한다.

 


Commit c5019208

 

class God private constructor(private val index: Int) {

    companion object {
        private var uniqueInstance: God? = null

        fun getInstance(): God {
            uniqueInstance ?: run {
                uniqueInstance = God(count++)
            }

            return uniqueInstance!!
        }

        private var count = 0
    }

    fun bless(townName: String) {
        println("$index God bless $townName")
    }
}

 

짠! 가장 기본적으로 많이 보는 싱글톤 코드라고 생각한다.

나만 이렇게 사용했을 수도 있지만...

 

가장 기본적인 원칙

1. 여러개의 인스턴스를 만들 수 없게 생성자를 제한한다.

2. 인스턴스를 받을 수 있는 메소드에서 하나의 인스턴스만 만들어 리턴해준다.

 

전혀 문제될 부분이 없어 보인다.

물론 나도 그런줄 알고 이렇게 사용해 왔고..

 

 

 

하지만 이 코드는 인스턴스를 둘 이상 만들 수 있다.

 

class World {
    private val aTown = Town("ATown")
    private val bTown = Town("BTown")

    fun run() {

        Thread {
            aTown.pray()
        }.start()

        Thread {
            bTown.pray()
        }.start()
    }
}
class Town(private val name: String) {
    fun pray() {
        God.getInstance().bless(name)
    }
}

 

두 개 이상의 스레드에서 동시에 getInstance를 호출하면

 

와 같이 두개의 인스턴스가 만들어 지는 경우가 있다.

 

두 스레드가 동시에 null 체크를 통과할 수 있기 때문이다.

 

물론 비동기를 사용하지 않는다면 발생하지 않는다.

 

 

 


Commit 25874942

 

class God private constructor(private val index: Int) {

    companion object {
        private var uniqueInstance: God? = null

        @Synchronized
        fun getInstance(): God {
            uniqueInstance ?: run {
                uniqueInstance = God(count++)
            }

            return uniqueInstance!!
        }

        private var count = 0
    }

    fun bless(townName: String) {
        println("$index God bless $townName")
    }
}

 

딱 한 줄이 늘었다.

 

Synchronized 키워드를 통해

동시 접근할 수 없게 만들었기 때문에 인스턴스가 두개 만들어질 걱정은 없다.

 

하지만 동기화 키워드의 값을 비싸다.

그리고 처음 초기화를 위해 동기화 키워드가 필요로 하지만

초기화 이후에도 동기화로 인해 손해를 보고 있다.

 

 


Commit 18eff495

 

class God private constructor(private val index: Int) {

    companion object {

        @Volatile
        private var uniqueInstance: God? = null

        fun getInstance() = uniqueInstance ?: run {
            synchronized(God) {
                uniqueInstance ?: run {
                    uniqueInstance = God(count++)
                }
                uniqueInstance!!
            }
        }

        private var count = 0
    }

    fun bless(townName: String) {
        println("$index God bless $townName")
    }
}

 

null일 때만 동기화 블록에서 동시에 초기화할 수 없게 막아두었다.

 

이를 DCL(Double-Checking Locking)이라고 한다.

여기까지가 Head First Design Pattern에 나오는 Singleton이다.

 

 

하지만 싱글톤에 대해 찾아보다 DCL 역시 자바에서 문제가 생길 수 있다는 포스팅을 보았다.

 

uniqueInstance = God(count++)

 

uniqueInstance 에는 주소값이 할당되었지만 God 생성자가 돌지 않은 상태에서

다른 스레드가 getInstance를 호출해 not-null 상태인 uniqueInstance를 리턴하지만 제대로 된 객체를 받지 못한다는 이야기였다.

이에 관해 깊은 이해가 없어 마지막에 링크를 첨부했다.

 

  


Commit 42282b09

 

class God private constructor(private val index: Int) {

    companion object {

        private var count = 0

        private var uniqueInstance: God = God(count++)

        fun getInstance() = uniqueInstance
    }

    fun bless(townName: String) {
        println("$index God bless $townName")
    }
}

 

그렇기 때문에 getInstance 자체에 동기화를 거는 방식을 택하거나

위의 코드와 같이 전역변수를 선언하고 선언과 함께 초기화하는 방식으로 사용하면 된다.

 

사실 멀티스레드에서 싱글톤 객체를 경쟁적으로 호출하거나

늦은 초기화를 하는게 이점이 될만큼 무거운 싱글톤 객체를 사용해본 적이 없어

어떤 방식이든 상관이 없긴했다.

 

 

 

 


 

자바에서 DCL에서 생기는 문제점 포스팅

https://gampol.tistory.com/entry/Double-checked-locking%EA%B3%BC-Singleton-%ED%8C%A8%ED%84%B4

 

Double-checked locking과 Singleton 패턴

난이도 : 초급 Peter HaggarIBM 2002 년 5 월 01 일 2003 년 1 월 07 일 수정 모든 프로그래밍 언어에는 고유의 이디엄이 있다. 이중 대부분이 유용하다. 문제는 몇몇 이디엄의 경우 원래 표명했던 것이 아니라는..

gampol.tistory.com

 

Volatile 포스팅

https://nesoy.github.io/articles/2018-06/Java-volatile

 

Java volatile이란?

 

nesoy.github.io

 

 

 

 

'SoftwareDesign' 카테고리의 다른 글

SRP  (0) 2020.03.01
SOLID 개요  (0) 2020.03.01
3. Observer  (0) 2020.01.19
2. Strategy  (0) 2020.01.07
DesignPattern 왜 알아야 할까?  (0) 2019.12.23

+ Recent posts