- 데이터와 기능을 객체 내부로 함께 묶는 것을 캡슐화라 한다.
- 프로그래머의 역할을 클래스 작성자(Creator)와 클라이언트 프로그래머 -> 클래스 소비자(?) 로 구분하는 것이 유용하다. - 클래스를 개발할때마다 인터페이스와 구현을 깔끔하게 분리하기 위해 노력해야 한다.
- 객체간에는 메시지를 전송하는 것뿐
- 수신된 메시지를 처리하는 자신만의 방법을 메서드라 부른다.
- 메시지 / 메서드의 구분
- 메시지 전송 / 수신은 public 메서드를 호출하는 것으로 표현
-
- 메시지와 메서드를 구분하자!
- 부모 클래스에 기본적인 알고리즘의 흐름을 구현하고, 중간에 필요한 처리를 자식클래스에게 위임하는 디자인 패턴을 TEMPLATe METHODS 패턴이라 한다
- 코드의 의존성과 실행시점의 의존성은 다를 수 있다 - 코드는 추상클래스에 의존하고, 실행시점에는 구현체에 의존하도록 유연한 구조 - 코드와 실행시점 의존성이 다르면 코드를 이해하기 어려워지나, 유연성을 얻을수 있는 트레이드 오프 관계. - 유연성 vs 가독성
- 차이에 의한 프로그래밍 - 공통된 부분을 상속받아 다른부분만 작성한다. - 상속은 두 클래스간의 관계를 정의하는 방법이다. - 자식클래스가 부모클래스를 대신하는 것을 UpCasting 이라 한다. - 자식클래스가 부모 클래스 타입으로 변환되는 Up-Casting
- 다형성 - 메시지와 메서드는 다르다! - 상속 - 컴파일 시간 의존성과 실행시간 의존성을 다르게 만들 수 있는 객체지향 특성을 이용해 서로다른 메서드를 실행할 수 있게 한다. - 다형성? : 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력. 다형적인 협력에 참여하는 객체들은 모두 같은 메시지를 이해할 수 있어야 한다 --> Interface가 동일해야 한다. - 인터페이스 통일을 위해.. 상속을 사용 (
DiscountPolicy
) -Lazy
orDynamic
Binding - 다형성은 상속이외에 다른 여러가지 방법으로 구현이 가능하다. - 인터페이스 -isSatisfy
를 실체화(implementation
) 하여 구현 - 이 경우에도 업캐스팅이 적용되며, 협력은 다형적이다! - 추상화와 유연성 - 추상화 계층만 들여다보면 요구사항을 높은 수준에서 서술이 가능해진다 - 추상화를 이용하면 설계가 유연해진다 - 설계가 구체적인 상황에 결합되는것을 방지해 준다! - Context Independency! - 유연성이 필요한 곳에 추상화를 사용하라!
- 추상클래스 vs 인터페이스 Trade-off
- 코드재사용 - 상속 - 캡슐화를 위반한다. : 자식클래스는 부모클래스를 잘 알아야 하고, 강결합 된다. - 관계가 컴파일 시점에 결정되므로 실행시점에 객체를 변경하는게 불가능하다. - 인스턴스 변수로 연결되어 있으면, 실행시점에 인스턴스 변경이 가능하다! - 합성 (Composition) - 인터페이스에 정의된 메시지를 통해서만 코드를 재사용한다. -> 합성 - 합성은 상속의 두가지 문제점을 모두 해결한다. 1. 인터페이스에 정의된 메시지로만 재사용이 가능하므로, 구현을 효과적으로 캡슐화 할 수 있다. 2. 의존하는 인스턴스를 교체하기 쉬우므로 설계를 유연하게 만든다. - 코드 재사용을 위해서는 합성보다 상속을 선호하기도 한다.
- 객체를 자율적으로 만드는 가장 기본적인 방법은 내부 구현을 캡슐화 하는것이다.
- 캡슐화를 통해 변경에 대한 파급효과를 제한할 수 있다.
- 때문에, 자율적인 객체는 변경하기 쉬워진다.
- 객체들 사이의 협력을 구성하는 일련의 요청과 응답의 흐름을 통해 기능이 구현된다.
- 객체의 행동을 결정하는 것은 협력이다.
- 객체의 상태를 결정하는 것은 행동이다.
- 협력 -> 행동 -> 상태 (?)
- 협력은 객체를 설계하는데 필요한 문맥을 제공한다.
- 책임은 객체에 의해 정의되는 응집도 있는 행위의 집합
- 객체의 책임은 무엇을 알고 있는가(knowing)와, 무엇을 할수 있는가(doing)로 구성되어 있다 - 하는것 (Doing) - 객체를 생성하거나 계산을 수행하는 등의 스스로 하는 것 - 다른 객체의 행동을 시작시키는 것 - 다른 객체의 활동을 제어하고 조작하는 것 - 아는것(Knowing) - 사적인 정보에 관해 아는 것 - 관련된 객체에 관해 아는 것 - 자신이 유도(Derive/ Computed)하거나 계산할 수 있는 것에 대해 아는것
- 객체지향 개발에서 가장 중요한 능력은 책임을 능숙하게 객체에 할당하는 것
- 적절한 협력 -> 적절한 책임 -> 적절한 객체에 제공
- CRC(Candidate, Responsibility, Collaborator) Card
- 객체지향 설계는 책임을 완료하는데 필요한 더 작은 책임을 찾아내고 이를 객체들에게 할당하는 반복적인 과정을 통해 모양을 갖춰 나간다.
- 책임주도설계 - 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악한다. - 시스템 책임을 더 작은 책임으로 분할한다 - 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다. - 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾는다 - 해당 객체 또는 역할에게 책임을 할당함으로 두 객체가 협력하게 된다.
- 메시지를 먼저 식별하고 메시지를 처리할 객체를 나중에 선택한다.
- 객체가 최소한의 인터페이스를 가질 수 있게 된다.
- 객체는 충분히 추상적인 인터페이스를 가질 수 있게 된다.
- 객체는 다른객체에게 제공하는 행동에 의해 결정된다.
- 객체의 상태가 아니라 행동에 초점을 맞춰야 한다!!!
- 협력이라는 문맥 안에서 객체를 생각해야 한다.
- 상태는 객체가 행동을 정상적으로 수행하기 위해 필요한 재료일 뿐이다.
- 익명의 역할을 찾고, 수행가능한 객체를 선택하는 방식으로 설계를 진행함.
- 역할을 통해 유연하고 재사용 가능한 협력을 얻을 수 있다.
- 역할은 두 종류의 구체적인 객체를 포괄하는 추상화 이다.
- 역할을 이용하면 불필요한 중복코드를 제거할 수 있다. + 협력이 더 유연해졌다.
- 역할의 구현 : 추상클래스 / 인터페이스 - 추상클래스 : 책임의 일부를 구현 - 인터페이스 : 책임의 집합만을 나열
- 책임을 수행하는 대상이 한종류이면 객체
- 여러종류의 객체들이 참여할 수 있으면 역할
- 협력 -reference-> 역할 -select from-> 객체 -instance of-> 클래스
- 중요한 것은 책임이다.
- 객체지향 설계에서는 '상태'(=데이터)를 분할의 중심으로 삼거나 - 데이터를 조작하는데 필요한 오퍼레이션을 정의 - 객체의 상태에 초점 - 객체는 독립된 데이터 덩어리
- '책임'을 분할의 중심축으로 삼는 방법이 있다. - 다른 객체가 요청할 수 있는 오퍼레이션 처리를 위한 상태를 보관. - 객체의 행동에 초점 - 객체는 협력하는 공동체의 일원
- 변경의 여파를 조절할 수 있다. (내부를 감춘다)
- 변경이 잦으면 (내부) 구현, 안정되면 인터페이스
- 캡슐화의 정도가 객체의 응집도와 결합도를 결정한다.
- 응집도 (High Cohesion) - 모듈내의 요소들이 하나의 목적을 위해 긴밀히 협력하면 높은 응집도 - 변경의 관점에서 : 변경이 발생할 때 모듈 내부에서 발생하는 변경의 정도. - 하나의 변경에 대해 하나의 모듈만 변경되면 응집도가 높음. (좋은것) - 응집도가 높으면 변경의 대상과 범위가 명확해져서 변경이 쉬워짐.
- 결합도 (Low Coupling) - 다른 모듈에 대해 얼마나 많이 알고 있는지 - 변경의 관점에서, 한모듈이 변경되기 위해 다른 모듈의 변경을 요구하는 정도.
- 데이터 중심의 설계는 캡슐화를 위반하기 쉽다.
- 캡슐화 위반 - 객체가 다양한 상황에서 사용될 수 있을거라는 막연한 추측을 기반으로 설계를 하게되면 과도한 내부구현을 노출하는 메소드를 추가할 수 밖에 없다.
- 높은 결합도 - get methods의 노출은 결국 그 인스턴스 변수를 노출시키는 꼴이 된다. - 여러 데이터 객체를 사용하는 제어로직이 특정 객체안에 집중되기 때문에, 하나의 제어 객체가 다수의 데이터 객체에 강하게 결합이된다. -
ReservationAgency
에 모든 의존성이 모이게 된다. (CommonView.ts
) - 시스템 안의 어떠한 변경되ReservationAgency
의 변경을 유발하게 된다. - 낮은 응집도 - 여러가지 다른 목적의 로직이 하나의 모듈안에 있을때 낮은 응집도. - 하나를 고치면 다른쪽 코드에도 영향이 가게된다. - 변경시 여러 코드를 동시에 고쳐야 한다. -
Single Responsibility Principle
- 캡슐화를 지켜라 - 캡슐화는 설계의 제1원리이다. - 높은 응집도와 낮은 결합도가 필요.
Rectangle
의 너비와 높이를 증가시키는 코드는,Rectangle
안에 두어서책임
을 이동시킨다. 이것이 바로 객체가 자기 스스로 책임진다는 말의 의미이다.- 스스로 자신의 데이터를 책임지는 객체
- 캡슐화는 변하는 어떤 것이든 감추는 것이다.
DiscountCondition
-Movie
간에 결합도가 높다.
- 할인조건이 변경되면,
DiscountCondition
,Movie
,Screening
을 함께 변경해야 한다.
- 데이터에 대한 결정 강요 -> 내부구현에 초점을 맞추게 됨.
- 올바른 객체지향 설계의 무게중심은 객체의 외부에 맞춰져야 한다.
- 책임에 초점을 맞춰 설계 -> 어떤객체에게 어떤 책임을 할당할것인가?
GRASP
패턴을 알아보자
- 객체는 데이터보다, 외부에 제공하는 행동이 중요하다.
- 객체에게 어떤 책임을 할당해야 하는가?
협력
에서 실마리를 찾을 수 있다.
- 책임은 객체가 참여하는 협력에 적합해야 한다.
- 메시지를 전송하는 클라이언트의 의도에 적합한 책임을 할당해야 한다.
- 메시지를 결정한 후에 객체를 선택해야 한다.
메시지를 누구에게 전송할 것인가?
- 메시지를 전송하기 때문에 객체를 갖게된 것이다.
- 시스템 책임을 파악한다
- 더 작은 책임으로 분할한다
- 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다.
- 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾는다.
- 해당 객체 또는 역할에게 책임을 할당함으로써 두 객체가 협력하게 한다.
- 메시지를 전송할 객체는 무엇을 원하는가? -
예매하라
- 메시지를 수신할 적합한 객체는 누구인가? - 책임을 수행할 정보를 잘 알고 있는 객체에게 책임을 할당한다. - 책임을 수행하는데 필요한 정보를 가지고 있는 객체에게 할당하라. 그래야 책임을 어떻게 수행할지를 스스로 결정할 수 있기 때문이다.
예매하라
-->Screening
가격을 계산하라
--> 영화가격을 계산하는 데 필요한 정보를 가장 잘 아는Movie
가 메시지를 수신한다.Movie
-->할인여부를 판단
->DiscountCondition
에게 할당- 객체지향의 가장 기본 원리를 책임할당 관점에서 표현.
- 설계는 트레이드오프 (Trade-off) 활동이다.
- 책임할당을 설계할때 낮은 결합도를 유지할 수 있는 설계를 선택하라
- B가 A객체를 포함하거나 참조한다.
- B가 A객체를 기록한다
- B가 A객체를 긴밀하게 사용한다
- B가 A객체를 초기화 하는데 필요한 데이터를 가지고 있다 (B는 A에 대한 정보전문가이다.)
Reservation
을 잘 알고 있는 객체는Screening
이다.- 어떤 객체를
Creator
로 정할것인가?올바르게 설계하고 있는지 궁금한가? 코드를 작성하라
Movie
에 전송하는 메시지 시그니처를calculateMovieFee
로 송신자의 의도를 표현한다.
- 새로운 할인조건 추가 :
isSatisfiedBy
메소드 수정필요 - 순번조건 판단로직 변경 :
isSatisfiedBySequence
메소드 수정 - 기간조건을 판단하는 로직이 변경되는 경우 :
isSatisfiedByPeriod
메서드 내부구현 수정낮은 응집도 -> 변경의 이유에 따라 클래스를 분리해야 한다.
- 서로다른 이유로 변경되는 두 메서드를 가지는
DiscountCondition
클래스의 응집도는 낮아질 수 밖에 없다.
- 인스턴스 변수가 초기화 되는 시점을 살펴보자. 응집도가 낮은 클래스는 객체속성중 일부만 초기화 한다.
- 함께 초기화 되는 속성을 기준으로 코드를 분리해야 한다.
- 메서드들이 인스턴스 변수를 사용하는 속성에 따라 그룹이 나뉜다면 응집도가 낮다.
- DiscountCondition -> PeriodCondition / SequenceCondition으로 분리
- 분리하고 Interface / Implement 로 나눠준다. : THIS IS Polymorphism!
DiscountCondition
덕분에 새로운 할인조건이 추가되거나 할인조건의 구현이 변경되어도Movie
는 코드수정이 필요없다.변경을 캡슐화
한다.
- 구현을 공유할 필요가 있어서
abstract
를 이용해서 역할을 구현한다.
- 코드를 이해하기 쉽고 수정하기쉽게 최대한 단순하게 설계한다.
- 코드를 수정하지 않고도 변경을 수용할 수 있도록 코드를 더 유연하게 만든다.
DiscountPolicy
를 분리하고, Movie에합성
한다.
- 잘 모르겠으면 일단 동작하는 코드를 만든다.
- 그리고 겉으로 드러나는 동작이 바뀌지 않게 고쳐나간다 REFACTORING!
ReservationAgency.ts
를 가지고 살펴보면 - 하나의 명확한 기능만 수행하는 메서드로 잘개 쪼갠다. - 작고, 명확하며, 한가지 일에 집중하는 응집도 높은 메서드는 변경 가능한 설계를 이끌어 내는 기반이 된다. - 여전히ReservationAgency
자체의 응집도는 여전히 낮고, 응집도를 높이기 위해서는 변경의 이유가 다른 메서드들을 적절한 위치로 분배해야 한다. 적절한 위치란 바로 각 메서드가 사용하는 데이터를 정의하고 있는 클래스이다!
- 객체지향 애플리케이션의 가장 중요한 재료는 객체들이 주고받는
메시지
다 - 메시지들이 객체의 퍼블릭 인터페이스를 구성한다.
- 메시지를 매개로 하는 요청과 응답의 조합이 두 객체 사이의 협력을 구성한다.
- 메시지(Message) - 한객체가 다른 객체에게 도움을 요청 : Message sending, Message passing - Message sender, Message receiver - Message는 Operation 과 argument, receiver 로 구성된다. -
condition.isSatisfiedBy(screening)
-수신자.오퍼레이션(인자)
- 메시지를 수신했을 때 실제로 실행되는 함수 또는 프로시저를
메서드
라 부른다.계산을 메시지와 메서드로 분리하고, 실행시간에 수신자의 클래스에 기반해서 메시지를 메서드에 바인딩하는 것은. 일반 적인 프로시저 호출 관점에서는 아주 작은 변화처럼 보이지만, 이 작은 변화가 큰 차이를 만든다
- 퍼블릭 인터페이스에 포함된 메시지를 오퍼레이션이라 한다.
- 메시지를 수신했을 때 실제 실행되는 코드를 메서드라 한다.
메서드는 오퍼레이션의 구현이다. 메서드는 오퍼레이션과 연관된 알고리즘 또는 절차를 명시한다.
- 오퍼레이션 이름과 파라미터 목록을
시그니처
라 부른다. - 오퍼레이션은 실행코드 없이 시그니처만을 정의한 것이다.
- 다형성이란 동일한 오퍼레이션 호출에 대해 서로 다른 메서드들이 실행되는것이라 정의할 수 있다.
- 메시지 : 객체가 다른 객체와 협력하기 위해 사용하는 의사소통 매커니즘. 일반적으로 객체의 오퍼레이션이 실행되도록 요청하는 것을
메시지 전송
이라고 부른다. 메시지는 협력에 참여하는 전송자와 수신자 양쪽 모두를 포함하는 개념이다. - 오퍼레이션 : 객체가 다른 객체에게 제공하는 추상적인 서비스이다. 메시지가 전송자와 수신자 사이의 협력 관계를 강조하는 데 비해, 오퍼레이션은 메시지를 수신하는 객체의 인터페이스를 강조한다. 다시 말해서, 메시지의 전송자는 고려하지 않은 채 메시지 수신자의 관점만을 다룬다. 메시지 수신이란 메시지에 대응되는 객체의 오퍼레이션을 호출하는 것을 의미한다.
- 메서드 : 메시지에 응답하기 위해 실행되는 코드 블록을 메서드라 부른다. 메서드는 오퍼레이션의 구현이다. 동일한 오퍼레이션이라고 해도 메서드는 다를 수 있다. 오퍼레이션과 메서드의 구분은 다형성의 개념과 연결된다.
- 퍼블릭 인터페이스 : 객체가 협력에 참여하기 위해 외부에서 수신할 수 있는 메시지의 묶음. 클래스의 퍼블릭 메서드들의 집합이나 메시지의 집합을 가리키는 데 사용된다. 객체를 설계할 때 가장 중요한 것은 훌륭한 퍼블릭 인터페이스를 설계하는 것이다.
- 시그니처 : 시그니처는 오퍼레이션이나 메서드의 명세를 나타낸 것으로, 이름과 인자의 목록을 포함한다. 대부분의 언어는 시그니체의 일부로 반환 타입을 포함하지 않지만 반환타입을 시그니처의 일부로 포함하는 언어도 존재한다.
- 좋은 인터페이스란 최소한의 인터페이스(꼭 필요한 오퍼레이션 만을 포함)와 추상적인 인터페이스(어떻게가 아니라 무엇을 하는지?)라는 조건을 만족해야 한다.
- 가장 좋은 방법은 책임설계 방법을 따르는 것이다. 메시지를 먼저 선택함으로, 협력과는 무관한 오퍼레이션이 인터페이스에 스며드는 것을 방지한다.
- 퍼블릭 인터페이스의 품질에 영향을 미치는 원칙과 기법 - 디미터 법칙 - 묻지말고 시켜라 - 의도를 드러내는 인터페이스 - 명령-쿼리 분석
- 객체의 내부구조에 강하게 결합되지 않도록 협력 경로를 제한하라. (접근 경로를 제한한다) - 낯선 자에게 말하지 말라 - 오직 인접한 이웃하고만 말해라 - 오직 하나의
.
만 사용하라 - 디미터법칙을 따르려면 - M의 인자로 전달된 클래스 - C의 인스턴스 변수의 클래스
- 또는 - this - 메서드의 매개변수 - this의 속성 - this의 속성인 컬렉션의 요소 - 메서드 내에서 생성된 객체
- 디미터 법칙을 지키지 않는 전형적인 코드
screening.getMovie().getDiscountConditions();
훌륭한 메시지는 객체 상태에 관해 묻지 말고, 원하는 것을 시켜야 한다 (Tell, Don't Ask!)
- 인터페이스는 객체가 어떻게 하는지가 아니라 무엇을 하는지를 서술해야 한다.
- 어떻게 하는지를 드러내는 코드
class PeriodCondition {
isSatisfiedByPeriod(screening: Screening): boolean;
}
class SequenceCondition {
isSatisfiedBySequence(screening: Screening): boolean;
}
- 무엇을 하는지를 드러내는 코드
class PeriodCondition implements DiscountCondition {
isSatisfiedBy(screening: Screening): boolean
}
class SequenceCondition implements DiscountCondition {
isSatisfiedBy(screening: Screening): boolean
}
interface DiscountCondition {
isSatisfiedBy(screening: Screening): boolean
- 클라이언트 관점에서 동일한 작업을 수행하는 메서드들을 하나의 타입 계층으로 묶을 수 있는 가능성이 커지게 된다.
의도를 드러내는 선택자(Intention Revealing Selector
: 하나의 구현을 가진 메서드의 이름을 일반화 하도록 하는 훈련 => 매우 다른 두번째 구현을 상상하고, 이 메서드에 동일한 이름을 붙인다고 상상하면, 할수 있는 한 가장 추상적인 이름을 메서드에 붙이게 될 것이다.
// 전형적인 디미터 법칙 위반
audience.getBag().minusAmount(ticket.getFee())
- 클라이언트에게 내부구현이 노출되고, 작은 요구사항 변경에도 쉽게 무너지는 불안정한 코드가 되어버린다.
Ticket ticket = ticketSeller.getTickOfficer().getTicket();
audience.getBag().minusAmount(ticket.getFee());
- 오퍼레이션의 이름은
협력
이라는 문맥을 반영해야 한다. - 오퍼레이션은
클라이언트
가 객체에게 무엇을 원하는지를 표현해야 한다. 객체 자신이 아닌 클라이언트의 의도를 표현하는 이름을 가져야 한다. - 디미티 법칙 - 묻지말고 시켜라 - 의도를 드러내는 인터페이스
- 묻지마 코드로 변경해야 하는 코드 - 질문하고 : 어떤 객체의 상태를 물어본다 - 판단하고 : 반환된 상태를 기반으로 결정을 내린다 - 상태변경 : 결정에 따라 객체의 상태를 변경한다.
- Audience에게 위임 메서드를 추가한다.
- 결합도를 낮추고 응집도를 높이는 방향으로 가야한다.
- 객체는 내부구조를 숨겨야 하므로 디미터 법칙을 따르는것이 좋지만, 자료구조라면 당연히 내부구조를 노출해야 하므로 디미터 법칙을 적용할 필요가 없다.
설계는 트레이드 오프의 산물이다.
프로시저
: 정해진 절차에 따라 내부의 사애를 변경하는 루틴의 한 종류. (Has side-effect)함수
: 필요한 값을 계산해서 반환하는 루틴의 한 종류 (no side-effect)질문이 답변을 수정해서는 안된다
- 객체의 상태를 변경하는 명령은 반환값을 가질 수 없다.
- 객체의 정보를 반환하는 쿼리는 상태를 변경할 수 없다.
- 명령과 쿼리를 뒤섞으면 실행결과를 예측하기 어려워지게 된다.
- 겉으로는 쿼리로 보이지만, 내부적으로 부수효과를 가지는 메서드는 이해하기 어렵고, 잘못 사용하기 쉬우며, 버그를 양산한다.
class Event {
isSatisfied(schedule: RecurringSchedule): boolean
reschedule(schedule: RecurringSchedule): void
- 메서드가 값을 반환하면 : 쿼리 (순수함수)
- 메서드가 값을 반환하지 않으면 : 명령 (부수효과)
if(!event.isSatisfied(schedule)) {
event.reschedule();
}
- 참조투명성 : 어떤 표현식 e가 있을때, e의 값으로, e가 나타나는 모든 위치를 교체하더라도 결과가 달라지지 않는 특성
- 수학의 함수가 대표적인 참조투명성을 가진다.
IMMUTABILITY!
- 메시지를 먼저 선택하고, 그 후에 메시지를 처리할 객체를 선택한다.
- 분해 : 큰 문제를 해결 가능한 작은 문제로 나누는 작업
- 추상화 : 불필요한 정보를 제거하고 현재의 문제 해결에 필요한 핵심만 남기는 작업
- 기능분해 / 알고리즘 분해
- 타입추상화(Abstract Data Type)
- 프로시저 추상화(Object-Oriented) - 역할과 책임을 수행하는 객체 - 협력하는 공동체
- 직원의 급여를 계산한다 - 사용자로부터 소득세율을 입력받는다 - '세율을 입력하세요' 라는 문장을 화면에 출력한다. - 키보드를 통해 세율을 입력받는다. - 직원의 급여를 계산한다 - 전역변수에 저장된 직원의 기본급 정보를 얻는다. - 급여를 계산한다. - 양식에 맞게 결과를 출력한다. - '이름: {직원명}, 급여: {계산된 급여}' 형식에 따라 출력 문자열을 생성한다.
- 하나의 메인함수라는 비현실적인 아이디어
- 메인함수의 빈번한 재설계
- 코드 안에서 비즈니스 로직과 사용자 인터페이스가 결합된다.
- 하향식 접근법의 첫번쨰 질문은
무엇(what)
이 아니라어떻게(how)
이다. - 함수는 상위 함수가 강요하는 Context 와 강하게 결합된다.
- 데이터 변경으로 인한 파급효과. : 어떤 데이터를 어떤 함수가 사용하는지 모른다.
작은 프로그램과, 개별 알고리즘을 문서화 하고 서술하는데는 하향식 분해가 유용하다.
- 함께 변경되는 부분을 하나의 구현단위로 묶고, 퍼블릭 인터페이스를 통해서만 접근하도록 만든다.
정보은닉
: 자주 변경되는 부분을 덜 변경되는 인터페이스 뒤로 감춰야 한다. - 복잡성 : 외부에 추상화된 개념을 제공하기 위해 복잡함을 감춘다. - 변경가능성 : 변경 가능한 설계 결정을 모듈 내부로 감춘다. (파급도를 낮출 수 있다.)- 모듈은 책임의 할당.
- 모듈은 데이터를 설계의 중심요소로 부각시킨다.
- 모듈은 인스턴스 개념을 제공하지 않는다.
- ADT : 상속과 다형성을 지원하지 않음, 타입을 추상화 - 오퍼레이션을 기준으로 타입을 묶는다 - 인스턴스 변수에 저장된 값을 기준으로 메서드 내에서 타입을 명시적으로 구분하는 방식 (isHourly 를 통해 if로 구분) - 오퍼레이션이 추가된다면 ADT가 유리!
- OOP : 상속과 다형성 지원!, 절차를 추상화 - 타입을 기준으로 오퍼레이션을 묶는다. - 동일한 메시지에 대해 서로 다르게 반응한다 -> 다형성 -> 절차 추상화(절차에 대한 차이를 감춘다) - if 대신에
Replace Type code with Class
방식으로 처리. - 새로운 타입이 추가된다면 OOP가 유리! Open-Closed Principle
개방-폐쇄 원칙 : 기존 코드에 아무런 영향을 미치지 않고 새로운 객체 유형과 행위를 추가할 수 있는 객체지향의 특성
- 의존성 : 의존되는 요소가 변경될때 의존하는 요소도 함께 변경될 수 있다는 것을 의미 -> 변경에 의한 영향의 전파 가능성
- 의존성이 전이될 가능성에 따라 직접의존성 / 간접의존성으로 나눔
- 어떤 클래스의 인스턴스가 다양한 클래스 인스턴스와 협력하기 위해서는, 협력할 인스턴스의 구체적인 클래스를 알아서는 안된다. 실제로 협력할 객체가 어떤 것인지는 런타임에 해결해야 한다.
- 컴파일 타임 구조와 런타임 구조의 사이의 거리가 멀면 멀수록 설계가 유연해지고 재사용이 가능해진다.
- 설계가 유연해지려면, 자신이 실행될 컨텍스트에 대한 구체적인 정보를 최대한 적게 알아야 한다.
- 적으면 적을수록 더 다양한 컨텍스트에서 재사용될 수 있기 때문이다.
- 컴파일 타임 의존성을 실행 컨텍스트에 맞게 적절한 런타임 의존성으로 교체하는 것을 의존성 해결이라 한다. - 객체를 생성하는 시점에 생성자를 통해 의존성 해결 - 객체 생성 후 setter 메서드를 통해 의존성 해결 - 메서드 실행 인자를 이용해 의존성 해결 (일시적으로만 알아도 될 경우에)
- 의존성을 바람직하게 만들어라. 자신이 전송하는 메시지를 이해할 수 있다면 어떤 타입의 객체와 협력하더라도 상관이 없도록 만들어야 한다.
- 바람직한 의존성? 재사용성과 관련이 있다.컨텍스트 독립적인 의존성이 바람직하다.
- 결합도 (Coupling)
- 의존성은 존재하거나 존재하지 않거나. (Existence)
- 결합도는 의존성의 상대적인 강도
- 서로에 대해 알고 있는 지식의 양이 결합도를 결정한다.
- 더 많이 알수록 더 많이 결합된다. 더 많이 알고있다는 것은 더 적은 컨텍스트에서 재사용 가능하다는 것을 의미한다.
- 구체 클래스 의존성
- 추상 클래스 의존성 - 구체클래스에 비해 알아야 할 지식의 양이 적다.
- 인터페이스 의존성 - 상속 계층을 몰라도 협력이 가능해진다.
- 모든 경우에 의존성은 명시적(Explicit)으로 Public 인터페이스에 노출된다.
- 명시적이지 않으면, 내부구현을 살펴보거나, 변경이 필요할때 내부구현을 같이 변경해야 한다.
- new를 사용하는 클라이언트는 구체클랫에 의존하게 된다.
- new를 사용하면 생성자를 호출할때의 인자도 알아야 한다. 알아야 하는 지식의 양이 늘어나게 된다.
- 인스턴스 생성 로직과, 인스턴스 사용 로직을 분리해야 한다.
- 디폴트 값으로 생성하고, 필요시에 setter로 교체하거나
- 메소드 오버로딩으로 디폴트 값을 정한다
- 표준클래스 처럼 변경이 거의 없는 클래스에는 의존하거나 직접 인스턴스를 생성해도 문제가 없다.
- 할인정책이 존재하지 않는것을
null
예외로 처리하지말고, DiscountPolicy
할인정책의 한 종류로 간주한다.- 그러면,
if
문을 제거할 수 있다. - 중복할인도
DiscountPolicy
를 따르면서, 내부적으로 여러 할인조건을 List로 받아서 구현하면, 인터페이스를 변경하지 않고도 확장/구현이 가능하게 된다.
- 클래스의 인스턴트슬 생성하는 코드를 보는것 만으로도 객체가 어떤 일을 하는지를 쉽게 파악할 수 있다. -> Declarative (선언적으로 객체의 행동을 정의)
- 유연하고 재사용 가능한 설계는 작은 객체들의 행동을 조합함으로써 새로운 행동을 이끌어낼 수 있는 설계다.
- 객체들의 조합을 선언적으로 표현함으로써, 객체들이 무엇을 하는지를 표현하는 설계
- 수정에는 닫혀있고,
- 확장에는 열려있어야 한다.
- 추상화 부분은 수정에 대해 닫혀 있어야 한다.
- 추상화를 통해 생략된 부분은 확장의 여지를 남긴것이다.
- 추상화에 대해서만 의존하도록 관계를 제한해야 설계를 유연하게 확장할 수 있다.
- 변하는 것과 변하지 않는 것을 잘 구분하고 이를 추상화의 목적으로 삼아야 한다.
소프트웨어 시스템은
시작단계
와실행단계
를 분리해야 한다.
- 사용으로부터의 분리는 객체를 생성할 책임을 클라이언트로 옮기는것이다.
Movie
는 오직 인스턴스를 사용하는데만 주력한다.
- 생성과 사용을 분리하기 위해, 객체 생성에 특화된 객체를 FACTORY라 한다.
- 시스템을 객체로 분해하는데는 - 표현적 분해(Representational decomposition) - 도메인에 존재하는 사물 또는 개념을 표현하는 객체들을 이용해 시스템을 분해하는 것. - 행위적 분해(Behavioral decomposition) - 도메인과 무관한 인공적인 객체를
Pure Fabrication(순수가공물)
이라 한다. -FACTORY
는 객체의 생성 책임을 할당할 만한 도메인 객체가 존재하지 않을 때 선택할 수 있는Pure Fabrication
이다. 대부분의 디자인 패턴은Pure Fabrication
을 포함한다.
- 외부에서 의존성의 대상을 해결한 후 이를 사용하는 객체쪽에 주입한다.
- 의존성 주입에서는 의존성을 해결하는 세가지 방법을 가리키는 별도의 용어를 정의한다. - 생성자 주입 : 객체를 생성하는 시점에 생성자를 통한 의존성 해결 - setter 주입 : 객체 생성 후 setter 메서드를 통한 의존성 해결 - 메서드 주입 : 메서드 실행 시 인자를 이용한 의존성 해결
- Service Locator : 의존성을 해결할 객체들을 보관하는 저장소.
- 객체가 직접 Service Locator 에게 의존성을 해결해 줄 것을 요청한다.
- Service Locator는 DiscountPolicy의 인스턴스를 등록하고 반환할 수 있는 메서드를 구현한 저장소이다.
- ServiceLocator의 가장 큰 단점은 의존성을 감춘다는 것이다. - Movie는 DiscountPolicy에 의존하고 있지만, Movie의 퍼블릭 인터페이스 어디에도 이 의존성에 대한 정보가 표시되어 있지 않다. - 의존성은 암시적이며, 코드 깊숙한 곳에 숨겨져 있다.
- 의존성을 구현 내부로 감추면, 의존성 관련된 문제를 컴파일 타임 --> 런타임으로 늦추게 된다.
- 의존성을 숨기는 코드는 단위테스트 작성도 어렵다.
- 클래스의 퍼블릭 인터페이스만으로 사용방법을 이해할 수 있는 코드가 캡슐화 관점에서 훌륭한 코드다.
- 클래스 사용법을 익히기 위해 구현 내부를 샅샅이 뒤져야 한다면, 그 클래스는 캡슐화가 무너진 것이다.
명시적인 의존성
이숨겨진 의존성
보다 좋다.- 가급적 의존성을 객체의 퍼블릭 인터페이스에 노출하라.
- 상위수준/하위수준 클래스 모두 추상화에 의존한다.
추상화에 의존하라
- 상위 수준의 모듈은 하위수준의 모듈에 의존해서는 안된다. 둘 모두 추상화에 의존해야 한다.
- 추상화는 구체적인 사항에 의존해서는 안된다. 구체적인 사항은 추상화에 의존해야 한다.
- DIP를 따르는 설계는 의존성의 방향이 절차형 프로그래밍(상위수준 모듈이 하위수준에 의존) 과 반대로 나타난다.
- 추상화(Interface)를 별도의 독립적인 패키지가 아니라, 클라이언트가 속한 패키지에 포함시켜야 한다.
- 그리고, 함께 재사용될 필요가 없는 클래스들은 별도의 독립적인 패키지에 모아야 한다.
- Separated Interface 패턴
- 추상화가 제공하는 인터페이스의 소유권 역시 역전시켜야 한다.
- 변경하기 쉽고 확장하기 쉬운 구조는 단순함과 명확함의 미덕을 버리게 될 가능성이 높다.
- 유연한 설계 (=복잡한 설계)
- 설계가 유연할수록 클래스 구조와 객체구조 사이의 거리는 점점 멀어진다. 유연함은 단순성과 명확성의 희생 위에서 자라난다.
- 협력에 참여하는 객체가 다른 객체에게 어떤 메시지를 전송하는지가 중요하다.
- 역할, 책임, 협력에 초점을 맞춰야 한다.
- 객체 생성 매커니즘을 결정하는 시점을 책임 할당의 마지막 단계로 미뤄야 한다.
- 불필요한 Singleton 패턴은 객체 생성에 관해 너무 이른 시기에 고민하고 결정할때 도입되는 경향이 있다.