Object Calisthenics
Jeff Bay
라는 사람이ThoughtWorks Anthology
라는 공학 에세이 모음집에서 등장한 개념이라 한다.- 객체지향 프로그래밍을 잘하기 위한 9가지 체크리스트라 보면 될듯 하다.
1. One level of indentation per method
한 메서드에 오직 한 단계의 들여쓰기만 한다.
Why?
- 메소드에 들여쓰기가 많아지면 가독성이 떨어진다.
- 객체지향의 사상 중,
하나의 메소드는 한 가지 기능만 담당한다
를 위반할 수 있다.
example
❌
String board() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
sb.append(data[i][j]);
}
sb.append("\n");
}
return sb.toString();
}
위 코드는, 기본적인 이중포문 구조이다.
위와 같은 메소드 구조를, 아래와 같이 3개의 메소드로 나눈다.
✅
String board() {
StringBuilder sb = new StringBuilder();
collectRows(sb);
return sb.toString();
}
void collectRows(StringBuilder sb) {
for (int i = 0; i < 10; i++) {
collectRow(sb, i);
}
}
void collectRow(StringBuilder sb, int row) {
for (int i = 0; i < 10; i++) {
sb.append(data[row][i]);
}
sb.append("\n");
}
기초적인 예시라 별 차이가 없어보이지만, 기능들이 잘 나뉜 것을 볼 수 있다.
2. Don't use the ELSE keyword
else 예약어를 쓰지 않는다.
Why?
- 가독성이 떨어진다.
- 중첩된 조건문이 발생할 수 있다.
- 오류처리가 비효율적이다.
- 코드의 명확성이 떨어진다.
- 다형성과 같은 객체지향프로그래밍의 중요 특성을 무시하게 될 수 있다.
example
Early Return
- 조건에 대해 미리 리턴시켜버리는 방법이다.
- 코딩문제 풀 때 엣지케이스 처리에대해 자주 사용했던 방법
✅
String getName(Person person) {
if (person == null) {
return "Unknown";
}
return person.name;
}
- 매우 간단한 예시긴 하지만, 위와 같은 방법으로 미리 리턴시켜버리면
else
예약어를 피할 수 있다.
Map 사용
- 이것도 뭔가했는데, 당연한 것 일 수 있다.
- 예시를 바로 보자
❌
public String something(String action) {
if (action.equals("0")) {
return "USA"
} else if (action.equals("1")) {
return "KR"
}
...
else {
return "CN"
}
}
엄청 길 텐데, 이를 맵을 사용한다면 더 효과적일 것이다.
✅
/**
Map<String, String> map = new HashMap<>();
map.put("0", "USA");
map.put("1", "KR");
...
*/
public String something(String action) {
if (!map.containsKey(action)) {
throw new Exception();
}
return map.get(action);
}
다형성 이용
- if/else/switch의 좋은 대체제이다.
- 예를들어, 전략패턴이나 상속을 사용할 수 있다.
❌
public class ProcessOrder {
public int getOrderGrandTotal(Customer customer, int subTotal) {
if (customer.type.equals("EMPLOYEE")) {
return subTotal - 20;
}
else if (customer.type.equals("NON_EMPLOYEE")) {
return subTotal - 10;
}
return subsTotal;
}
}
- 위 코드는 다형성을 전혀 활용하고 있지 않다.
- 아래와 같이 상속을 이용한 다형성을 활용한다면, 코드가 간결해지고 가독성도 좋아진다.
✅
public abstract class Customer {
public abstract int getDiscount();
}
public class Employee extends Customer {
@Override
public int getDiscount() {
return 20;
}
}
public class NonEmployee extends Customer {
@Override
public int getDiscount() {
return 10;
}
}
public class ProcessOrder {
public int getOrderGrandTotal(Customer customer, int amount) {
return amount - customer.getDiscount();
}
}
3. Wrap all primitives and Strings
모든 원시값과 문자열은 포장한다.
Why?
- Domain Driven Design (DDD)를 지키기 위해
- 특히 자바에서 원시값은 실제 객체가 아니기 때문에 다른 객체들과 다른 규칙을 갖게된다.
- 객체지향적이지 않은 syntax를 사용한다.
- 특히
int
의 경우, 그저 스칼라 값이기에 아무런 의미가 없다. - 변수명으로 모든 의미를 부여해야한다.
- 특히
example
- 의미를 부여하는 클래스를 만든다.
- 포장을 해야하는 근본적인 의미는, int 변수명만 봐서는 어떤 역할을 하는 지 판단하기 어렵기 때문이다.
- 속성을 하나만 가지더라도, 작은 객체들을 만들자
❌
public class SMSSubscription {
private String id;
private int quantity;
private int month;
private int year;
public SMSSubscription(String id, int quantity, int month, int year) {
this.id = id;
if (quantity < 1 || quantity > 250) {
throw new IllegalArgumentException();
}
this.quantity = quantity;
if (month < 1 || month > 12) {
throw new IllegalArgumentException();
}
this.month = month;
if (year < 1970) {
throw new IllegalArgumentException();
}
this.year = year;
}
}
- 위 예시의 경우,
SMSSubscription
생성자 내부가 예외로 가득차버린다. - quantity, month, year에 대해 Wrapper를 적용하게 되면, 아래와 같이 간결하게 된다.
✅
public class Quantity {
private int quantity;
public Quantity(int quantity) {
if (quantity < 1 || quantity > 250) {
throw new IllegalArgumentationException();
}
this.quantity = quantity;
}
}
public class Month {
private int month;
public Month(int month) {
if (month < 1 || month > 12) {
throw new IllegalArgumentationException();
}
this.month = month;
}
}
public class Year {
private int year;
public Year(int year) {
if (year < 1970) {
throw new IllegalArgumentationException();
}
this.year = year;
}
}
public class SMSSubscription {
...
public SMSSubscription(String id, Quantity q, Month m, Year y) {
this.id = id;
this.quantity = q;
this.month = m;
this.year = y;
}
}
- 원시타입을 클래스화 하면서, 변수에 의미가 생기고 의미에 따라서 예외처리를 직접 해줄 수 있게 되었다.
4. First class Collections
일급 콜렉션을 쓴다.
Collection
을 속성으로 가지고 있는 클래스는 다른 속성을 가지면 안된다.Why?Collection
의 불변성을 보장하기 위해- 객체의 의미를 부여하기 위해?
- 비즈니스에 종속적인 자료구조로 만들기 위해
- 상태와 행위를 한 곳에서 관리하기 위해
example
❌
Map<String, String> map = new HashMap<>();
map.put("1", "A");
...
✅
public class Ranking {
private Map<String, String> ranks;
public Ranking(Map<String, String> ranks) {
this.ranks = ranks;
}
}
5. One dot per line
한 줄에 점 하나만 찍는다.
Why?
- 점이 여러개다 보면, 행동에 책임을 갖는 객체를 찾기 힘들어진다.
- 점이 많다는 의미는, 객체가 다른 객체에 더 깊게 의존하게 된다는 의미가 된다.
- 캡슐화를 위반하게 된다.
- Null Pointer Exception가 발생할 우려가 있다.
example
- Demeter의 법칙을 따른다.
❌
class Driver {
private final Car car;
void goLeft() {
car.getHandle().left();
}
}
class Car {
private final Handle handle;
Handle getHandle() {
return handle;
}
}
class Handle {
void left() {}
}
✅
class Driver {
private final Car car;
void goLeft() {
car.goLeft();
}
}
class Car {
private final Handle handle;
void goLeft() {
handle.left();
}
}
class Handle {
void left() {}
}
괜찮은 경우
a.foo(b.foo());
list.stream()
.filter(item -> item != null)
.map(item -> item.toString())
.collect(Collectors.toList());
6. Don't abbreviate
줄여쓰지 않는다.
Why?
변수명이 길어진다는 것은, 단일책임원칙([[SRP]])을 위반하고 있을 가능성이 있다.
example
- 클래스를 세분화한다.
- 클래스나 메소드 이름을 1-2 단어로 만드려 노력해라.
- 메소드명이 길어진다면, 책임이 잘못된 곳에 있을 가능성이 있다.
7. Keep all entities small
모든 엔티티를 작게 유지한다.
- 클래스의 길이는 50~ 150, 패키지의 파일 수는 10개로 제한해라.Why?
- 클래스 길이가 길어지면 가독성을 해치고, 유지보수하기 어려워진다.
8. No classes with more than two instance variables
3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
Why?
- 2개 이하로 속성을 줄이면서, 높은 결합력과 나은 캡슐화가 된다.
example
- 위 그림과 같이 묶어서 구현한다.
9. No getters/setters/properties
게터,세터,프로퍼티를 쓰지 않는다.
- DTO 의 경우 허용
Why?
- 기본적으로 캡슐화를 위반한다.
- 구현 세부 정보를 노출하게 된다.
- 객체의 상태를 변할 우려가 생기게 된다.
- getter의 경우에도 불러오는 객체가 컬렉션과 같은 참조값이라면, 상태가 변할 수 있다.
- 리팩토링에 어려움이 생긴다.
- 사이드 이팩트가 발생할 수 있다.
example
Tell, Don't Ask
- 객체에게 묻지 말고, 무엇을 할 지 명령해라
❌
public class MusicPlayer {
private List<Music> playList;
public List<Music> getPlayList() {
return playList;
}
}
✅
public class MusicPlayer {
private List<Music> playList;
public void addMusic(Music music) {
playList.add(music);
}
}
Reference
https://developerhandbook.stakater.com/content/architecture/object-calisthenics.html
'Language > Java' 카테고리의 다른 글
[Java] Enum 활용하기 (0) | 2023.10.24 |
---|---|
[Java] Functional Interface (0) | 2023.10.24 |
[Java] Google Java Style Guide 번역 (0) | 2023.10.17 |
[Java] JDK Java version 변경하기 (M1) (0) | 2023.09.25 |
[Java] 정규식 정리 (Regular Expression) (0) | 2023.09.21 |