가독성 높은 코드
API를 적절히 사용하라
class Menus {
public Menus(final List<String> menus) {
for (int i = 0; i < menus.size(); i++) {
for (int j = 0; j < i; j++) {
if (menus.get(i).equals(menus.get(j))) {
throw new IllegalArgumentException("메뉴는 중복될 수 없습니다.");
}
}
}
}
}
class Menus {
public Menus(final List<String> menus) {
if (menus.size() != menus.stream().distinct().count()) {
throw new IllegalArgumentException("메뉴는 중복될 수 없습니다.");
}
}
}
그러나 API에 매몰되어서는 안된다.
기능을 구현하기 위해 API를 사용해야 하지, 이것이 API로 해결해야 할 문제인가도 따져봐야 함.
예를 들어, 단순히 map.get 으로 해결될 문제인데 stream을 사용한다고 코드가 더 길어질 수 있음.
따라서 API는 기능 해결만을 위해 적절히 사용하자!
예측 가능한 코드
의미있는 값을 반환하라
만약 리스트에서 어떤 원소를 찾지 못했다고 가정해보자.
그렇다면 예외를 throw할 것인가, 만약 예외가 던져지지 않아도 되는 상황이라면?
그런 경우 -1을 리턴하거나 할 수 있음.
하지만 이런 매직 값은 버그를 유발할 수 있다.
따라서 Optional이나 Null object 패턴을 사용하면 됨.
Optional을 반환하는 경우는 또한, 메소드를 호출하는 클라이언트에게 null이 반환되는 것을 암시할 수도 있다.
또한 Null Object의 경우, null 마저 객체로 처리할 수 있어서 객체지향적인 측면에서 좋음.
public class RacingGame {
private List<Car> participants;
Position averagePosition() {
return participants.stream()
.mapToInt(Car::position)
.average()
.stream()
.map(Position::new)
.findAny()
.orElseGet(Position::ready);
}
}
중요한 입력에 대해 무시하지 마라
class Car {
private int position;
void move(final int power) {
if (power < 4) {
return;
}
position++;
}
}
위 예시에서는 중요한 입력에 대해 무시를 하고 있다.
자동차 경주 게임에서 핵심이 되는 도메인은 자동차이고, 따라서 자동차가 움직이는 행위는 중요한 책임이다.
위 코드에서는 power 값에 따라 position을 변경할지 말지를 결정한다.
클라이언트는 이러한 행동을 예측하기가 어렵다.
만약 List 구현체를 사용하는데, 원소 개수에 따라 add가 안될 수 있다고 해보자.
이런 경우는 적절한 피드백이 필요하다.
클라이언트는 메소드 이름에 따라 무조건 add가 될 것으로 예상하기 때문이다.
따라서 다음과 같은 코드로 개선해야 한다.
class Car {
private int position;
boolean isMovable(final int power) {
return power > 4;
}
void move() {
position++;
}
}
// 클라이언트는 다음과 같이 사용하게 된다.
if (car.isMovable(power)) {
car.move();
}
아니면 move 메소드의 반환 값을 boolean 으로 설정하고, 성공 여부에 따라 true, false를 반환할 수도 있겠지만,
이 방식은 기존 POSIX 방식처럼 명령과 조회를 분리하지 못한다는 점에서 지양해야 할 것 같다.
중요한 것은, 객체 자체도 하나의 서비스라고 보는 것이다.
시스템 자체를 하나의 객체로 볼 수 있듯이 말이다.
열것값을 암묵적으로 처리하지 마라
enum은 언제든 추가될 수 있다.
따라서 enum의 객체 종류에 따라 처리하는 무언가 처리해야 할 때,
public void run(final Command command) {
if (command == RETRY) {
retry();
return;
}
if (command == QUIT) {
quit();
return;
}
throw new UnsupportedOperationException();
}
위와 같이 throw문을 아래에 정의함으로써, 새로운 enum 객체가 정의되었을 때 안전장치 역할을 수행할 수 있다.
열거형 사용 방식을 명확하게 하라
public enum Command {
RETRY(true) {
@Override
public boolean match(Position position, String tile) {
return position == Position.UP && position.equalPosition(tile);
}
},
...
protected abstract boolean match(Position position, String tile);
}
열거형은 어떻게 정의하느냐에 따라 객체에 가까워질 수도, 상수에 가까워질 수도 있다!
상황에 맞춰 사용하도록 하자.
실수를 방지하는 코드
원시값을 포장하고, 방어적 복사를 통해 변경에 따른 파급효과를 없애자!
아니면 불변 구조를 사용하는 것도 좋다.
답해보기
- 가독성 높은 코드를 작성하기 위해 어떠한 노력을 해봤는가?
내려가기 규칙을 사용했다.
메소드를 추상화 수준에 따라 선언하려고 최근에 많이 노력했다!
이외에도 메소드명, 변수명에서 의도가 드러나도록 했고, 최대한 코드를 모르는 사람이 보더라도 이해할 수 있을만하게 작성했다.
- 예측 가능한 코드를 작성하기 위해 어떠한 노력을 해봤는가?
명령과 조회를 분리했다.
더불어, 부수효과를 만들지 않게 하려고 노력해왔다.
마찬가지로 함수나 클래스를 사용하는 제 3자(클라이언트)가 메소드를 통해 책임을 한번에 예상할 수 있게 작성하려고 노력했다!
- 실수를 방지하는 코드를 작성하기 위해 어떠한 노력을 해봤는가?
최근 불변 객체, 깊은 복사, 방어적 복사에 대해 고민하고 있다 :)
이런 메커니즘이 없어도 시스템이 책임을 수행하는 방식은 동일하다.
다만 사용자의 올바르지 못한 입력, 개발자의 실수 및 실수 가능성을 방지하지 위해 사용하는 것이다.
그러나 가독성, 예측 가능성, 실수 방지는 trade-off의 영역이라고 생각한다.
하나를 위해서는 다른 하나를 포기해야 할 때가 있고, 결정은 상황에 맞춰서 하는 것이라 생각한다!
'우테코 5기' 카테고리의 다른 글
[레벨 1 미션] 블랙잭 게임 미션 회고 (7) | 2023.03.22 |
---|---|
[레벨 1 미션] 사다리 타기 미션 회고 & 학습기록 (1) | 2023.03.12 |
[레벨 1 미션] 자동차 경주 미션 회고 (0) | 2023.02.18 |
[레벨 1 미션] 자동차 경주 학습 기록 (0) | 2023.02.18 |
[레벨 1 강의] TDD, 리팩터링, 자바에 대한 이해 (0) | 2023.02.18 |