자료 추상화
// 구체적인 Point 클래스
// 구현을 노출한다.
public class Point {
public var x: Double
public var y: Double
}
// 추상적인 Point 클래스
// 클래스 메서드가 접근 정책을 강제한다.
public protocol Point {
func getX() -> Double
func getY() -> Double // 조회는 각각 가능하지만
func setCartesian(Double x, Double y) // 설정을 2개의 값을 동시에 넣어주어야 한다.
func getR() -> Double
func getTheta() -> Double
func setPolar(Double r, Double theta)
}
변수를 private로 선언하더라도 각 값마다 get, set함수를 제공한다면 구현을 외부로 노출하는 셈이다.
변수 사이에 함수라는 계층을 넣는다고 구현이 저절도 감춰지지는 않는다. 구현을 감추려면 추상화가 필요하다! 추상 인터페이스(프로토콜)을 제공해 사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있어야 진정한 의미의 클래스다.
// 구체적인 Vehicle 클래스
// 변수를 그대로 리턴하는 함수일 것이 틀림없다.
public interface Vehicle {
public getFuelThankCapacityInGallons();
public getGallonsOfGasoline();
}
// 추상적인 Vehicle 클래스
// 백분율이라는 추상적인 개념으로 반환하기에 어디서 오는지 사용자에게 드러나지 않는다.
public interface Vehicle {
double getPercentFuelRemaining();
}
개발자는 객체가 포함하는 자료를 표현할 가장 좋은 방법을 심각하게 고민해야 한다. 아무생각 없이 get/set 설정 함수를 추가하는 방법이 가장 나쁘다.
객체 vs 자료구조
객체 - 추상화 뒤로 자료를 숨긴 채 자료를 다루는 함수만 공개한다.
자료구조 - 자료를 그대로 공개하며 별다른 함수는 제공하지 않는다.
절차적 vs 객체적
// 절차적인 도형
// 각 도형 클래스는 간단한 자료구조
// 도형이 동작하는 방식은 Geometry 클래스에서 구현
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.141592653589793;
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) {
Rectangle 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 클래스에 새 함수를 추가한다면 도형 클래스는 아무 영화도 받지 않는다.
반대로 새 도형을 추가하고 싶다면 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.141592653589793;
public double area() {
return PI * radius * radius;
}
}
새 도형을 추가해도 기존 함수에 아무런 영향을 미치지 않는다.
반면 새 함수를 추가하고 싶다면 도형 클래스 전부를 고쳐야 한다.
(자료구조를 사용하는) 절차적인 코드 - 기존 자료 구조를 변경하지 않으면서 새 함수를 추가하기 쉽다.
객체 지향 코드 - 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽다.
==
절차적인 코드 - 새로운 자료 구조를 추가하기 어렵다. 그러려면 모든 함수를 고쳐야 한다.
객체 지향 코드 - 새로운 함수를 추가하기 어렵다. 그러려면 모든 클래스를 고쳐야 한다.
디미터 법칙
모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙.
"클래스 C의 메서드 f는 다음과 같은 객체의 메서드만 호출해야 한다."
- 클래스 C
- f가 생성한 객체
- f 인수로 넘어온 객체
- C 인스턴스 변수에 저장된 객체
하지만 위 객체에서 허용된 메서드가 반환하는 개체의 메서드는 호출하면 안 된다.
- 기차 충돌
여러 객차가 한 줄로 이어진 기차처럼 보이는 코드.
// Before
let outputDir: String = ctxt.getOptions().getScratchDir().getAbsolutePath()
// After
let opts: Options = ctxt.getOptions()
let scratchDir: File = opts.getScratchDir()
let outputDir: String = scratchDir.getAbsolutePath()
ctxt, Options, ScratchDir이
개체라면 내부 구조를 숨겨야 하므로 디미터 법칙을 위반한다.
자료구조라면 당연히 내부 구조를 노출하므로 디미터 법칙이 적용되지 않는다.
만약 자료구조였다면 이렇게 구현했어야 한다.
let outputDir: String = ctxt.options.scratchDir.absolutePath
만약 객체라면 임시 디렉터리의 절대 경로는 어떻게 얻어야 좋을까?
// 1
// ctxt 객체에 공개해야 하는 메서드가 너무 많음
ctxt.getAbsolutePathOfScratchDirectoryOption();
// 2
// ctxt가 객체라면 뭔가를 하라고 해야 하는데
// getScratchDirectoryOption()에서 속을 드러내라고 말하는 느낌이다.
ctxt.getScratchDirectoryOption().getAbsolutePath()
임시 디렉터리의 절대 경로를 얻으려는 이유가 임시 파일을 생성하기 위함이기에 ctxt 객체에 임시 파일을 생성하라고 시키면 객체에게 맡기기에 적당한 임무로 보인다!
// ctxt는 내부 구조를 드러내지 않으며, 모듈에서 해당 함수는 여러 객체를 탐색할 필요가 없다.
// 디미터 법칙 충족
let bos: BufferedOutputStream = ctxt.createScratchFileStream(classFileName);
결론
어떤 시스템을 구현할 때, 새로운 자료 타입을 추가하는 유연성이 필요하면 객체가 더 적합하다.
다른 경우로 새로운 동작을 추가하는 유연성이 필요하면 자료 구조와 절차적인 코드가 더 적합하다.
편견 없이 직면한 문제에 최적인 해결책을 선택해야 한다.
댓글