kotlin-korea / Study-Log

스터디 로그 및 기타 자료
MIT License
62 stars 4 forks source link

[Note] Properties 101 #30

Closed cwdoh closed 7 years ago

cwdoh commented 7 years ago

왜 field가 아니고 property라고 부르는가에 대해 고민해봤는데, 많이 살펴보지는 않았지만, 괜찮은 답변이 있다. (솔직히 필드와 프로퍼티는 코드 수준이라기 보다는 개념적인 느낌만 가지고 있어 매번 헷갈리는 부분이다. ㅜ)

간략하게 field를 정적이지 않은 인스턴스의 멤버로, property를 getter/setter를 가진다는 의미로 보았을 때 Android 내부의 convention인 m prefix는 그다지 어울리지 않는다. 이는 Naming Convention 등에도 영향을 미칠 수 있는 부분이다.

이런 고통 때문인가 golang의 경우는 코딩컨벤션을 일종의 언어적 룰로 결정하고, 이러한 부분을 지원하기 위해 gofmt같은 도구로 강제 포메팅을 하기도 한다.

예를 들어 var mName: String을 지정했을 때 생성된 자바 코드에서 getMName(), setMName()과 같이 애매한 메소드가 탄생하기 떄문이다. 아래의 경우를 보자

class Person {
    val mLastName : String
    val mFirstName : String

    constructor(lastName: String, firstName: String) {
        mLastName = lastName
        mFirstName = firstName
    }
}
public final class Person {
   @NotNull
   private final String mLastName;
   @NotNull
   private final String mFirstName;

   @NotNull
   public final String getMLastName() {
      return this.mLastName;
   }

   @NotNull
   public final String getMFirstName() {
      return this.mFirstName;
   }

   public Person(@NotNull String lastName, @NotNull String firstName) {
      Intrinsics.checkParameterIsNotNull(lastName, "lastName");
      Intrinsics.checkParameterIsNotNull(firstName, "firstName");
      super();
      this.mLastName = lastName;
      this.mFirstName = firstName;
   }
}

기본적으로 멤버 등의 prefix rule은 만들지 말자.

특히 Java와 연동할 때는 더욱 그렇다.

class Person {
    val lastName : String
    val firstName : String

    constructor(lastName: String, firstName: String) {
        this.lastName = lastName
        this.firstName = firstName
    }
}
public final class Person {
   @NotNull
   private final String lastName;
   @NotNull
   private final String firstName;

   @NotNull
   public final String getLastName() {
      return this.lastName;
   }

   @NotNull
   public final String getFirstName() {
      return this.firstName;
   }

   public Person(@NotNull String lastName, @NotNull String firstName) {
      Intrinsics.checkParameterIsNotNull(lastName, "lastName");
      Intrinsics.checkParameterIsNotNull(firstName, "firstName");
      super();
      this.lastName = lastName;
      this.firstName = firstName;
   }
}

굳이 이런 룰을 벋어난다면 아래처럼 getter/setter가 없는 경우 정도겠지만, 상수의 경우는 더더욱 mPrefix가 어울리지 않는다. 그렇다면 @JvmField에만 해당될 수도 있겠다.

class Person {
    @JvmField var mCountry: String = "Korea"

    companion object {
        const val age: Int = 40
    }
}
public final class Person {
   @JvmField
   @NotNull
   public String mCountry = "Korea";
   public static final int age = 40;
   public static final Person.Companion Companion = new Person.Companion((DefaultConstructorMarker)null);

   public static final class Companion {
      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}
soulduse commented 7 years ago

어제 변환 작업을 하다가 궁금한 것이 있어 질문 드립니다! (~클래스 내용이 비슷한 구조라서~)

public class Person {

    private String name;
    private int age;
    private Dog dog;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Dog getDog() {
        return dog;
    }

    public void setDog(Dog dog) {
        this.dog = dog;
    }

    private class Dog{
        private String name;
        private int age;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }
    }
}

이와 같은 클래스가 있다고 가정하면 이 클래스를 코틀린으로 변환했을때

데이터 클래스로 만들어서 사용하려면 어떻게 해야할까요?

현재 클래스 안에 inner클래스로 추가로 들어가 있는 상황인데 따로 분할하지 않고 사용하는게 가능할지

그리고 이런 상황에서는 데이터 클래스를 사용하는 것이 효율적일지에 대한 의문이 생겨서 질문드려요 !

cwdoh commented 7 years ago

일단 제가 아직 data class는 공부를 하지 못해서 뭐라고 답변드리기는 어려울 것 같아요. Nested class에 대해 정리를 하고 넘어가는게 옳지 않을까 싶습니다.

이 문서를 참조해보시면 목적은 다음과 같습니다.

궁금한게 Dog이 왜 inner class로 정의된 것인지가 중요할 것 같습니다.

특히 private로 정의되어 있는데, 이는 클래스 내부에서 생성해서 사용하겠다라는 뜻으로 보이거든요. 그렇다면 외부에서 Dog을 생성해서 전달받는 형태는 아닌 것 같기도 하고요.

용도와는 무관하지만 굳이 하자면 외부에 인터페이스가 존재하는 형태여야 할 것 같습니다. 비슷한 사례를 구현해보자면 아래와 같은 경우는 존재할 것 같네요. inner 키워드는 inner class에서 outer class의 멤버를 액세스하는 키워드입니다.

abstract class Pet(val name: String, val age: Int) {
    abstract fun getHomeAddress() : String
}

class Person(val name: String, val age: Int, var homeAddress: String = "Unknown") {
    private inner class MyDog(name: String, age: Int, val type: String = "Unknown") : Pet(name, age) {
        var nickName: String = ""
        // method will be used within Person class
        override fun getHomeAddress() : String = homeAddress
    }

    private val dog : MyDog = MyDog("똘똘이", 3, "믹스").apply {
        nickName = "똘이"
    }
    fun getDog() : Pet? = dog as Pet
}

아, 그래서 결론은...

  • private class Dog 과 outer class에 존재하는 getDog() 등의 접근은 코드 상으로는 동작하겠지만 구조는 이상해보여요.
  • 저는 data class를 잘 모릅니다. ㅋ 이 부분은 @tonyjs @taehwandev @Pluu @ganadist 님을 소환해봅니다.
tonyjs commented 7 years ago

data class 용도 자체가 어렵고 그렇지 않아요. https://kotlinlang.org/docs/reference/data-classes.html

The compiler automatically derives the following members from all properties declared in the primary constructor:

equals()/hashCode() pair,
toString() of the form "User(name=John, age=42)",
componentN() functions corresponding to the properties in their order of declaration,
copy() function (see below).

자바에서 해당 클래스 같은 지를 비교해야 할 때 보통 equalshashcode를 override 하잔아요? 이걸 자동으로 해주는 용도도 있고

ex) 
Dog dog = new Dog();
...
List<Dog> dogs = new ArrayList<>();
dogs.add(dog);
...
dogs.indexOf(dog);

toString 같은 것도 override 해주고 등등 Immutable 유틸리티? 적인 측면이 많죠.

Nested class 를 data class 로 만들어야 할까? 는 작업 방향에 따라 달라질 것 같아요. Person 이 같은지를 비교하려면 Person 이 가진 Dog도 같은지를 비교해야 된다면 Dogdata class 로 지정해주던가 Personequals, hashcode 를 override 해줘야 겠죠?

cwdoh commented 7 years ago

TODO: custom getter/setter에 대해 정리하기

soulduse commented 7 years ago

좋은 내용 감사합니다!

제가 위의 코드를 사용하고자 했던 의도는 아래의 의도가 더 가까운것 같습니다.

컴퓨터 클래스가 있는데 컴퓨터마다 내부 디테일한 스팩들이 있다.

스팩은 컴퓨터에 종속(?) 되는 개념이니깐 내부클래스로 선언하자

(~아직 코알못 신입이라 제가 객체지향을 잘 생각하는지는 모르겠어요~)

public class Computer{

    private String name;
    private String brand;

    ...

    private class Spec{
        private String cpu;
        private String ram;

        ...
    }
}

제가 의도한 바의 코드는 약간 이쪽이 좀 더 가까울 것 같습니다. 말해주신 내용 공부를 열심히 해봐야되겠네요

감사합니다 ^^

cwdoh commented 7 years ago

@soulduse 설계의 문제에 가까운 것 같아요.

abstract class Spec(val cpu: String, val ram: Int)
open class Computer(
        cpu: String,
        ram: Int,
        val storage: String = "ssd"
) : Spec(cpu, ram)

class BrandComputer(
        val model: String,
        val brand: String,
        cpu: String,
        ram: Int,
        storage: String
) : Computer(cpu, ram) {
    fun getDescription() : String = """
        |$brand $model has:
        |- $cpu CPU
        |- ${ram}mb memory
        |- $storage storage
        """.trimMargin()
}

fun main(args: Array<String>) {
    val computer = BrandComputer(
            brand = "Kotlin",
            model = "STUDY_MACHINE",
            cpu = "i7",
            ram = 8096,
            storage = "hdd"
    )

    println(computer.getDescription())
}

위와 같은 전통적 상속이라던지 혹은 인터페이스를 이용해서 아래처럼

interface BoardSpec {
    fun getCPU() : String
    fun getMemoryCapacity() : Int
}

interface ComputerSpec : BoardSpec {
    fun getStroage(): String
}

class ComputerModule(
        val cpu: String,
        val ram: Int,
        val storage: String = "ssd"
) : ComputerSpec {
    override fun getCPU() : String = cpu
    override fun getMemoryCapacity() : Int = ram
    override fun getStroage(): String = storage
}

// Coding convention from https://kotlinlang.org/docs/reference/coding-conventions.html
class BrandComputer(
        val model: String,
        val brand: String,
        computerModule: ComputerModule
) : ComputerSpec by computerModule {
    fun getDescription() : String = """
        |$brand $model has:
        |- ${getCPU()} CPU
        |- ${getMemoryCapacity()}mb memory
        |- ${getStroage()} storage
        """.trimMargin()
}

fun main(args: Array<String>) {
    val module = ComputerModule("i7", 8096, "hdd")
    val computer = BrandComputer(
            brand = "Kotlin",
            model = "STUDY_MACHINE",
            computerModule = module
    )

    println(computer.getDescription())
}

data class는 공부해보고 추가할께요.

cwdoh commented 7 years ago

TODO: data class 조사 & sample

Pluu commented 7 years ago

delegation 공식 자료 https://kotlinlang.org/docs/reference/delegation.html

cwdoh commented 7 years ago

Delegation은 #25 를 참조하셔도 됩니다.