아래의 두 클래스는 모두 2차원 점을 표현한다.
그런데 한 클래스는 구현을 외부로 노출하고 다른 클래스는 구현을 완전히 숨긴다.
// 구체적인 Point 클래스
public class Point {
public double x;
public double y;
}
// 추상적인 Point 클래스
public interface Point {
double getX();
double getY();
void setCartesian(double x, double y);
double getR();
double getTheta();
void setPolar(double r, double theta)l
}
변수 사이에 함수라는 계층을 넣는다고 구현이 저절로 감춰지지는 않는다.
구현을 감추려면 추상화가 필요하다. 그저 (형식 논리에 치우쳐) 조회(get) 함수와 설정(set) 함수로 변수를 다룬다고 클래스가 되지는 않는다.
그보다는 추상 인터페이스를 제공해 사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있어야 진정한 의미의 클래스이다.
public interface Vehicle {
double getFuelTankCapacityInGallons();
double getGallonsOfGasoline();
}
public interface Vehicle {
double getPercentFuelRemaining();
}
각 두번째 코드들처럼 자료를 세세하게 공개하기보다는 추상적인 개념으로 표현하는 편이 좋다.
(그렇다고 아무 생각 없이 인터페이스 조회/설정 함수를 추가하는 방법이 가장 나쁘다.)
개발자는 객체가 포함하는 자료를 표현할 가장 좋은 방법을 심도있게 고민해야 한다.
앞서 나온 두 가지 예제는 객체와 자료 구조 사이에 벌어진 차이를 보여준다.
[절차적인 도형 예시]
public class Square {
public Point topLeft;
public double side;
}
public class Rectangle {
public Point topleft;
public double height;
public double width;
}
public class Circle {
public Point center;
public double radius;
}
public class Geometry {
public final double PI = 3.14;
public double area(Object shape) throws NoSuchShapeException
{
if (shape instanceof Square) {
Square s = (Square)shape;
return s.side * s.side;
}
else if (shape instanceof Rectangle) {
Retangle r = (Rectangle)shape;
return r.height * r.width;
}
else if (shape instanceof Circle) {
Circle c = (Circle)shape;
return PI * c.radius * c.radius;
}
throw new NoSuchShapeException();
}
}
💡 만약 Geometry 클래스에 둘레 길이를 구하는 perimeter() 함수를 추가하고 싶다면?
도형 클래스는 아무런 영향도 받지 않는다. 도형 클래스에 의존하는 다른 클래스도 마찬가지이다.
반대로 새 도형을 추가하고 싶다면?
Geometry 클래스에 속한 함수를 모두 고쳐야 한다.
[다형적인 도형 예시]
public class Square implements Shape {
private Point topLeft;
private double side;
public double area() {
return side*side;
}
}
public class Rectangle implements Shape {
private Point topLeft;
private double height;
private double width;
public double area() {
return height * width;
}
}
public class Circle implements Shape {
private Point center;
private double radius;
public final double PI = 3.14;
public double area() {
return PI * radius * radius;
}
}
💡 area() 메서드는 다형 메서드이다. Geometry 클래스는 필요 없다.
새 도형을 추가해도 기존 함수에 아무런 영향을 미치지 않는다.
반면 새 함수를 추가하고 싶다면 도형 클래스 전부를 고쳐야 한다.
(자료 구조를 사용하는) 절차적인 코드는 기존 자료 구조를 변경하지 않으면서 새 함수를 추가하기 쉽다. 반면 객체 지향 코드는 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽다.
절차적인 코드는 새로운 자료 구조를 추가하기 어렵다. 그러려면 모든 함수를 고쳐야 한다. 객체 지향 코드는 새로운 함수를 추가하기 어렵다. 그러려면 모든 클래스를 고쳐야 한다.
다시 말해, 객체 지향 코드에서 어려운 변경은 절차적인 코드에서 쉬우며, 절차적인 코드에서 어려운 변경은 객체 지향 코드에서 쉽다.
분별있는 프로그래머라면 이를 융통성있게 적절히 사용해야 한다.
객체는 자료를 숨기고 함수를 공개한다.
객체는 조회 함수로 내부 구조를 공개하면 안 된다.
“클래스 C의 메서드 f는 다음과 같은 객체의 메서드만 호출해야 한다.”
위 객체에서 허용된 메서드가 반환하는 객체의 메서드는 호출하면 안 된다.
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
위와 같은 코드를 흔히 기차 충돌이라 부른다. 아래와 같이 나누는 편이 좋다.
Option opts = ctxt.getOption();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();
💡 그렇다면 위의 변경된 코드는 디미터 법칙을 위반할까?
→ ctxt, Options, ScratchDir이 객체인지, 자료 구조인지에 달려 있다.
그런데 위의 예제는 조회(get) 함수를 사용하기 때문에 혼란을 일으킨다.
코드를 다음과 같이 바꾼다면 디미터 법칙을 거론할 필요가 없어진다.
final String outputDir = ctxt.option.scratchDir.absolutePath;
자료 구조는 무조건 함수 없이 공개 변수만 포함하고, 객체는 비공개 변수와 공개 함수를 포함한다면, 문제는 훨씬 간단해진다.
💡 만약 위의 ctxt, options, scratchDir이 객체라면?객체에게 뭔가를 하라고 말해야지 속을 드러내라고 말하면 안 된다.
다음은 같은 모듈에서 가져온 코드이다.
String outFile = outputDir + "/" + className.replace('.', '/') + ".class";
FileOutputStream fout = new FileOutputStream(outFile);
BufferedOutputStream bos = new BufferedOutputStream(fout);
위 코드를 보아, 임시 디렉터리의 절대 경로를 얻으려는 이유가 임시 파일을 생성하기 위한 목적이라는 사실을 알 수 있다.
그렇다면, ctxt 객체에 임시 파일을 생성하라고 시켜보자.
BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
💡 ctxt는 내부 구조를 드러내지 않으며, 모듈에서 해당 함수는 자신이 몰라야 하는 여러 객체를 탐색할 필요가 없다.
자료 구조체의 전형적인 형태는 공개 변수만 있고 함수가 없는 클래스이다. 이런 자료 구조체를 때로는 자료 전달 객체(Data Transfer Object)라고 한다. DTO는 데이터베이스와 통신하거나 소켓에서 받은 메시지의 구문을 분석할 때 유용하다.
DTO의 특수한 형태이다. 공개 변수가 있거나 비공개 변수에 조회/설정 함수가 있는 자료 구조지만, 대개 sava나 find와 같은 탐색 함수도 제공한다.
활성 레코드는 데이터베이스 테이블이나 다른 소스에서 자료를 직접 변환한 결과다.
활성 레코드는 자료 구조로 취급한다. 비즈니스 규칙을 담으면서 내부 자료를 숨기는 객체는 따로 생성해야 한다.
객체는 동작을 공개하고 자료를 숨긴다. 그래서 기존 동작을 변경하지 않으면서 새 객체 타입을 추가하기는 쉬운 반면, 기존 객체에 새 동작을 추가하기는 어렵다.
자료 구조는 별다른 동작 없이 자료를 노출한다. 그래서 기존 자료 구조에 새 동작을 추가하기는 쉬우나, 기존 함수에 새 자료 구조를 추가하기는 어렵다.
시스템을 구현할 때, 새로운 자료 타입을 추가하는 유연성이 필요하면 객체가 더 적합하다. 다른 경우로 새로운 동작을 추가하는 유연성이 필요하면 자료 구조와 절차적인 코드가 더 적합하다.