-
Notifications
You must be signed in to change notification settings - Fork 0
[99 bottles] 디미터 원칙
-
지난번: (~8.5) BottleVerse를 Bottles에서 분리하여 종속성을 주입하는 방식으로 바꾸었다.
-
이번섹션:
- Demeter 법칙
- 오브젝트의 생성과 사용을 분리하기(오브젝트의 생성을 바깥으로 밀어내기)
- 8.19를 보면 verse 메소드가 알고 있어야 하는 것이 많다.
- this.verseTemplate가 생성자이고, new 키워드로 호출될 수 있음
- argument로 number를 받는다는 것
- 그렇게 호출한 생성자가 리턴한 객체의 lyrics 메세지를 보내야 한다는 것
- 그 lyrics가 가사를 만들어 리턴한다는 것
이것들은 Bottles가 신경쓰지 않는 것들인데도 불구하고 알아야하는 것들이다. => 다른말로 하면 종속성
-
종속성은 약점이다. 만약에 verseTemplate이 바뀌면, Bottles에 영향을 끼치고, Bottles가 바뀌어야만 하는 상황이 된다. 종속성은 최소화 해야한다.
-
이제부터 그 작업을 해보자(예시 코드) 8.20 => bestFriend 종속성은 주입되었다. return 문을 보면, 메세지의 체인이 이어지고 있다.
여기서 Foo가 알아야하는 것은
- pet 메세지는 bestFriend 객체로 보낼 수 있다.
- 이 메세지가 리턴한 객체로 preferredToy 메세지를 보낼 수 있다.
- 이 메세지가 리턴한 객체로 .. 등
bestFriend 객체를 Foo가 알고 있으므로, direct collaborator로 볼 수 있다. 그런데 Pet은 Foo의 direct collaborator가 아니다.(collaborator의 collaborator에게 의존) Toy => collaborator의 collaborator의 collaborator에게 의존
지금은 작동하지만 좋은 코드가 아님을 다이어그램을 보면 알 수 있다.
이런식으로 코드를 짜면, 예상치못한 컨텍스트에서 Foo를 사용할 수 있는 능력이 떨어진다. (Foo만 제공하는 것으로 요청을 수행할 수 없고, toy를 가진 Pet을 가진, Friend를 제공해야함)
이 메세지들은 Foo로 하여금 다른 객체들과 밀접하게 결합되도록 한다. (모든 객체들이 이용가능해야 제대로 작동)
테스트는 많은 목적이 있지만, 얼마나 코드를 다시 쓰기가 쉬운지를 재현하는 역할도 한다.(재사용이 얼마나 어려운지) 밀접하게 커플링된 코드는 테스트하기 어렵다. 왜냐면 모든 테스트를 돌리기 위해 많은 context를 제공해야하기 때문이다.
이걸 차치하고서 또 나쁜 점이 있다. 이런식으로 메세지 체이닝 코드를 짜면 객체가 정말로 원하는게 무엇인지 알아채지 못한상태로 코드를 일하게 한다. 지금 이 예제의 경우, 메세지 체이닝이 길어지는 이유가 있는데, 그 이유가 아직 드러나지 않은 것이다.
이 프로그램은 추상화에 의존하는데 추상화에 이름이 없다. 작성자는 그 뜻을 알아도 나중에 읽는 사람은 모를 수 있다. 이걸 해결하기 전에 LoD의 정의에 대해 알아보자
OOP: An Objective Sense of Style에서의 정의
모든 클래스 C와 C에 연결된 메서드 M에 대해서, M이 메세지를 보내는 모든 객체는 아래의 클래스와 연관되어야 한다.
- M의 argument classes
- C의 인스턴스 variable classes
공식적인 정의라서 함축되어있기 때문에 풀어서 보면
메세지는 다음 대상에만 전송되어야한다.
- 메소드에 인자로 넘겨지는 오브젝트
- 직접적으로 이용가능한 오브젝트(self)
- 디미터의 법칙은 모든 각 객체가 아니라 리턴된 오브젝트의 API에 주목한다.
'AbCdE'.repeat(2).replace('C', '!').toLowerCase().slice(1) // -> 'b!deabcde'이 코드는 디미터의 법칙을 벗어나지 않는데, 그 이유는 각 메세지가 리턴하는 객체가 같은 API를 준수하는 객체이기 때문이다. 점의 개수가 중요한게 아니다.
이 법칙은 객체가 메세지를 보낼 수 있는 대상의 목록을 효과적으로 제한한다. 목적은 객체 사이의 결합을 낮추는 것이다. 메세지 전송자의 입장에서 보면, 객체는 그 이웃에게는 이야기할 수 있지만, 이웃의 이웃에게는 하지 못한다. 객체는 direct collaborator에게만 메세지를 보낼 수 있다.
메세지 체인을 해결하는 명백한 한가지 방법은 메세지 포워딩이다. (위임이라고 하는 기술)
Friend에게 위임함 이제 Foo를 테스트하기 쉬워졌고 이는 재사용이 쉬워졌음을 의미한다.
근본적인 문제가 해결된 것은 아니지만 결합을 느슨하게 했다. (Foo는 여전히 Pets와 Toys가 포함된 컨텍스트에서만 사용될 수 있음)
어플리케이션에 존재하는 객체와 메세지에 대한 지식으로 디미터의 법칙을 위반하는 것을 피하려는 시도를 할때 이런식으로 이름을 짓게 되는데, 기존 객체의 이름으로 짓는 것을 피하려면 발신자의 관점에서 생각하는 것이다.
Foo가 원하는 것: 친한 친구의 애완동물의 가장 좋아하는 장난감의 내구성
여기서 Foo는 pet과 노는 적합한 시간을 알고 싶어한다. 그러면 이 장난감이 얼마나 오래 살아남을지 알아야한다.
메세지 이름을 이렇게 쓸 수 있다 => playdateTimeLimit
이제 메서드의 이름은 다른 객체의 존재를 암시하지 않는다. 이름을 이렇게 새로 짓고 나면 코드 바꾸기는 간단하다.
이제 Foo는 직접 연결된 객체에만 말을 걸고 있다. 새로운 요구사항이 생겨도 Foo에 대한 수정없이 새로운 요구를 충족시킬 수 있다.
코드에는 FriendWithChild가 있고 FriendWithPet이 있어서, 다형적으로 구현되어 있다. (같은 api로 여러 요구사항 충족)
이제 다시 Bottles로 돌아오자
4번째 줄이 디미터의 법칙을 위반하고 있다. (new는 클래스에 보내는 메세지로 여긴다면: new-is-just-a- message perspective)
-
주입된 객체에만 메세지를 보내야 하는데
여기서는 클래스를 주입받아서 인스턴스를 생성하고 인스턴스에 메세지를 보내고 있다. -
인스턴스를 주입하면 될텐데 생성자에 넣어줘야 하는 number라는 인자를 바깥에서는 알기 어렵기 때문에,
인스턴스를 만들어서 주입할 수가 없다. -
그래서 여기서는 클래스에게 그 역할을 pass하는 옵션이 있다.
-
이 위반을 수정할 가치가 있는지 판단하기 위해 테스트를 한다고 생각해봄(verse 메서드) (추출된 클래스에 대한 테스트가 없는데, 이것은 뒤에서 다룰 것이다.)
- 테스트를 만들면 재사용이 어느 정도로 어려운지 알 수 있다.
- 세팅이 얼마나 복잡할지 떠올려보면 해당 클래스가 다른 객체에 얼마나 강하게 결합되어 있는지 판단할 수 있다.
new this.verseTemplate(number).lyrics();
- lyrics를 포함한 객체를 얻기 위해 new 키워드를 통해 생성해야한다.
- 이런 번거로움은 디미터 원칙 위반 때문이다. 만약에 Bottles가 verseTemplate에게 직접 lyrics 메세지를 보낸다면 훨씬 쉬울 것이다.(8.26)
이제 코드를 수정하자.
- BottleVerse에 number를 인자로 받는 static 메서드를 추가한다.
효과 => 포워딩을 써서 한 step을 제거함
- static 메서드에서 모든 것을 할수도 있다.
- 다양한 다른 데이터와 결합할 필요성
- 클래스에서 인스턴스로 옮기는 것은 매우 큰 비용이 든다.
- new 타이핑을 피하는 것은 보잘 것 없는 이득이다.
그러므로 항상 인스턴스를 만드는 것이 경제적이다.
지금까지 포워딩 메소드를 이용한 디미터 위반을 fix했다.
- 이 위반을 고치는 게 더 나쁜 예시도 있지만 그건 예외적인 사항이고, 디미터 원칙을 위반하면 안된다. 이제 마지막 걱정이 남았다.
구체성보다 추상화에 기대려는 노력에도 불구하고, 31번째에 여전히 concreation이 남아있다.
하드코딩된 BottleVerse 문제
8.30
- lyrics(static)은 number를 받아서 BottleVerse 인스턴스를 생성하고, lyrics(인스턴스 메서드)를 호출하고 있다.
- lyrics(인스턴스 메소드)는 여러가지로 우려되는 점을 가지고 있다.
-
- BottleNumber와 for에 의존한다.(BottleNumber와 for를 알아야 함)
-
- blank line을 포함한다. => 하나 이상의 역할을 함(Single Responsibility Principle.)
-
- this.number를 사용하는데, 메소드에서 이걸 참조하는 곳이 한군데 밖에 없다.
- 그리고 이 참조는 number를 다른 어떤 것(여기서는 BottleNumber)로 바꾸기 위한 것이다.
- lyrics를 적합한 number와 함께 호출할 수 있다면, 적합한 BottleNumber를 제공할 수도 있을 것이다.
-
- 이 우려들을 종합해보면 한가지 수정이 모든 것을 고칠 수 있다.
잘 디자인된 어플리케이션은 여러 행동을 하기 위해 다형성에 의존하는 느슨하게 결합된 객체로 구성된다.
협력 객체를 주입한다는 것은 위의 관점에 잘 맞는다.
객체로 하여금 변경되지 않고도 다른 새로운 협력 객체와 일할 수 있게 만든다. 즉 엄청나게 유연하다.
새로운 것이 필요하면 리시버 입장에서는 뭔가를 추가로 알 필요가 없고 새로운 클래스를 만들어 주입만 하면 된다.
의존성을 주입하면 객체가 사용되는 곳과 만들어지는 곳이 분리되어 객체를 만드는 것이 바깥으로 밀려난다.
-
미래에도 잘 적응하는 응용 프로그램은...
- 구체적인 클래스 이름의 정보를 인스턴스 메서드에게 제공하지 않고
- 객체의 생성을 어플리케이션의 바깥으로 밀어 내려고 한다
-
BottleVerse를 미래에도 잘 적응하게 하려면... (8.31)
- static 메서드인 lyrics에서 number가 아니라 BottleNumber를 생성해서 인자로 넘겨주는 방식으로 수정
- 그러면 하드 코딩 문제도 해결된다.
(8.31~8.34) BottleVerse가 받는 초기화 인자의 타입이 변한다. 예전에 인자를 검사해서 가드를 줬던 것처럼, 인자의 타입이 instance일 경우의 가드를 먼저 해준다. 이제 BottleNumber 객체를 넘길 수 있다.(종속성이 주입되는 방식으로 바뀜)
(8.35~8.36) 남은 것은 BottleVerse 내부에서 사용하는 멤버변수 이름을 바꾸는 것이다.
(8.37~8.38)
- one-undo changes를 소개함(a multi-line one-undo change)
정적 메소드 lyrics가 여러가지 일을 하고 있는데 이 또한 팩토리를 만들어 만드는 곳(BottleVerse.for(number))과 사용하는 곳(lyrics())을 분리할 수도 있지만 너무 추상적이고 간접적이기 때문에 이 수정을 나중으로 미룰 수 있다.
-
템플릿을 만들어 BottleVerse를 주입하도록 했다.
-
커플링을 헐겁게 만듬
-
새로운 요구사항에 맞게 동작할 수 있게됨(새로운 가사)
-
이렇게 해야 맞을것 같다고 하는 생각을 설명할 수 있게되면 그게 programming aesthetic의 일부가 된다.
-
객체 지향 프로그래밍의 미학
- 인스턴스가 상수의 이름을 알도록 하지 말것
- 하드 코딩된 구체화보다는 주입된 추상화에 의존할 것 ** - 오브젝트가 만들어지는 것을 어플리케이션의 바깥에 위치하도록 하고 다른 곳에서 사용하기** ** - 디미터 법칙을 위반하지 않기**
-
이 미학들을 따르면 객체간의 연결을 느슨하게 할 수 있다.
-
이미 동작하는 코드라면 옵션처럼 느껴질 수 있고 이것들을 따르는데 비용도 발생하지만, 결합을 느슨하게 만드는 것이 압도적으로 비용을 줄이는 방향이다.
- 변화는 일어날 것이기 때문에
- 그리고 어디서 일어날지는 모르기 때문에