Study/object

[엘레강트 오브젝트] 2-2장 최소한 뭔가는 캡슐화하세요

에디개발자 2021. 8. 16. 23:27
반응형

나를 닮았다고 한다...

이 글은 엘레강트 오브젝트 새로운 관점에서 바라본 객체지향 도서를 보며 스터디한 글입니다.

책에서 주장하는 내용을 정리하였으며 예제들은 모두 코틀린 코드로 변환하여 작성하였습니다.

토론하기

 

아무것도 캡슐화하지 않은 클래스

정적 메서드와 동일하며 객체지향 프로그램 관점에서는 잘못된 설계입니다. 

class Year {
    fun read() = System.currentTimeMillis() / (1000 * 60 * 60 * 24 * 30 * 12) - 1970
}
  • 예제에서 클래스의 모든 객체는 동일
  • 아무런 상태를 가지고 있지 않으며 행동만을 포함
  • read() 메서드에서 정적 메서드를 사용 ( 순수한 OOP에서는 정적 메서드를 사용하지 않음 )

 

실행으로 부터 인스턴스 생성 고립

생성자에서만 new 연산자를 허용해야합니다. 올바른 설계의 클래스를 작성하려면 정적 메서드 대신 어떤 클래스의 인스턴스를 생성한 후 이 인스턴스를 통해 시스템 클럭을 얻어야합니다.

이전에 작성한 잘못 설계한 Year 클래스를 수정해보겠습니다.

class Year(
    private val millis: Millis
) {
    fun read() = millis.read() / (1000 * 60 * 60 * 24 * 30 * 12) - 1970
}

Year 클래스에서 Millis라는 객체를 받아 객체의 메서드인 read() 를 호출합니다.

  • 클래스의 모든 객체는 동일하지 않음
  • millis라는 상태를 갖고 있음
  • read() 메서드에서 정적메서드를 호출하지 않음

책에 나와있지 않지만 이렇게 사용할 것이다.

책에는 나와있지 않지만 Millis 클래스와 Year를 사용하는 예제를 상상하여 작성해보았습니다.

class Millis(
    private val milliSeconds: Long
) {
    fun read() = milliSeconds
}

Millis 클래스 또한 milliSeconds를 캡슐화하고 있습니다. read() 메서드에서는 milliSeconds 속성을 그대로 리턴합니다.

Millis 클래스는 속성값을 그대로 리턴만 하는 거라면 굳이 사용할 필요가 없지 않나요? 
라는 의문이 생길 수 있지만 Millis 클래스 내부에서 plus(), minus() 메서드를 사용하여 milliSeconds 상태값을 변경하는 메서드를 추가할 수도 있기에 충분히 클래스로 사용할 수 있다고 생각합니다.

 

다음으로 Test 클래스에서 Year 인스턴스를 사용하는 예제입니다.

class Test {
    fun yearTest() {
        // Millis 생성
        val millis = Millis(System.currentTimeMillis())

        // Year 얻어온다.
        val year = Year(millis).read()
    }
}
  1. Millis 클래스의 속성값으로 시스템 클럭 Millis 값을 넘겨 생성
  2. 생성된 millis 인스턴스를 Year 클래스의 속성값으로 넘겨 생성
  3. read 메서드를 사용하여 year를 얻음

완벽한 객체지향

이 책에서 말하는 완벽한 객체지향을 Year 클래스에 적용한다면 아래와 같을 것 입니다.

class Year(
    private val num: Number
) {
    constructor(millis: Millis): this(
        Minus(
            Divide(
                millis,
                Multiply(1000, 60, 60, 24, 30, 12)
            ),
            1970
        )
    )

    fun read() = this.num.toInt()
}

이전 예제에서 Int 연산처리한 로직을 모두 객체로 묶어서 처리하였습니다. 

 


책에 나와있지 않는 연산 클래스 Minus, Divide, Multiply

책에 나와있지 않은 내용이다보니 이해에 어려워하시는 분들을 위해 작성해봅니다. 

 

Miliis

먼저 Millis 클래스도 변경합니다. Number 클래스 상속

class Millis(
    private val milliSeconds: Long
): Number() {
    fun read() = milliSeconds

    override fun toByte(): Byte {
        TODO("Not yet implemented")
    }

    override fun toChar(): Char {
        TODO("Not yet implemented")
    }

    override fun toDouble(): Double {
        TODO("Not yet implemented")
    }

    override fun toFloat(): Float {
        TODO("Not yet implemented")
    }

    override fun toInt(): Int {
        TODO("Not yet implemented")
    }

    override fun toLong(): Long {
        TODO("Not yet implemented")
    }

    override fun toShort(): Short {
        TODO("Not yet implemented")
    }
}

 

Minus

같은 맥락으로 Number 클래스 상속합니다.

class Minus(
    private vararg val num: Number
): Number() {
    override fun toByte(): Byte {
        TODO("Not yet implemented")
    }

    override fun toChar(): Char {
        TODO("Not yet implemented")
    }

    override fun toDouble(): Double {
        TODO("Not yet implemented")
    }

    override fun toFloat(): Float {
        TODO("Not yet implemented")
    }

    override fun toInt(): Int {
        val r = num.first().toInt()
        num.filterIndexed { index, i -> index != 0 }.forEach {r - it.toInt()}
        return r
    }

    override fun toLong(): Long {
        TODO("Not yet implemented")
    }

    override fun toShort(): Short {
        TODO("Not yet implemented")
    }
}

 

Divide

class Divide(
    private vararg val num: Number
): Number() {
    override fun toByte(): Byte {
        TODO("Not yet implemented")
    }

    override fun toChar(): Char {
        TODO("Not yet implemented")
    }

    override fun toDouble(): Double {
        TODO("Not yet implemented")
    }

    override fun toFloat(): Float {
        TODO("Not yet implemented")
    }

    override fun toInt(): Int {
        val r = num.first().toInt()
        num.filterIndexed { index, i -> index != 0 }.forEach {r / it.toInt()}
        return r
    }

    override fun toLong(): Long {
        TODO("Not yet implemented")
    }

    override fun toShort(): Short {
        TODO("Not yet implemented")
    }

}

 

Multiply

class Multiply(
    private vararg val num: Number
): Number() {
    override fun toByte(): Byte {
        TODO("Not yet implemented")
    }

    override fun toChar(): Char {
        TODO("Not yet implemented")
    }

    override fun toDouble(): Double {
        TODO("Not yet implemented")
    }

    override fun toFloat(): Float {
        TODO("Not yet implemented")
    }

    override fun toInt(): Int {
        val r = num.first().toInt()
        num.filterIndexed { index, i -> index != 0 }.forEach {r * it.toInt()}
        return r
    }

    override fun toLong(): Long {
        TODO("Not yet implemented")
    }

    override fun toShort(): Short {
        TODO("Not yet implemented")
    }

}

최소한 무언가를 캡슐화해야하는 이유

책에서 주장하는 이유는 다음과 같습니다.

  • 어떤 일을 수행하는 객체는 다른 객체들과 공존하면서 이들을 사용
  • 자기 자신을 식별할 수 있도록 다른 객체를 캡슐화

반대로 이런 것들을 행하지 않는 객체는 객체가 세계를 의미하는 아래와 같은 객체가 있을 수 있습니다. 이 클래스가 존재하는 이유가 없을 것 입니다. 

class Universe {}

 

이 책의 내용이나 제 생각과 다른 의견이 있으시면 댓글을 달아주세요. :)
반응형