객체지향 생활체조 원칙에 대해 알아봅니다.
객체지향 생활체조란 Jeff Bay가 발명한 9가지 규칙의 집합으로 공식화된 프로그래밍 훈련입니다.
대부분 사람들이 Java라는 언어로 프로그래밍을 시작하면 객체 지향적인 사고방식이 부족하다 것을 느낄 수 있습니다. 객체지향 생활체조는 객체 지향적인 사고방식을 단순하고 좀 더 쉽게 적응할 수 있는 9가지 원칙입니다.
만약 거대한 메소드가 여러 로직과 많은 줄의 코드로 어질러져 있다면 코드를 파악하기 어렵고 유지보수성에서도 매우 떨어질 것입니다. 그렇기 때문에 메소드는 한 가지의 일을 담당하는 것이 좋습니다.
한 가지의 일만 하는 메소드라면 애플리케이션의 각 단위가 작아짐에 따라 재사용의 수준이 기하급수적으로 늘어나게 됩니다. 이를 위해 들여쓰기가 1단계만 남을 때까지 동작 코드를 뽑아냅니다.
class Board {
...
String board() {
StringBuffer buf = new StringBuffer();
for (int i = 0; i < 10; i++) { // 0
for (int j = 0; j < 10; j++) // 1
buf.append(data[i][j]); // 2
buf.append("\n");
}
return buf.toString();
}
}
메서드를 분리하였습니다.
class Board {
...
String board() {
StringBuffer buf = new StringBuffer();
collectRows(buf);
return buf.toString();
}
void collectRows(StringBuffer buf) {
for (int i = 0; i < 10; i++)
collectRow(buf, i);
}
void collectRow(StringBuffer buf, int row) {
for (int i = 0; i < 10; i++)
buf.append(data[row][i]);
buf.append("\n");
}
}
이렇게 분리된 메서드는 디버깅에 훨씬 쉬워집니다.
리팩토링보다 기존의 조건문에 분기를 하나 더 추가하는것이 쉽습니다.
이러한 작업방식은 코드의 중복을 발생시키며 코드의 품질을 떨어뜨립니다.
else 예약어 사용 시
public static void endMe() {
if (status == DONE) {
doSomething();
} else {
// 다른 코드
}
}
else 예약어 생략 시
public static void endMe() {
if (status == DONE) {
doSomething();
return ; // early return
}
// 다른 코드
}
else 대신에 early return 또는 다형성을 통해 설계하여 코드의 가독성과 유지성 및 코드의 의도를 더욱더 분명히 할 수 있습니다.
원시형 타입의 값 자체는 의미 없는 스칼라값일 뿐입니다. 만약 어떤 메서드가 int를 매개변수로 받는다면 매개변수의 의미를 나타내기 위해서는 변수명으로만 의미를 추론할 수 있습니다. 하지만 원시값을 포장(Wrap)하여 이름을 가진다면 컴파일러와 프로그래머에게 정보를 더욱 정확히 전달할 수 있습니다.
int number = 1; // 원시값
--------------------------------------------------------------------------------
class Time { // 클래스로 Wrap
int hour;
}
클래스를 사용하여 원시값을 감싸서 코드의 의미를 더욱더 명확하게 하였습니다.
이 규칙의 적용은 간단합니다.
콜렉션을 포함한 클래스에 다른 멤버 변수가 없어야 합니다.
아래와 같이 Wrapping 하는 것이 일급 콜렉션 입니다.
public class Game {
private Map<String, String> cars;
public Game(Map<String, String> cars) {
this.cars = cars;
}
}
주사위 게임이 있다고 가정하겠습니다.
주사위 게임의 규칙은 주사위의 크기가 6을 넘거나 주사위의 숫자가 중복이 되지 않아야 합니다.
이러한 일을 서비스 메서드에서 진행해보겠습니다.
public class GameService {
public void createNumber() {
List<Long> numbers = createDuplicateNumbers();
validateSize(numbers);
validateDuplicate(numbers);
}
}
위의 코드를 보면 만약 다른 곳에서 주사위 번호가 필요하다면 검증 로직을 또 인증하여야 합니다.
이러한 문제를 해결하기 위해선 위에서 말한 두가지의 조건으로만 생성할 수 있는 자료구조를 만들면 문제가 해결됩니다.
이러한 클래스를 일급 컬렉션이라고 합니다.
public class Dice {
private final List<Long> numbers;
public Dice(List<Long> numbers) { // 6의 사이즈와 중복이 되지않는 자료구조
validateSize(numbers);
validateDuplicateNumbers(numbers);
this.numbers = numbers;
}
private void validateDuplicateNumbers(List<Long> numbers) {
throw new IllegalArgumentException("주사위의 숫자는 중복 될 수 없습니다.");
}
private void validateSize(List<Long> numbers) {
throw new IllegalArgumentException("주사위의 사이즈는 6입니다.");
}
}
이후에 필요한 작업은 일급 컬렉션만 있으면됩니다.
컬렉션과 관련된 코드의 중복을 막을 수 있고 데이터를 캡슐화한다는 점에서 객체 지향적인 코드를 작성할 수 있습니다.
일급 컬렉션은 컬렉션의 불변을 보장해줍니다.
현재와 같이 소프트웨어가 커지고 있는 상황에서는 불변 객체는 아주 중요합니다.
각각의 객체가 변하지 않는다는 것을 보장한다면 코드를 이해하고 수정하는데 사이드 이펙트가 최소화되기 때문입니다.
하지만 자바에서는 final로는 재할당만 막을 수 있을 뿐 불변 객체를 만들 수 없습니다. 그렇기 때문에 일급 컬렉션과 래퍼 클래스를 이용하여 해결합니다.
public class Car {
private final List<Car> cars;
public Car(List<Car>cars) {
this.cars =cars;
}
public long getAmount() {
return cars.stream()
.mapToLong(Car::getAmount)
.sum();
}
}
위의 클래스는 값을 새로 할당하거나 값을 가져오는 메서드 밖에 존재하지 않습니다.
List에 접근할 방법이 없기 때문에 값을 변경또는 추가할 수 없습니다.
일급컬렉션은 값과 로직이 함께 존재합니다.
아래의 코드는 단순히 Car의 상태만이 아닌 행위(메소드)를 가지게 됩니다.
public class Car {
private final List<Car> cars;
public Car(List<Car> cars) {
this.cars = cars;
}
public List<String> getWinner() { // 승리자를 구하는 메소드
...
}
public Long findMaxDistance() { // 최대거리 구하는 메소드
...
}
}
만약에 우승자를 구하는 메소드가 다른 곳에 있다면 Cars를 생성하면 다시 우승자를 만들어 주는 메소드를 중복으로 생성할 수 있습니다.
일급 컬렉션을 사용함으로써 상태와 로직을 한곳에서 관리할 수 있습니다.
컬렉션에 이름을 붙일 수 있습니다.
가장 간단히 구분하는 방법은 변수명을 다르게 하는 것입니다.
하지면 변수명으로 구분한다면 변수명에 불과하기 때문에 명확한 의미를 부여하기가 힘듭니다.
위의 단점들도 일급 컬렉션을 사용하여 쉽게 해결할 수 있습니다.
Hyundai hyundai = new Hyundai(createHyundai());
BMW bmw = new BMW(createBmw());
참고
https://jojoldu.tistory.com/412
객체에 접근하기 위해서는 .을 이용하게 되는데 너무 깊게 들어가 다른 객체를 사용하는 것을 지양하라는 의미입니다. 확장에는 열려있고 수정에는 닫혀 있는 개방-폐쇄 원칙을 지킬 수 있습니다.
class Location {
public Piece current;
}
class Piece {
public String representation;
}
class Board {
public String boardRepresentation() {
StringBuilder buf = new StringBuilder();
for (Location loc : squares()) {
buf.append(loc.current.representation.substring(0, 1));
}
return buf.toString();
}
}
buf.append(loc.current.representation.substring(0, 1));
변경 후
개방 - 폐쇄 원칙을 지킬수 있습니다.
// 변경 후
class Location {
private Piece current;
public void addTo(StringBuilder buf) {
current.addTo(buf);
}
}
class Piece {
private String representation;
public String character() {
return representation.substring(0, 1);
}
public void addTo(StringBuilder buf) {
buf.append(character());
}
}
class Board {
public String boardRepresentation() {
StringBuilder buf = new StringBuilder();
for (Location location : squares()) {
location.addTo(buf);
}
return buf.toString();
}
}
클래스, 메서드, 변수의 이름을 축약을 하지 말아야 합니다.
프로그래밍을 하다 보면 index는 idx, count는 cnt, temp는 tmp등 이름을 함축하여 사용할 때가 있습니다.
위와 같이 축약한 이름을 개발자라면 알아볼 수도 있지만 만약 개발자가 아니거나 초급 개발자인 경우에는 의미를 파악하지 못할 수가 있습니다.
또한 일반적으로 이름이 길어진다는 의미는 클래스가 많은 책임을 안고 있어 여러 가지의 값을 가지고 있어 이름이 길어질 수 있습니다. 이는 단일 책임 원칙에 위배되기 때문에 좋지 않습니다.
50줄 이상의 클래스나 10개 이상의 패키지는 없어야 한다.
만약 클래스가 너무 많은 메소드를 가지고 있다면 단일 책임 원칙을 벗어날 것입니다. 그렇기 때문에 클래스는 하나의 책임, 패키지는 하나의 목적을 다할 수 있도록 작게 유지하는 편이 코드의 재사용과 가독성에서도 좋습니다.
대부분의 클래스의 하나의 인스턴스 변수를 사용하지만 때로는 두 개가 필요할 수도 있습니다.
새로운 인스턴스 변수를 클래스에 추가 시 클래스의 응집도는 떨어지게 됩니다. 많은 인스턴스를 가진 클래스는 단일 작업을 하기란 어렵습니다.
Name 클래스
class Name {
String first;
String middle;
String last;
}
두 클래스로 분해할 수 있습니다.
class Name {
Surname family;
GivenNames given;
}
class Surname {
String family;
}
class GivenNames {
List<String> names;
}
속성의 집합에서 객체의 협력으로 객체를 계층구조로 분해하면 더 효율적인 객체 모델로 사용할 수 있습니다.
이 규칙은 “말은 하되, 묻지는 말라.”로 대변됩니다. 객체의 상태를 가져오는 접근자를 사용하는 것은 괜찮지만 그 결과값을 사용하여 객체에 대한 결정을 내리는 것은 안 된다. 한 객체의 결정은 객체안에서만 이루어져야 합니다. 그렇기 때문에 getter/setter를 사용하면 open/closed 원칙을 위배하게 됩니다.
지금까지의 규칙들을 모두 다 지키기는 어려울것 입니다. 하지만 의식적으로 규칙을 지키는 노력을 한다면 좀더 객체지향 프로그래밍을 더 이해할 수 있고 어제보다 나은 코드가 나올수 있을것 입니다.
https://developerfarm.wordpress.com/2012/02/03/object_calisthenics_summary/
https://williamdurand.fr/2013/06/03/object-calisthenics/#8-no-classes-with-more-than-two-instance-variables