[OOAD] 객체지향 원칙 1. SRP

참조: 
헤드 퍼스트 OOAD (영문판) 390p
실용주의 디자인 패턴 (Holub on Patterns) 6p

Single Responsibility Principal, 보통 단일 책임 원칙이라고 하는데 여기서 “책임”이란 “변경 이유”에 해당한다. 따라서 이 원칙은 다음과 같이 재해석 할 수도 있다. 

어떤 클래스는 한 가지 이유로만 변경되어야 한다.
즉, 여러가지 이유로 해당 클래스가 변경된다면 그 클래스에 여러가지 책임 있다는 말이다. 또는 반대로 책임이 여러 크래스로 분산되어 있어도 문제다. 어떤 기능을 하나 손 보려면 여러 클래스를 두루 두루 손봐야 할지도 모르기 때문이다.
“한 가지 일”이라는게 클래스에게 지나친 제약이지는 않을까? 그렇치 않다. 스윙의 JTable은 백여개의 메서드를 가지고 있다. 하지만 그 모든게 테이블과 관련된 작업들이고 여러 리스너 인터페이스들을 구현하였기에 SRP를 잘 지키고 있는 예제로 볼 수 있다. 즉 지나치게 클래스를 제약하지 않는다.
응집도(cohesion)는 SRP의 또다른 이름이기도 하다. 높은 응집도를 가진 소프트웨어를 작성할수록 SRP를 잘 적용하고 있는 것이다.  
자 위에 있는 봄싹 코드에서 SRP를 위반한 코드를 찾아서 해당 코드를 작성한 사람에게 따지도록 하자. 

[Tell, Don’t Ask] 물어보지 말고 시켜라

http://c2.com/cgi/wiki?TellDontAsk

주요 내용
– Shy Code를 작성하라.
– 어떤 객체로부터 정보를 가져와서 판단하지 말고 그 객체가 판단하도록 시키자.
– Command와 Query를 구분하자.
– Law of Demeter를 지키자.
메서드가 어디에 어느 클래스에 위치 해야 하는지에 대한 이야기인데, 가끔 코딩하다보면 이 코드가 여기 있어야 되는건지..저기로 가야하는건지 판단의 기준이 잘 서지 않을 때가 있는데 그럴 때 이 말을 떠올리면 도움이 될 것 같습니다.
 public boolean confimMember(String email, String authCode) {
Member storedMember = repository.findByEmail(email);
if (storedMember == null) {
throw new UsernameNotFoundException(email + ” 에 해당하는 사용자가 없습니다.”);
}
boolean result = storedMember.getAuthCode().equals(authCode);
if (!result) {
throw new InsufficientAuthenticationException(authCode
+ ” 인증 코드가 올바르지 않습니다.”);
} else {
storedMember.join();
storedMember.makeAvatar();
addMemberRole(storedMember);
update(storedMember);
}
return result;
}
이 코드는 봄싹의 MemberServiceImpl 코드중 일부입니다. 즉 위와 같은 코드는 저 원칙에 위배되는 코드로 객체 지향적이지 못한 코드입니다. 객체 지향적인 코드라면 함수 호출이 아니라 어떤 작업을(method) 지시해야 합니다. 그런데 지금은 authCode를 가져와서 매개변수의 authCode와 같은지 판다하고 있지요. 바로 이 예제가 Tell, Don’t Ask에서 지적하고 있는 대표적인 사례입니다.
boolean result = storedMember.isConfirmed(authCode);
이렇게 매개변수로 필요한 정보를 전달하고 결과를 돌려받으면 됩니다. 즉 isConfirmed()라는 Query 메서드를 사용해서 정보를 조회하면 됩니다. Command 메서드가 아니라 Query 메서드기 때문에 당연히 객체 내부 정보를 변경하지 않도록 주의해야 합니다. Command 메서드와 Query 메서드를 잘 구분하는 것이 저 원칙을 지키는 지름길입니다. 또한, 이렇게 하는 것이 ‘데메테르 법칙’을 지키는 일이기도 합니다. 최측근에게만 일을 시키게 되니까 말이죠.
단, 단점이 있는데 그건 바로. 저 상태로 끝나면 모를까. 그게 아니라 Member 클래스에 isConfirm() 메서드를 만들어 줘야 한다는 거죠. 만약 3중, 4중으로 건너 건너 요청하던 코드를 저 원칙에 따라 고친다면 그만큼 자잘한 코드들이 생길 겁니다. 
그런 단점들 대신에 얻을 수 있는 장점으로 시스템 내부의 클래스간 의존도를 낮출 수 있다는 것입니다. 따라서 그 둘간의 저울질을 잘 하고 선택하시기를…
봄싹 코드는 이제 저 기준에 따라 모든 클래스의 코드를 리뷰하고 코드 뜯어고치기 작업을 진행합니다. 그게 끝나면 패키지 간의 의존성을 점검해봐야겠습니다.

Visitor 패턴

http://c2.com/cgi/wiki?VisitorPattern
http://en.wikipedia.org/wiki/Visitor_pattern

Composition 패턴을 구현한 구조물에 어떤 기능을 수행하고 싶을 때 해당 기능을 별도의 인터페이스로 분리해낸것이 Visitor 패턴. 이렇게 하여 기존의 구조물에 해당하는 코드는 변경하지 않고 그것들을 사용하는 새로운 기능을 새로 추가할 수 있으니 Open-Closed 원칙을 달성했다고 볼 수 있다.


생각해보니 이런 패턴이 적용된 것들이 꽤 많으며, 적용할 수 있는 곳도 많은 것 같다. 윈도우에서 폴더나 파일을 우클릭한 다음에 저장, 복사, 이동 등을 하는 것들이 모두 이 패턴을 이용해서 구현하지 않았을까..

게다가 ASM은 이 패턴을 사용하여 BCEL이나 SERP 같은 코드 조작 툴보다 훨씬 뛰어난 성능의 라이브러리를 제공하고 있다.

그렇다 사실은 ASM 공부하다가 맨 첨에 등장하는게 이 패턴이라 잠깐 보고 넘어가기로 한 것이다. ㅋㅋ

흠… 하지만 ASM이 조작하는 클래스 처럼 클래스 아래 변수 쫙! 메서드 쫙! 변수와 메서드에는 어트리뷰터가 쫙! 있는 단순구조가 아니라, 파일이나 폴더 처럼 파일 안에 파일 쫙! 그 안에 또 파일 쫙! 이런 트리구조라면 Visitor 패턴을 개조한 Hierarchical Visitor 패턴이 더 효율적이고 유용하겠다.

http://c2.com/cgi/wiki?HierarchicalVisitorPattern

자 어서 ASM도 보도록 하자. ㄱㄱㄱㄱ

Holub on Pattern 좋은데요~

사용자 삽입 이미지
수영장에서 놀다가 5대 원칙 부분만 읽어봤는데 아주 맘에 듭니다.

SRP: 단일 책임 원칙. 클래스 하나는 한 가지 책임만 지녀야 한다.

DIP: 의존성 관계 역전 원칙. 구체 클래스가 아닌 추상 클래스 또는 인터페이스에 의존할 것.

ISP: 인터페이스 분리 원칙. 세세한 인터페이스가 큰 한 덩어리 인터페이스보다 좋다.

LSP: 리스코프 대체 원칙. 상속 제대로 할 것.

OCP: Open-Closed 원칙.

무엇보다 번역이 정말 세심하게 되어 있는 것 같습니다. 간혹 독자가 긴 단락을 이해하지 못할까봐 역자 분의 요약도 볼 수 있는데 이 부분이 저한텐 많이 도움이 됩니다. 책 내용에 집중해서 좀 더 빠르게 읽게 도와준달까요.

책이 절판되서 없어지기 전에 장만 해 두시는 건 어떨까 싶습니다. 🙂

프로토타입 패턴(Prototype Pattern)

참조: Java 언어로 배우는 디자인 패턴 입문

“복사해서 인스턴스 만들기”
“클래스에서 인스턴스를 만들지 않고 인스턴스에서 인스턴스 만들기”

객체를 클래스를 사용하여 new로 생성하지 않고 복사해서 만들고 싶은 경우
– 취급하는 객체가 다양해서 각각을 별도의 클래스로 두기엔 무리가 있을 때..
– 클래스에서 객체 생성이 어려운 경우(ex. 런타임에 마우스 조작으로 만들어 내는 객체)
– 프레임워크와 생성할 인스턴스를 분리하고 싶을 때. 모형이 되는 객체를 등록해 놓고 그 객체를 복사해서 인스턴스 생성.

예제 코드 from Wikipedia

/** Prototype Class **/
public class Cookie implements Cloneable {

public Object clone() {
try {
Cookie copy = (Cookie)super.clone();

//In an actual implementation of this pattern you might now change references to
//the expensive to produce parts from the copies that are held inside the prototype.

return copy;

}
catch(CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}

/** Concrete Prototypes to clone **/
public class CoconutCookie extends Cookie { }

/** Client Class**/
public class CookieMachine {

private Cookie cookie;//could have been a private Cloneable cookie;

public CookieMachine(Cookie cookie) {
this.cookie = cookie;
}
public Cookie makeCookie() {
return (Cookie)cookie.clone();
}
public static void main(String args[]) {
Cookie tempCookie = null;
Cookie prot = new CoconutCookie();
CookieMachine cm = new CookieMachine(prot);
for (int i=0; i<100; i++)
tempCookie = cm.makeCookie();
}
}

Prototype
– 원형 인터페이스로 객체 복사에 사용할 메소드를 정의한다.
– Cloneable 인터페이스 상속.

ConcretePrototype
– Prototype 인터페이스 구현하여 실제 복사 로직 구현.

Client
– Prototype 인터페이스를 사용하여 새로운 객체를 만든다.

주의할 것.
– Cloenable은 마커 인터페이스.
– 실제 clone 메소드는 Object 클래스에 들어있다.
– 필드 대 필드 복사라서 배열의 경우 레퍼런스를 복사해서 위험할 수 있다. 그럴 땐 직접 clone 메소드를 재정의 해야 한다.
– clone을 재정의 할 떄는 super.clone()을 호출해야 한다.
– clone은 복사만 하지 생성자를 호출하지 않는다. 그래서 생성시에 로직이 필요한 땐 그 로직도 clone에 같이 넣어 준다.
– 자세한건 API를 보시라…