Item 31. 문서로 규약을 정의하라
- 함수가 무엇을 하는지 명화갛게 설명하고 싶다면 KDoc 주석을 붙여주는 것이 좋음
/**
* 이 프로젝트에서 짧은 메시지를 사용자에게 출력할 때 사용하는 기본 방식
* @param message 사용자에게 보여 줄 메시지
* @param length 메시지의 길이가 어느정도 되는지 나타내는 enum 값
*/
fun Context.showMessage(
message: String,
duration: MessageLength = MessageLength.LONG,
) {
val toastDuration = when (duration) {
MessageLength.SHORT -> Length.LENGTH_LONG
MessageLength.LONG -> Length.LENGTH_LONG
}
Toast.makeText(this, message, toastDuration).show()
}
- 행위가 문서화되지 않고 요소의 이름이 명확하지 않다면 이를 사용하는 사용자가 의도했던 추상화 목표가 아닌 현재 구현에 의존할 수 있기 때문에 예상되는 행위를 문서로 설명하면 좋음
규약
- 어떤 행위를 설명하면 사용자는 이를 일종의 약속으로 취급하며 이를 기반으로 스스로 자유롭게 생각하던 예측을 조정
- 이처럼 예측되는 행위를 요소의 규약이라고 부름
- 잘 짜여진 규약은 클래스를 만든 사람은 클래스가 어떻게 사용될지 걱정 안해도 됨
규약 정의하기
- 이름 : 일반적인 개념과 관련된 메소드는 이름만으로 동작을 예측할 수 있음
- 주석과 문서 : 필요한 모든 규약을 적을 수 있는 방법
- 타입 : 자주 사용되는 타입의 경우에는 타입만 보아도 어떻게 사용하는지 알 수 있지만 일부 타입은 문서에 추가로 설명해야할 필요가 있음
주석
- 로버트 마틴 : 주석없이 읽을 수 없는 코드를 짜야함
- 하지만 모든 일을 극단적으로 진행하는건 좋지 않고 주석을 적절하게 활용하면 더 많은 내용의 규약을 설명할 수 있음
- listOf 와 같이 코틀린 표준 라이브러리에 적힌 주석은 코드를 이해하는데 도움이 됨
KDoc
주석을 사용하여 기능을 문서화 할 때 주석을 표시하는 공식 형식
/** 로 시작해서 */ 로 끝남
주석 구조
- 첫번째 줄 : 기능 요약 설명
- 두번째 줄 : 기능 자세한 설명
- 그 뒤에 모든 줄 : @ tag로 시작, 파라미터를 설명하는데 사용
- @param <name> : 함수의 값 매개변수 또는 클래스를 문서화
- @return : 함수의 리턴 값
- @constructor : 클래스의 기본 생성자
- @receiver : 익스텐션 함수의 리시버
- @property <name> : 명확한 이름을 갖고 있는 클래스의 프로퍼티를 문서화
- @throw <class>, @exception <class> : 메소드 내부에서 발생할 수 있는 예외 사항
- @sample <identifier> : 정규화된 형식 이름을 사용해서 함수의 사용 예를 문서화
- @author : 요소의 작성자를 지정
- @since : 요소에 대한 버전을 지정
- @supress : 이를 지정하면 만들어진 문서에서 해당 요소가 제외
- …
타입 시스템과 예측
- 클래스와 인터페이스에도 여러 가지 예측이 들어가는데 클래스가 어떤 동작을 할 것이라 예측되면 서브 클래스도 이를 보장해야함
- 이를 리스코프 치환 원칙이라고 부름
- 해당 원칙은 객체 지향 프로그램에서 중요한데, S가 T의 서브 타입이라면 별도의 변경 없어도 T 타입 객체를 S 타입 객체로 대체할 수 있어야함
조금씩 달라지는 세부 사항
- 구현의 세부 사항은 항상 달라질 수 있지만 최대한 많이 보호하는 것이 좋음
- 즉 캡슐화가 많이 적용될 수록 사용자가 구현에 신경을 많이 쓸 필요없고 자유를 많이 갖게 됨
정리
- 외부 API를 구현할 때는 규약을 잘 정의해야함
- 이러한 규약은 이름, 문서, 타입을 통해 구현할 수 있음
Item 32. 추상화 규약을 지켜라
- 규약은 개발자들의 단순한 합의이기 때문에 한쪽에서 규약을 위반할 수 있음
class Employee {
private val id: Int = 2
override fun toString() = "User(id=$id)"
private fun privateFunction() {
println("Private function called")
}
}
fun callPrivateFunction(employee: Employee) {
employee::class.declaredFunctions
.first { it.name == "privateFunction" }
.apply { isAccessible == true }
.call(employee)
}
fun changeEmployeeId(employee: Employee, newId: Int) {
employee::class.java.getDeclaredField("id")
.apply { isAccessible = true }
.set(employee, newId)
}
fun main() {
val employee = Employee()
callPrivateFunction(employee)
changeEmployeeId(employee, 1)
println(employee)
}
- 위와 같이 리플렉션을 활용하면 우리가 원하는 것을 열고 사용할 수 있는데 이런 코드는 private 프로퍼티와 private 함수의 이름과 같은 세부적인 정보에 매우 크게 의존하고 있기 때문에 변경에 매우 취약할 수 있음
상속된 규약
- 클래스르 상속받거나 인터페이스를 확장할때는 규약을 지키는 것은 더더욱 중요
- 예를 들어 equals와 hashCode 메서드를 가진 Any 클래스를 상속 받는데 이러한 메소드는 모두 우리가 반드시 존중하고 지켜야하는 규약이 있음
- 만약 규약을 지키지 않는다면 객체가 제대로 동작 안할 수 있음
- 아래와 같이 equals가 제대로 구현되지 않는다면 중복을 허용하게 되어 제대로 동작하지 않음
class Id(val id: Int) {
override fun equals(other: Any?): Boolean = other is Id && other.id == id
}
fun main() {
val set = mutableSetOf(Id(1))
set.add(Id(1))
set.add(Id(1))
println(set.size) // 3
}
정리
- 프로그램 안정성을 위해 규약을 최대한 지키는 것이 좋음
- 만약 규약을 깨야한다면 문서화를 잘해두는 것이 좋음
'Reading Record > 이펙티브 코틀린' 카테고리의 다른 글
[이펙티브 코틀린] Item 34. 기본 생성자에 이름 있는 옵션 아규먼트를 사용하라 (0) | 2022.06.07 |
---|---|
[이펙티브 코틀린] Item 33. 생성자 대신 팩토리 함수를 사용하라 (0) | 2022.06.07 |
[이펙티브 코틀린] Item 28 ~30. API 안정성 및 가시성 (0) | 2022.05.29 |
[이펙티브 코틀린] Item 27. 변화로부터 코드를 보호하려면 추상화를 사용하라 (0) | 2022.05.29 |
[이펙티브 코틀린] Item 26. 함수 내부의 추상화 레벨을 통일하라 (0) | 2022.05.29 |