Kotlin in Action 4장

존스노우·2023년 3월 5일
0

코틀린

목록 보기
4/10

코틀린의 클래스

  • 코틀린 선언은 기본적으로 final , public
  • 코틀린의 인터페이스는 프로퍼티 선언이 들어갈 수 있다
interface Person {
    var name: String
    val age: Int
}

- 왜 이렇게 사용하지?
- 인터페이스를 사용하는 클래스에 공통 프로퍼티 명시적 정의
- 코드의 가독성을 높이고 사용성 향상-
- 구현체에 프로퍼티 구현 
- 그닥 와닿지 않는다. 

interface MyInterface {
    fun doSomething()
}

interface MyInterface {
    val data: String
    fun doSomething()
}

class MyClass(override val data: String) : MyInterface {
    override fun doSomething() {
        println("Doing something with $data")
    }
}

fun main() {
    val obj = MyClass("Hello")
    obj.doSomething() // Doing something with Hello
    println(obj.data) // Hello
}

- 예시코드를 보면서 생각에 잠겼다.
- 결국 인터페이스에 프로퍼티를 선언하는건.
- 인터페이스를 구현할 클래스가 메소드를 사용할때 좀더 명확히 설명해주는 역할?
- 구현체에서 반드시 그 프로퍼티르 구현해야함을 명시적으로 나타내는거 같다.
- 프로퍼티를 선언하고 사용하지 않으면?? 컴파일 오류 발생!
- 결론적으로 인터페이스에 프로퍼티를 선언하면 구현체에 프로퍼티로 뭔가를 하는 명시적 역활

## 마지막 예시
interface Car {
    val maxSpeed: Int // 최대 속도 프로퍼티
    fun start()
    fun stop()
}

class SportsCar : Car {
    override val maxSpeed: Int = 300 // 명시적으로 구현
    override fun start() { /* 구현 */ }
    override fun stop() { /* 구현 */ }
}

코드의 안정성과 가독성을 높여 준다.

++ getter ,setter 자동으로 생성됨. 구현체는 자동으로 쓸 수 있다. 
++ 간결해진다 코드가.
  • 코틀린의 내부클래스는 기본적으로 자바에서 정적 중첩클래스로 선언되 외부클래스 참조X

  • 이는 중첩 클래스가 외부 클래스의 인스턴스에 대한 참조를 갖지 않기 때문

  • ineer 키워드 사용하면 외부 클래스 인스턴트 참조를 가져서 클래스 멤버 접근가능

    왜 기본적으로 정적 중첩클래스 ?

  • 내부 클래스시 외부클래스의 참조를 가지고 있어서

  • 이로인한 메모리 사용량 증가,.

  • 객체지향 설계 -> 바깥 클래스와 결합도가 낮아

  • 의존성 역전원칙을 적용하기 쉬워짐.

  • 바 깥클래스의 인스턴스가 변경되어도 중첩 클래스에는 영향을 주지않음

    class OuterClass(private val outerField: Int) {
       
       class NestedClass(private val nestedField: Int) {
           
           fun printFields(outer: OuterClass) {
               println("outerField: ${outer.outerField}")
               println("nestedField: $nestedField")
           }
       }
    }
  • val outer = OuterClass(2)

  • nested.printFields(outer) // 출력: outerField: 2, nestedField: 1

이런식으로 전달 해야됨

클래스 계층 정의

코틀린 인터 페이스

  • 자바 8과 비슷하게 구현 메소드 정의 가능.

  • 구현체에서 사용할땐 : (인터페이스 명)

  • override 변경자 필수 - 상위 클래스 메소드와 같을경우 명시하지않으면 컴파일 오류

  • 두 개의 인터페이스에서 같은이름으 default method 구현
  • 구현체에서 두 개의 인터페이스를 구현하면 어느 쪽 메서드도 선택 X
  • override를 통해 직접 구현해야됨

  • override fun showoff() = super.ShowOfE()
  • 이런식으로 구현, 자바와 마찬가지로 상위 타입구현시 super 사용.
  • 구체적 타입 지정시 꺽세 괄호 안에 기반 타입 지정 함.
  • 추가로 코틀린에서는 자바6 기반이기 때문에
  • 디폴트 메소드가 들어있는 인터페이스는 일반 인터페이스+ 디폴트 메소드 구현이 정적 메소드로 들어 있는 클래스 조합해 구현.
  • 코틀린 컴파일러가 디폴트 메소드 구현위해 내부 클래스에 해당 정적 메소드를 포함하는 방식으로 이루어 짐. (위에서 나온 중첩클래스)
  • 정리하면 코틀린 인터페이스 디폴트메서드는 정적메서드이고
  • 구현체는 이 정적메서드를 직접 사용 또는 오버라이드해서 변경해서 사용한다.
  • 인터페이스 선언 시점에 정적 메서드가 생성되고, 구현체가 이를 사용 할때 컴파일시점에 정적으로 결정됨 .

open , final ,abstarct 변경자: 기본적으로 Final

  • 상속에대해 정확한 규칙 명세가 없을 경우. 상속을 금지해야된다.
  • 하위 클래스 동작이 예기치 못하게 바뀌어 문제가 발생.
  • final을 사용해 문제를 방지한다.

코틀린에선?

  • 기본적으로 상속에 열려있지만 클래스와 메소드는 기본적으로 final임
  • open 변경자로 상속을 허용 가능함.

  • 예제.
  • 추가로 위예제 클릭 함수는 기본적으로 열려있는데 Final선언시 하위 클래스는 오버라이드 하지 못한다. ( 기본적으로 열려있어서 Final이 중복이 아니다)

왜 기본적으로 final?

  • 그리고 상속을 위한 명시적인 선언 요구 / 상속하려면 open 키워드 사용해라
  • 보안과 성능 최적화 때문.
  • final이 아니면. 다른 개발자가 상속받아 메소드 오버라이딩
  • 기존 메소드를 다르게 사용해 원래클래스 동작방식 어긋남 -> 보안 문제!
  • final없는 메소드는 가상 호출이라해 메소드 호출시 동적 디스패치사용
  • 이는 실행시간에 메소드를 찾아야해 성능에 영향을 미침.
  • 반면 final 메소드는 컴파일 시간에 호출할 메소드 결정해 가상호출필요X 성능 이점
  • 스마트 캐스트가 가능해 타입캐스팅 없이 바로 간결한 코드 작성 가능
  • 책에서 지속적으로 코틀린의 지향점인 간결한 코드에대해 강조하고 있다.

Abstract

  • 코틀린에서 abstract 클래스를 선언가능하고 추상 메서드는 open 변경자 필요 X

  • 코틀린에선 인터페이스경우 final,open ,abstract 사용하지않음.

  • 인터페이스는 항상열려있어 Final로 변경할 수없고 ,abstract 붙일 필요 없다.

  • 코틀린에서 인터페이스는 자동으로 open으로 취급되기 때문에 final, open, abstract 키워드를 사용하지 않습니다.

  • 인터페이스는 final 키워드를 사용할 수 없습니다

가시성 변경자

  • 코드 기반에 선언에 대한 클래스 외부접근을 제어함.

  • 클래스 구현에 대한 접근을 제한함으로서 클래스 의존하는 외부코드를 깨지않고 내부 구현 변경 가능

  • 코틀린의 기본가시성은 Public

  • 코틀린의 패키지는 네임스페이스에만 이용 (가시성제어 X)

  • 대신 코틀린에선 최상위 선언부터 모든 클래스에대해 가시성 제한자 지정해 줘야됨.

  • 이렇게하면? 클래스나 멤버에대해 접근을 명확히 정해 줌!

    internal

  • 모듈 내부에서만 본다는 코틀린의 새로운 가시성

  • 모듈 구현에 대해 캡슐화 제공 및 자세한 구현은 외부에 감출때 유용

  • 정리하면 internal은 각 모듈별로 무슨 역할만 알고있고 내부구현은 몰라도되는 캡슐화가 됨.

internal class InternalClass {
    fun someFunction() {
        // ...
    }
}

// file2.kt
import some.package.InternalClass

class OtherClass {
    fun someFunction() {
        val internalClass = InternalClass() // 컴파일 에러!
        internalClass.someFunction() // 컴파일 에러!
    }
}
 
  1. 코틀린은 하나의 모듈로 구성되면 이 파일은 한번에 컴파일 되고 동시실행됨.
  2. 이 파일들으 서로 내부 구현을 알수 있지만...
  3. Internal 가시성 제한자 사용하면 같은 모듈 만 내부에서 접근가능
  4. 다른 모듈은 몰라도되.. 

나머지 설명

  • 자바에 접근 제한자랑 비슷하게 자기랑 가시성이 같거나 더 높으면 접근 가능 아니면 안됨
  • 코틀린에서 private 클래스를 컴파일이 되면 자바에서는 Private 클래스가 없기때문에
    패키지 전용 클래스로 컴파일함.
package com.example;

class PackagePrivateClass {
    // 클래스 내용
}

위 예시 코드에서 PackagePrivateClass 클래스는 com.example 패키지 내에서만 접근 가능한 클래스입니다. 따라서 com.example 패키지 외부에서는 접근할 수 없음

  • 정리하면 결론적으로 Internal은 컴파일되면 Public으로 같은 모듈끼리만 접근 가능.

내부클래스 / 중첩된 클래스 : 기본은 중첩 클래스


interface State: Serializable

interface View {
	fun getCurrentState : State
	fun restoreState (state: State) ()
}

public class Button implements View {
  @Override
  public State getCurrentState() {
      return new ButtonState();
  }

  @Override
  public void restoreState(State state) {
      // Implementation details go here
  }

  public static class ButtonState implements State {
      // Implementation details go here
  }
}

문제점 ?

  • 기본적으로 자바는 Inner class가 된다.

  • 내부 클래스인 ButtonState는 Button 클래스를 묵시적으로 참조하고 있고.

  • Button을 참조하고 있어서 직렬화가 불가능하기 때문! (외부클래스도 직렬화가가능해야되니깐)

  • 문제 해결을위해 Inner class 대신 static으로 ButtonState클래스를 선언해야 됨.

    class Button : View {
      override fun getCurrentState(): State {
          return ButtonState()
      }
    
      override fun restoreState(state: State) {
          // Implementation details go here
      }
    
      class ButtonState : State {
          // Implementation details go here
      }
    }
    
  • 코틀린은 기본적으로 중첩 클래스라 바깥클래스를 참조하지 않아 오류가 발생하지 않음

  • 책에서는 코틀린은 Inner Class를 명시하거나 자바는 static을 명시해서 바꿔 주라.

    코틀린에서 중첩 클래스 활용

  • 클래스 계층을 만들때 계층 속 클래수 제한 할때 편리?

    sealed class Expr {
       class Num(val value: Int) : Expr()
       class Sum(val left: Expr, val right: Expr) : Expr()
    }
    
     fun eval(e: Expr): Int =
       when (e) {
           is Expr.Num -> e.value
           is Expr.Sum -> eval(e.left) + eval(e.right)
       }
    
     
     fun main() {
       val num1 = Expr.Num(3)
       val num2 = Expr.Num(4)
       val sum = Expr.Sum(num1, num2)
    
       val result = eval(sum)
       println("The result of the expression is: $result")
    }
  • sealed 클래스는 상위 클래스에 sealed 변경자를 붙여 하위 클래스의 정의를 제한

  • 그러므로 디폴트 분기가 필요가 없어짐.

  • 만약 없다면 새로운 클래스 추가시 잊어버려 디폴트분기로 빠져서 버그가 발생할 수 있다.

  • 글고 자동으로 open 이므로 별도로 붙일 필요없음

  • 그리고 새로운 클래스 추가시 when절에서 분기를 추가하지 않을시 컴파일 에러가남

뻔하지 않은 생정자와 프로퍼티를 갖는 클래스 선언

크틀린의 주 생성자

  • class User(val nickname: String)
    class User constructor(val nickname: String) {
    init {
    	println("User initialized with nickname: $nickname")
     }
    }```
    
    
  • 생성자와 초기화 블록
  • 주 생성자에 별도의 코드가 포함될 수 없어 그럴 땐 초기화 블록 (자바 생성자?)
  • 주 생성자의 파라미터로 프로퍼티를 초기화하면 val을 추가하는 방식으로 정의 초기화
  • 이 때 nickname은 읽기전용 프로퍼티를 나타냄
  • 여기서도 생성자 파마티러를 이용하면 코드가 좀더 간결하고 가독성이 좋아짐.
  • class User(val nicname: String ="test" 이런식으로 디폴트 값 정의도가능
  • 인스턴스는 User("현석") 이런식으로 직접 호출 가능.
  • 만약 모든 파라미터에 디폴트 값 지정시 자동으로 파라미터없는 생성자를
  • 컴파일러가 만들어줌
  • 결국 의존관계주입(DI) 프레임워크와 같이 파라미터가 없는 생성자를 필요로 하는 라이브러리와의 통합을 쉽게 할 수 있다 .

기반 클래스 ?

  • 상속 클래스를 말하는 거 같다.
  • 클래스 정의시 별도 생성자 정의하지않으면 껍데기 디폴트 생성자 만들어 줌.
  • class RadioButton: Button() 하위클래스는 반드시 상위 클래스 생성자 호출해야됨.
  • 이런 규칙으로 빈 괄호가 들억게 됨. 반면 인터페이스는 생성자가 없어
  • 구현시 이름뒤에 괄호가 없다.
  • 주 생성자를 private? 외부에서 인스턴스화하지 못하게 함.
  • 뒷 쪽에 동반 객체에 대해서 자세한 설명.
  • 코틀린에선 최상위 함수(외부 인스턴스)
  • 싱글톤을 객체선언(object) 등으로 위에 대한 것을 기본 지원 함.

부 생성자와 : 상위클래스를 다른 방식으로 초기화

  • 여러가지 부 생성자를 만드는 대신 파라미터 디폴트 값을 생성자 시그니처에 직접 명시!
  • 부 생성자 여러개 시 코드 복잡도 증가 유지보수 어려워짐,
  • 이는 생성자의 시그니쳐가 모호해지고 클래스 초기화 방법이 여러개 존재하기 때문
  • 따라서 디폴트값을 사용해 클래스 초기화방법을 명확히 표시

생성자가 여럿 필요할 땐?

open class View constructor(ctx: Context) {
  constructor ( c t : Context) {
    }

  constructor(ctx: Context, attr: AttributeSet) : this(ctx) {
      // attr를 이용한 추가 작업 수행
  }
}

class MyButton : View {
  constructor(ctx: Context) : super(ctx) {
      // Initialize view
  }
  constructor(ctx: Context, attr: AttributeSet) : super(ctx, attr) {
      // Initialize view
  }
}
  • 부 생성자는 constructor로 시작함.

  • super() 키워드를 통해 자신에 대응하는 상위 클래스 생성자 호출!

  • constructor (ctx: Context): this (ctx, MYSTYLE) 이런식으로 자신의 다른생성자 호출.

  • 부 생성자는 반드시 상위클래스를 초기화 하거나 다른 생성자에게 생성위임

  • 클래스에주 생성자가없다면모든부 생성자는 반드시 상위 클래스를초기화하거나
    다른생성자에게생성을위임해야한다.

  • 부 생성자는 생성자 오버로딩을 구현하기 위해 사용되며, 다양한 매개변수 조합을 지원하기 위해 활용

    class Person(val name: String, val age: Int) {
      // 주 생성자로 프로퍼티를 정의하고 초기화
      // 생략 가능한 경우 생략 가능
      init {
          println("Name: $name, Age: $age")
      }
    
      // 부 생성자
      constructor(name: String) : this(name, 0) {
          println("Name: $name")
      }
    }
    
    fun main() {
        val person1 = Person("John", 30) // Name: John, Age: 30
        val person2 = Person("Kate") // Name: Kate, Age: 0
    }
    
  • 이런 식으로 예시도 들수 있다.

  • 여러가지 예시가 있지만.. 디폴트 파라미터로 왠만한건 처리될거 같아 부생성자 사용에대해 의문이 생김.

    인터페이스에 선언된 프로퍼티 구현

  • 인터페이스에는 선언된 추상프로퍼티는 하위클래스에 반드시 오버라이드 되야됨.

  • 인터페이스는 상태를 가질수 없으므로 구현체는 오버라이드해서 프로퍼티를 직접 만들어 줘야 됨.

    interface MyInterface {
        val myProperty: String
        fun myFunction(): Int
    }
    
    class MyClass : MyInterface {
        override val myProperty: String = "Hello, World!"
    
        override fun myFunction(): Int {
            return myProperty.length
        }
    }
    
  • myClass에서 myProperty 구현 및 myfunction을 구현함.

  • 뒷받침하는 필드가 없어 override val 사용해 값을 초기화 함

    뒷받침 필드가 있다면 ?

    interface MyInterface {
        val myProperty: String
    }
    
    class MyClass : MyInterface {
        override var myProperty: String = "initial value"
            set(value) { field = value.toUpperCase() }
    }
    
      interface MyInterface {
        val myProperty: String
    }
    
    class MyClass : MyInterface {
        override val myProperty: String = "initial value".toUpperCase()
    }
    
    
  • 두개는 동일한 코드고 이런식으로 field를 써서 직접 값에 접근해 읽거나 쓸수 있다.

  • 뒷받침 필드란 프로퍼티 값을 저장하는 실제 변수를 말하며 코틀린의 프로퍼티 구현방식 이다.

  • 프로퍼티 값을 변경할대마다 필드 값얼 업데이트 해준다, 기본적으로 코틀린은 Field 자동 생성

  • 사용자는 직접 접근할 수없기에 Field라는 특별한 식별자를 제공한다.

  • 해당 프로퍼티에 참조하기때문에 직접 값을 할당하거나 값을 읽을 수 있다.

  • 설명만들어서는 이해가 잘되지 않았다,

    interface MyInterface {
        val myProperty: String
    }
    
    class MyClass : MyInterface {
        private var _myProperty: String = ""
    
        override var myProperty: String
            get() = _myProperty
            set(value) {
                _myProperty = value.toUpperCase()
            }
    }
    
      fun main() {
          val obj = MyClass()
          obj.myProperty = "hello"
          println(obj.myProperty) // 출력 결과: HELLO
      }
    
  • 위 코드를 보고 결국 이해한것은 상위클래스에서 명시한 프로퍼티를

  • 하위클래스에서 게터 세터를 통한 뒷받침 필드를 이용해 재정의 한것.

  • 이게 뒷받침 필드의 정의 같다 프로퍼티값을 정의하는 구현 방식.

  • ChatGPT와 대화... 계속팔수록 어려워지는거 같다.
  • 추가
  • 뒷받침 필드가 있고 없고 차이?
  • 컴파일러는 게터나 세터에서 field를 사용하는 프로퍼티에 뒷받침 필드 생성.
  • but 커스텀 접근자 구현정의시 뒷받침 필드 존재 X
  • 논의 필요..

데이터 클래스 / 클래스 위임

  • 코틀린에서는 == 연산자는 참조 동일성이아니라 객체의 동등성이다.

  • equlas 는 서로 동등한가?(속성) 해시코드(객체의 고유하게 식별하는데 사용)

  • equals 메서드 사용시 hashcode도 함께 사용해야 한다.

  • 왜냐하면 equals()- > 객체동등성이 True면 hashcode는 반드시 같은 값을 반환해야하는

  • JVM언어 제약 때문.(특성이기도 한다고 한다)

  • hashcode는 객체의 고유한 해시 코드를 반환함. (속성에 따라)

  • hashSet , hashMap 등 컬렉션은 해쉬코드를 사용해 객체 위치를 찾고,

  • equals 메서드이용해 동등성 확인 해쉬코드를 이용해 동일 객체인지 확인해

  • equlas가 true이면 Hashcode()메서드도 트루여야 한다.

  • 만약 서로 값이 다르면 예기치 않는 동작 발생 한다.

  • 주생성자 밖에 설정된 프로퍼티는 두 메소드의 고려대상이아님을 유의!

    hashcode를 구현하지 않으면?

  • object 클래스 클래스에 hashcode()를 상속받게 됨.

  • 상속받은 클래스는 객체의 메모리주소기반을 하기때문에 두개의 객체는 다를수 있다.

     Person person1 = new Person("John", 30);
     Person person2 = new Person("John", 30);
    
     System.out.println(person1.equals(person2));  // true
     System.out.println(person1.hashCode() == person2.hashCode()); // false
     
     
    ## Copy()
     
  • 코틀린의 데이터 클래스는 프로퍼티 val 불변 클래스로 만드는것을 권장

  • hashmap등 컨테이너에 데이터 클래스 객체담을경우 불변성이 필수이기 때문

왜?

  • hashMap과 같은 해시 기반 컨테이너는 내부적으로 해시 테이블 사용하여

  • 객체 저장하고 검색 -> 객체를 고유한 해시코드로 매핑 저장 이를 이용 검색.

  • 따라서 객체의 해시코드는 변하면 안됨 만약 변경되면 찾을수가 없기 때문!

  • 다중 스레드 프로그램인경우 이런 성질이 더 중요하다.

  • 왜냐하면 불변 객체를 사용하면 스레드가 사용 중인 데이터를 다른스레드가 변경을 못하니 동기화
    필요가 줄어드니깐!

  • 불변 객체를 더 쉽게 활용? copy() 메서드 제공

  • 객체 복사 -> 일부 프로퍼티 변경 가능한 메서드.

     class Client(val name: String, val postalCode: Int) {
         fun copy(name: String = this.name, postalCode: Int = this.postalCode) =
             Client(name, postalCode)
     }
     
     fun copy (name: String = this.name,
     postalCode: Int = this.postalCode) = Client (name, postalCode)
     
     >>>val lee=Client("이계영", 4122)
     >>> println (lee. copy (postalCode = 4000))
     client(name=이계영, postalCode=4000)
    
      
       // 허나 얕은 복사이기  때문에 서로 같은 참조를 공유하는 속성을 가지고 있어 변경시 주의해야 된다.
     
     data class Person(val name: String, val address: Address)
     data class Address(val city: String, val street: String)
    
     fun main() {
         val person1 = Person("John", Address("Seoul", "Gangnam-gu"))
         val person2 = person1.copy()
    
         person1.address.city = "Busan"
         println(person1) // Person(name=John, address=Address(city=Busan, street=Gangnam-gu))
         println(person2) // Person(name=John, address=Address(city=Busan, street=Gangnam-gu))
     }
    
      
     
     
     

언제쓰는게 좋을까?

// 일부 속성을 변경해 새로운 객체 만들 때
data class Person(val name: String, val age: Int, val address: String)

fun main() {
  val person1 = Person("John", 30, "Seoul")
  val person2 = person1.copy()

  println(person1) // Person(name=John, age=30, address=Seoul)
  println(person2) // Person(name=John, age=30, address=Seoul)

  val person3 = person1.copy(name = "Jane", age = 25)
  println(person3) // Person(name=Jane, age=25, address=Seoul)
}

// 불변 리스트에서 요소를 추가하거나 제거할 때. (map에서도 활용가능)
fun main() {
  val list1 = listOf("apple", "banana", "orange")
  val list2 = list1.toMutableList()

  val newList1 = list1 + "grape"
  val newList2 = list2.apply { add("grape") }

  println(newList1) // [apple, banana, orange, grape]
  println(newList2) // [apple, banana, orange, grape]
}

클래스 위임 by

  • 객체지향설계시 상속에 의해 문제가 많이 발생함.
  • 상위 클래스의 세부사항 구현에 의존해 하위클래스는 상위 클래스에 바뀜에따라 하위코드가 동작을 하지 않을 가능성이 존재함 (LSP의 원칙)
  • 이런 문제는 코틀린은 final로 기본 클래스를 지정하고 open 변경자로 열어둔 클래스만 확장함.

데코레이터 패턴

  • 종종 상속을 허용하지 않는 클래스에 새로운 동작을 추가할시 사용하는 패턴.

  • 핵심은 기존 클래스와 같은 인터페이스 제공, 기존 클래스 내부 필드를 유지하고

  • 새로운 동작을 추가하는 새로운 클래스(데코레이터) 만듬.

    class DelegatingCollection<T: Collection<> (
    private val innerList = arrayListOf<T> ()
    override val size: Int get () = innerList.size
    override fun isEmpty () : Boolean = innerList.isEmpty ()
    override fun contains (element: T) : Boolean =
    
  • 위임 해야될 코드가 너무 많다..

  • 그래서 by 등장. 인터페이스 구현을 다른 객체에 위임 가능함!

  • 코드 중복을 줄이고 객체지향적 유연성을 높힘.

interface Shape {
    fun calculateArea(): Double
}

class Circle(private val radius: Double) : Shape by Math {
    // Shape 인터페이스에 정의된 메서드를 구현하지 않아도 된다
}

object Math : Shape {
    override fun calculateArea(): Double {
        // 원의 넓이를 계산하는 로직
        return 3.14 * radius * radius
    }
}

fun main() {
    val circle = Circle(5.0)
    val area = circle.calculateArea()
    println(area)
}
  
// 위임을 사용하지않고 직접 구현 .
  
interface Shape {
    fun calculateArea(): Double
}

class Circle(private val radius: Double) : Shape {
    override fun calculateArea(): Double {
        // 원의 넓이를 계산하는 로직
        return 3.14 * radius * radius
    }
}

fun main() {
    val circle = Circle(5.0)
    val area = circle.calculateArea()
    println(area)
}
  • 결국 인터페이스를 직접상속받아 직접 구현하는거보다. 위임클래스를만들어서 위임하는게
  • 코드 중복을 줄이고 코드의 유연성을 높여 유지보수 쉽게 함.
  • By를 사용안하면 모든 메서드에서 직접 구현해야됨..
  • 같은 동작을하는 메서드를 구현해야될때 위임을 사용하면 위임클래스만 변경하면 되기때문에 좋은거같다.

Object 키워드

  • 클래스 정의 및 해당 클래스 인스턴스 생성하는데 사용함.
object Singleton {
    // 싱글톤 객체의 프로퍼티와 메서드를 정의한다.
    // 전역적으로 접근하는 단 하나의 객체
    val name: String = "Singleton"
    fun printName() {
        println(name)
    }
}
  // 객체표현식 생성  무명클래스 정의 및 바로 인스턴스 
  val sum = object : Function2<Int, Int, Int> {
  override fun invoke(a: Int, b: Int): Int = a + b
  }

  println(sum.invoke(1, 2)) // 3

 // 동반객체 생성   
 // 동반 객체는 클래스의 인스턴스와 별도로 생성되서 
    동반 객체의 프로퍼티와 메서드에 접근할 때에는 MyClass.Companion 형태로 접근
// 해당 클래스와 밀접한 관련이 있는 정적 변수(static variable)와 메서드를 구현하는
   데에 사용됨. 주로 팩토리 메서드(factory method)를 구현할 때 사용  
  
  class MyClass {
    companion object {
        // 동반 객체의 프로퍼티와 메서드를 정의한다.
        val name: String = "MyClass"
        fun printName() {
            println(name)
        }
    }
}
  
  // 동반 객체의 프로퍼티에 접근하여 값을 출력한다.
println(MyClass.Companion.name) // MyClass

// 동반 객체의 메서드를 호출하여 값을 출력한다.
MyClass.Companion.printName() // MyClass

  // 싱글톤 예제
    class MySingleton private constructor() {
      companion object {
          private val INSTANCE = MySingleton()

          fun getInstance(): MySingleton {
              return INSTANCE
          }
      }

      fun doSomething() {
          println("Hello, World!")
      }
  }

  fun main() {
      val singleton1 = MySingleton.getInstance()
      val singleton2 = MySingleton.getInstance()

      println(singleton1 === singleton2) // true

      singleton1.doSomething() // Hello, World!
      singleton2.doSomething() // Hello, World!
  }
  • private 생성자를 가진 싱글톤 객체를 동반 객체로 구현. 이런 예시들이 있다.

객체선언

  • object 키워드로 시작, 클래스를 정의하고 인스턴스를만들어 변수에 저장하는 작업을 한문장으로 처리

    대표 적인 싱글톤 예제 클래스 선언과 그 클래스의 속한 단일 인스턴스의 선언을 합침.

  • 주로 한번만 초기화 되는 싱글턴 객체를 쉽게 만들기 위해 사용 된다.

  • 음 아마도 어디서든지 공통적인 무언가를 수행할때 사용하는 전역 메서드 개념으로 이해했다.

  • 그리고 싱글톤으로 만들어지기때문에 자원효율성도 좋다.

  // 싱글톤
  // getDatabase를 통해 전역으로 메서드를 사용 가능. 
  // DatabaseManager.getDatabase() 메소드를 통해 접근가능.
  object DatabaseManager {
    private val database = Room.databaseBuilder(
        context.applicationContext,
        AppDatabase::class.java, "database-name"
    ).build()

    fun getDatabase(): AppDatabase {
        return database
    }
}
  
----------
  
 // 기존 클래스를 이용해 생성 <-> object를 이용해 생성
class Logger {
    fun log(message: String) {
        println("[$message]")
    }
}

fun main() {
    val logger = Logger()
    logger.log("Hello, World!")
}
  
  
object Logger {
    fun log(message: String) {
        println("[$message]")
    }
}

fun main() {
    Logger.log("Hello, World!")
}  
  
  
---------------------
  
  class AppConfig private constructor() {
    companion object {
        private var instance: AppConfig? = null

        fun getInstance(): AppConfig {
            if (instance == null) {
                instance = AppConfig()
            }
            return instance!!
        }
    }

    var apiUrl: String = "https://api.example.com"
    var apiKey: String = "1234567890"
}
  
  fun main() {
    val config = AppConfig.getInstance()
    println(config.apiUrl)
    println(config.apiKey)
}
  
  
  object AppConfig {
    var apiUrl: String = "https://api.example.com"
    var apiKey: String = "1234567890"
}
  
  fun main() {
    println(AppConfig.apiUrl)
    println(AppConfig.apiKey)
}
  • 작은 규모 소프트웨어 에선 단일 인스턴스가 전역으로 사용해 자원활용이 좋지만?
  • 대규모 소프트웨어에선 객체 생성 및 의존성 관리가 복잡 해짐.
  • 이럴땐 DI관리하는게 명확하며 더 적합함.

동반객체: 팩토리메소드와정적멤버가들어갈장소

  • 클래스 내부에 정의되어 클래스와 함께 사용되는 객체
  • 동반 객체는 클래스 인스턴스와 관계없이 사용하는 정적인 멤버를 가짐.
  • 그러므로 클래스 내부 private 멤버도 접근 함.-> 팩토리 메서드 OR 정적메서드 구현 좋음

객체 선언과 동반 객체의 차이점

동반 객체

  • 동반 객체는 클래스 내부에 정의되는 싱글턴 객체
  • 동반 객체는 해당 클래스의 인스턴스가 아니지만, 그 클래스 내부에서 정의되므로 클래스의 private 멤버에 접근
  • 주로 정적 메서드와 속성을 구현할 때 사용

    객체선언

  • 객체 선언은 싱글턴 객체를 선언할 때 사용되는 문법
  • 클래스와 비슷하지만, 단일 인스턴스를 가지며, 해당 객체의 이름을 사용하여 언제든지 접근
  • 유틸리티 함수나 팩토리 패턴을 구현할 때 사용

?? 둘다 팩터리 메서드 패턴에 유용하다 ?


객체 선언 
interface Animal {
  fun speak()
}

class Dog : Animal {
  override fun speak() {
      println("Woof!")
  }
}

class Cat : Animal {
  override fun speak() {
      println("Meow!")
  }
}

object AnimalFactory {
  fun createAnimal(type: String): Animal? {
      return when (type.toLowerCase()) {
          "dog" -> Dog()
          "cat" -> Cat()
          else -> null
      }
  }
}

fun main() {
  val dog: Animal? = AnimalFactory.createAnimal("dog")
  val cat: Animal? = AnimalFactory.createAnimal("cat")

  dog?.speak()
  cat?.speak()
}

// 동반객체

interface Animal {
  fun speak()
}

class Dog : Animal {
  override fun speak() {
      println("Woof!")
  }
}

class Cat : Animal {
  override fun speak() {
      println("Meow!")
  }
}

object AnimalFactory {
  fun createAnimal(type: String): Animal? {
      return when (type.toLowerCase()) {
          "dog" -> Dog()
          "cat" -> Cat()
          else -> null
      }
  }
}

fun main() {
  val dog: Animal? = AnimalFactory.createAnimal("dog")
  val cat: Animal? = AnimalFactory.createAnimal("cat")

  dog?.speak()
  cat?.speak()
}

차이는 ?

  • 객체 선언은 여러 구현 클래스의 인스턴스를 생성하는 전역 팩토리를 구현하는 데 적합
  • 동반 객체는 클래스 내부에 정의되어 클래스와 관련된 팩토리 메서드를 구현하는 데 사용
  • 객체 선언은 독립적인 팩토리 역할에 적합하고, 동반 객체는 특정 클래스와 관련된 팩토리 역할에 적합
profile
어제의 나보다 한걸음 더

0개의 댓글