코틀린

Do it! 코틀린 프로그래밍, 07-1 추상 클래스와 인터페이스

조요피 2021. 7. 21. 22:51

추상 클래스(Abstract Class)는 선언 등의 대략적인 설계 명세와 공통의 기능을 구현한 클래스.

추상이라는 말은 구체적이지 않은 것을 나타냄.

추상 클래스를 살속하는 하위 클래스에서 추상 클래스의 내용을 더 구체화해야함.

인터페이스(interface)도 대략적인 설계 명세를 가지고 몇 가지 기본적인 부분은 구현할 수 있지만,

하위에서 더 자세히 구현해야 하는 점은 추상 클래스와 동일.

하지만, 인터페이스에서는 프로퍼티에 상태 정보를 저장할 수 없음.

단, 다중 상속과 같이 여러 개의 인터페이스를 하나의 클래스에서 구현하는 것이 가능.

추상 클래스

abstract라는 키워드와 함께 선언하며 일반적인 객체를 생성하는 방법으로는 인스턴스화 할 수 없다.

추상 클래스를 상속하는 하위 클래스를 어떻게 만들어야 하는지 나타내는 용도로 사용된다.

 

멤버인 프로퍼티나 메서드도 abstract로 선언될 수 있다.

이때 추상 프로퍼티나 추상 메서드라고 부른다.

abstract로 선언된 프로퍼티나 메서드는 아직 완성되지 않았다는 의미를 줄 수 있다.

추상 프로퍼티나 메서드가 하나라도 있다면 해당 클래스는 추상 클래스여야 한다.

 

클래스를 상속하려면 open 키워드로 정의되어야 하지만

추상 클래스에서는 open 키워드를 사용할 필요가 없다.

추상 프로퍼티나 추상 메서드에도 마찬가지.

abstract 키워드 자체가 상속과 오버라이딩을 허용한다.

 

abstract class Demo(val name: String, val age: Int) {
    var adress = "suwon"
    abstract var weight: Int

    abstract fun f()

    fun f2() {
        println("$name $age $adress $weight")
    }
}

class Cemo(name: String, age: Int, override var weight: Int) : Demo(name, age) {
    override fun f() {
        println("call f()")
    }

}

fun main() {
    val c = Cemo("yyj", 11, 99)
    c.f2()
    c.f()
}

// yyj 11 suwon 99
// call f()

만약 추상 클래스로부터 하위 클래스를 생성하지 않고

단일 인스턴스로 객체를 생성하려면 object 키워드를 사용할 수 있다.

abstract class Demo{
    abstract fun f()
}

fun main() {
    val d = object : Demo() {
        override fun f(){
            println("hi")
        }
    }

    d.f()
}

// hi

인터페이스

인터페이스에는 abstract로 정의된 추상 메서드나 일반 메서드가 포함된다.

다른 객체 지향 언어와는 다르게 메서드에 구현 내용이 포함될 수 있다.

하지만, 추상 클래스처럼 프로퍼티를 통해 상태를 저장할 수 없다. 선언만 가능.

인터페이스 또한 객체를 생성할 수 없고 하위 클래스를 통해 구현 및 생성해야 한다.

 

인터페이스는 '계약서'와 비슷하다.

계약서에는 추상적인 활동이 적혀 있다.

'계약자'는 계약서에 있는 활동을 실행해야 한다.

계약 자체로는 실행되지 않는다.

그래서 인터페이스를 다른 말로 표현하면 '기본 설계도'라고 할 수 있다.

 

그렇다면 인터페이스를 사용하는 이유는 무엇인가?

인터페이스는 클래스가 아니다.

상속이라는 형태로 하위 클래스에 프로퍼티와 메서드를 전하지 않는다.

그래서 하위 클래스라고 하지 않고 구현 클래스라고 한다.

인터페이스는 구현 클래스와 강한 연관을 가지지 않는다.

상속은 하나만 허용했으나 인터페이스는 원하는 만큼 구현 클래스에 사용할 수 있다.

인터페이스가 바뀌어도 구현하는 클래스에는 크게 영향이 없다.

 

*코틀린의 인터페이스는 메서드에 구현 내용을 넣을 수 있으나 자바 8 이전 버전에는

인터페이스 메서드를 구현할 수 없었다.

자바 8부터 인터페이스에 구현 기능을 추가했는데 default 키워드를 통해 구현 내용을 넣을 수 있다.

자바에서 기본 메서드 기능을 추가한 가장 큰 이유는 인터페이스를 여러 클래스가 사용하는 경우에

필요한 메서드를 모두 구현해야 하기 때문.

 

interface inter {
    val name: String // abstract 키워드가 없어도 추상 프로퍼티
    fun f() // 추상 메서드
    fun f2() { // 일반 메서드
        println("f2()")
    }
}

class Demo(override val name : String) : inter {
    override fun f() {
        println("f()")
    }
}

fun main() {
    val d = Demo("yopi")
    println(d.name)
    d.f()
    d.f2()
}

//yopi
//f()
//f2()

*자바에서 상속은 extends, 구현은 implements로 구별하지만 코틀린에서는 콜론(:)으로 사용한다.

인터페이스 구현의 필요성

interface Pet {
    val name: String
    fun feeding()
    fun patting(){
        println("Keep patting!")
    }
}

class Dog(override val name: String) : Pet {
    override fun feeding() {
        println("Feed the dog a bone")
    }
}

class Cat(override val name: String) : Pet {
    override fun feeding() {
        println("Feed the cat a tuna can!")
    }
}

class Demo {
    fun playWithPet(pet : Pet) {
        println("Enjoy with my ${pet.name}")
    }
}

fun main() {
    val d = Demo()
    d.playWithPet(Dog("견우"))
    d.playWithPet(Cat("애옹"))
}

//Enjoy with my 견우
//Enjoy with my 애옹

인터페이스 위임

interface A {
    fun funA() {
        println("funA")
    }
}

interface B {
    fun funB() {
        println("funB")
    }
}

class C(val a: A, val b: B) {
    fun funC() {
        a.funA()
        b.funB()
    }
}

class D(a: A, b: B) : A by a, B by b {
    fun funD() {
        funA()
        funB()
    }
}

class Demo : A, B {
    override fun funA() {
        super.funA()
    }

    override fun funB() {
        super.funB()
    }
}

fun main() {
    val c = C(Demo(), Demo())
    c.funC()

    val d = D(Demo(), Demo())
    d.funD()
}

//funA
//funB
//funA
//funB

커피 제조기 만들어 보기

요구사항

물을 데우는 히터(heater)가 필요.

히터는 on, off 가능하며 충분히 뜨거운지 확인할 수 있다.

커피는 열사이펀(thermosiphon)을 통해 추출한다.

열사이펀은 기압차를 통해 펌핑하는 원리이다.

커피 제조기는 열사이펀을 사용해 드립 커피를 제조한다.

커피를 제조하는 브루잉(Brewing) 과정을 실행할 수 있다.

브루잉은 '전원 on -> 펌핑 -> 커피 완성 -> 전원 off' 순서로 진행한다.

interface Heater {
    fun on()
    fun off()
    fun isHot(): Boolean
}

class ElectricHeater(var heating: Boolean = false) : Heater {
    override fun on() {
        println("히터가 켜졌습니다. 물을 데웁니다.")
        heating = true
    }

    override fun off() {
        println("히터가 꺼졌습니다.")
    }

    override fun isHot(): Boolean {
        return heating
    }

}

interface Pump {
    fun pump()
}

class Thermosiphon(heater: Heater) : Pump, Heater by heater { // 위임 사용
    override fun pump() {
        if (isHot()) {
            println("펌핑을 시작합니다.")
        }
    }
}

interface CoffeeMachine {
    fun getThermosiphon(): Thermosiphon
}

class MyCoffeeMachine : CoffeeMachine {
    companion object {
        val electricHeater: ElectricHeater by lazy { // 지연 초기화
            ElectricHeater()
        }
    }

    private val _thermosiphon: Thermosiphon by lazy { // 임시 private 프로퍼티
        Thermosiphon(electricHeater)
    }

    override fun getThermosiphon(): Thermosiphon {
        return _thermosiphon
    }
}

class CoffeeMaker(val coffeeMachine: CoffeeMachine) {
    fun brew() {
        val theSiphon = coffeeMachine.getThermosiphon()
        theSiphon.on()
        theSiphon.pump()
        println("커피가 완성됐습니다.")
        theSiphon.off()
    }
}

fun main() {
    val coffeeMaker = CoffeeMaker(MyCoffeeMachine())
    coffeeMaker.brew()
}

//히터가 켜졌습니다. 물을 데웁니다.
//펌핑을 시작합니다.
//커피가 완성됐습니다.
//히터가 꺼졌습니다.

*제대로 이해하지 못했다. 다시 공부할 것.