객체지향스럽게 만들자(feat.읽기 좋은 코드 만들기)

코드스쿼드 레벨2 과정이 거의 끝나가고 있다. 레벨2의 목표는 객체지향프로그래밍 익히기이다. 그동안 5가지의 미션을 하면서 책을 통해서만 공부하던 객체지향을 직접 느껴보았다. 과정 중에 와닿았던 것, 그리고 객체지향프로그래밍을 한다면 반드시 그래야만 하는 것, 기억해야할 것들을 이 글을 쓰면서 정리해보았다. 글에 쓰일 예제 코드들은 모두 5번째 미션이었던 볼링 프로그램 코드 중 일부(직접 짠 코드보다 다 짜고난 뒤 비교해본 마스터의 코드이다 - 코드 비교 리뷰를 하면서 느낀점이 굉장히 많아 마스터의 코드를 바탕으로 글을 써보았다) 이다. 볼링 프로그램을 택한 이유는 가장 많은 것을 느낄 수 있게 해주었기때문이다. 이 글을 올리고 난 뒤에는 정리한 내용들을 의식하면서 다시 5가지 미션들을 다시 해볼 생각이다. 다시 구현해본 뒤 이전에 구현했던 코드와 비교해보면서 어떤 점이 나아졌는지, 어떤 생각 느낌이 들었는지 또 한번 정리해볼 것이다.

객체지향의 핵심은 “돌아가는 코드가 아니라 읽기 좋은 코드를 만들자” 이다.

  • 유지보수하기 쉬운 코드, 알아보기 쉬운 코드, 변경사항에 유연한 코드(변경 포인트 한 곳으로 몰기)를 만들자다.
  • 여러 경우의 수를 하나의 메소드에 몰아두고 if ~ else 처리하려고 한다면 모든 흐름을 파악하고 있어야 가능함 : 변경이 어려운 코드
  • 캡슐화를 위해 추상화 잘해놓는 것, 상속을 활용해서 다형성을 사용할 수 있는게 핵심이다 : 코드 가독성이 핵심이다

설계와 개발, 테스트는 병행되어야한다

  • 어떤 경우의수가 있는지 체크하고, 어떻게할지 설계하고 테스트를 하면서 기능 구현(어떤 기능이 있는지 파악부터)을 한다.
  • 해당 클래스에서 문제 해결이 되지않는 것 같다면 구조적으로 생각을 해보기
  • 설계한 부분이 있더라도 첫 스텝부터 차근차근 테스트 - 개발해나가면서 실제로 필요한 설계가 맞는지 확인해나간다 : 필요할 때 만든다
class FirstBowl {
    private final int first;

    public FirstBowl(int first) {
        this.first = first;
    }

    /* TODO : 상속, interface가 필요한 부분 */
    public Spare bowl(int second) {
        if (first + second == 10) {
            return new Spare(first);
        }
        return null;
    }

    public String displayTxt() {
        return null;
    }
}

상태를 만드는 것은 상태다.

  • 누가 관련 상태값(정보)을 가져있는지 그것을 따져보면 누가 할지 정해진다.
  • 바깥으로 정보를 빼내서 만드는 로직을 만들지않고, 가진 놈이 만들도록 하자 : get하지말고 메세지를 보내라
class Ready implementes State {
 
    public State bowl(int pin) {
        if (pin == 10) {
            return new Strike();
        }
        return new FirstBowl(pin);
    }

    public String displayTxt() {
        return first + "|" + "\";
    }
}

이런 상태값을 저장해야하는데, 누가 가지고 있어야할까?

  • 꼭 필요한 놈(객체)에게 값을 전달해서 저장하도록 하자
  • 필요한 값만 저장하자 : 굳이 저장할 필요없이 고정된 값이라면 확인만 하든지 아예 저장하지말자
class Spare implements State {
    private int first;

    public Spare(int num) {
        first = num;
    }

    public String displayTxt() {
        return first + "|" + "\";
    }
}

원시값, null 객체로 만들어서 메세지를 던질 수 있도록 하자

  • 내가 모든 것을 처리하려고 하지말고 위임하자 : 굳이 할 필요가 없는데, 굳이 한다면 굳이 필요없는 코드가 생기고, 복잡도가 높아진다.
/* null 추상화 전 */
public class Frame {
    private State state;
    
    public void bowl(int pin) {
        if (state == null) {
        
        }
    }
}

/* null 추상화 후 : Ready */
class Ready implements State {
    public Strike roll(int num) {
        if (num == 10) {
            return new Strike();
        }
        return null;
    }
}

/* 바깥에서 null 체크를 해주지않아도 됨 */
public class Frame {
    private State state = new Ready();

    private State state;
    
    public void bowl(int pin) {
        state = state.roll(pin);
    }
}

if문이 많다면 구조를 의심해라

  • 다형성을 활용할 수 있도록 구조를 변경해라 : 처리할 수 없는 곳에서 억지로 처리한다고, 다양한 상황을 모두 커버한다고 if문을 사용하고 있다!
  • 하나의 타입 아래 다양한 타입을 만들어서 해결하도록 만들어라 : 인덴트가 늘어나면 그만큼 해결해야할 일을 모두 떠안고 있다는 것이다
public interface State {

    State roll(int num);

    String displayTxt();
    
    boolean isFinish();
}

class Strike implements State {
    
    @Override
    public State bowl(int pin) {
        throw new IllegalArgumentException();
    }
}

public class Frame {
    private State state = new Ready();
    
    public void bowl(int pin) {
        state = state.roll(pin);        
    }

}
  • 예외를 발생시키는 것도 그런 역할을 가진 쪽에서 발생시키는 것임
  • 객체지향 생활체조를 일상화하자 : 또 자기만의 클린코드를 만들기위한 원칙을 가지도록 해보자
규칙 1: 한 메서드에 오직 한 단계의 들여쓰기만 한다.
규칙 2: else 예약어를 쓰지 않는다.
규칙 3: 모든 원시값과 문자열을 포장한다.
규칙 4: 한 줄에 점을 하나만 찍는다.
규칙 5: 줄여쓰지 않는다(축약 금지).
규칙 6: 모든 엔티티를 작게 유지한다.
규칙 7: 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
규칙 8: 일급 콜렉션을 쓴다.
규칙 9: 게터/세터/프로퍼티를 쓰지 않는다.

인터페이스로 만들고 중복되는 코드가 생기면 추상클래스로 해결해보자

  • 중복되는 인터페이스 구현 코드를 하나에다가 구현할 수 있는 abstract class를 사용하기
  • 다른 부분만 구현하도록 만든다 : 간단하게 만들자(인스턴스 메소드가 많아지면서 중복도 많이 생긴다면 그땐 구조적으로 해결해야할 때다)
abstract class Finished implements State {

    @Override
    public void bowl(int pin) {
        throw new IllegalArgumentException(); 
    }
}

class Strike implements State {
    
    @Override
    public String displayTxt() {
        return "X";	
    }
}

로직을 구현해두고 모두 오픈(public)하지 말것

  • 모두 오픈 해둔 채로 사용하기 시작하면 코드들이 들러붙기 시작할 여지가 생김 : 변경에 유연한 코드를 만들어두고 결합성을 만들 수 있음

public interface State {

}

public class StateFactory {

}

abstract class Finished {

}


class Strike implements Finished {

}

  • 인터페이스만(팩토리 사용) 노출하고 구현 클래스는 숨기자 : 이상하게 사용할 가능성을 아예 없애자

결과 객체는 미리 만들어두고 동기화 시키기보다 요청 받았을 때 만들자

  • 어차피 결과 객체를 만드는 원천 데이터를 가진 객체는 null 할당하지않는 이상 계속해서 존재한다는 것을 잊지말기
  • 결과가 생길 때마다 내부적으로 동기화 작업을 하지말고 꼭 필요할 때 요청을 날리자
/* 결과를 저장하는 객체 - Board */
public class Board {
    private List<FrameResult> results = new ArrayList<>();
    public void addResult(FrameResult result) {
        results.add(result);
    }
}

/* 여러 Frame을 가지고 있는 Frames */
public class Frames {
    private List<Frame> frames = new ArrayList<>();
    /* private Board board : 만들지말자 */
}

/* Frame 중 NormalFrame */
public class NormalFrame implements Frame {
    private State state;
    private Frame nextFrame;
    private final int frameNum;
}

/* FrameStatus 중 Strike */
class Strike implements State {
    private Pin pin;
    /* private FrameResult result : 만들지말자 */
}
  • 결과가 필요할 때는 볼링을 1회 투구한 후 점수판에 결과를 출력할 때라서 그때 구성해주면 됨 : 프레임이 굳이 Board를 인스턴스 변수로 가지면서 기록을 다시 넘겨주는 번거로운 작업을 하지말고(기록을 저장하고 또 기록을 저장하는 이중 저장이 되어버림) 기록된 상태에서 Board에 넘겨주면 됨