엘레강트 오브젝트 - 새로운 관점에서 바라본 객체 지향 2장 (조영호 옮김, 지앤선)
가능하면 적게 캡슐화하세요
최댓값은 4입니다. 더 많은 객체가 필요하다면, 클래스를 더 작은 클래스들로 분해해야 합니다.
- 41p
최소한 뭔가는 캡슐화하세요.
또 다른 극단에는 어떤 것도 캡슐화하지 않는 객체가 존재합니다.
class Year {
int read() {
return system.currentTimeMills) / ...
프로퍼티가 없는 클래스는 객체지향 프로그래밍에서 악명이 높은 정적 메서드와 유사합니다. 이런 클래스는 아무런 상태와 식별자도 가지지 않고 오직 행동만을 포함합니다.
- 42p
class Year {
private Millis millis;
Year(Millis msec) {
this.millis = msec;
}
int read() {
return this.millis.read() / ...
... 어떤 일을 수행하는 객체는 다른 객체들과 공존하면서 이들을 사용합니다. 이 객체는 자기 자신을 식별할 수 있도록 다른 객체들을 캡슐화해야 합니다.
- 43p
class Year {
private Number num;
Year(final millis msec) {
this.num = msec.div {
1000.mul(60).mul(60).mul(24).mul(30).mul(12))
.min(1970);
}
int read() {
return this.num.intValue();
}
}
- 44p
항상 인터페이스를 사용하세요
애플리케이션 전체를 유지보수 가능하도록 만들기 위해서는 최선을 다해서 객체를 분리해야(decouple) 합니다. 기술적인 관점에서 객체 분리란 상호작용하는 다른 객체를 수정하지 않고도 해당 객체를 수정할 수 있도록 만든다는 것을 의미합니다. 이를 가능하게 하는 가장 훌륭한 도구가 바로 인터페이스입니다. 예를 살펴 보겠습니다.
interface Cash {
Cash multiply(float factor);
}
Cash는 인터페이스입니다. 다시 말해서 우리의 객체가 다른 객체와 의사소통하기 위해 따라야 하는 계약입니다. ...
class DefaultCash implements Cash {
private int dollars;
DefaultCash(int dlr) {
this.dollars = dlr;
}
@Override
Cash multiply(float factor) {
return new DefaultCash(this.dollars * factor);
}
}
금액이 필요하다면 실제 구현 대신 계약에 의존하면 됩니다.
class Employee {
private Cash salary;
}
- 45 ~ 46p
다음과 같이 설계해서는 안됩니다.
class Cash {
public int cents() {
// 어떤 작업을 수행한다
}
}
cents() 메서드는 어떤 것도 오버라이드하지 않기 때문에 문제가 있습니다. 이 설계는 클래스의 사용자(다른 클래스)로 하여금 이 클래스에 강하게 결합되도록 조장합니다.
...
서비스는 계약이자 인터페이스이기 때문에 클래스가 제공하는 서비스는 어딘가에 문서화되어야 합니다. 게다가 서비스 제공자들은 서로 경쟁합니다. 다시 말해서 동일한 인터페이스를 구현하는 여러 클래스들이 존재한다는 뜻입니다. 그리고 각각의 경쟁자는 서로 다른 경쟁자를 쉽게 대체할 수 있어야 합니다. 이것이 느슨한 결합도(loose coupling)의 의미입니다.
- 46 ~ 47p
메서드 이름을 신중하게 선택하세요
빌더의 이름은 명사로, 조정자의 이름은 동사로 짓는 것입니다.
빌더란 무너가를 만들고 새로운 객체를 반환하는 메서드를 가리킵니다. 빌더는 항상 뭔가를 반환합니다. => 명사
int pow(int base, int power);
float speed();
Employee employee(int id);
String parsedCell(int x, int y);
조정자 => 객체로 추상화한 실세계 엔티티를 수정하는 메서드를 조정자라고 부릅니다.
void save(String content);
void put(String key, Float value);
void remove(Employee emp);
void quicklyPrint(int id);
빌더는 어떤 것을 만들고 조정자는 뭔가를 조작합니다.
위반하는 예
int save(String content);
boolean put(String key, Float value);
float speed(float val);
save() 메서드는 조정자이기 때문에 이 메서드들의 이름은 올바르지 않습니다. save() 메서드는 content를 '저장'하지만 빌더처럼 int 값을 반환합니다. void를 반환하도록 바꾸거나 bytesSaved()와 같은 이름으로 바꿔야 합니다.
put() 메서드 역시 조정자처럼 동작하지만 빌더처럼 boolean을 반환하기 때문에 동일한 문제가 발생합니다. 문제를 해결할 수 있는 유일한 방법은 아무 것도 반환하지 않도록 메서드를 수정하는 것 뿐입니다. ... speed() 메서드는 값을 저장하는 동시에 이전 값을 반환합니다. 이 역시 나쁜 설계의 또 다른 예라고 할 수 있는데 speed() 메서드는 빌더인 동시에 조정자이기 때문입니다.
- 49p
어떤 것을 반환하는 메서드의 이름을 동사로 짓는 것은 잘못입니다.
- 50p
제과점 => "브우니를 요리해주세요" 라거나 "커피 한 잔 끓여주세로"라고 하지 않는다.
정확한 방법에 대한 지시는 필요 없음
=>class Bakery { Food cookBrownie(); Drink brewCupOfCoffee(String flavor); }
=> 이들은 객체의 메서드라기 보다는 procedure이다.
객체에게 할 일을 일일이 명령
취향에 맞는 커피를 주문하고나서 커피를 만드는 방법을 제과점에게 전적으로 위임하는 대신, "가서 이러이러한 커피를 끓여오세요"라고 구체적으로 지시하는 것이다.
- 50p
이런 이유 때문에 메서드의 이름을 동사로 지을 때에는 객체에게 무엇을 할지(what to do)를 알려주어야 합니다. 객체에게 무엇을 만들라고(build) 요청하는 것은 협력자에 대한 존중이 결여되어 있을뿐만 아니라 예의에도 어긋나는 방식입니다. 무엇을 만들어야 하는지만 요청하고, 만드는 방법은 객체 스스로 결정해야 합니다.
잘못된 예시
InputStream load(URL, url);
String read(File file);
int add(int x, int y);
이 메서드들을 다음과 같이 수정해야 합니다.
InputStream stream(Url, url);
String content(File file);
int sum(int x, int y);
add 대신 sum을 제안하고 있다는 사실에 주목
=> x, y를 더하라고 요청하는 것이 아니라 두 수의 합을 계산하고 새로운 객체를 반환해 달라고 요청
=> 객체가 정말로 합계를 알아낼까? 모르는 것임
- 51p
class Pixel {
void paint(Color color);
}
Pixel center = new Pixel(50, 50);
center.paint(new Color("red"));
paint() 메서드는 값을 반환하지 않습니다. ... 마치 바텐더에게 음악을 틀어 달라고 요청하는 것과 같습니다. 바텐더가 볼륨을 높일까요? 그럴 수도 있고, 아닐 수도 있습니다. 우리의 요청이 그냥 무시될 지도 모릅니다. 그렇더라도 처음부터 뭔가를 돌려받을 것이라고 기대하지 않았기 때문에, 절대 모욕적이거나 무례한 일이 아닙니다.
- 52p
class Document {
int write(InputStream content);
}
이 메서드는 얼핏 보면 문제가 없어 보이지만, 사실 앞에서 설명한 원칙을 위반하고 있습니다. ... write() 메서드는 데이터를 쓰는 동시에, 쓰여진 바이트 수를 카운트합니다. 다시 말해서 하나의 메서드 안에서 너무 복잡한 일을 처리하고 있습니다. ... 다음과 같은 리팩토링을 추천합니다.
class Document {
OutputPipe output();
}
class OutputPipe {
void write(InputStream content);
int bytes();
long time();
}
output() 메서드는 빌더입니다. 이 메서드는 문서에 내용을 쓸 준비를 하는 OutputPipe 타입의 객체를 생성합니다. (이 객체의 이름을 'writer'로 짓지 않았다는 사실에 주의하기 바랍니다) 내용은 아직 쓰여지지 않았습니다. 단지 연산을 수행할 객체를 준비한 것뿐입니다. 그리고 나서 OutputPipe 인스턴스에 write()를 호출하면, 객체는 관련된 데이터를 모으기 시작합니다.
- 53 ~ 54p
boolean은 형용사로 짓자
- 55p
퍼블릭 상수(Public Constant)를 사용하지 마세요
public class Constants {
public static final String EOL = "\r\n";
}
불행하게도 코드 중복이라는 하나의 문제를 해결하기 위해 두 개의 더 큰 문제를 추가하고 말았습니다. 첫번째 문제는 결합도(coupling)가 높아진 것이고, 두 번째 문제는 응집도(cohesion)가 낮아진 것입니다.
- 57 ~ 58p
class Records {
void write(Writer out) {
for (Record rec : this.all) {
...
out.write(Constants.EOL); // 여기
class Rows {
void print(printStream pnt) {
...
pnt.printf( "{%s}", ros, Constants.EOL // 여기
두 클래스는 모두 같은 객체에 의존하고 있으며, 이 의존성은 하드 코딩되어 있습니다. ...
Constants.EOL 객체는 사용 방법과 관련된 어떤 정보도 제공하지 않은 채 모든 곳에서 접근 가능한 전역 가시성 안에 방치되어 있습니다.
- 56 ~ 60p
퍼블릭 상수를 사용하면 객체의 응집도가 낮아집니다. 낮은 응집도는 객체가 자신의 문제를 해결하는데 덜 집중한다는 것을 의미합니다. 객체들은 상수를 다루는 방법을 알고 있어야 합니다. 객체는 아주 멍청한 상수 위에 자신만의 의미론을 덧붙여야 합니다. ... Cosntatnt.EOL은 자신에 관해 아무 것도 알지 못하며, 자신의 존재 이유를 이해하지 못하는 하나의 텍스트 덩어리에 불과합니다. 자신에게 주어진 사명과 목적조차 이해하지 못합니다. 철학적으로 말해서 이 상수는 삶의 의미가 명확하지 않습니다.
- 60p
그럼 어떤 대안이 있을까요? ... 객체 사이에 데이터를 중복해서는 안됩니다. 대신, 기능을 공유할 수 있도록 새로운 클래스를 만들어야 합니다.
class EOLString {
private final String origin;
EOLString(String src) {
this.origin = src;
}
@Override
String toString() {
return String.format("%s\r\n", origin);
}
}
- 61p
애플리케이션을 구성하는 클래스의 수가 많을수록 설계가 더 좋아지고 유지보수하기도 쉬워집니다. 이해를 돕기 위해 일상생활에서 사용하는 언어에 비유해 보겠습니다. 깊은 인상을 남기기 위해 의도적으로 동의어를 사용하는 경우가 아니라면, 단어를 더 많이 사용할 수록 문장을 읽기가 더 쉬워집니다. 반대로 같은 단어에 하나 이상의 의미를 부여하고 자주 재사용할 경우에는 문장을 읽기가 어려워집니다.
=> 내 고양이는 생선을 먹고 우유를 마시는 것을 좋아한다
=> 내 것은 그것을 먹고 다른 것을 마시는 것을 좋아한다
- 63p
불변 객체로 만드세요
예를 들어 다음의 Cash 클래스를 이용해서 생성한 객체는 상태 변경이 가능하기 때문에 가변 객체라고 부릅니다.
class Cash {
private int dollars;
public void setDollars(int val) {
this.dollars = val;
}
}
다음 클래스는 상태를 변경할 수 없는 불변 객체를 생성합니다.
class Cash {
private final int dollars;
Cash(int val) {
this.dollars = val;
}
}
금액을 곱하는 간단한 연산자 => 가변 클래스
class Cash {
private int dollars;
public void mul(int factor) {
this.dollars *= factor;
}
}
다음은 동일한 작업을 수행하는 불변 클래스의 예입니다.
class Cash {
private final int dollars;
public Cash mul(int factor){
return new Cash(this.dollars * factor);
}
다음은 가변 객체의 사용 방법
Cash five = new Cash(5);
five.mul(10);
Cash five = new Cash(5);
Cash fifty = five.mul(10);
... 일단 five 객체를 생성하고 나면 five는 fifty가 될 수 없습니다. 5는 5일 뿐입니다. ... 만약 50이 필요하다면 다른 객체를 인스턴스화해야 합니다.
첫 번째 경우의 문제
Cash five = new Cash(5)
five.mul(10);
System.out.println(five);
=> 50을 리턴
five를 했는데 50을 리턴 !
money로 바꾸면 문제가 해결될까?
Cash money = new Cash(5);
money.mul(10);
-> 50
구체적인 이름을 추상적인 이름으로 대체한 것 뿐이라서 해결된 것이 아니다.
제 이야기의 요지는 가변 객체는 존재해서는 안된다는 것입니다. 가변 객체의 사용을 엄격하게 금지해야 합니다.
- 63 ~ 66p
'Book' 카테고리의 다른 글
[독서 기록] 엘레강트 오브젝트 - 새로운 관점에서 바라본 객체 지향 3장 - 2 (2) | 2022.11.19 |
---|---|
[독서 기록] 엘레강트 오브젝트 - 새로운 관점에서 바라본 객체 지향 3장 -1 (1) | 2022.11.19 |
[독서 기록] 엘레강트 오브젝트 - 새로운 관점에서 바라본 객체 지향 1장 (0) | 2022.11.16 |
[독서 기록] 헤드퍼스트 디자인 패턴 11장 | 프록시 패턴 - 객체 접근 제어하기 (0) | 2022.11.15 |
[독서 기록] 헤드퍼스트 디자인 패턴 10장 | 상태 패턴 - 객체의 상태 바꾸기 (0) | 2022.11.15 |