Search
Duplicate
🪡

Object Calisthenics

1. Only One Level Of Indentation Per Method

하나의 메소드에서 너무 깊은 수준의 들여쓰기는 종종 가독성과 유지보수성을 해치곤한다.
만약 다양한 조건과 깊이가 존재하거나 반복문이 존재한다면 대부분의 시간들을 컴파일 과정 없이 본인의 머리로 오롯이 이해하는데 사용해야 할 것이다.
다음 예시를 봐보자.
개선 전
class Board { public String board() { StringBuilder buf = new StringBuilder(); // 0 for (int i = 0; i < 10; i++) { // 1 for (int j = 0; j < 10; j++) { // 2 buf.append(data[i][j]); } buf.append("\n"); } return buf.toString(); } }
Java
복사
개선 후
class Board { public String board() { StringBuilder buf = new StringBuilder(); collectRows(buf); return buf.toString(); } private void collectRows(StringBuilder buf) { for (int i = 0; i < 10; i++) { collectRow(buf, i); } } private void collectRow(StringBuilder buf, int row) { for (int i = 0; i < 10; i++) { buf.append(data[row][i]); } buf.append("\n"); } }
Java
복사
여기서 얘기하는 원칙에 따르면 메소드를 분리해내야 한다. 리팩토링에서 마틴 파울러는 Extract Method 패턴에 대해 소개했다.
코드 줄의 수는 줄어들지 않지만 가독성이 향상될 수 있을 것이다.

2. Don’t Use The else Keyword

else 키워드는 대부분의 프로그래밍 언어에서 if/else 구조에 포함되는 것으로 잘 알려져있다.
중첩 조건문을 가장 최근 보았을때 어땠는가? 이는 읽을만 했는가? 필자는 동의하지 않는다. 이것이 왜 반드시 else 키워드를 피해야하는 가에 대한 이유가 된다.
기존에 존재하는 코드를 리팩토링을 통해 더 나은 코드를 만드는 것보다 새로운 코드를 추가하는 것이 더 쉽기 때문에 우리는 종종 나쁜 코드를 생산해내곤 한다.
다음 예시를 보자.
public void login(String username, String password) { if (userRepository.isValid(username, password)) { redirect("homepage"); } else { addFlash("error", "Bad credentials"); redirect("login"); } }
Java
복사
여기서 단순하게 else 키워드를 제거하는 방법은 early return 패턴을 사용하는 방법이다.
public void login(String username, String password) { if (userRepository.isValid(username, password)) { return redirect("homepage"); } addFlash("error", "Bad credentials"); return redirect("login"); }
Java
복사
조건문 내용의 내용이 낙관적이든 아니든 에러 케이스에 대한 조건이나 메소드의 나머지 영역들은 메소드명에서 유추할 수 있는 기본적인 시나리오를 따라야 한다. 또는 defensive approach를 적용해볼 수 있다.
Defensive Programming과 연관되어 있다.
만약 당신이 기본 시나리오를 조건에 넣은 상황에서 이것이 만족되지 못할 경우, 당신은 에러 상황을 마주하게 될 것이다.
이것은 당신이 생각하지 못한 잠재적인 에러들을 예방할 수 있으므로 더 낫다.
또 다른 대안으로 early return 패턴을 사용하는 것 대신에 변수를 이용해볼 수 있다. 물론 이것이 언제나 긍정적인 방향은 아니다.
public void login(String username, String password) { String redirectRoute = "homepage"; if (!userRepository.isValid(username, password)) { addFlash("error", "Bad credentials"); redirectRoute = "login"; } redirect(redirectRoute); }
Java
복사
객체지향 프로그래밍이 우리에게 제공해주는 강력한 특징들인 다형성을 사용해볼 수 있다.
State 그리고 Strategy 패턴이 우리를 도와줄 수 있다.
예를 들어 상태에 따라 동작을 결정하기 위해 if/else 조건문을 사용하는 것 대신에 객체의 상태에 따라 동일한 루틴에 대한 다양한 행동을 캡슐화한 State 패턴을 사용해보자.

3. Wrap All Primitives And Strings

public class Positive { private int number; public Positive(int number) { if (number < 0) { throw new RuntimeException(); } this.number = number; } public Poisitive add(Positive other) { return new Positive(this.number + other.number); } public int getNumber() { return number; } } private static Positive[] toPositives(int[] values) { Positive[] numbers = new Positive[values.length]; for (int i = 0; i < values.length; i++) { numbers[i] = new Positive(values[i]); } return numbers; } private static int sum(Positive[] numbers) { Positive result = new Positive(0); for (Positive number : numbers) { result = result.add(number); } return result.getNumber(); }
Java
복사
이 규칙을 따르는 것은 꽤나 쉽다. 당신은 단순하게 모든 원시 값들을 객체 안으로 숨김으로써 해낼 수 있다.
Primitive Obsession 안티 패턴을 피하기 위해서다.
만약 원시형 변수가 행동을 가지게 된다면 당신은 이것을 반드시 캡슐화해야 한다.
이런 패턴은 또한 DDD에서 추구하는 철학과 일치한다. Money, Hour와 같은 Value 객체들이 이렇게 동작한다.

4. First Class Collections

public class Store { private Set<Brand> brands; public Store(List<Brand> brands) { validSize(brands); this.brands = brands; } private void validSize(List<Brand> brands) { if(brands.size() >= 10) { throw new IllegalArgumentException("브랜드는 10개 초과로 입점할 수 없습니다."); } } }
Java
복사
일급 컬렉션에 대한 내용으로 Collection을 래핑하면서 Collection 외의 다른 멤버 변수도 가지지 않는 상태를 의미한다.
만약 어떠한 요소의 집합을 가지고 있고 그것을 다루길 원한다면 그 집합들에 대한 클래스를 따로 만드는 것을 고려하라.
일급 컬렉션은 필요한 도메인 로직을 담을 수 있다. 이로써 컬렉션을 사용하는 클래스에서 검증하는 것이 아닌 일급 컬렉션에서 자율적으로 검증이 가능하다.
일급 컬렉션을 사용하면 컬렉션의 불필요한 메소드에 대한 가시성 문제도 해결할 수 있다.

5. One Dot Per Line

class Board { public String boardRepresentation() { StringBuilder buf = new StringBuilder(); for (Location loc : squares()) { buf.append(loc.current.representation.substring(0, 1)); } return buf.toString(); } }
Java
복사
class Piece { private String representation; public String character() { return representation.substring(0, 1); } public void addTo(StringBuilder buf) { buf.append(character()); } } class Location { private Piece current; public void addTo(StringBuilder buf) { current.addTo(buf); } } class Board { public String boardRepresentation() { StringBuilder buf = new StringBuilder(); for (Location location : squares()) { location.addTo(buf); } return buf.toString(); } }
Java
복사
한 줄에 하나의 점만 찍는다.
여기서 의미하는 점이란 자바나 C#의 객체에서 메소드를 호출하기 위해 점을 찍는 것을 의미한다.
기본적으로 이 규칙은 당신이 연쇄적으로 메소드를 호출하지 말라는 것을 의미한다.
하지만 Fluent 인터페이스나 대부분의 일반적인 구현 클래스들에서 Method Chaining 패턴을 사용하지 말라는 것은 아니다.
디미터 법칙을 따르라는 것이다.
디미터 법칙에서는 객체의 의존관계를 따라 친하지 않은 객체에게 뭔가를 시도하는 설계를 피하라고 한다.
이런 설계는 불필요한 결합도를 만들고 적절하지 못한 추상화 수준을 유지하게 되며 캡슐화를 깨친다.
속성의 가시성을 변경하는 것을 통해 우리는 OCP가 얘기하는 확장에 열려있고 변경에 닫혀있는 설계를 구현할 수 있다.

6. Don’t Abbreviate

더 정확한 요지는 왜 당신은 왜 축약어를 사용하기 원하는가?이다.
당신은 똑같은 이름을 계속 사용하기 때문이라고 답할 것이다.
필자는 메소드가 여러번 재사용된다면 그것은 중복된 코드라고 답할 것이다.
당신은 메소드 네이밍이 너무 길어서라고 대답할 것이다.
필자는 당신의 클래스가 적절하지 않게 너무 많은 책임을 가지고 있기 때문이라고 답할 것이다.
필자는 종종 클래스나 메소드의 네이밍을 줄이길 원한다면 이것은 아마 잘못됐다고 얘기한다. 소프트웨어 명명 규칙을 적절히 따르지 않았기 때문이다.

7. Keep All Entities Small

클래스는 50줄을 넘지 않는 것이 좋고 클래스는 10개 이상의 파일을 가지지 않는 것이 좋다.
물론 이것은 당신에게 달려있으나 필자가 생각하기에 당신이 최대한 늘릴 수 있는 양은 150줄이라고 본다.
이 규칙의 주요 아이디어는 긴 파일은 읽기 어렵다는 것이다. 이는 이해하기 어렵고 따라서 유지보수도 힘들게 만든다.

8. No Classes With More Than Tow Instance Varialbes

필자가 이를 소개할 때, 사람들이 아마 강한 거부의사를 내비칠지도 모른다. 하지만 이것은 일어나지 않았다.
이 규칙은 아마 가장 어렵지만 응집도를 향상시켜주고 더 나은 캡슐화를 촉진한다.
앞서 규칙 3번에서 우리는 모든 원시 자료형들은 래핑되어야한다고 얘기했다. 위 사진을 봐보자.
이 규칙에 대한 주요 질문은 왜 속성이 두개여야하냐?일 것이다. 필자의 대답은 왜 안되는가?
가장 좋은 설명은 아니지만 그의 주장에서 핵심요지는 클래스를 두 개의 인스턴스 변수로 분리하는 것이다.
하나는 단일 인스턴스 변수의 상태를 유지하고 다른 하나는 두 개의 개별 변수를 조정하는 것이다.

9. No Getters/Setters/Properties

필자가 가장 좋아하는 룰로 각각 다음과 같이 표현될 수 있다. 말해라, 물어보지 마라.
객체의 상태를 접근하는 것은 용도로 사용하는 것은 괜찮다. 어떻게든 당신이 외부 객체에게 결정을 내리도록한 결과를 사용하지 말라는 것이다.
하나의 객체의 상태에 전적으로 관련이 있는 결정은 반드시 그 객체 내부에서 일어나야 한다.
// Game private int score; public void setScore(int score) { this.score = score; } public int getScore() { return score; } // Usage game.setScore(game.getScore() + ENEMY_DESTROYED_SCORE);
Java
복사
위 코드에서 getScore 메소드는 점수를 변경하는 메소드인 setScore의 파라미터로 사용된다.
이런 책임을 Game 인스턴스에게 전가하라는 것이다.
getter/setter를 제거하기위 한 가장 좋은 해결책은 의미있는 메소드를 제공하는 것이다. 즉 객체에게 스스로 얘기하게끔 만들어야지 물언가를 물어봐서는 안 된다.
다음 코드를 살펴보자.
// Game public void addScore(int delta) { score += delta; } // Usage game.addScore(ENEMY_DESTROYED_SCORE);
Java
복사
setScore라는 불분명한 의미대신 명확한 의도인 addScore라는 네이밍이 사용되었다.
기존 코드를 읽을 땐, 의식의 흐름이 다음과 같을 것이다.
game 객체의 스코어를 변경하는 구나
game 객체의 현재 스코어를 가져오는 구나
거기에 적을 제거한 점수를 더하는 구나
아! 적을 제거했으니 점수가 오르는구나!
개선된 코드를 읽을 땐, 의식의 흐름이 다음과 같을 것이다.
game 객체에 점수를 더하는 구나
적을 제거한 점수구나
아! 적을 제거했으니 점수를 더해주는 구나!
이것은 객체에게 점수를 어떻게 변경할 것인가에 대한 결정을 내릴수 있도록 책임을 적절히 분배해주는 것이다.
setter는 값을 변경하는 이유를 충분히 드러내주지 않는다. 서술적인 메소드명을 사용해야하는 이유다.
물론 이 케이스에선 점수를 표시하기 위해 getter를 사용할 수 있다. 다만 setter 메소드는 절대 허락되지 않는다는 것을 명심하자.

참고