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 메소드는 절대 허락되지 않는다는 것을 명심하자.