코틀린의 생성자는 주생성자, 부생성자로 나뉠 수 있다.
부생성자는 반드시 주생성자를 호출하게 된다.
부생성자는 주생성자를 호출하고 주생성자는 init 을 호출한다.
호출순서를 정리하면 부생성자 호출 -> 주생성자 호출 -> init 블록
실행 순으로 이어진다.
추가로 코틀린의 init 블록과 자바의 static 블록은 차이가 있다.
코틀린의 init 은 주생성자에 병합되는 코드이다.
init 블록을 여러개 사용한다면 위 예제처럼 순차적으로 실행이 된다.
따라서 보통 코틀린에서는 init 블록을 가장 마지막에 넣는다. 왜냐하면 모든 정의가 끝난 후에 사용하기 위해서이다.
코드 리팩토링 과정 중 init 블록 순서가 뒤바뀐다면 코드 결과가 바뀔 수 있다.
주 생성자 속성할당이 먼저 일어나고 그 다음 순차적으로 실행이 된다.
자바 코드를 보면 대부분의 클래스에서 생성자의 역할은 단순히 프로퍼티에 값을 할당하는 역할만 하게된다.
코틀린에서는 코드를 직접 작성할 필요 없이 생성자에 property 를 선언할 수 있게 지원한다,
class TestUser(
private val user: String,
private val pw: String
) {
constructor(user: String): this(user + "+", "")
init {
println("test!")
this.user = user + "-"
}
}
주생성자에 프로퍼티를 선언한 경우, 최상위 init 블록보다도 높은 순서로 매칭된다.
public final class TestUser {
private final String user;
private final String pw;
public TestUser(@NotNull String user, @NotNull String pw) {
Intrinsics.checkNotNullParameter(user, "user");
Intrinsics.checkNotNullParameter(pw, "pw");
super();
this.user = user; // 순서가 init 보다도 높음
this.pw = pw;
String var3 = "test!";
System.out.println(var3); // init 블록
}
public TestUser(@NotNull String user) {
Intrinsics.checkNotNullParameter(user, "user");
this(user + "+", "");
}
}
class TestUser(
private val user: String,
private val pw: String
) {
init {
this.pw = pw + "-"
}
}
주생성자 전략에서는 왠만한 속성을 받은 다음, 가공이 필요한경우 init 블록에서 하도록 작성하는 방법이다.
class TestUser(
private val user: String,
private val pw: String = ""
) {
init {
this.pw = pw + "-"
}
}
위 예시처럼 작성을 하게 되면 user,pw 를 받은 경우, user 만 받은 경우 모두 대응이 가능하다.
즉 생성자 하나로 두개의 생성자 역할을 하는 효과를 볼 수 있다.
class TestUser(
private val user: String,
private val pw: String = ""
) {
constructor(user; String, pw: String) {
this.user = user
this.pw = pw
}
constructor(user; String, pw: Int) {
this.user = user
this.pw = pw
}
}
이런 케이스에서 부생성자만을 사용하는 것보다는 팩토리 메서드를 사용하는 것이 좋다.
- 하위형은 상위형으로 대체할 수 있음
- Parent 에 Child 를 대입가능
- 어떤 형으로 객체를 참조해도 원래 형으로 동작한다.
open class Parent {
protected open fun print() {
println("parent")
}
fun message() {
println()
}
}
class Child : Parent() {
override fun print() {
println("child")
}
}
fun main() {
val actor: Parent = Child() // 대체가능성, 하위형은 상위형에 들어갈 수 있다.
actor.message()
}
위 예시를 보면, actor는 Parent 타입임에도 불구하고, Chile 로 생성이 되었기에 message 를 호출하였을 때 "child" 가 출려깅 된다.
정리하자면, 상위 타입을 사용하더라라도 생성할 때 사용한 객체의 성질을 계속 가지고 있는 것이 내적 동질성이라고 볼 수 있다.
유명한 아키텍트들은 클래스가 최대한 오픈되어 있어야한다 왜냐하면 미래에 객체 확장을 막는다. <- 고수의 영역 ??
코틀린이 적용한 사상은 함부로 확장을 열어두면 기본적으로 막고 필요할때 열어야한다는 것이다.
클래스가 있느냐, 프로토타입이냐는 중요하지 않다.
어떤 시스템을 사용하든 위 두 가지를 만족해야 객체지향 언어라고 말할 수 있다.
마지막으로 언어의 기법과 프로그래밍 기법은 전혀다르다. 아래에서 그 내용을 다룬다.
- 메세지를 보낼 수 없는 객체는 객체가 아니다.
- 객체는 오직 메소드로만 정체성을 알 수 있다.
역할이 아닌 구체적인 대상으로 나누기 되면 안된다. 대상이 바뀌게 된다면 변화가 발생할 수 밖에 없다.
즉, OCP 를 만족해야한다고 볼 수 있다. 인터페이스를 바라보도록
내가 코드를 바꿔야하는 이유, 코드를 바꾸는 이유는 다양하다. 정책이나 사람의 변심 등등
따라서 코드를 바뀌는 이유를 하나로 맞추는 것이 필요하다. 쪼개고 쪼갠다. <- SRP
리마인더: 정해진 내용(Item)이 정해진 시간에 전달된다.
내가 어떠한 주기로 보낼것인지에 대한 변화요소가 있음
내가 메일로 보낼지, 카카오톡으로 보낼지 등등으로 인한 변화요소가 있음
TDD 파는 보통 작은 단위부터 개발하기 떄문에 sender 부터 개발할 것 이다.
Pull-Push 구조 Event Architecture Socket 통신을 생각하면 편함.
순환참조
객체지향에서 가장 위험한 포인트는 순환 참조 고리가 생길때이다.
따라서 양방향 통신이 아닌 단방향 통신으로 프로그래밍해야 좋다.
static
static 한 수준으로 올라가는 경우가 많다면, 무언가 잘못된 것. static level 까지 올라가면 유연하지 못하다.
static 으로 선언할 수 있는 것들은 팩토리밖에 없디. (유틸성 메소드들은 괜찮지 않을까 ???)
유연한 설계
유연한 설계는 딱딱한 설계를 대체할 수 있다.
객체지향에서 유연한 설계는 무엇이냐고 물어보면 함수나 고정필드로 선언할 것을 인스턴스화해서 소유하는 것이라고 말할 수 있다. (단방향 소유 ??)
이러한 주장은 C# 진영에서 주로 주장한다. 반면에 켄트백파는 딱딱하게 만들자고 주장한다.
푸쉬기반 서비스
이벤트 드리븐은 푸쉬기반 서비스이다. 이벤트가 들어모면 반응하는 로직만 개발한다.
이벤트를 계속 보내기만 하면 되기 때문에 정말 편한 개발 기법이다.
하지만 마법은 없다. 모든 푸쉬는 시스템 기저에 풀이 깔려있다 (계속 가져오는 것을 시도함. 에를들어 스핀락같은게 있지 않을까 ??)
Identifier
객체지향에서는 객체를 판단하는 Identifier 가 중요하다.
객체를 식별하는 근본적인 비교는 객체를 담고있는 주소로한다.
이것을 참조 컨텍스트(Reference Context) 라고 한다. 반대로 값에 의존하는 것을 값 컨텍스트(Value Context) 라고 한다.
객체지향프로그래밍에서는 참조 컨텍스트를 사용하는 것이 기본이고 언어별로 Structure / Class 를 구분해서 제공한다.
Structure 는 값 기반이고 Class 는 참조 기반이다. 코틀린에서는 Data class / Class 두 가지를 제공한다.
Kotlin Object 코틀린에서 싱글톤 객체를 만들때 사용한다., static 초기화 시점에 만들어진다.
stiatc 은 실제로 로딩하는 시점(사용 시점)에 초기화가 된다.
자바도 마찬가지로 static 클래스를 사용할 때, 로딩하는 시점에 초기화가 이루어진다.
추상 레이어 인터페이스는 상태를 가질 수 없다.
객체지향설계는 구상 레이어 / 추상 레이어 어디에 둘지 고민을 하게 된다. 둘을 나누어 변화율을 고려하는 것이 필요하다.
이러한 패턴을 템플릿 메소드 패턴이라고 부름
상속을 올바르게 사용하는 방법은 템플릿 메소드 패턴밖에 없다고 함
공통된 부분 처리를 맡기고, 달리지는 부분만 자식이 처리하는 것을 템플릿 메소드 패턴이라고 함
그리고 템플릿 메소드 패턴은 전략 패턴으로 고칠 수 있다.
그 둘의 차이는 템플릿 메소드 패턴은 상속을 사용하고, 전략 패턴은 합성(조합)을 사용한다. 합성을 이용하는 경우에는 계층 구조는 필요하지 않다.
코틀린에서 추상클래스를 구현할때는 무조건 부모의 생성자를 호출할 책임이 있기 때문에디ㅏ.
위 예시를 전략패턴으로 수정하면 started, ended 에 해당하는 객체를 받아온다.
전략패턴의 단점은 클래스 정의 레벨이 아니라 런타임 변수 레벨로 잡는 것이 단점이다.
템플릿 메소드 패턴은 클래스에 정의를 담았기 때문에 인스턴스 만들기 전에도 코드가 완성이 되어 있다.
대규모 시스템을 만들때 전략패턴을 사용하면 객체로 넘아가서 일일이 찾아봐야하기 떄문에 템플릿 메소드 패턴으로 바꾸기도 한다.
정리하자면 런타임에 유연하다는 것은 반대로 정적 타임에 스펙을 알 수 없다.
스펙으로 프레임워크로 문서화가 되어 있는게 좋을 때도 있다 <- 템플릿 메소드 패턴
절차지향 프로그래밍 ?? (Procedure oriented programming)
우선 절치지향 프로그래밍은 오역이다.
객체지향의 뜻은 리턴이 있다는 것이고, Procedure 는 절차적이 아닌 리턴 값이 없다는 것을 의미한다.
프로시저 지향이라는 것은 프로시저처럼 코드를 작성한다는 것이다. 즉 그 안에 if, swtich 등이 있을 것이다.
왜냐하면 모든 기능을 프로시저가 구현을 하기 때문이다.
만면에 객체지향은 케이스가 나오면 형(type)을 더 많이 만드는 것으로 해결한다.