DIP (Dependency Inversion Principle; 의존 역전 법칙)

프로그래머는 “추상화에 의존해야지, 구체화에 의존하면 안된다.". DI(의존성 주입) 역시 이 원칙을 따르는 방법 중에 하나이다.

 

 

 


구현체에 의존

 

 

 

메인 화면과 메모 상세 화면에서 메모 데이터를 조작하고 싶다.

그래서 MainPresenter와 MemoDetailPresenter는 MemoLocalRepository를 가지고 있다.

 

로컬 데이터베이스(임베디드 데이터베이스)를 사용하여 앱을 만드려한다.

MemoLocalRepository는 *RoomDatabase에서 메모 데이터를 받아오도록 작성했다.

*RoomDatabase : Android 임베디드 데이터베이스

 

 

 

개발도중 서버를 사용하는 것으로 요구사항이 변경되었다.

 

이로 인해 MemoLocalRepository를 고쳐야했고 이를 사용중이던 MainPresenter와 MemoDetailPresenter 역시 수정이 필요했다. (의존성)

 

 

 

 


DIP 적용

 

 

위와 같은 일을 막기 위해 구조부터 변경했다.

MemoRepository를 인터페이스로 만들었다.

 

MemoLocalRepository는 MemoRepository 인터페이스상속받아 만들었고

MainPresenter와 MemoDetailPresenter는 MemoRepository 인터페이스로 가지고 있다.

 

사용하는 부분에서도 구현하는 부분에서도 추상화에만 의존하고 있다.

 

이로 인해 MainPresenter와 MemoDetailPresenter는

어디서 어떻게 받아오는지 상관없이 인터페이스 함수대로 데이터를 조작할 수 있다.

 

MemoLocalRepository 대신 MemoRemoteRepository로 변경하던 MemoDummyRepository로

변경하던 상관없이 MemoRepository에만 의존성을 가지고 있으면 된다.

 

 

 

 

 


DI

 

MemoPresenter는 MemoRepsotiroy 인터페이스에만 의존하면 된다.

그럼 MemoRepository는 어디서 어떻게 생성해주지?

 

어디선가 MemoLocalRespository를 생성해서 전달해준다면 

그 생성한 클래스는 결국 구현체에 의존하게 되는데 조삼모사가 아닌가?

 

그래서 DI가 존재한다.

 

전문적으로 생성해서 전달해주는 모듈이 있고

구현체를 몰라도 모듈에 설정한대로 외부에서 구현체를 생성해 전달해주어

사용하는 부분은 구현체에 의존성없이 사용할 수 있다.

 

class MainPresenter(
    private val memoRepository: MemoRepository
)

private val presenter: MainPresenter by inject()

 

위와 같은 코드 형태이다.

by inject()를 통해 외부에서 인스턴스를 만들어 준다.

 

어떤 구현체의 인스턴스인지 명시하지 않고 사용할 수 있어 의존성이 전혀 없다. 

 

DI 역시 DIP의 방법 중에 하나이고

DI를 통해 DIP를 더 잘 지킬 수 있다.

 

 

 

 

 


외부 포스팅

 

좋은 DIP 포스팅

https://medium.com/@lightsoo/dependency-inversion-principle-52a7bb6dd6be

 

Dependency Inversion Principle

본부장님의 클린코드 강의중 Function Structure를 공부하면서 DIP에 대한 설명을 듣는데 이해가 잘 되지않아서 다시 위의 내용을 번역 + DIP강의를 듣고 정리한 문서이다.

medium.com

 

 

 

'SoftwareDesign' 카테고리의 다른 글

ISP  (0) 2020.03.01
LSP  (0) 2020.03.01
OCP  (0) 2020.03.01
SRP  (0) 2020.03.01
SOLID 개요  (0) 2020.03.01

 

 

 

ISP (Interface Segregation Principle; 인터페이스 분리 법칙)

특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.

 

 

 

 


범용 인터페이스

 

캐셔가 필요하지 않은 기능들을 포함한 계산기를 사용하고 있다.

 

 

 


인터페이스 분리

 

 

인터페이스를 작은 단위로 분리함으로 캐셔에게만 맞는 계산기와

신입생에거 필요한 계산기를 인터페이스를 가지고 있을 수 있게 되었다.

 

인터페이스를 작은 단위로 만들게 되면 사용성이나 확장성이 좋아진다.

 

 

 


FIN

 

사실 인터페이스를 범용으로 쓸 일이 없었다.

인터페이스를 사용보다 크게 만들어 문제되는 일도 없었다.

 

다른 포스팅들을 더 읽어보니

'SRP와 같이 인터페이스가 하나의 책임을 가져야 한다'

설명하는 경우가 지배적이다.

 

interface SmartPhoneFunction {
    
    fun call()
    fun sms()
    fun showInternet()
    
}

class SmartPhone : SmartPhoneFunction

 

interface CallFunction {
    fun call()
}

interface SMSFunction {
    fun sms()
}

interface InternetFunction {
    fun showInternet()
}

class SmartPhone : CallFunction, SMSFunction, InternetFunction

 

와 같이 분리하는 예시를 많이 보여준다.

 

이 예시가 더 잘 드러나 보인다.

 

하지만 위에 계산기 예시 역시

NormalCalculator는 기본적인 계산기의 책임을 다하고

ScientificCalculator는 공학 계산기의 책임을 다하는

SRP와 같은 맥락의 예시이다.

 

 

결론은 '인터페이스를 하나의 책임을 갖도록 분리해라.' 가 가장 적합해보인다.

 

 

SOLID에 대해 포스팅하면서 책임이라는 단어가 정말 많이 나온다.

 

그 만큼 중요하지만 책임의 크기를 정하는 것에 대해서도 고민해야할 것이다.

 

무조건 작고 많이 분리한다고 좋은 것도 아니다.

오히려 너무 많은 인터페이스와 클래스가 복잡도를 높일 수가 있다.

 

 

 

 

 

 

 

'SoftwareDesign' 카테고리의 다른 글

DIP  (0) 2020.03.01
LSP  (0) 2020.03.01
OCP  (0) 2020.03.01
SRP  (0) 2020.03.01
SOLID 개요  (0) 2020.03.01

 

 

 

LSP (Liskov Substitution Principle; 리스코브 치환 법칙)

프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.

 

위키의 설명을 보고 이해하려다가 혼란에 빠졌었다.

 

쉽게 설명해서

부모 클래스로 형변환하여 사용해도 아무런 이상없이 사용하면 된다.

자식 클래스 부모 클래스의 만든 전제 내에서 확장하면된다.

 

 

 

 


위반 예시

 

동물은 먹는다. (O)

고양이는 물고기를 먹는다. (O)

고양이 동물이다. (O)

동물은 물고기를 먹는다. (O)

 

직사각형은 가로 세로가 같던 다르던 상관없다. (O)

정사각형은 가로 세로가 같아야 한다. (O)

정사각형 직사각형이다. (O)

직사각형은 가로 세로가 같아야 한다. (X)

 

자식이 부모를 모순되게 만들지 말라는 원칙이다.

 

다형성을 통해 다양한 자식 클래스들을 부모클래스로 통합해 처리하는 로직이 있을 때

자식 중에 로직의 기대를 벗어나게 오버라이딩했다면 문제가 발생할 수 있다.

 

 

 

 


FIN

 

사실 모순을 발생하는 예시가 적으며

발생하더라도 문제가 생기지 않을 확률이 높다.

 

그렇게 신경쓰고 있어야할 원칙은 아닌듯 싶다.

 

 

 

 

'SoftwareDesign' 카테고리의 다른 글

DIP  (0) 2020.03.01
ISP  (0) 2020.03.01
OCP  (0) 2020.03.01
SRP  (0) 2020.03.01
SOLID 개요  (0) 2020.03.01

 

 

 

OCP (Open/Cloesed Priciple; 개방-폐쇄 원칙)

소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀있어야 한다.

 

 

SRP의 연장선에 있는 원칙이라고 생각한다.

 

SRP에 맞게 하나의 책임을 가지고 있는 클래스를 확장하여 새로운 책임에 집중한 클래스를 만들 수 있다. 

그렇게 되면 해당 클래스는 자신의 책임에 집중하고 다른 책임을 변경할 필요가 없다.

 

만약 변경이 일어날 수 있는 코드가 있다면 추상화를 통해 구현을 상속받는 클래스에 맡겨야 한다.

추상화한 코드로 인해 상위 클래스는 수정할 필요가 없고 하위 클래스는 구현을 통해 확장할 수 있다.

 

 

 

 

 


OCP 예시

 

 

 

이미지를 포함하고 있는 메모를 작성하는 안드로이드 코드이다.

 

 

메모를 작성할 때, 메모를 수정할 때 모두 이미지를 받아올 수 있어야 하기 때문에

중복된 코드를 GetImageActivity로 캡슐화했다.

 

물론 이미지를 필요로 하는 모든 액티비티에서 재사용 가능하다.

 

 

1. 이미지를 Gallery 또는 Camera에서 받아온다. (getImageFromGallery, getImageFromCamera)

2. 받아온 이미지를 적합한 사이즈로 가공한다. (workAfterGetFromGallery, workAfteGetFromCamera)

3. 가공한 이미지를 onGetImageFile를 호출해 전달한다.

 

 

GetImageActivity를 상속받는 클래스들은 이미지를 받아오는 책임에 대해 건드릴 필요가 없다. (폐쇄)

상속받아 이미지를 받아 어떻게 자신의 책임을 다 할지만 생각하면 된다. (개방)

 

그렇기 때문에

 

GetImageActivity의 책임과 관련된 메소드들 최대한 폐쇄해두었다.

getImageFromGallery, getImageFromCamera : FINAL

workAfterGetImageFromGallery, workAfterGetImageFromCamera : FINAL, PRIVATE

 

추상 메소드 onGetImageFile을 구현해서 자신의 책임에 대해 확장하도록 했다.

 

 

 

 


FIN

 

내가 포스팅한 내용은 상속하여 확장하는 클래스의 입장에서의 OCP이다.

 

다른 사람들의 포스팅을 보니 정말 많은 설명들이 있었다.

 

인터페이스를 사용한 방법, 추상 메소드를 사용한 방법

상속에서의 OCP, 조립에서의 OCP

 

많은 OCP의 예가 있으리라 생각하고

그 모든 것들이 OCP라고 생각한다.

 

초기 원칙을 정할 때 정해진 케이스가 있었을지 몰라도..

 

 

결국 OCP의 핵심은

 

 

상속을 통한 확장, 인터페이스를 구현한 클래스의 증가, 기능 변경, 알고리즘 교체

같은 어떤 확장을 하더라도 기존의 코드들이 영향을 받지 않는다는 것이다.

 

 

 

 

 

 

 

'SoftwareDesign' 카테고리의 다른 글

ISP  (0) 2020.03.01
LSP  (0) 2020.03.01
SRP  (0) 2020.03.01
SOLID 개요  (0) 2020.03.01
3. Observer  (0) 2020.01.19

 

 

 

SRP (Single Reponsibility Principle; 단일 책임 원칙)

 

한 클래스는 하나의 책임을 가져야 한다.

클래스가 변경되는 이유가 하나여야 한다.

 

 

 

 

 


SRP가 뭐지?

 

 

사진을 추가할 수 있는 메모 앱을 만드려고 한다.

메모를 작성하는 화면수정하는 화면이 필요하다.

 

거의 비슷한 동작을 할 것이다.

두 개의 클래스를 만든다면 분명 중복 코드가 생기게 된다.

 

그래서 천재적으로 하나의 MemoManipulateAcitivity를 만들었다.

 

 

 

짠! 아주 책임이 가득한 클래스가 완성되었다.

 

SRP고 뭐고 잘 동작한다.

나는 훌륭하게 일을 해냈다.

 


작성일자와 수정일자를 표시하는 요구사항이 추가되었다.

 

코드를 수정하고 메모를 작성해 보니 잘 표시한다. 출시!

 

출시하고 보니 수정할 때마다 작성일자가 오늘일자로 수정된다.

아 수정하는 일도 했었지...

 

한 클래스에 두가지의 책임이 공존해 실수가 나왔다.

 

 


프로필 화면에서 사진을 추가하는 요구사항이 추가되었다.

 

프로필 화면에서도 이미지를 받아와야 하는데 코드가 중복된다....

 

하나의 책임에 집중하지 못한 클래스는 재사용하기 힘들다.

 

 

 

 

 


 

하나의 책임에 집중해보자

 

 

클래스당 하나의 책임만 가지도록 만들어 보자

 

 

하나의 책임을 가지도록 클래스를 분리했다.

각 클래스는 하나의 책임에 집중한 적은 양의 코드만 남았다.

 

 

이미지를 받아오는 코드를 캡슐화 했다.

이를 통해 중복코드없이 프로필 설정화면에도 재사용할 수 있게 되었다.

 

메모 작성 화면과 수정 화면의 공통 조작 부분을 MemoManipulateActivity로 캡술화하고

두 클래스에 의존적인 메모를 만드는 부분은 추상 메소드 getMemo로 만들었다.

 

이로 인해 작성 화면과 수정 화면은 서로 불필요한 영향을 받을 일이 줄고 자신의 책임에 집중할 수 있다.

 

 

 

 

 


외부 포스팅

 

더 자세하게 깊게 알고 싶다면..

https://velog.io/@amobmocmo/Software-Design-SRP-Single-Responsibility-Principle-4pk2aklhqo

 

[Software Design] SRP (Single Responsibility Principle)

책임 로버트 C. 마틴은 책임을 변경하려는 이유라고 정의했다. 변화의 시기와 이유가 같다면 같은 책임 아래 있다고 보는 것이다. 반대로, 한 객체 내에서 변화의 시기, 이유가 다른 부분이 존재한다면 그 객체는 여러 책임을 가지고 있는 것이다. 그에 따라 이렇게 좀 더 알아보기 쉽게 정의할 수 있을 것 같다. > 책임은 객체에 의해 정의되는 응집도있는 ...

velog.io

 

 

 

 

'SoftwareDesign' 카테고리의 다른 글

LSP  (0) 2020.03.01
OCP  (0) 2020.03.01
SOLID 개요  (0) 2020.03.01
3. Observer  (0) 2020.01.19
2. Strategy  (0) 2020.01.07

 

 

로버트 C. 마틴 제시한 5가지 원칙

물론 다 본인이 만든건 아니라고 한다.

LSP 법칙만 봐도 아니라는 것을 알 수 있다.

 

 

개발자라면 무조건 듣게 되는 원칙이라고 생각한다.

나도 많이 들었고 봤다가 잊어버렸다가를 반복하기에 정리하려 한다.

 

개인적으로는 완전히 이해하고 정리하기 쉽지 않았다.

(지금도 완전히 이해했는지 잘모름)

 

실무에서 상황상 적용하기 힘들 수도 있고

적용한 것이 더 안좋은 경우가 있을 수도 있다.

 

하지만 알고 적용하지 않는 것과 몰라서 적용하지 못하는 것은 다른 이야기이다.

 

 

 


위키피디아 내용

 

 

위키피디아에 써있는 한줄 설명이다.

 

 

SRP (Single Reponsibility Principle; 단일 책임 원칙)

한 클래스는 하나의 책임만 가져야 한다.

 

OCP (Open/Cloesed Priciple; 개방-폐쇄 원칙)

소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀있어야 한다.

 

LSP (Liskov Substitution Principle; 리스코브 치환 법칙)

프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.

 

ISP (Interface Segregation Principle; 인터페이스 분리 법칙)

특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.

 

DIP (Dependency Inversion Principle; 의존 역전 법칙)

프로그래머는 “추상화에 의존해야지, 구체화에 의존하면 안된다.". DI(의존성 주입) 역시 이 원칙을 따르는 방법 중에 하나이다.

 

https://ko.wikipedia.org/wiki/SOLID_(%EA%B0%9D%EC%B2%B4_%EC%A7%80%ED%96%A5_%EC%84%A4%EA%B3%84)

 

SOLID (객체 지향 설계) - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전.

ko.wikipedia.org

 

 

완벽하게 이해하려고 각각의 원칙들의 위키를 읽다가 더 이해하기 힘들었다.

 

많은 포스팅을 보고 원칙이 전하려는 의미를 파악하는게 더 쉬울 것이라고 생각한다.

 

하지만 포스팅들을 보면 이해하는 것도 가지각색이고 아다르고 어다르게 설명하는 경우도 부지기수이다.

최대한 많이 보며 공통의 내용이 무엇인지 파악해보자.

다른 포스팅 잘못된 포스팅들을 보면서 공부하고 생각하다 보면 이 또한 공부가 된다.

 

 

 

 

 

'SoftwareDesign' 카테고리의 다른 글

OCP  (0) 2020.03.01
SRP  (0) 2020.03.01
3. Observer  (0) 2020.01.19
2. Strategy  (0) 2020.01.07
1. Singleton  (0) 2019.12.25

 

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

 

 

프로그래밍을 잘 한다는 것은 무엇일까?

 

우선 만들고자 하는 기능을 만들 수 있어야 한다.

이건 너무 당연한 이야기이고

 

버그가 없어야 한다.

유지보수에 용이하게 변경과 확장에 유연해야 한다.

코드가 깔끔해야 한다.

성능이 좋아야 한다.

 

사람마다의 기준이 다를 것이고 추구하는 방향성이 다를 것이다.

 

그 많은 기준 중에 프로젝트를 구조적으로 잘 만들고 싶은 욕심이 있다면

공부해야하는 것이 디자인 패턴이라고 생각한다.

 

어떻게 하면 더 유연성이 있는 코드를 만들 수 있을까 고민하고 정의한 것을

패턴으로 만든 것이다.

 

머리가 너무 좋아서 추상클래스, 인터페이스, 상속, 포함 관계로

상황에 맞는 유연성 있는 좋은 코드를 만들 수 있다면 공부할 필요가 없다.

 

하지만 그렇지 않다면 상황에 맞는 패턴들을 공부하고 그 상황이 나왔을 때

사용함으로 더 좋은 코드를 만들 수 있다.

더 나아가 기존의 패턴에서 발전시킨 자신만의 패턴을 만들 수도 있을 것이다.

 

이미 알게모르게 많이 사용하는 패턴이라 이해하기 쉬울 수도 있을 것이고

그렇지 않은 경우에 와닿지 않는 패턴들도 있을 것이다

 

해당 디자인 패턴이 어떤 상황에서 필요한지 정리하고

꾸준히 그 패턴을 인지하고 있다면 언젠가 도움이 될 것이다.

 

 

 

Head First Desgin Pattern을 가지고 정리하려 한다.

 

Github에 패턴별로 branch를 나누고

Commit을 통해 어떻게 무엇을 개선하기 위해 발전했는지 과정을 보여주려고 한다.

 

코드를 돌려보며 확인하면 좋을 것 같다.

 

 

 

 

 

Strategy

알고리즘군을 정의하고 각각을 캡슐화하여 바꿔 쓸 수 있게 만든다. 스트래티지 패턴을 이용하면 알고리즘을 활용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있다.


Observer

한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의

 

Decorator

객체에 추가적인 요건을 동적으로 첨가한다. 데코레이터는 서브클래스를 만드는 것을 통해서 기능을 유연하게 확정할 수 있는 방법을 제공한다.


Factory

Factory

객체 생성하는 부분을 캡슐화하여 사용한다.

Factory Method

객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하게 만든다. 

Abstract Factory

인터페이스를 이용하여 서로 연관된, 또는 의존하는 객체를 구상 클래스로 지정하지 않고도 생성할 수 있습니다.

 

Singleton

해당 클래스의 인스턴스가 하나만 만들어지고, 어디서든지 그 인스턴스에 접근할 수 있도록 하기 위한 패턴


Command

요구 사항을 객체로 캡슐화할 수 있으며, 매개변수를 써서 여러 가지 다른 요구 사항을 집어넣을 수도 있다. 또한 요청 내역을 큐에 저장하거나 로그를 기록할 수도 있으며, 작업취소 기능도 지원 가능


Adapter

한 클래스의 인터페이스를 클라이언트에서 사용하고자 하는 다른 인터페이스로 변환한다. 어댑터를 이용하면 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있다.

 

Facade

서브시스템에 있는 일련의 인터페이스에 대한 통합 인터페이스를 제공한다.


Template Method

메소드에서 알고리즘의 골격을 정의한다. 알고리즘의 여러 단계 중 일부는 서브클래스에서 구현할 수 있다. 템플릿 메소드를 이용하면 알고리즘의 구조는 그대로 유지하면서 서브클래스에서 특정 단계를 재정의할 수 있다.


Iterator

컬랙션 구현 방법을 노출시키지 않으면서도 그 집합체 안에 들어있는 모든 항목에 접근할 수 있게 해 주는 방법을 제공해 준다.

EX) ArrayList, HashSet


Composite

객체들을 트리 구조로 구성하여 부분과 전체를 나타내는 계층구조로 만들 수 있다. 이 패턴을 이용하면 클라이언트에서 개별 객체와 다른 객체들로 구성된 복합 객체를 똑같은 방법으로 다룰 수 있다.

EX) Android의 View와 ViewGroup


Compound

객체의 내부 상태가 바뀜에 따라서 객체의 행동을 바꿀 수 있다. 마치 객체의 클래스가 바뀌는 것과 같은 결과를 얻을 수 있다.


Proxy

어떤 객체에 대한 접근을 제어하기 위한 용도로 대리인이나 대변인에 해당하는 객체를 제공하는 패턴

 

디자인 패턴의 공통점은 변하지 않는 본질을 추상화하는 것

 

 

 

 

'SoftwareDesign' 카테고리의 다른 글

SRP  (0) 2020.03.01
SOLID 개요  (0) 2020.03.01
3. Observer  (0) 2020.01.19
2. Strategy  (0) 2020.01.07
1. Singleton  (0) 2019.12.25

+ Recent posts