클래스 계층과 상속

·2021년 12월 12일
0
post-thumbnail

📌 인터페이스와 추상 클래스 생성


인터페이스 만들기

의미적으로 코틀린의 인터페이스는 Java의 인터페이스와 유사하다. 하지만 문법적으로 봤을 땐 많이 다르다.

✔ 인터페이스 안에 추상 메소드를 작성하여 명세에 의한 설계를 할 수도 있다.
✔ Java에서 필요한 default 키워드 없이 인터페이스 안에 메소드를 구현할 수도 있다.
✔ 코틀린 클래스의 static 메소드가 컴패니언 객체에 들어있는 것과 유사하게 인터페이스 역시 인터페이스 안에 컴패니언 객체를 작성하여 static 메소드를 가질 수 있다.

코틀린의 인터페이스에 대해 알아보기 위해 Remote인터페이스를 만들어보자

Remote

interface Remote {

    fun up()
    fun down()
    fun doubleUp() {
        up()
        up()
    }
}

up메소드와 같은 추상 메소드는 인터페이스에 정의만 되어 있다.doubleUp() 같은 구현된 메소드는 클래스에서 구현된 것과 유사해 보인다.
인터페이스를 구현하는 어떤 클래스라도 추상 메소드를 오버라이드해야만 한다. 하지만 doubleUp() 메소드는 구현하지 않아도 상관없다.

인터페이스가 클래스에서 어떻게 상호작용 하는지 보기 위해서 Remote 인터페이스를 구현하는 TvRemote 클래스를 만들어보자.

TvRemote

class TV {
    var volume = 0
}

class TVRemote(val tv: TV) : Remote {


    override fun up() {
        tv.volume++
    }

    override fun down() {
        tv.volume--
    }

}

TvRemote가 Remote 인터페이스를 구현한다는 것을 명시하기 위해서 주 생성자 뒤에 콜론을 사용하고 Remote를 적었다. Remote 인터페이스의 doubleUp() 메소드는 오버라이드 하지 않았다.

Remote 메소드 사용

val tv = TV()
var remote: Remote = TVRemote(tv)
println("VolumeL ${tv.volume}")
remote.up()
println("After increasing: ${tv.volume}")
remote.doubleUp()
println("After doubleUp: ${tv.volume}")

💻 출력

VolumeL 0
After increasing: 1
After doubleUp: 3

remote는 Remote 타입이지만 실행시간에 TvRemote의 인스턴스를 참조한다. up()메소드는 TvRemote 인스턴스에 의해 다뤄지지만 doubleUp() 메소드는 Remote에 구현된 메소드를 사용한다.

인터페이스 안에 static 메소드를 사용하기 위해서는 컴패니언 객체를 사용해야 한다. 컴패니언 객체를 이용하여 두 개의 리모콘을 묶어 동시에 명령을 내릴 수 있는 combile() 메소드를 만들어보자

companion

interface Remote {

    companion object {
        fun combine(first: Remote, second: Remote): Remote = object : Remote {
            override fun up() {
                first.up()
                second.up()
            }

            override fun down() {
                first.down()
                second.down()
            }

        }
    }

    fun up()
    fun down()
    fun doubleUp() {
        up()
        up()
    }
}

val tv = TV()
var remote: Remote = TVRemote(tv)
val anotherTV = TV()
val combineRemote = Remote.combine(remote, TVRemote(anotherTV))
combineRemote.up()
println(tv.volume) //4
println(anotherTV.volume) //1

컴패니언 객체는 Remote 인터페이스에 바로 작성하면 된다. 컴패니언 객체의 메소드에 접근하기 위해서 Remote 인터페이스를 사용한다.

추상 클래스 생성하기

abstrac로 선언을 시작하는 클래스는 추상 클래스이다. 그리고 추상클래스에서도 추상 메소드는 abstract라고 표시한다.

companion

abstract class Musician(val name:String, val activeFrom:Int){
    abstract fun instrumentType():String
}
class Cellist(name:String,activeFrom:Int) :Musician(name,activeFrom){
    override fun instrumentType()="String"
}

val ma=Cellist("Yo-Yo Ma",1961)

instrumentType() 메소드가 베이스 클래스에 구현되어 있지 않기 때문에 abstract라고 표시되어 있다. 자식 클래스에서 Cellist의 주 생성자는 2개의 파라미터를 받은 후 베이스 클래스에게 넘겨준다. 자식클래스의 생성자는 베이스 클래스에서 필요하지 않은 파라미터를 추가적으로 받을 수 있다.

추상 클래스와 인터페이스의 차이 🔥

  • 인터페이스에 정의된 속성엔 백킹 필드가 없다. 인터페이스는 구현 클래스로부터 속성을 얻는 것을 추상 메소드에 의존한다. 반면에 추상클래스는 백킹 필드를 가진다.

  • 인터페이스는 한 번에 여러 개를 구현할 수 있지만, 클래스는 추상 클래스든지 일반 클래스든지 하나만 확장 가능하다.

인터페이스? 추상클래스?

인터페이스는 필드를 가질 수 없다. 하지만 클래스에서 여러 개의 인터페이스를 한 번에 구현할 수 있다. 반면에 추상 클래스는 필드를 가질 수 있다. 하지만 한번에 하나의 클래스만 확장할 수 있다.

여러 클래스 사이에서 상태를 다시 사용해야 한다면 추상 클래스가 좋은 선택이다. 추상 클래스에서는 공통 상태를 구현할 수 있다. 클래스레서 구현할 때 추상 클래스가 제공해주는 상태를 재사용하며 메소드를 오버라이드한다.

✔하나 이상 명세와 요구사항을 만족하는 클래스들을 원하지만 각각의 클래스들이 각각의 구현 하는 것을 원한다면 인터페이스가 좋은 선택이다.

📌 인터페이스와 추상 클래스 생성


TvRemote

class TV {
    var volume = 0
}

class TVRemote(val tv: TV) : Remote {


    override fun up() {
        tv.volume++
    }

    override fun down() {
        tv.volume--
    }

}

val tv = TV()
var remote: Remote = TVRemote(tv)

이전 예제를 보면 TV가 Remote 인터페이스를 직접 구현하지 않고, Remote 인터페이스를 구현한 TVRemote에 분리된 TV를 가지고 있다.
TVRemote처럼 직접 구현한 게 아니고 분리된 클래스를 가지고 있는 것에는 각각 장/단점이 있다.

🔎 장점

  • TV 인스턴스 하나에 여러 개의 TVRemote를 가질 수 있다
    리모컨이 하나일 경우 리모컨을 가지고 있는 사람을 방해하고 TV 인스턴스를 제어해달라고 요청해야 하는 것에 반해서 리모컨이 여러 개 있다면 각각의 사람들이 자유롭게 TV인스턴스를 컨트롤 할 수 있다.

  • TVRemote의 인스턴스들은 TV 인스턴스의 상태와 분리된 채로 내부 상태를 가질 수 있다.

🔎 단점

  • Remote 인터페이스를 구현하고 있는 TVRemote 메소드들은 TV의 public 메소드로 사용해야만 한다.
    만일 TV가 Remote인터페이스를 구현하고 있다면 public 메소드에 의존 할 필요가 없어진다.

⭕ 장점은 유지하면서 단점을 피할 수 있는 방법은 내부 클래스를 사용하면 된다.

내부 클래스

class TV {
    private var volume = 0
    val remote: Remote
        get() = TVRemote()

    override fun toString(): String = "Volume : ${volume}"
    inner class TVRemote : Remote {
        override fun up() {
            volume++
        }

        override fun down() {
            volume--
        }

        override fun toString(): String = "Remote: ${this@TV.toString()}"

    }

}

TV 클래스의 volume 속성이 private이기 때문에 TV 인스턴스 외부에서 접근할 수 없다. 하지만 TVRemote 는 내부 클래스이기 때문에 TVRemote는 volume이 TVRemote의 속성인 것처럼 접근할 수 있다.
내부 클래스는 외부 클래스의 private 멤버를 포함한 보든 멤버에 직접 접근이 가능하다. 이제 TV 클래스는 TVRemote의 인스턴스를 리턴하는 public 속성인 remote를 제공한다.

📌 상속


인터페이스와 다르게 코틀린의 클래스는 디폴트가 final이다. 그러므로 open 이라고 명시해주지 않으면 클래스로부터 상속을 받을 구 없다.
자식클래스는 open으로 명시된 열려있는 클래스의 열려있는 메소드만 오버라이드할 수 있고, 자식 클래스에서는 override라고 명시 해줘야 한다.
속성도 오버라이딩 가능하다. val로 정의된 속성은 val과 var 모두를 사용해서 오버라이딩 가능하고 var로 정의된 속성은 var로만 오버라이딩 가능하다. val로 오버라이딩 할 경우 setter을 제거할 수 없기 때문이다.

Vehicle

open class Vehicle(val year: Int, open var color: String) {
    open val km = 0
    final override fun toString() = "year : $year, Color:$color, Km:$km"
    fun repaint(newColor: String) {
        color = newColor
    }
}

Vehicle 클래스의 두 번째 속성은 오버라이드 될 수 있고 속성 km 역시 오버라이딩 가능하다. toString() 메소드는 final로 설정되어 있기 때문에 더 이상 오버라이드하지 못한다.

Car

open class Car(year: Int, color: String) : Vehicle(year, color) {
    override var km: Int = 0
        set(value) {
            if (value < 1) {
                throw RuntimeException("can't set negatuve value")
            }
            field = value
        }

    fun drive(distance: Int) {
        km += distance
    }

}

Car 클래스느 Vehicle 클래스에서 파생되었다. km 속성을 오버라이드하고 setter 에서 0보다 큰 수만 받을 수 있도록 체크하는 로직을 넣었다. 체크를 통과한 숫자만 km 속성의 백킹 필드에 전달되도록 하였다.

val car = Car(2019, "Orange")
println(car.year)
println(car.color)
car.drive(10)
println(car)
try {
    car.drive(-30)
} catch (ex: RuntimeException) {
    println(ex.message)
}

💻 출력

2019
Orange
year : 2019, Color:Orange, Km:10
can't set negatuve value


Car 클래스를 부모로 자식 클래스를 생성할 수도 있다.

FamilyCar

class FamilyCar(year: Int, color: String) : Car(year, color) {
    override var color: String
        get() = super.color
        set(value) {
            if (value.isEmpty()) {
                throw RuntimeException("Color required")
            }
            super.color = value
        }
}

val familyCar = FamilyCar(2019, "Green")
println(familyCar.color)
try {
    familyCar.repaint("")
} catch (ex: RuntimeException) {
    println(ex.message)
}

💻 출력

Green
Color required

color는 Vehicle 안에 저장되어있지만, FamilyCar의 인스턴스가 속성의 값을 검증하고 color의 값이 비어있는 경우를 허용하지 않는다.

코틀린이 합리적인 제약사항을 만들어 놨지만, 오버라이딩 할 때 접근 권한에 관한 제약사항이 좀 더 관대하고 느슨하게 만들 수 있다. 예를 들어, private이나 protected 멤버를 자식 클래스에서 public으로 만들 수 있다. 하지만 베이스 클래스의 public 멤버를 자식 클래스에서 protected로 만들 수는 없다.

📌 씰드 클래스


코틀린의 sealed 클래스는 동일한 파일에 작성된 다른 클래스들에게 확장이 허용되지만 그 외의 클래스들은 확장할 수 없는 클래스이다.

Sealed

sealed class Card(val suit: String)
class Ace(suit: String) : Card(suit)
class King(suit: String) : Card(suit) {
    override fun toString() = "King of $suit"
}

class Queen(suit: String) : Card(suit) {
    override fun toString() = "Queen of $suit"
}

class Jack(suit: String) : Card(suit) {
    override fun toString() = "Jack of $suit"
}

class Pip(suit: String, val number: Int) : Card(suit) {
    init {
        if (number < 2 || number > 10) {
            throw RuntimeException("Pip has to be between 2 and 10")
        }
    }
}

sealed 클래스의 생성자는 private 표기되지 않았지만 private로 취급된다.

sealed 클래스의 자식 클래스는 여러 개의 인스턴스를 생성할 수 있고, 속성을 가질 수 있고, 매소드를 가질 수 있다. 자식 클래스는 sealed 클래스의 싱글톤 객체가 될 수 있다.

sealed 클래스의 생성자는 private으로 취급되기 때문에 객체를 인스턴스화 할 수 없다. 하지만 sealed 클래스로부터 상속받은 클래스의 생성자를 private으로 명시하지 않으면 상속받은 클래스를 통해서 객체를 생성할 수 있다.

Sealed

fun process(card: Card) = when (card) {
    is Ace -> "${card.javaClass.name} of ${card.suit}"
    is King, is Queen, is Jack -> "$card"
    is Pip -> "${card.number} of ${card.suit}"

}

fun main() {
    println(process(Ace("Diamond")))
    println(process(Queen("Clubs")))
    println(process(Pip("Spades", 2)))
    println(process(Pip("Hearts", 6)))

}

💻 출력

chapter8.Ace of Diamond
Queen of Clubs
2 of Spades
6 of Hearts

sealed 클래스의 자식 클래스의 인스턴스 생성은 간단하다. 하지만 when 표현식을 사용할 때 else를 사용하면 안된다. 만일 when에 sealed 클래스의 자식 클래스가 모두 속해 있을때는 else는 절대 사용되지 않는다. 자식이 누락된 경우 else 추가하는 것을 제안하더라도 절대 추가하면 안된다. 만약에 나중에 새로운 sealed 클래스가 추가되었을 때 새로운 케이스가 처리되지 않았다는 컴파일 오류가 나타나지 않고 의도하지 않은 코드를 실행하게 될 수도 있다.

📌 Enum의 생성과 사용


위의 클래스를 보면 사실 suit는 4가지 값만 가지면 된다. 짧게 말하면 이 경우 클래스가 필요없다.

CardWithEnum

enum class Suit { CLUBS, DIAMONDS, HEARTS, SPADES }

sealed class Card(val suit: Suit)
class Ace(suit: Suit) : Card(suit)
class King(suit: Suit) : Card(suit) {
    override fun toString() = "King of $suit"
}

class Queen(suit: Suit) : Card(suit) {
    override fun toString() = "Queen of $suit"
}

class Jack(suit: Suit) : Card(suit) {
    override fun toString() = "Jack of $suit"
}

class Pip(suit: Suit, val number: Int) : Card(suit) {
    init {
        if (number < 2 || number > 10) {
            throw RuntimeException("Pip has to be between 2 and 10")
        }
    }
}

생성자에 String을 전달하지 않고 enum 클래스인 suit를 전달한다.

UseCardWithEnum

fun process(card: Card) = when (card) {
    is Ace -> "${card.javaClass.name} of ${card.suit}"
    is King, is Queen, is Jack -> "$card"
    is Pip -> "${card.number} of ${card.suit}"

}

fun main() {
    println(process(Ace(Suit.DIAMONDS)))
    println(process(Queen(Suit.CLUBS)))
    println(process(Pip(Suit.SPADES, 2)))
    println(process(Pip(Suit.HEARTS, 6)))

}

Suit.DIAMONDS가 표현하고 있는 것은 Suit 클래스의 인스턴스이고, enum 클래스의 static 속성을 참조한다. enum은 열거형 겂을 만드는데 적합할 뿐만 아니라, enum을 커스터마이즈해서 쉽게 enum값들을 반복할 수도 있다.

val diamonds = Suit.valueOf("DIAMONDS")

println(diamonds.name)


for (suit in Suit.values()) {
    println("${suit.name}--${suit.ordinal}")
}

💻 출력

DIAMONDS
CLUBS--0
DIAMONDS--1
HEARTS--2
SPADES--3

values() 메소드는 enum 클래스의 인스턴스의 모든 겂을 배열로 제공해 준다. enum 인스턴스의 name과 ordinal 속성이 이름과 인스턴스에 정의된 인덱스로 리턴된다.

enum 클래스는 상태와 메소드를 가질 수 있다. enum으로 상태와 메소드를 정의할 때는 값이 끝나는 곳에 세미콜론을 이용해서 상태와 메소드를 분리시켜야 한다.

enum class Suit(val symbol: Char) {
    CLUBS('\u2663'),
    DIAMONDS('\u2666'),
    HEARTS('\u2665') {
        override fun display() = "${super.display()} $symbol"
    },
    SPADES('\u2660');

    open fun display() = "$symbol $name"
}

for (suit in Suit.values()) {
    println(suit.display())
}

💻 출력

♣ CLUBS
♦ DIAMONDS 
♥ HEARTS ♥ 
♠ SPADES

출처 : 다재다능 코틀린 프로그래밍

profile
개발하고싶은사람

0개의 댓글