객체와 자료 구조

변수를 private으로 선언하는 이유는 남들이 변수에 의존하지 않게 만들고 싶어서다.
변수 타입, 구현을 맘대로 바꾸고 싶어서다.

자료 추상화

두 코드를 비교해보자
한 클래스는 구현을 외부로 노출하고 다른 클래스는 구현을 완전히 숨긴다.

//1번 - 구체적인 Point Class
public class Point{
    public double x;
    public double y;
}

//2번 - 추상적인 Point Class
public interface Point{
    double getX();
    double getY();
    void setCartesian(double x, double y);
    double getR();
    double getTheta();
    boid setPolar(double r, double theta);
}

2번은 직교좌표계인지 극좌표계인지 모른다.
자료 구조 이상을 표현한다. 클래스 메서드가 접근 정책을 강제한다.
1번은 직교좌표계를 사용한다. 개별적으로 값을 읽고 설정하게 강제한다.
구현을 노출한다. -> 변수를 private으로 하더라도 get/set이 노출된다면 구현이 외부로 노출되는 셈이다.

//3번
public interface Vehicle{
    double getFuelTankCapacityInGallons();
    double getGallonsOfGasoline();
}

//4번
public interface Vehicle{
    double getPercentFuelRemaining();
}

3번가 4번 사이에서는 4번이 더 좋다.
자료를 세세하게 공개하기보다는 추상적인 개념으로 표현하는 편이 좋다.

자료/객체 비대칭

앞서 소개한 두 예제는 객체와 자료구조의 차이를 보여준다.
객체는 추상화 뒤로 자료를 숨긴채 자료를 다루는 함수만 공개한다.
자료구조는 자료를 그대로 공개하며 별다른 함수는 제공하지 않는다.

//절차적인 코드
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 double area(Object shape) throws NoSuchShapeException{
        if (shape instanceof Rectangle){
            Rectangle r = (Rectangle)shape;
            return r.height * r.width;
        }else if(shape instanceof Circle){
            Circle c = (Circle)shape;
            return 3.14 * c.radius * c.radius;
        }else{
            throw new NoSuchShapeException();
        }
    }
}

//객체 지향 코드
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 double area(){
        return 3.14 * radius * radius;
    }
}

객체 지향 vs 절차적인 코드

  • 객체 지향 코드에서는 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽다.

  • 절차적인 코드에서는 기존 자료 구조를 변경하지 않으면서 새 함수를 추가하기 쉽다.

  • 객체 지향 코드에서는 새로운 함수를 추가하기 어렵다. 그러려면 모든 클래스를 고쳐야 한다.

  • 절차적인 코드에서는 새로운 자료 구조를 추가하기 어렵다. 그러려면 모든 함수를 고쳐야 한다.

개발을 하다보면 새로운 함수가 아니라 자료 타입이 필요한 경우가 필요하다 -> 객체 지향
새로운 자료 타입이 아닌 새로운 함수가 필요하다 -> 절차적인 코드 + 자료구조

디미터 법칙

디미터 법칙은 휴리스틱으로 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙이다.
객체는 자료를 숨기고 함수를 제공한다. 객체는 조회 함수로 내부 구조를 공개하면 안된다는 의미다.

클래스 C의 메서드 f는 다음과 같은 객체의 메서드만 호출해야 한다

  1. 클래스 C
  2. f가 생성한 객체
  3. f 인수로 넘어온 객체
  4. C 인스턴스 변수(프로퍼티)에 저장된 객체

final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();

기차 충돌

위와 같은 코드를 기차 충돌이라 부른다.
위 코드는 나누는 편이 좋다.

Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final Strin goutputDir = scratchDir.getAbsolutePath();

자료 전달 객체

자료 구조체의 전형적인 형태는 공개 변수만 있고 함수가 없는 클래스다.
때로는 DTO라 일컫는다.
특히 DB와 통신 or 소켓으로 받은 Msg를 분석할 때 유용하다.

-> DTO는 자료구조이므로 함수를 제공하지 않는다.

결론

객체는 동작을 공개하고 자료를 숨긴다.
기존 동작을 변경하지 않으면서 새 객체 타입을 추가하기는 쉽다.
하지만 기존 객체에 새 동작을 추가하기는 어렵다.

자료구조는 별다른 동작없이 자료를 노출한다.
그래서 기존 자료 구조에 새 동작을 추가하기는 쉽다.
하지만 기존 함수에 새 자료 구조를 추가하기는 어렵다.

형식 맞추기

팀으로 진행한다면 팀에서 합의한 규칙을 정하고 팀원 모두가 그 규칙을 따라야 한다.

형식을 맞추는 목적

코드 형식은 중요하다! 너무 중요하다!
'돌아가는 코드'가 개발자의 1차원적인 의무일 수도 있지만, 사실상 오늘 구현한 기능은 다음 버전에서 바뀔 확률은 아주 높다.
그런데 오늘 구현한 코드의 가독성은 앞으로 바뀔 코드의 품질에 거대한 영향을 준다.
코드가 지속적으로 바뀌어도 처음 잡아놓은 스타일과 가독성 수준은 이후에도 계속 영향을 끼친다.

적절한 행 길이를 유지하라.

개념은 빈 행(줄바꿈)으로 분리하라

일련의 행 묶음은 생각 하나를 표현한다.
생각 사이사이는 빈 행을 넣어 분리해야 마땅하다.

세로 밀집도

수직거리

이 함수에서 호출하는 다른 함수를 찾기 위해 미로같은 코드를 뒤진 경험이 있는가?
이 조각, 저 조각이 어디에 있는지 찾고 기억하는 데에 시간과 노력이 너무 많이 든다.
-> 서로 밀접한 개념은 세로로 가까이 둬야한다.

팀 규칙

프로그래머라면 각자 선호하는 규칙이 있다. 하지만 해당 팀에 속한다면 팀의 규칙에 따라야한다.
소프트웨어 시스템의 스타일은 일관적이고 매끄러워야 한다.
여러 스타일이 섞인 시스템은 독자에게 신뢰감을 주지 않는다.

주석

잘 달린 주석은 어떤 정보보다 유용하다. 하지만 경솔하고 근거없는 주석은 코드를 이해하기 어렵게 만든다
사실 주석이 필요한 코드는 실패다. 코드만으로 의도를 표현해야 한다. 이를 실패했기 때문에, 주석을 다는 것이다.

코드만이 정확한 정보를 제공하는 유일한 출처다

주석은 나쁜 코드를 보완하지 못한다

엉망인 코드에 주석을 다는 것보다 엉망인 코드를 손보는 것이 훨씬 좋다.

코드로 의도를 표현하라

코드만으로 의도를 설명하기 어려운 경우가 존재한다.

// 직원이 복지 혜택을 받을 자격이 있는지 검사한다
if((employee.flags & HOURLY_FLAG) && (employee.age > 65)){
    ...
}
VS
if(employee.isEligibleForFullBenefits())

좋은 주석

어떤 주석은 필요하거나 유익하다.

법적인 주석

정보를 제공하는 주석

의도를 설명하는 주석

의미를 명료하게 밝히는 주석

a.compareTo(a) == 0; // a==a
a.compareTo(b) != 0; // a!=b

결과를 경고하는 주석

  1. //여유 시간이 충분하지 않으면 실행하지 마십시오.
    최근에는 @Ignore("실행이 너무 오래걸린다.") 라는 Anotation을 이용해 테스트 케이스를 꺼버리지만, 이전에는 주석을 달았다.

  2. //해당 객체는 스레드에 안전하지 못하다.
    // 따라서 각 인스턴스를 독립적으로 생성해야 한다.

TODO 주석

실제로 나도 자주 사용한다.
요즘 IDE는 TODO 주석을 찾아주는 기능을 제공해서 잊어버릴 염려는 없다. 그래도 너무 많은 TODO 주석은 좋지 않다.

중요성을 강조하는 주석

//여기서 a라는 객체는 정말 중요하다. a의 역할은 이며 ~으로 사용해야 한다.

나쁜 주석

위에서 언급되지 않은 주석들이 나쁜 주석이다.
좋지 않은 코드/실패한 코드를 커버하기 위한 주석 들이 대다수다.

주절거리는 주석

같은 이야기를 중복하는 주석

오해할 여지가 있는 주석

의무적으로 다는 주석

  • 코드로도 충분히 이해할 수 있지만, 의무적으로 주석을 남겨 더 헷갈릴 여지를 주는 주석

반복하지 마라

소프트웨어에서 중복은 모든 악의 근원이다.

  • 관계형 DB에서도 정규화를 통해 중복을 제거한다.
  • OOP에서도 부모 클래스를 통해 중복을 없앤다.
  • 이렇듯 다양한 프로그래밍 방법에서 중복을 없애기 위한 여러 시도가 있다.

구조적 프로그래밍

모든 함수와 함수 내 모든 블록에 입구와 출구가 하나만 존재해야 한다고 말한다.
return 문이 하나여야 한다.

함수를 어떻게 짜죠?

논문이나 기사를 쓸 때도, 생각을 기록한 후에 읽기 좋게 다듬는다. 초안은 대게 어수선하다.
함수 또한 마찬가지다.

  • 처음에는 길고 복잡하다.
  • 인수 목록도 아주 길다.
  • 중복 코드, 루프도 많다.
  • 이름도 즉흥적으로 짓는다.
  • 하지만 코드를 빠짐없이 테스트하는 Unit Text Case를 작성한다*

그런 후, 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거하고
메서드를 줄이고, 전체 클래스를 쪼갠다. 이러한 작업을 하더라도 단위 테스트는 항상 통과한다.

결론

함수는 동사며, 클래스는 명사다.
정말 잘하는 프로그래머는 시스템을 프로그램이 아니라 이야기로 여긴다.
프로그래밍 언어라는 수단을 사용해 좀 더 풍부하고 표현력이 강한 언어를 만들어 이야기를 풀어간다.

규칙을 지킨다면 길이가 짧고 이름이 좋은 체계 잡힌 함수가 나온다

+ Recent posts