0. 시작하기 전에
이 문서는
Clean Code 클린 코드 : 애자일 소프트웨어 장인 정신 / 케이앤피북스 / 로버트 C. 마틴 저, 박재호 이해영 역
Refactoring / 대청 / 마틴 파울러 저, 윤성준 조재박 역
디자인패턴 이렇게 활용한다: C++로 배우는 패턴의 이해와 활용 / 한빛미디어 / 장세찬저
GoF의 디자인 패턴 / 피어슨에듀케이션코리아 / John Vlissides 저, 김정아 역
CODE COMPLETE 2/E / 정보문화사 / 스티브 맥코넬 저, 서우석 역
패턴을 활용한 리펙터링 / 인사이트 / 조슈아 케리에브스키 저, 윤성준, 조상민 역
소프트웨어 개발의 지혜 : 원칙, 디자인패턴, 실천방법 / 야스미디어 / 로버트 C. 마틴 저, 이용원 역
Elemental Design Patterns / 한빛미디어 / 체이슨 맥컴 스미스 저 / 김지원 역
내용을 정리할 예정입니다
몇개의 책을 구입을 더 해야 하고 아직 다 보지 못한 책도 있으며
마무리를 할 수 있을지 의문이긴 하지만 꾸준히 해보려고 도전해 보겠습니다
몇개 책에서 정리를 할수 없는 내용이 발견되었습니다
그래서 정리를 기법 위주로 정리를 하려고 합니다
1) 디자인패턴이란?
디자인 패턴(Design Pattern) 이란 방식을 통해 소프트웨어 설계에서 얻은 세세한 경험들을 기록해 놓도록 하는 것
패턴 "어떤 상황의 문제에 대한 해법" 일반적으로 하나의 패턴에는 다음의 네가지 요소가 반드시 들어가 있다
1 패턴 이름(pattern name)은 한두 단어로 설계 문제와 해법을 서술합니다.
패턴에 이름을 부여하는 것은 설계 어휘를 늘이는 일이며 높은 수준의 추상화된 설계를 할 수 있도록 해준다.
패턴의 이름을 정의해 두면 문서에서 이 이름을 사용하여 설계의 의도를 표현할 수 있게 된다.
또 이렇게 이름을 갖게 되면 설계에 대한 생각을 더욱 쉽게 할 수 있고 개발자들 간의 의사소통이 원활해진다.
이 때문에 좋은 이름을 생각해 내는 것은 카탈로그를 설정하는데 있어서 가장 힘든 부분 중의 하나이기도 합니다
2 문제(problem)는 언제 패턴을 사용하는가를 서술하며 해결할 문제와 그 배경을 설명한다.
즉 "어떤 알고리즘을 객체로 만들까"와 같은 설계의 세밀한 문제를 설명할 수 있다
때론 유연성 없는 설계가 될 징조를 보이는 크래스나 객체의 구저를 제시한다
문제를 제시함으로써 패턴을 적용하는 것이 의미있는 사례들을 정의하기도 합니다.
3 해법(solution)은 설계를 구성하는 요소들과 그 요소들 간의 관계 책임
그리고 협력 관계를 서술한다.
그렇다고 해법이 어떤 구체적인 설계나 구현을 설명하지는 않는다.
왜냐하면 패턴은 다양한 경우에 적용할 수 있는 템플릿(template)이기 때문이다.
구체적인 부분 대신 디자인 패턴은 문제에 대한 추상적인 설명을 제공하고
문제를 해결하기 위해서 클래스나 객체들의 나열 방법을 제공합니다
4 결과(consequence)는 디자인 패턴을 적용해서 얻는 결과와 장단점을 서술합니다
어떤 설계를 결정할 때 그 설계의 결과를 고려하지 않기가 쉬운데 어떻게 보면 선택하는 과정에서
또는 비용과 효과를 측정하는 과정에서 서계의 결과는 가장 중요한 부분이다.
소프트웨어에서 결과란 가끔 시간이나 공간 사이의 균형일 수도 있다.
즉 시간을 중요한 요소로 볼 것인지 아니면 저장 공간의 효율을 중요한 요소로 볼것인지에 따라
다른 설계 방법을 선택해야 한다는 것입니다. 또한 언어에 따라서 차이가 있다.
재사용은 객체지향 설계의 주요 요소 이므로, 패턴의 결과는 시스템의 유연성, 확장성, 이식성 등의
커다란 영향을 준다. 그래서 이런 설계의 결과들을 잘 정리해 두면 나중에 패턴들을 이해하거나
평가하는 데 도움을 받을 수 있다.
이책에서 말하는 디자인 패턴은 "특정한 전후 관계에서 일반적 설계 문제를 해결하기 위해
상호 교류하는 수정 가능한 객체와 클래스들에 대한 설명" 이다.
2) 디자인패턴 기술하기
패턴 이름과 분류(Pattern Name and Classification)
하나의 패턴에 붙은 이름은 그 자체가 핵심을 간결하게 전달해 준다.
개발자가 설계를 할 때 직접 사용할 단어이기 때문에
좋은 이름은 패턴의 생명이다. 패턴의 분류는 패턴의 구성방식을 반영한다.
의도(Intent)
이 디자인 패턴은 무엇을 하는 가? 의도와 논리적인 근거가 무엇인가?
어떤 특정한 문제나 이슈를 해결하기 위한 것일까요? 라는 질문에 간결한 답을 제시하는 부분이다.
다른 이름(Also Known As)
이 패턴을 다르게 부르는 이름이 있다면 그것을 제시 한다.
동기(Motivation)
설계 문제를 제시하고, 패턴안에서 클래스나 객체 구조가 어떻게 문제를 해결하는 지
설명해주는 일종의 시나리오 이 시나리오는 패턴에 대한 좀 더 추상화된 설명을 이해할 수 있게 도와 준다.
활용성(Applicability)
해당 패턴을 어떤 상황에 적용할 수 있을까? 패턴이 문제로 삼는 잘못된 설계에는 무엇일까?
이 상황을 어떻게 파악할 수 있을까?
구조(Structure)
객체 모델링 기법(Object Modeling Technique)1에 기반을 둔 표기법을 이용하여 해당 패턴에서
쓰는 클래스들을 시각적으로 나타낸다 또한 객체 사이에 오가는 요청과 협력 관계의
순차를 표현하기 위해서 상호작용 다이어그램을 사용했다.
참여자(Participant)
주어진 패턴을 구성하고 책임을 수행하는 클래스나 객체들을 설명한다.
협력방법(Collaboration)
참여자들이 작업을 수행하기 위한 참여자들 간의 협력관계를 정의한다.
결과(Consequence)
이 패턴이 자신의 목표를 어떻게 지원할까? 이 패턴을 이용한 결과는 무엇이고 장단점은 무엇?
이 패턴을 사용하면 시스템 구조의 어떤 면을 독립적으로 다양화 시킬수 있을까?
구현(Implementation)
패턴을 구현할 때 주의해야 할 함정, 힌트, 기법등은 무엇? 특정 언어에 국한된 특이사항은 무엇?
예제코드(Sample Code)
주어진 패턴을 실제로 C++나 스몰토크를 이용해서 어떻게 구현할 수 있는가를 보여주는 코드
잘 알려진 사용 예(Known Use)
실제 시스템에서 찾아볼 수 있는 패턴들의 예로서 서로 다른 개발 분야에서는 쓰는
예제를 두 가지 이상 포함 시켰다
관련 패턴(Related Pattern)
이 패턴과 밀접하게 관련된 다른 패턴들은 무엇? 이들의 중요한 차이점은 무엇?
어떤 다른 패턴에 이 패턴이 사용되어 질까?
3) 디자인 패턴 카탈로그
Abstract Factory - 추상 팩토리
구체적인 클래스를 지정하지 않고 관련성을 갖는 객체들의 집합을 생성하거나
서로 독립적인 객체들의 집합을 생성할 수 있는 인터페이스를 제공하는 패턴
Adapter - 어댑터
클래스의 인터페이스를 사용자가 기대하는 다른 인터페이스로 변환하는 패턴으로
호환성이 없는 인터페이스 때문에 함께 동작할 수 없는 클래스들이 함께 작동하도록 함
Bridge - 브릿지
구현부에서 추상층을 분리하여 각자 독립적으로 변형할 수 있게 하는 패턴
Builder - 빌더
복합 객체의 생성 과정과 표현방법을 분리하여 동일한 생성 절차에서 서로 다른 표현 결과를 만들 수 있게 하는 패턴
Chain of Responsibility 책임 연쇄
요청을 처리할 수 있는 기회를 하나 이상의 객체에게 부여하여 요청을 보내는 객체와 그 요청을 받는 객체 사이의 결합을 피하는 패턴
요청을 받을 수 있는 객체를 연쇄적으로 묶고, 실제 요청을 처리할 객체를 만날 때까지 객체 고리를 따라서 요청을 전달합니다
Command - 커맨드
요청을 객체의 형태로 캡슐화하여 서로 요청이 다른 사용자의 매개변수화,
요청 저장 또는 로깅, 그리고 연산의 취소를 지원하게 만드는 패턴
Composite - 컴포짓
객체들의 관계를 트리 구조로 구성하여 부분-천체 계층을 표현하는 패턴으로
사용자가 단일 객체와 복합 객체 모두 동일하게 다루도록 한다.
Decorator - 데커레이터
주어진 상황 및 용도에 따라 어떤 객체에 책임을 덧붙이는 패턴으로, 기능 확장이 필요할 때 서브클래싱 대신 쓸수 있는 유연한 대안
Facade - 퍼사드
서브 시스템에 있는 인터페이스 집합에 대해서 하나의 통합된 인터페이스를 제공하는 패턴으로,
서브시스템을 좀더 사용하기 편하게 만드는 상위 수준의 인터페이스를 정의
Factory Method - 팩토리 메서드
객체를 생성하는 인터페이스는 미리 정의하되, 인스턴스를 만들 클래스의 결정은 서브클래스 쪽에서 내리는 패턴
팩토리 메서드 패턴에서는 클래스의 인스턴스를 만드는 시점을 서브 클래스로 미룬다.
Flyweight - 플라이웨이
크기가 작은 객체가 여러개 있을 때, 공유를 통해 이들을 효율적으로 지원하는 패턴
Interpreter - 인터프리터
주어진 언어에 대해 그 언어의 문법을 위한 표현 수단을 정의하고
이와 아울러 그 표현 수단을 사용하여 해당 언어로 작성된 문장을 해석하는 해석기를 정의하는 패턴
Iterator - 이터레이터
내부 표현부를 노출하지 않고, 어떤 객체 집합에 속한 원소들을 순차적으로 접근할 수 있는 방법을 제공
Mediator - 중재자
한 집합에 속해있는 객체들의 상호작용을 캡슐화하는 객체를 정의하는 패턴
객체들이 직접 서로 참조하지 않도록 함으로써 객체들 사이의 소결합(loose coupling)을 촉진 시키며, 개발자가 객체들의
상호작용을 독립적으로 다양화시킬 수 있게 만든다.
Memento - 메멘토
캡슐화를 위해바지 않은 채 어떤 객체의 내부 상태를 잡아내고 실체화 시켜 이후에 해당 객체가
그 상태로 다시 되돌아올 수 있도록 하는 패턴
Observer - 옵저버
객체 사이에 일 대 다의 의존관계를 정의해 두어, 어떤 객체의 상태가 변할 때
그 객체의 의존성을 가진 다른 객체들이 그 변화를 통지 받고 자동으로 갱신 될 수 있게 만드는 패턴
Prototype - 프로토타입
생성할 객체의 종류를 명세화하는 데에 원형이 되는 예시물을 이용하고,
그 원형을 복제함으로써 새로운 객체를 생성하는 패턴
Proxy - 프록시
어떤 다른 객체로 접근하는 것을 통제하기 위해서 그 객체의 대리자(surrogate)또는 자리채움자(Placehoder)를 제공하는 패턴
Singleton - 싱글톤
어떤 클래스의 인스턴스는 오직 하나임을 보장하며,
이 인스턴스에 접근할 수 있는 전역적인 접촉점을 제공하는 패턴
State - 상태
객체의 내부 상태에 따라 스스로 행동을 변경할 수 있게 끔 허가하는 패턴으로
이렇게 하면 객체는 마치 자신의 클래스를 바꾸는 것처럼 보임
Strategy - 전략
동일 계열의 알고리즘 군을 정의 하고 각각의 알고리즘을 캡슐화하며 이들을 상호교환이 가능하도록 만드는 패턴
알고리즘을 사용하는 사용자와 상관없이 독립적으로 알고리즘을 다양하게 변경할수 있게 한다.
Template Method - 템플릿 메서드
객체의 연산에는 알고리즘의 뼈대만을 정의하고 각 단게에서 수행할 구체적 처리는 서브 클래스 쪽으로 미루는 패턴
알고리즘의 구조 자체는 그대로 놔둔채 알고리즘 각 단계의 처리를 서브클래스에서 재정의 할 수 있게 한다.
Visitor - 비지터
객체 구조를 이루는 원소에 대해 수행할 연산을 표현하는 패턴
연산을 적용할 원소의 클래스를 변경하지 않고도 새로운 연산을 정의할수 있게 함
4) 카탈로그 조직화 하기
패턴을 분류하는 기준은 두가지 목적(purpose), 범위(scope)
패턴은 생성, 구조, 행동 중의 한 가지 목적을 갖는다.
생성(creational) 패턴 - 객체의 생성 과정에 관여하는 패턴
구조(structural) 패턴 - 클래스나 객체의 합성에 관한 패턴
행동(behavioral) 패턴 - 클래스나 객체들이 상호작용하는 방법과 책임을 분산하는 방법을 정의하는 패턴
범위는 패턴을 클래스에 적용하는지 아니면 객체에 적용하는 지를 구분하는 것입니다
클래스 패턴 - 클래스와 서브 클래스 간의 관련성을 다루는 패턴, 관련성은 주로 상속, 컴파일 타임에 정적으로 결정됨
객체 패턴 - 객체 관련성을 다루는 패턴으로서, 런타임에 변경할 수 있으며 더 동적인 성격을 가집니다.
구분 목적
생성 구조 행동
범위 클래스 Factory Method - 팩토리 메서드 Adapter - 어댑터(class) Interpreter - 인터프리터
Template Method - 템플릿 메서드
객체 Abstract Factory - 추상 팩토리
Builder - 빌더
Prototype - 프로토타입
Singleton - 싱글톤 Adapter - 어댑터(object)
Bridge - 브릿지
Composite - 컴포짓
Decorator - 데커레이터
Facade - 퍼사드
Flyweight - 플라이웨이
Proxy - 프록시 Chain of Responsibility 책임 연쇄
Command - 커맨드
Interpreter - 인터프리터
Mediator - 중재자
Memento - 메멘토
Observer - 옵저버
State - 상태
Strategy - 전략
Visitor - 비지터
생성(creational) "클래스" 패턴 - 객체를 생성하는 책임의 일부를 서브클래스가 담당하도록 넘김
생성(creational) "객체" 패턴 - 이를 다른 객체에게 위임
구조(structural) "클래스" 패턴 - 상속을 이용해서 클래스를 복합
구조(structural) "객체" 패턴 - 객체를 합성하는 방법을 정의
행동(behavioral) "클래스" 패턴 - 상속을 이용해서 알고리즘과 제어 흐름을 기술
행동(behavioral) "객체" 패턴 - 하나의 작업을 수행하기 위해 객체 집합이 어떻게 협력하는 지를 기술
패턴을 조직하는 또 다른 방법
일부 패턴은 함께 사용해야 할 때
Composite - 컴포짓 패턴은 Iterator - 이터레이터 패턴과 Visitor - 비지터 패턴을 함께 사용해야 할 때가 많다
어떤 패턴은 다른 패턴의 대안
Prototype - 프로토타입 패턴은 Abstract Factory - 추상 팩토리 패턴의 대안이다.
패턴간 의도는 서로 다르지만 결과적으로 유사한 설계구조를 만드는 패턴
Composite - 컴포짓 패턴과 Decorator - 데커레이터 패턴의 의도는 다르지만 구조는 비슷하다.
디자인 패턴을 조직하는 다른 방법은
패턴간의 참조 관계에 따라 관리하는 것
DesignPatternRelationships
이는 패턴마다 기술한 "관련 패턴"의 참조 관계를 표현한 것
5) 디자인 패턴을 이용하여 문제를 푸는 방법
적당한 객체 찾기
객체의 크기 결정
객체 인터페이스의 명세
객체 구현 명세 하기
재사용을 실현 가능한 것으로
런타임 및 컴파일의 구조를 관계 짓기
변화에 대비한 설계
5.1) 적당한 객체 찾기
적당한 객체 찾기
객체 지향 프로그램은 객체(object)로 만듭니다. 객체는 데이터와 이 데이터에 연산을 가하는
프로시져(Procedure)를 함께 묶은 단위 프로시져를 일반적으로 메서드(method)또는
연산(operation) 이라고 합니다 객체는 요청(request) 또는 메시지(message)를 사용자에게 받으면
연산을 수행합니다
요청은 객체가 연산을 실행하게 하는 유일한 방법이고 연산은 객체의 내부 데이터의 상태를
변경하는 유일한 방법입니다. 이러한 접근의 제약 사항으로 객체의 내부 상태는 캡슐화(encapsulate) 됩니다
객체 외부에서는 객체의 내부 데이터에 직접 접근할 수 없고 객체의 내부 데이터 표현 방법을 알 수 없습니다
객체지향 설계의 가장 어려운 부분은 시스템을 구성할 객체의 분할을 결정하는 것 입니다
여러 요인을 고려해야 하기 때문에 매우 어려운 작업입니다 고려해야 할 요인으로는
캡슐화, 크기 정하기, 종속성, 유연성, 성능, 진화, 재사용성 등이 있습니다
객체 지향 설계 방법론들은 서로 다른 방법으로 접근합니다. 문제 기술서를 작성하고 명사와 동사를
추출해서 각각을 클래스와 연산으로 만드는 방법 시스템의 협력 관계나 책임성을 중심으로
설계하는 방법 * 실세계를 모델로 만들고 이를 분석해 설계로 전이하는 과정에서 객체로 바꾸는 방법
분석 모델의 객체는 실세계 객채들 설계 모델의 객체에는 배열, 리스트처럼 구현에 가까운 클래스들도 있습니다
실세계를 그대로 반영하는 모델링만 강조하면 현재의 실세계는 반영할 수 있지만 미래의 실세계는
반영할 수 없습니다. 설계 단계동안 만들어야 하는 새로운 추상화는 설계의 유연성을 증진하기
위한 중요한 노력 중 하나 입니다
5.2) 객체의 크기 결정
객체의 크기 결정
객체는 크기나 개수가 정해져 있지 않습니다 적당한 객체의 규모는 어떻게 결정할 수 있을까요?
Facade (퍼사드) 패턴는 서브 시스템을 어떻게 객체로 표현할 수 있는지 설명하고
Flyweight (플라이웨이) 패턴는 규모는 작지만 개수는 많은 객체를 다루는 방법을 설명합니다
어떤 패턴들은 객체를 좀더 작은 규모의 객체로 분할하는 구체적인 방법을 다루기도 합니다
Abstract Factory (추상 팩토리) 패턴과 Builder (빌더)패턴은 다른 객체를 생성하는 책임만 있는
객체를 만들어 냅니다. Visitor (비지터) 패턴과 Command (커맨드) 패턴은 요청을 자신이 처리하는 것이 아니라
다른 객체나 객체 집합이 요청을 처리하여 구현하도록 책임지는 객체를 만들어 냅니다.
5.3) 객체 인터페이스의 명세
객체 인터페이스의 명세
객체가 선언하는 모든 연산은 연산의 이름, 매개변수로 받아들이는 객체들,
연산의 시그니처(signature)라고 합니다. 인터페이스(interface)는 객체가 정의하는
연산의 모든 시그니처들을 일컫는말로 객체의 인터페이스는 객체가 받아서 처리할 수 있는 연산의 집합입니다.
객체 인터페이스에 정의된 시그니처와 일치하는 어떤 요청이 객체에 전달되면, 객체는 연산을 수행하여 그 요청을 처리합니다
타입(type)은 특정 인터페이스를 나타낼 때 사용하는 이름입니다 객체가 어떤 타입을 갖는다는 것은
그 타입 인터페이스에 정의한 연산들을 모두 처리할 수 있다는 것을 의미 합니다 객체는
여러 타입을 가질수 있고 서로 다른 객체가 하나의 타입을 공유 할수도 있습니다 인터페이스가
다른 인터페이스를 부분 집합으로 포합할 때 다른 인터페이스를 포함하는 인터페이스를
서브타입(subtype)이라고하고 다른 인터페이스가 포함하는 인터페이스를 슈퍼타입(supertype)이라 합니다
서브타입은 슈퍼타입의 인터페이스를 상속(inheritance)한다고 이야기 합니다
인터페이스 개념은 객체지향 시스템에서 가장 기본적인 것입니다 객체는 인터페이스를 통해
자신을 드러내고 인터페이스의 구현에 대해서는 전혀 알려주지 않습니다 그러므로 동일한
인터페이스를 갖는 두 객체가 완전히 다른 구현을 가질 수 있습니다
객체에 요청이 전달되면 요청과 이를 받는 객체에 따라서 수행되는 처리방식이 달라집니다
어떤 요청과 그 요청을 처리할 객체를 프로그램 실행 중 즉 런타임에 연결 짓는 것을
동적 바인딩(dynamic binding)이라고 합니다
동적바인딩은 요청이 어떻게 구현되어 어떤 결과를 만들어낼지를 런타임에 결정할 수 있음에
의미합니다 동적바인딩은 프로그램이 기대하는 객체를 동일한 인터페이스를 갖는 다른 객체로
대체할 수 있게 해 줍니다 이런 대체성을 다형성(polymorphism)이라고 하는데 이는
객체지향 시스템에서 핵심 개념입니다
디자인 패턴은 인터페이스에 정의해야 하는 중요 요소가 무엇이고 어떤 종류의 데이터를
주고 받아야 하는지 식별하여 인터페이스를 정의하도록 도와줍니다 가끔 디자인 패턴은
인터페이스에 넣지 말아야 할 것을 알려주기도 합니다 메멘토 패턴은 객체의 내부 상태를
어떻게 저장하고 캡슐화해야 하는지를 정의함으로써 객체가 나중에 그 상태로 복구할 수 있는 방법을
알려줍니다 이 패턴에서는 객체에 두개의 인터페이스르 정의하도록 규정합니다 이 두가지는
사용자가 상태를 저장하고 복사할 수 있도록 해주는 인터페이스와 원본 객체가 그 메멘토에서
상태를 저장하고 검색하기 위해 사용하는 인터페이스 입니다
디자인 패턴은 인터페이스 간의 관련성도 정의 합니다 특히 클래스 간에 유사한 인터페이스를
정의하도록 하거나 인터페이스에 여려 가지 제약을 정의합니다 데커레이터 패턴과 프록시 패턴은
장식되고 중재되는 객체와 동일한 인터페이스를 갖도록 데커레이터 객체와 프록시 객체의 인터페이스를
요청합니다 프록시 객의 인터페이스는 자신이 대리하는 다른 객체의 인터페이스와 동일하다는 것입니다
비지터 패턴에서 비지터 인터페이스는 방문자 객체가 방문하는 개체들의 클래스 인터페이스를 그 방문자
인터페이스에 모두 반영하도록 합니다
5.4) 객체 구현 명세 하기
객체 구현 명세 하기
어떤 객체의 구현은 클래스(class)에서 정의(define)합니다. 클래스는 객체의 내부 데이터와 표현 방법을 명세하고,
그 객체가 수행할 연산을 정의 합니다 객체는 클래스를 인스턴스로 만듦으로써 생성 됩니다. 즉, 객체는 클래스의
인스턴스라고 할 수 있습니다. 클래스의 인스턴스화 과정은 객체의 내부 데이터[인스턴스 변수(instance variable)]에
대한 공간을 할당하고, 이 데이터들을 연산과 관련 짓는 것입니다. 클래스의 인스턴스화 과정을 통해 객체의 인스턴스를
얻게 됩니다.
점선 화살표는 한 클래스(instantiator에 해당)가 다른 클래스(instaniatee에 해당)의 객체를 인스턴스화함을
의미합니다. 화살표 방향은 생성할 객체의 클래스로 향합니다
새로은 클래스는 기존 클래스에 기반을 둔 클래스 상속을 사용하여 정의할 수 있습니다. 서브클래스(subclass)는 부모 클래스(parent class)를 상속하면, 부모 클래스가 갖는 모든 데이터와 연산을 서브클래스가 갖게 됩니다. 서브클래스의 인스턴스는 부모 클래스가 정의한 모든 데이터를 가지며, 부모 클래스가 정의한 연산을 모두 수행할 수 있습니다.
추상 클래스(abstract class)는 모든 서브클래스 사이의 공통되는 인터페이스를 정의합니다. 추상 클래스는 정의한 모든 연산이나 일부 연산의 구현을 서브클래스에게 넘깁니다. 정의한 연산 모두가 추상 클래스로 구현된 것이 아니므로, 추상클래스는 인스턴스를 생성할 수 없습니다. 정의만하고 구현하지 않는 연산을 추상 연산(abstract operation)이라 하고, 추상 클래스가 아닌 클래스를 구체 클래스(concrete class)라고 합니다
서브클래스는 부모 클래스가 정의한 행동을 재정의하거나 정제할 수 있습니다. 서브클래스는 부모 클래스에 정의한 연산의 구현을 바꿀 수 있습니다. 즉, 오버라이드(override)로 서브클래스는 부모 클래스에 정의된 처리 방식을 변경할 수 있습니다. 클래스 상속은 다른 클래스를 확장하여 새로운 클래스를 정의할 수 있게 합니다.
믹스인(mixin) 클래스는 다른 클래스들에게 선택적인 인터페이스 혹은 기능을 제공하려는 목적을 가진 클래스입니다. 인스넡스로 만들 의도가 없다는 면에서 추상 클래스와 비슷합니다. 믹스인 클래스를 사용하기 위해서는 다중 상속이 필요합니다.
클래스 상속 대 인터페이스 상속
클래스와 타입의 차이는 꼭 이해해 두어야 합니다.
객체의 클래스 - 그 객체가 어떻게 구현되느냐를 정의합니다. 클래스는 객체의 내부 상태와 그 객체의 연산에 대한 구현 방법을 정의합니다.
객체의 타입 - 그 객체의 인터페이스, 즉 그 객체가 응답할 수 있는 요청의 집합을 정의합니다.
하나의 객체가 여러 타입을 가질 수 있고, 서로 다른 클래스의 객체들이 동일한 타입을 가질 수 잇습니다. 즉, 객체의 구현은 다를 지라도 인터페이스는 같을 수 있다는 의미 입니다.
클래스와 타입 간에는 밀접한 관련이 있습니다. 클래스도 객체가 수행할 수 있는 연산을 정의 하므로, 객체의 타입을 정의하는 것이기도 합니다. 그래서 어떤 객체가 어떤 클래스의 인스턴스라고 말할 때, 그 객체는 그 클래스가 정의한 인터페이스를 지원한다는 뜻이 숨어있다고 보면 됩니다.
클래스 상속과 인터페이스 상속의 차이
클래스 상속 - 객체의 구현을 정의할 때 이미 정의된 객체의 구현을 바탕으로 합니다. 코드와 내부 표현 구조를 공유하는 메커니즘입니다.
인터페이스 상속(서브타이핑) - 어떤 객체가 다른 객체 대신에 사용될 수 있는 경우를 지정하는 매커니즘입니다. 동적 바인딩을 설명할 때 이야기 했듯, 인터페이스 상속 관계가 있다면 프로그램에는 슈퍼타입으로 정의하지만 런타임에 서브타입의 객체로 대체할 수 있습니다.
많은 언어가 이 두 개념을 구분하지 않기 때문에 두 개념을 혼동하기 쉽습니다. C++언어에서 상속은 인터페이스와 구현 상속 모두를 의미합니다. C++에서 인터페이스를 상속하는 표준적인 방법은 (순수) 가상 함수를 갖는 클래스를 public으로 상속하는 것입니다. public으로 상속되면 서브클래스도 부모클래스가 갖는 가상 함수를 상속받고 서브클래스가 구현을 담당하며, 상속받은 인터페이스가 서브클래스의 사용자에게도 공개됩니다.
C++에서 인터페이스 상속
C++에서 순수한 인터페이스 상속은 순수 가상 함수를 정의한 추상 클래스를 public으로 상속하면 비슷하게 구현할 수 있습니다. 순수 가상 함수는 전혀 구현을 정의할 수 없6는 함수이기 때문에 이를 상속한다는 것은 진정한 의미의 인터페이스만을 상속받는다는 뜻입니다.
C++에서 클래스 상속
구현이나 클래스의 상속은 private상속으로 비슷하게 얻을 수 있습니다. private로 상속하면 부모 클래스에 정의된 연산은 서브클래스의 사용자에게는 공개되지 않기 때문에 상속의 목적은 인터페이스 확장이 아닌 부모 클래스 구현의 재사용입니다.
이 책에 등장하는 디자인 패턴 중 꽤 많은 것들에 이런 구분이 필요합니다. 책임 연쇄 패턴에 나오는 객체들은 반드시 동일한 타입을 가져야 하지만, 이들이 구현을 공유할 부분은 없습니다. 컴포짓 패턴에서 Component 클래스는 공통의 인터페이스를 정의하고, Composite 클래스는 공통의 구현을 정의합니다. 커맨트, 옵저버, 상태, 전략 패턴은 순수 인터페이스인 추상 클래스를 써서 구현될 때가 많습니다.
구현에 따르지 않고, 인터페이스에 따르는 프로그래밍
클래스 상속은 기본적으로 부모 클래스에서 정의한 구현을 재사용하여 응용프로그램의 기능성을 확장하려는 메커니즘입니다. 이미 있는 것을 이용해서 새로운 객체를 빨리 정의해 보려는 것입니다. 기존의 클래스를 그대로 상속할 수 있다면 새로운 구현에 드는 비용은 공짜인 셈입니다.
상속이 가진 다른 기능들 중에는 동일한 인터페이스를 갖는 객체군을 정의하는 것이 있는데, 매우 중요한 특징입니다. 객체군을 전의하는 것이 중요한 이유는 그것으로 다형성으로 끌어낼 수 있기 때문입니다.
상속을 적절하게 이용하면, 모든 클래스는 추상 클래스를 추상 클래스를 상속하도록 하여 인터페이스를 공유할 수 있게 됩니다. 이것은 서브클래스가 단순히 연산을 추가하거나 재정의할 뿐, 부모 클래스의 연산을 감추지 않는다는 뜻입니다. 모든 서브클래스들은 추상 클래스에 정의한 인터페이스를 처리할 수 있습니다. 다시 말해, 부모 클래스에 정의된 요청이 서브클래스에게 전달되어도 서브클래스는 이를 처리할 수 있다는 의미입니다.
추상 클래스를 정의하고 인터페이스 개념으로 객체를 다룰 때 얻을 수 있는 두 가지 이점 1. 사용자가 원하는 인터페이스를 그 객체가 만족하고 있는 한, 사용자는 그들이 사용하는 특정 객체 타입에 대해 알아야 할 필요는 없습니다. 2. 사용자는 이 객체들을 구현하는 클래스를 알 필요가 없고, 단지 인터페이스를 정의하는 추상 클래스가 무엇인지만 알면 됩니다.
구현이 아닌 인터페이스에 따라 프로그래밍 합니다
추상 팩토리, 빌더, 팩토리 메서드, 프로토타입, 컴포짓 패턴에서는 구체 클래스에서 인스턴스를 생성하도록 하고 있습니다. 이들 패턴에서는 객체 생성의 과정을 추상화 함으로써 인스턴스화할 때 이넡페이스와 구현을 연결하는 다른 방법을 제시합니다.
5.5) 재사용을 실현 가능한 것으로
재사용 가능한 소프트웨어 개발
상속 대 합성
객체지향 시스템에서 기능의 재사용을 위해 구사하는 가장 대표적인 기법은 클래스 상속, 그리고 객체 합성(object composition) 입니다.
클래스 상속은 서브 클래싱, 즉 다른 부모 클래스에서 상속받아 한 클래스의 구현을 정의하는 것입니다. 서브클래싱에 의한 재사용을 화이트박스 재사용(white-box reuse)이라고 합니다. "화이트박스"는 내부를 볼 수 있다는 의미에서 나온 말입니다. 상속을 받으면 부모 클래스의 내부가 서브클래스에 고개되기 때문에 화이트박스인 셈입니다.
객체 합성은 클래스 상속에 대한 대안 입니다. 다른 객체를 여러개 붙여서 새로운 기능 혹은 객체를 구성하는 것입니다. 객체를 합성하려면, 합성에 들어가는 객체들의 인터페이스를 명확하게 정의해 두어야 합니다. 이런 스타일의 재사용을 블랙박스 재사용(black-box reuse) 이라고 하는데, 객체의 내부는 공개되지 않고 인터페이스를 통해서만 재사용되기 때문입니다
클래스 상속의 장점 - 컴파일 시점에 정적으로 정의되고 프로그래밍 언어가 직접 지원하므로 그대로 사용하면 됩니다. 클래스 상속으로 부모 클래스의 구현을 쉽게 수정할 수도 있는데, 서브클래스는 모든 연산이 아닌 일부만 재정의할 수도 있습니다.
클래스 상속의 단점 - 첫번재 런타임에 상속받은 부모 클래스의 구현을 변경할 수는 없다는 점입니다. 왜냐하면 상속은 컴파일 시점에 결정되는 사항이기 때문입니다. 두번째 부모 클래스는 서브클래스의 물리적 표현의 최소 부분만을 정의하기 때문에 서브 클래스는 부모 클래스의 구현이 서브클래스에 다 드러나는 것이기 때문에 상속은 캡슐화를 파괴한다고 주장하는 의견도 있습니다. 서브클래스는 부모 클래스의 구현에 종속될 수밖에 없으므로 부모 클래스 구현에 변경이 생기면 서브클래스도 변경해야 합니다.
클래스 상속에서 구현의 종속성 해결 방법 - 추상 클래스에서만 상속 받는 것입니다. 추상 클래스에는 구현이 거의 없거나 전혀 없습니다. 이미 추상 클래스를 상속했다는 것은 구현이 아닌 인터페이스를 상속한 것이므로 구현 자체는 서브클래스가 정의합니다. 구현이 변경되면 서브클래스만 변경하면 되고 상위 추상 클래스는 고려할 필요가 없습니다.
객체 합성 특징 - 한 객체가 다른 객체에 대한 참조자를 얻는 방식으로 런타임에 동적으로 정의됩니다. 합성은 객체가 다른 객체의 인터페이스만 바라보게 하기 때문에 인터페이스 정의에 더 많은 주의를 기울여야 합니다. 객체는 인터페이스에서만 접근하므로 캡슐화를 유지할 수 있습니다. 동일한 타입을 갖는다면 다른 객체로 런타임에 대체가 가능합니다. 객체는 인터페이스에 맞춰 구현되므로 구현 사이의 종속성은 확실히 줄어듭니다.
클래스 상속보다 객체 합성이 더 선호 - 각 클래스의 캡슐화를 유지할 수 있고, 각 클래스의 한 가지 작업에 집중할 수 있기 때문입니다. 클래스와 클래스 계층이 소규모로 유지되면서 통제불능의 괴물로 자랄 가능성은 적습니다. 객체 합성으로 서계되면 클래스의 수는 적어지고 객체의 수는 좀 더 많아질 수 있지만, 시스템의 행동은 클래스에 정의된 정적인 내용보다는 런타임에 드러나는 객체 합성의 의한 상호 관련성에 따라 달라질 수 있습니다.
객체 합성이 클래스 합성보다 더 나은 방법 입니다.
완벽한 재사용 - 재사용을 위해서 새로운 구성요소를 생성할 필요 없이 필요한 기존의 구성요소를 조립해서 모든 새로운 기능을 얻어올 수 있습니다. 그러나 가능한 구성요소의 집합이 실제로 사용할 수 있을 만큼 충분하지 않기 때문에 기존 구성요소의 조합을 통한 재사용만으로 목적을 달성할 수 있는 경우는 드뭅니다. 상속에 의한 재사용은 기존 클래스들을 조합해서 새로운 구성요소를 쉽게 만들 수 있도록 해 줍니다. 그러므로 상속과 객체 합성을 적절히 조합되어야 완벽한 재사용이 가능합니다.
위임
위임(delegation)은 합성을 상속만큼 강력하게 만드는 방법입니다. 위임에서는 두 객체가 하나의 요청을 처리합니다. 수신 객체가 연산의 처리를 위임자(delegate)에게 보냅니다. 이는 서브클래스가 부모 클래스에게 요청을 전달하는 것과 유사한 방식입니다. 상속에서는 상속받은 연산이 늘 수신 객체를 참조하게 되는 데, C++에서는 this를 이용해서 수신 객체를 참조합니다. 위임과 동일한 효과를 얻으려면 수신 객체는 대리자에게 자신을 매개변수로 전달해서 위임된 연산을 수신자를 참조하게 합니다.
위임의 가장 중요한 장점 - 런타임에 행동의 복함을 가능하게 하고, 복합하는 방식도 변경해 준다는 것입니다.
위임의 단점 - 객체 합성을 통해 소프트웨어 설계의 유연성을 보장하는 방법과 동일하게 동적인데다가 고도로 매개변수화된 소프트웨어는 정적인 소프트웨어 구조보다 이해하기가 더 어렵다는 것입니다. 그 이유는 클래스에 상호작용이 다 정의되어 있는 것이 아니라 런타임 객체에 따라서 그 결과가 다르기 때문입니다.
위임을 사용하는 기회비용 - 위임이 만들어 내는 복잡함보다 단순화의 효과를 더 크게 할 수 있다면 그 설계는 사용하기 좋은 설계입니다. 그러나 이러한 유용성은 상황에 따라 다르고 얼마나 많은 경험을 갖고 있는가에 좌우되므로 위임은 고도로 표준화된 패턴에서 사용하는 것이 최상입니다.
위임이 사용되는 패턴 - 상태, 전략, 비지터 패턴에서 위임 방식을 사용합니다.
상태 패턴 - 객체는 현재 상태를 표현하는 상태 객체에 요청의 처리를 위임합니다.
전략 패턴 - 객체는 요청을 수행하는 추상화한 전략 객체에게 특정 요청을 위임합니다.
두 패턴의 목적은 처리를 전달하는 객체를 변경하지 않고 객체의 행동을 변경할 수 있게 하자는 것입니다.
비지터 패턴 - 객체 구조의 각 요소에 수행하는 연산은 언제나 비지터 객체에게 위임된 연산입니다
위임에 전적으로 의존하는 패턴들 - 중재자, 책임 연쇄, 브릿지 패턴
중재자 패턴 - 객체 간의 교류를 중재하는 객체를 도입하여 중재자 객체가 다른 객체로 연산을 전달하도록 구현합니다. 연산에 자신에 대한 참조자를 함께 보내고 위임받은 객체가 다시 자신에게 메시지를 보내서 자신이 정의한 데이터를 얻어가게 함으로써 진정한 위임을 구현합니다.
책임 연쇄 패턴 - 한 객체에서 다른 객체로 고리를 따라서 요청의 처리를 계속 위임합니다. 이 요청에는 요청을 처음 받은 원본 객체에 대한 참조자를 포함합니다.
브릿지 패턴 - 구현과 추상적 개념을 분리하는 패턴입니다. 추상화와 특정 구현을 대응시키고 추상화는 단순히 자신의 연산을 구현에 전달합니다.
상속 대 매개변수화된 타입
기능의 재사용에 이용할 수 있는 다른 방법이 매개변수화된 타입(parameterized type)입니다. Ada와 Eiffel에서는 제네릭(generic)이라고 하며, C++에서는 템플릿(template)이라고 합니다. 이 기법은 타입을 정의할 때 타입이 사용하는 다른 모든 타입을 다 지정하지 않은 채 정의합니다. 미리 정의하지 않는 타입은 매개변수로 제공합니다.
객체지향 시스템에서 행동을 복합할 수 있는 방법
클래스 상속
객체 합성
매개변수화된 타입
원소들을 비교하기 위한 정렬 루틴을 설계하는 세 가지 방법을 비교해 봅시다
서브클래스에 의해 연산을 구현하는 방법(템플릿 메서드 패턴의 응용) : 상속
정렬 루팀으로 전달된 객체(전략) : 합성
C++ 템플릿으로 정의한 클래스의 인자로 원소를 비교할 함수 이름을 명시 : 매개변수화
이 세가지 기법에는 중요한 차이가 있습니다
객체 합성 - 런타임에 행동을 변경할 수 있지만, 행동이 위임되기 때문에 비효율적일 수 있습니다.
상속 - 연산에 대한 기본 행동을 부모 클래스가 제공하고 이를 서브클래스에서 재정의하도록 하는 것 입니다.
매개변수화된 타입 - 클래스가 사용하는 타입을 변경하게 하는 것입니다.
상속도 매개변수화된 타입이라고 볼 수 있지만, 런타임에 변경이 일어나지는 않습니다. 어떤 방법이 최적의 방법인가 하는 것은 설계와 구현 제약 사항에 따라 달라질수 있습니다.
이 책에서 설명하는 패턴은 매개변수화된 타입은 사용하지 않습니다[^modern] [^modern] : 포인트 : Modern C++ Design - 템플릿을 사용해서 일부 패턴을 구현했습니다. 시간이 허락한다면 정리를...
5.6) 런타임 및 컴파일의 구조를 관계 짓기
객체지향 프로그램의 실행 구조는 종종 코드 구조와 일치하지 않습니다. 코드 구조는 컴파일 시점에 확정되는 것이고 이 구조에는 고정된 상속 클래스 관계들을 포함합니다. 그러나 프로그램의 런타임 구조는 교류하는 객체들에 따라서 달라질 수 있습니다.
객체 관계 중에는 집합(aggregation)과 인지(acquaintance)라는 것이 있습니다. 이들이 어떤 차이점을 가지고 있으며, 실제로 컴파일 타임과 런타임에 이 차이점이 드러나는지를 알아보도록 합시다.
집합 - 한 객체가 다른 객체를 소유하거나 그것에 책임을 진다는 뜻입니다. 보통 우리는 한 객체가 다른 객체를 포함(having)한다거나 다른 객체의 부분(part of)이라고 말하죠. 객체 통합에는 통합된 객체 및 그 객체를 소유한 객체의 생존주기가 똑같다는 의미도 들어 있습니다.
객체 인지 - 한 객체가 다른 객체에 대해 알고 있음(knows of)을 의미합니다. 이를 연관(association)관계 또는 사용(using)관계라고도 합니다. 인지를 받는 객체는 서로의 연산을 요청할 수도 있지만 서로에 대해 책임은 지지않습니다. 인지 관계는 통합 관계보다 관련성이 약해서 객체들 사이의 결합도가 약합니다.
집합 관계와 인지 관계가 구분하기 어려움 - 두 관계를 구현하는 방법이 구현상으로 동일할 때가 잦기 때문입니다. C++에서는 멤버 변수를 다른 객체의 인스턴스로 정의하여 집합 관계를 구현합니다. 그러나 집합 관계를 표현하는 더 일반적인 방법은 다른 인스턴스를 가리키는 포인터를 정의하는 것입니다. 인지 관계 역시 포인터로 구현합니다
class A{
B* b; //일반적인 집합 관계
C* c; //일반적인 인지 관계
};
인지 관계와 집합 관계의 구분 - 언어의 처리 방식이 아닌 사용목적에 따라 결정해야 합니다. 이러한 차이를 컴파일 시점에 발견하기는 힘들지만 중요한 의미를 갖습니다.
집합 관계 - 인지 관계보다는 강력한 영속성의 개념을 갖습니다. 즉, 자전거에 바퀴가 있어야 한다는 것입니다.
인지 관계 - 집합 관계보다는 자주 바뀌게 됩니다. 즉, 사람과 회사 관계는 근무한다는 관련성이 있을 수도 있고 없어질 수도 있습니다. 인지 관계가 더 동적이라는 의미 입니다
많은 디자인 패턴이 컴파일 시점과 런타임 구조를 명시적으로 구분하고 있습니다
컴포짓 패턴, 테커레이터 패턴 - 복잡한 실행 구조를 구축하는 데 유용한 패턴
옵저버 패턴 - 런타임 구조는 이 패턴을 잘 알고 있는 않는 한 이해하기가 종종 까다롭움
책임 연쇄 패턴 - 상속이 드러나지 않는 교류 패턴을 만들어 냄
일반적으로 런타임 구조는 패턴을 잘 이해할 때까지는 코드만 보고는 명확하게 이해할 수 없습니다.
5.7) 변화에 대비한 설계
재사용을 최대화하기 위해서는 새로운 요구사항과 기존 요구사항에 발생한 변경을 예즉하여 앞으로의 시스템 설계가 진화할 수 있도록 해야 합니다
변화에 잘 대응하기 위한 소프트웨어를 설계하기 위해서는 소프트웨어를 운영하는 동안 앞으로 일어날 변화를 어떻게 수용할 것인가를 미리 고려해야 합니다. 변화를 수용하지 못하는 설계는 앞으로 재설계가 필요하게 됩니다. 이런 변경들은 클래스의 재설계와 재구현, 사용자의 수정, 새로운 테스팅을 유발하는데, 재설계의 영향은 소프트웨어의 여러 부분에서 나타날 수 있으며 예측하지 못한 변경에 대해서는 엄청나게 비싼 대가를 지불할 수 밖에 없습니다.
디자인 패턴은 어떤 구체적인 원인으로 앞으로 시스템을 변경해야 한다는 것을 미리 보여줌으로써 이런 위험을 줄여줍니다. 디자인 패턴은 다른 부분에 독립적으로 시스템 구조를 변경할 수 있게 하여, 시스템이 어떤 특정 변화에 순응할 수 있도록 합니다.
1. 특정 클래스에서 객체 생성. 객체를 생성할 때 클래스 이름을 명시하면 어떤 특정 인터페이스가 아닌 어떤 특정 구현에 종속됩니다. 이런 종속은 앞으로의 변화를 수용하지 못합니다. 이를 방지하려면 객체를 직접 생성해서는 안됩니다. 디자인 패턴 - 추상 팩토리, 팩토리 메서드, 프로토타입
2. 특정 연산에 대한 의존성. 특정한 연산을 사용하면, 요청을 만족하는 한 가지 방법에만 매이게 됩니다. 요청의 처리 방법을 직접 코딩하는 방식을 피하면, 컴파일 시점과 런타임 모두를 만족하면서 요청 처리 방법을 쉽게 변경할 수 있습니다. 디자인 패턴 - 책임 연쇄, 커맨드
3. 하드우어와 소프트웨어 플랫폼에 대한 의존성. 기존에 존재하는 시스템 인터페이스와 응용프로그램 프로그래밍 인터페이스는 소프트웨어 및 하드웨어 플랫폼마다 모두 다릅니다. 특정 플랫폼에 종속된 소프트웨어는 다른 플랫폼에 이식하기도 어렵고요. 또한 본래의 플랫폼에서도 버전의 변경을 따라가기 어려울수도 있습니다. 이런 플랫폼 종속성을 제거하는 것은 시스템 설계에 있어 매우 중요합니다. 디자인 패턴 - 추상 팩토리, 브릿지
4. 객체의 표현이나 구현에 대한 의존성. 사용자가 객체의 표현 방법, 저장 방법, 구현 방법, 존재의 위치에 대한 모든 방법을 알고 있다면 객체를 변경할 때 사용자도 함께 변경해야 합니다. 이런 정보를 사용자에게 감춤으로써 변화의 파급을 막을 수 있습니다. 디자인 패턴 - 추상 팩토리, 브릿지, 메멘토, 프록시
5. 알고리즘 의존성. 알고리즘 자체를 확장할 수도, 최적화할 수도, 다른 것으로 대체할 수도 있는데, 알고리즘에 종속된 객체라면 알고리즘이 변할 때마다 객체도 변경해야 합니다. 그러므로 변경이 가능한 알고리즘은 분리해 내는 것이 바람직합니다. 디자인 패턴 - 빌더, 이터레이터, 전략, 템플릿 메서드, 비지터
6. 높은 결합도. 높은 결합도를 갖는 클래스들은 독립적으로 재사용하기 어렵습니다. 높은 결합도를 갖게 되면 하나의 커다란 시스템이 되어 버립니다. 이렇게 되면 클래스 하나를 수정하기 위해서 전체를 이해해야 하고 다른 많은 클래스도 변경해야 합니다. 또한 시스템은 배우기도 힘들고, 이식은 커녕 유지 보수하기도 어려운 공룡이 되어 버립니다. 약한 결합도는 클래스 자체의 재사용을 가능하게 하고 시스템의 이해와 수정, 확장이 용이해서 이식성을 증대시킵니다. 추상 클래스 수준에서 결합도를 정의한다거나 계층화시키는 방법으로 디자인 패턴은 낮은 결합도의 시스템을 만들도록 합니다. 디자인 패턴 - 추상 팩토리,브릿지, 책임 연쇄, 커맨드, 중재자, 옵저버
7. 서브클래싱을 통한 기능 확장. 서브클래싱으로 객체를 재정의하는 것은 쉬운일이 아닙니다. 새로운 클래스마다 매번 반드시 해야 하는 초기화, 소멸 등에 대한 구현 오버헤드를 늘 지게 됩니다. 서브클래스를 정의하려면, 최상위 클래스부터 자신의 직속 부모 클래스까지 모든 것을 이해하고 있어야 합니다. 일반적으로 객체 합성과 위임은 행동 조합을 위한 상속보다 훨씬 유연한 방법입니다. 기존 객체들을 새로운 방식으로 조합함으로써 새로운 서브클래스를 정의하지 않고도 응용프로그램에 새로운 기능성을 추가할 수 있습니다. 객체 합성을 많이 사용한 시슽ㅁ은 이해하기가 어려워집니다. 많은 디자인 패턴에서는 그냥 서브클래스를 정의하고 다른 인스턴스와 새로 정의한 클래스의 인스턴스를 합성해서 기능을 재정의하는 방법을 도입합니다. 디자인 패턴 - 브릿지, 책임 연쇄, 테커레이터, 옵저버, 전략
8. 클래스 변경이 편하지 못한 점. 가끔 클래스를 변경하는 작업이 그렇게 단순하지 않을 때가 많습니다. 소스 코드가 필요한데 없다고 가정해 봅시다. 또한 어떤 변경을 하면 기존 서브클래스의 다수를 수정해야 한다고 가정합시다. 디자인패턴은 이런 환경에서 클래스를 수정하는 방법을 제시합니다. 디자인 패턴 - 어댑터, 테커레이터, 비지터
6) 디자인 패턴을 고르는 방법
어떤 특정 문제에 어떤 패턴을 써야 할지 판단하는 것은 참 어려운 일입니다. 특히 이런 카탈로그가 생소한 개발자 라면 더욱 어려울 것입니다. 지금부터 문제에 적합한 디자인 패턴을 찾아내는 여러가지 접근 방법에 대해 살펴봅시다.
패턴이 어떻게 문제를 해결하는 지 파악합시다.
패턴의 의도 부분을 봅시다
패턴들 간의 관련성을 파악합시다.
비슷한 목적의 패턴들을 모아서 공부합시다
재설계의 원인을 파악합시다.
설계에서 가변성을 가져야 하는 부분이 무엇인지 파악합시다.
[2] 생성 패턴
생성 패턴(creational pattern)은 인스턴스를 만드는 절차를 추상화하는 패턴입니다 이 범주에 해당하는 패턴은 객체를 생성 합성하는 방법이나 객체의 표현 방법과 (소프트웨어) 시스템을 분리해 줍니다.
클래스 생성 패턴 - 인스턴스로 만들 클래스를 다양하게 만들기 위한 용도로 상속을 사용함
객체 생성 패턴 - 인스턴스화 작업을 다른 객체에게 떠넘김
생성 패턴의 특징
생성 패턴은 시스템이 어떤 구체 클래스를 사용하는 지에 대한 정보를 캡슐화합니다.
생성 패턴은 이들 클래스의 인스턴스들이 어떻게 만들고 어떻게 서로 맞붙는지에 대한 부분을 완전히 숨깁니다.
생성 패턴을 이용하면 무엇이 생성되고 누가 이것을 생성하며, 이것이 어떻게 생성되는지, 언제 생성할 것인지 결정하는 데 유연성을 확보할 수 있게 됩니다.
생성 패턴 중에서 선택의 어려움
생성 패턴으로 분류되는 패턴은 여러 개인데, 이런 여러 생성 패턴들은 서로 보완적일 수도 있고 선택되기 위해 서로 경쟁적일 수도 있습니다. 즉, 동일한 문제 해결을 위해서 어떤 생성 패턴을 사용해야 할지 결정을 내리기 어렵습니다.
인스턴스를 생성하고 복합하는 방법에 해당하는 부분과 이들 인스턴스를 사용하는 프로그램을 분리하고자 할 때 어떤 패턴을 적용해야 하는지 판단하기는 어렵습니다.
프로토타입 패턴과 추상 팩토리 패턴 중 무엇을 선택할 지 고민해야 할 때가 있습니다.
빌더 패턴은 어떤 구성요소를 만들지 구현하는 데에 다른 생성 패턴 중 하나를 사용할 수 있습니다.
원형 패턴은 자기 자신의 구현을 위해 컴포짓 패턴을 사용하기도 합니다.
생성 패턴을 설명하기 위한 예제
생성 패턴 간에는 매우 밀접한 관련성이 있기 때문에, 이들 간의 공통점과 차이점을 중심으로 다섯 개의 패턴을 공부하기로 하겠습니다. 그리고 하나의 예제를 통해 각 패턴의 설명을 진행할까 합니다. 컴퓨터 게임에 넣을 미로를 만드는 문제입니다. 패턴에 따라 미로와 게임이 어떻게 조금씩 다르게 만들어지는지 잘 보시기 바랍니다. 주어진 예제에서 중요한 사항은 미로를 어떻게 만들것인가 입니다. 미로는 방들의 집합이고, 각 방은 옆에 무엇이 있는지 알고 있는데, 방 옆에 있는 것이 방일 수도 있고, 문일 수도 있고, 벽일 수도 있는 것입니다.
클래스 Room, Door, Wall은 우리가 개발하는 미로를 만드는 데 필요한 구성요소입니다.
각 방은 네개의 방향을 갖는데, C++ 구현 시에는 열거형 타입으로 정의한 Direction을 사용하여 각 방향을 다음과 같이 선언합니다
enum Direction { North, South, East, West };
MapSite 클래스 설명
MapSite 클래스는 미로의 구성요소들에 필요한 모든 연산을 정의한 공통 추상 클래스 입니다. 예를 단순하게 하기 위해서 MapSite는 Enter() 연산 하나만을 정의하도록 합니다. Enter() 메서드는 무엇에 들어가느냐에 따라 그 의미가 달라질 것입니다. 즉, 방에들어간다면 위치가 바뀌도록 구현해야 할 것입니다. 들어가는 것이 문이라면 이렇게 구현합니다. 문이 열려있으면 문을 통해 다른 방으로 들어가도록 구현하고, 문이 닫혀있다면 상처를 입도록 구현하면 됩니다.
class MapSite {
public:
virtual void Enter() = 0; //구현부를 갖지 않는 순수 가상 메서드
}
MapSite에 정의된 Enter()는 좀더 섬세한 게임 동작을 만드는 데 쓸 수 있는 기본 연산입니다. 만약, 방에 있을 때 "동쪽으로 가시오"라고 구현하고 싶으면, 게임은 어떤 MapSite가 동쪽에 인접한 것인지 판단한 후 그 대상에 정의된 Enter()를 호출하도록 메서드를 구현합니다. MapSite의 서브클래스가 어떤 것이냐에 따라 Enter()는 위치를 변경하도록 구현할 수도 있고, 이동을 못하고 상처를 입도록 구현할 수도 있습니다.
Room 클래스 설명
Room 클래스는 MapSite를 상속받은 구체적인 클래스로 미로에 있는 다른 요소와 관련성을 갖도록 정의합니다. 다른 요소들 어느 것과도 관련성을 가질 수 있으므로 Room은 다른 요소들의 부모 클래스인 MapSite와 연결 관계를 갖는 것으로 모델링 합니다. Room 클래스는 방 번호를 저장하는데, 이 번호로 미로에 있는 방을 식별할수 있습니다.
class Room : public MapSite {
public:
Room(int RoomNo);
MapSite* GetSide(Direction) const;
void SetSide(Direction, MapSite*);
virtual void Enter();
private:
MapSite* _sides[4]; //방은 네 개의 방향을 갖고 있고 각 방향에는
//MapSite의 서브클래스 인스턴스가 올 수 있습니다
int _roomNumber;
Wall와 Door 클래스
class Wall : public MapSite {
public:
Wall();
virtual void Enter();
};
class Door : public MapSite {
public:
Door( Room* = 0, Room* = 0 );
//문을 초기화하기 위해서는 문이 어느 방 사이에 있는지 알아야 합니다
virtual void Enter();
Room* OtherSideFrom(Room*);
private:
Room* _room1;
Room* _room2;
bool _isOpen;
};
미로를 복하는 요소에 대해서는 좀더 살펴봐야 합니다. 또한 방들의 집합을 표현하기 위해 클래스 Maze는 RoomNo()연산의 힘을 빌어서 방 번호가 주어진 특정 방을 찾을 수도 있습니다.
class Maze {
public:
Maze();
void AddRoom(Room*);
Room* RoomNo(int) const;
private:
// ...
};
RoomNo() 연산은 선형 탐색이나 해시 테이블 또는 단순 배열을 통해서 구현할 수 있습니다. 이 책에서 이런 부분은 별로 중요하지 않으므로 다루지 않을 것이고, 그 대신 미로 객체의 구성요소를 어떻게 설정하느냐에 집중하려고 합니다.
단순한 미로 생성
다른 필요한 클래스로는 MazeGame이 있습니다. 실제로 미로를 생성하는 클래스입니다. 미로를 생성하는 가장 간단하고 일반적인 방법은 일련의 연산을 통해 빈 미로에 미로의 구성요소를 추가하고 이들을 연결하는 것입니다.
예를 들어 멤버함수 CreateMaze()는 방 사이에 문이 있는 두 개의 방으로 구성된 미로를 만듭니다.
Maze* MazeGame::CreateMaze() {
Maze* aMaze = new Maze;
Romm* r1 = new Room(1);
Romm* r2 = new Room(2);
Door* theDoor = Door(r1,r2);
aMaze->AddRoom(r1);
aMaze->AddRoom(r2);
r1->SetSide(North, new Wall);
r1->SetSide(East, theDoor);
r1->SetSide(South, new Wall);
r1->SetSide(West, new Wall);
r2->SetSide(North, new Wall);
r2->SetSide(East, new Wall);
r2->SetSide(South, new Wall);
r2->SetSide(West, theDoor);
return aMaze;
}
CtreateMaze() 코드의 문제는 코드의 유연성이 떨어진다는 것입니다 생성패턴은 이런 상황에서 어떻게 유연한 설계를 할 수 있는 지에 대한 해법을 제공합니다. 특히 미로의 구성요소를 정의하는 클래스를 쉽게 변경할 수 있는 방법을 제공합니다.
기존 미로가 갖고 있는 레이아웃을 재사용하면서 마법 주문이 걸린 미로가 있는 게임을 만들고 싶다면 가정해 봅시다. 이 마법의 미로 게임을 만들기 위해서는 단어를 맞추면 문이 열리는 DoorNeedingSpell 이라든지, 마법 키나 단어 등 특별한 항목을 포함하는 EnchantedRoom과 같은 새로운 구성요소가 필요합니다.
지금 시점에서 가장 큰 장애 요인은 클래스들의 인스턴스를 직접 생성하도록 하드코딩한다는 점입니다. 생성 패턴은 이런 어려움을 이길 수 있는 여러가지 방법을 제공합니다.
CreateMaze가 방, 벽, 문을 생성하기 위해서 생성자를 이용하지 않고 가상 함수를 호출하도록 구현되어 있다면, 이 가상 함수의 실제 구현을 다양한 방법으로 변경할 수 있을 것입니다. 이 방법은 팩토리 메서드 패턴의 한 예입니다.
CreateMaze가 방, 벽, 문을 생성하기 위해 생성 방법을 알고 있는 객체를 매개변수로 넘겨받을 수 있다면, 생성 방법이 바뀔 때마다 새로운 매개 변수를 넘겨받음으로써 생성할 객체의 유형을 달리할 수 있습니다. 이는 추상 팩토리 패턴의 예입니다.
CreateMaze가 생성하고자 하는 미로에 방, 문, 벽을 추가하는 연산을 사용해서 새로운 미로를 만들 수 있는 객체를 넘겨받는다면 미로를 만드는 방법이나 변경을 이 객체의 상속을 통해서 해결할 수 있습니다. 이는 빌더 패턴의 예입니다.
CreateMaze를 이미 만든 다양한 방, 문, 벽 객체로 매개변수화하는 방법도 가능한데, 이미 만든 객체를 복사해서 미로에 추가하면, 이들 인스턴스를 교체하여 미로의 복합 방법을 변경할 수 있습니다. 이는 프로토타입 패턴의 예입니다.
다섯 개 생성 패턴 중에서 위에 쓰지 않은 싱글턴 패턴이란 것이 있습니다. 이 패턴은 한 게임에 오로지 하나의 미로 객체만 존재할 수 있고 그 게임에서 돌아가는 모든 게임 객체들이 이 미로에 접근이 가능하도록 보장합니다. 전역 변수나 전역 함수에 의존할 필요 없이 이런 일이 가능합니다. 또한 싱글턴 패턴은 기존 코드를 건드리지 않고도 미로를 쉽게 대체하거나 확장할 수 있도록 해 줍니다.
1) Abstract Factory - 추상 팩토리
객체 생성(Object Creational)
의도
상세화된 서브클래스를 정의하지 않고도 서로 관련성이 있거나 독립적인 여러 객체의 군을 생성하기 위한 인터페이스를 제공합니다
다른 이름
키트(Kit)
동기
모티프와 프레젠테이션 메니저와 같은 사용자 인터페이스 툴킷을 살펴보면, 서로 다른 룩앤필 표준을 가지고 있습니다. 서로 다른 룩앤필은 서로 다른 사용자 인터페이스의 표현 방식과 행동을 갖습니다. 스크롤바, 윈도우 버튼은 모양이 다르고 동작방식도 서로 다릅니다. 개발한 응용프로그램이 서로 다른 룩앤필 표준에 상관없이 이식성을 가지려면, 응용프로그램이 각 사용자 인터페이스 툴킷에서 제공하는 위젯을 직접 사용하지 못하도록 해야 합니다.
이런 문제는 추상 클래스인 WidgetFactory를 정의하여 해결하는 게 좋습니다. WidgetFactory 클래스는 위젯의 기본 유저 인터페이스 요소(윈도우,스크롤바,버튼 등)를 생성할 수 있는 인터페이스를 정의합니다. 실제적으로 구현 종속적인 인스턴스를 생성하기 위해서는 팩토리와 구분하여 각각의 위젯별로 추상화된 클래스를 정의해야 하고 이를 상속하는 구체적인 서브클래스를 정의하여 구체적 룩앤필 표준에 대한 구현을 제공합니다
이 패턴을 사용하기 위해서는 AbsractFactory에 해당하는 WidgetFactory뿐만 아니라 각 룩앤필 표준에 대한 WidgetFactory를 상속받는 구체 서브 클래스들을 정의해야 합니다
활용성
추상 팩토리는 다음의 경우에 사용합니다
객체가 생성되거나 구성, 표현되는 상식과 무관하게 시스템을 독립적으로 만들고자 할 때
여러 제품군 중 하나를 선택해서 시스템을 설정해야 하고 한번 구성한 제품을 다른것으로 대체할 수 있을 때
관련된 제품 객체들이 함께 사용되도록 설계되었고, 이부분에 대한 제약이 외부에도 지켜지도록 하고 싶을 때
제뭎에 대한 클래스 라이브러리를 제공하고, 그들의 구현이 아닌 인터페이스를 노출시키고 싶은 때
구조
그림삽입 예정
참여자
AbstractFactory: 개념적 제품에 대한 객체를 생성하는 연산으로 인터페이스를 정의합니다
ConcreteFactory: 구체적인 제품에 대한 객체를 생성하는 연산을 구현합니다
AbstractProduct: 개념적 제품 객체에 대한 인터페이스를 정의합니다
ConcreteProduct: 구체적으로 팩토리가 생성할 객체를 정의하고 AbstractProduct가 정의하는 인터페이스를 구현합니다
Client : AbstractFactory와 AbstractProduct 클래스에 선언된 인터페이스를 사용합니다
협력방법
일반적으로 ConcreteFactory클래스의 인스턴스 한개가 런타임에 만들어집니다. 이 구체 팩토리(Concrete Factory)는 어떤 특정 구현을 갖는 제품 객체를 생성합니다 서로 다른 제품 객체를 생성하려면 사용자는 서로 다른 구체 팩토리를 사용해야 합니다
AbstractFactory는 필요한 제품 객체를 생성하는 책임을 ConcreteFactory 서브클래스에게 위임합니다
결과
추상 팩토리 패턴을 쓰면서 얻는 이익과 부담은 다음과 같습니다
구체적인 클래스를 분리합니다 추상 팩토리 패턴을 쓰면 응용프로그램이 생성할 객체의 클래스를 제어할 수 있습니다 팩토리는 제품 객체를 생성하는 과정과 책임을 캡슐화한 것이기 때문에 구체적인 구현 클래스가 사용자에게서 분리됩니다. 일반 프로그램은 추상 인터페이스를 통해서만 인스턴스를 조작합니다. 제품 클래스 이름이 구체 팩토리의 구현에서 분리 되므로 사용자 코드에는 나타나지 않는 것입니다
제품군을 쉽게 대체할 수 있도록 합니다 구체 팩토리의 클래스는 응용 프로그램에서 한 번만 나타나기 때문에 응용프로그램이 사용할 구체 팩토리를 변경하기는 쉽습니다. 또한 구체 팩토리를 변경함으로써 응용프로그램은 서로 다른 제품을 사용할 수 있게 됩니다.추상 팩토리는 필요한 모든 것을 생성하기 때문에 전체 제품군은 한번에 변경이 가능합니다
제품 사이에 일관성을 증진시킵니다 하나의 군 안에 속한 제품 객체들이 함께 동작하도록 설계되어 있을 때 응용프로그램은 한번에 오직 한 군에서 만든 객체를 사용하도록 함으로써 프로그램의 일관성을 갖도록 해야 합니다 추상 팩토리를 쓰면 이 점을 아주 쉽게 보장할 수 있습니다
새로운 종류의 제품을 제공하기 어렵습니다 새로운 종류의 제품을 만들기 위해 기존 추상 팩토리를 확장하기가 쉽지 않습니다. 생성하는 제품은 추상 팩토리가 생성할 수 있는 제품 집합에만 고정되어 있기 때문입니다
관련 패턴
AbstractFactory 클래스는 팩토리 메서드 패턴을 이용해서 구현되는데, Prototype 패턴을 이용할 때도 있습니다 구체 팩토리는 Singleton 패턴을 이용해 구현하는 경우가 많습니다
2) Builder - 빌더
Builder - 빌더
2. 리펙터링
Refactoring / 대청 / 마틴 파울러 저, 윤성준 조재박 역
패턴을 활용한 리펙터링 / 인사이트 / 조슈아 케리에브스키 저, 윤성준, 조상민 역
Java 언어를 중심으로 리펙토링을 이야기 합니다
(저는 C++만 하기에 개인적으로 아쉽습니다)
리팩토링이란 무엇인가?
리팩토링은 외부 동작을 바꾸지 않으면서 내부 구조를 개선하는 방법으로 소프트웨어 시스템을
변경하는 프로세스이다. 이것은 버그가 끼어 들 가능성을 최소화 하면서 코드를 정리하는 정형화된 방법이다.
본질적으로 우리가 리팩토링을 할 때, 우리는 코드가 작성된 후에 코드의 디자인을 개선하는 것이다.
검색어를 입력하세요.
코드, 패턴 그리고 소프트웨어
0. 시작하기 전에
-1. 업데이트 히스토리
1. 디자인 패턴
[1] 서론
1) 디자인패턴이란?
2) 디자인패턴 기술하기
3) 디자인 패턴 카탈로그
4) 카탈로그 조직화 하기
5) 디자인 패턴을 이용하여 문제를 푸는 방법
5.1) 적당한 객체 찾기
5.2) 객체의 크기 결정
5.3) 객체 인터페이스의 명세
5.4) 객체 구현 명세 하기
5.5) 재사용을 실현 가능한 것으로
5.6) 런타임 및 컴파일의 구조를 관계 짓기
5.7) 변화에 대비한 설계
6) 디자인 패턴을 고르는 방법
[2] 생성 패턴
1) Abstract Factory - 추상 팩토리
2) Builder - 빌더
3) Factory Method - 팩토리 메서드
4) Prototype - 프로토타입
5) Singleton - 싱글톤
6) 생성패턴의 논의
[3] 구조 패턴
1) Adapter - 어댑터
2) Bridge - 브릿지
3) Composite - 컴포짓
4) Decorator - 데커레이터
5) Facade - 퍼사드
6) Flyweight - 플라이웨이
7) Proxy - 프록시
8) 구조 패턴에 대한 논의
[4] 행동 패턴
01) Chain of Responsibility - 책임 연쇄
02) Command - 커맨드
03) Interpreter - 인터프리터
04) Iterator - 이터레이터
05) Mediator - 중재자
06) Memento - 메멘토
07) Observer - 옵저버
08) State - 상태
09) Strategy - 전략
10) Template Method - 템플릿 메서드
11) Visitor - 비지터
12) 행동 패턴에 대한 논의
2. 리펙터링
[1] 리펙터링, 첫 번째 예제
[2] 리팩토링의 원리
[3] 코드속 나쁜 냄새
[4] 테스트 만들기
[5] 리펙터링 카탈로그
[6] 메소드 정리(Composing Method)
1) Extract Method
2) Inline Method
3) Inline Temp
4) Replace Temp with Query
5) Introduce Explaining Variable
6) Split Temporary Variable
7) Remove Assignments to Parameters
8) Replace Method with Method Object
9) Substitue Algorithm
[7] 객체간의 기능 이동
1) Move Method
2) Move Field
3) Extract Class
4) Inline Class
5) Hide Delegate
6) Remove Middle Man
7) Introduce Foreign Method
8) Introduce Local Extension
[8] 데이터 구성(Organizing Data)
[9] 조건문의 단순화
1) Decompose Conditional
2) Consolidate Conditional Expression
3) Consolidate Duplicate Conditional Fragments
4) Remove Control Flag
5) Replace Nested Conditional with Guard Clauses
6) Replace Conditional with Polymorphim
7) Introduce Null Object
8) Introduce Assertion
3. 패턴을 활용한 리펙토링
[1] 이 책을 쓴 이유
[2] 리팩터링
[3] 패턴
4. 클린 코드
[1] 클린코드
[2] 의미 있는 이름
[3] 함수
Published with WikiDocs
코드, 패턴 그리고 소프트웨어 2. 리펙터링 [1] 리펙터링, 첫 번째 예제 WikiDocs
[1] 리펙터링, 첫 번째 예제
리펙터링을 어떻게 하는지 예제를 보여주는 부분 입니다
분량도 많고 어떻게 정리할 지 고민이 되는 부분이라 추후에 정리할 예정입니다
[2] 리팩토링의 원리
리팩토링의 정의
리랙토링(Refactoring) - 소프트웨어를 보다 쉽게 이해할 수 있고, 적은 비용으로 수정할 수 있도록
겉으로 보이는 동작의 변화 없이 내부 구조를 변경하는 것
리랙토링 하다(Refactoring) - 일련의 리펙토링을 적용하여 겉으로 보이는 동작의 변화 없이 소프트웨어 구조를 바꾸다.
첫째 리팩토링의 목적은 소프트웨어를 보다 이해하기 쉽고, 수정하기 쉽도록 만드는 것이다
둘째 리팩토링은 겉으로 보이는 소프트웨어의 기능을 변경하지 않는다는 것이다
두 개의 모자
두가지 구별된 작업(기능 추가와 리팩토링)을 위해 시간을 나눠야 한다
기능을 추가할 때는 기존 코드를 건드려서는 안되고 단지 새로운 기능만 추가해야 한다
리팩토링을 할 때는 기능을 추가해서는 안되고 단지 코드의 구조에만 신경써야 한다.
왜 리팩토링을 해야 하는가?
리팩토링은 소프트웨어의 디자인을 개선 시킨다.
리팩토링은 소프트웨어를 더 이해하기 쉽게 만든다.
리팩토링은 버그를 찾도록 도와준다.
리팩토링은 프로그램을 빨리 작성하도록 도와준다.
언제 리팩토링을 하는가?
삼진규칙 - 3번 중복되면 그때 리팩토링을 한다.
기능을 추가할 때 리팩토링을 하라
버그를 수정할 때 리팩토링을 하라
코드검토(code review)를 할 때 리팩토링을 하라
관리자에게는 뭐라 말해야 하나?
리팩토링을 할 때의 문제
데이터베이스
인터페이스 변경
리팩토링이 어려운 디자인 변경
언제 리팩토링을 하지 말아야 하는가?
리팩토링과 디자인
리팩토링과 퍼포먼스
리팩토링의 기원
[3] 코드속 나쁜 냄새
이제 리팩토링이 어떻게 돌아가는지 잘 알게 되었을 것이다 리팩토링을 언제 시작하고 언제 끝낼지를 결정하는 것은 리팩토링을 어떤 절차에 따라 해야 하는지를 아는 만큼이나 중요하다 인스턴스 변수 하나 삭제하거나 클래스 구조를 만드는 것에 대해 설명하는 것은 쉽다. 프로그램 미학(aesthetics) 같은 모호한 개념에 호소하기 보다는 좀 더 구체적인 것을 원한다.
Kent Beck을 방문했을때 리팩토링을 필요한 시점에 대한 생각을 냄새의 관점에서 설명했다. 냄새 - 정확한 지점을 집어내기 보다는 뭉뚱그려서 찾아냅니다
중복된 코드(Duplicated Code)
한 곳 이상에서 중복된 코드 구조가 나타난다면, 그것을 합쳐서 프로그램을 개선할 수 있다.
한 클래스의 서로 다른 두 메소드 안에 같은 코드가 있는 경우
Extract Method - 뽑아낸 메소드를 두 곳에서 호출하도록 하는 것
동일한 수퍼 클래스를 갖는 두 서브 클래스에서 같은 코드가 있는 경우
양쪽 클래스에서 Extract Method 사용한 다음 Pull Up Method 사용한다
코드가 비슷하기는 하지만 같지는 않다면
비슷한 부분과 서로 다른 부분을 분리하기 위해 Extract Method 사용 후 Form Template Method 사용할 수 있는지 살펴본다
같은 작업을 하지만 다른 알고리즘을 사용한다면
두 알고리즘 중 더 명확한 것을 선택한 다음 Substitute Algorithm을 사용할 수 있다.
서로 관계 없는 두 클래스에서 중복된 코드가 있는 경우
한쪽 클래스에서 Extract Class를 사용한 다음 양쪽에서 이 새로운 클래스를 사용하도록 하는 것 고려
메소드가 클래스 중 하나에 포함되어 있고 다른 클래스에서 호출
세번째 클래스에 속하는 그 메소드가 원래 두 클래스에서 참조되어야 하는 경우
긴 메소드(Long Method)
우리가 따르는 방법은 어떤것에 대해서 주석을 달아야 할 필요를 느낄 때마다 메소드를 작성하는 것이다
메소드의 길이가 아니라 메소드가 하는 일과 일을 처리하는 방법 사이의 의미적 거리(semantic distance)
대부분의 경우 메소드의 길이를 줄이가 위해 해야 하는 경우
Extract Method 사용하자
메소드에 파라미터와 임시변수가 많다면
임시변수는 Replace Temp with Query를 사용할 수 있다
긴 파라미터 리스트는 Introduce Parameter Object, Preserve Whole Object 사용할 수 있다.
여전히 임시변수와 파라미터가 많다면
Replace Method with Method Object라는 중장비를 사용하자
뽑아낼 코드 덩어리의 식별은 주석을 찾는 것이다.조건문과 루프 또한 메소드 추출이 필요하다는 신호를 준다.
조건식을 다루기 위해서는
Decompose Conditional 사용한다.
루프의 경우는 루프와 그 안의 코드를 추출하여 하나의 메소드로 만든다
거대한 클래스(Large Class)
클래스 하나가 너무 많은 일을 하려 할때는 보통 지나치게 많은 인스턴스 변수가 나타난다 클래스가 지나치게 많은 인스턴스 변수를 갖는 경우, 중복된 코드가 존재할 확률이 높다
많은 변수를 묶기 위해 Extract Class 사용할 수 있다.
클래스 내에서 서로에게 의미가 있는 변수를 골라서 묶는다.
클래스 안에서 변수들의 어떤 부분 집합에 대한 공통적인 접두사, 접미사가 있으면 클래스를 따로 뽑아 낼 수 있다.
만약 새로 만들 클래스가 서브클래스로서 의미가 있으면
Extract Subclass가 더 쉽다는 것을 알게 될 것이다.
어떤때는 클래스가 모든 인스턴스 변수를 항상 다 사용하지 않는다.
이런 경우 Extract Class, Extract Subclass를 여러번 적용할 수 있을 것이다.
많은 변수를 가진 클래스와 마찬가지로, 코드가 많은 클래스에 대한 해결책
Extract Class, Extract Subclass 이다.
클라이언트가 클래스를 어떻게 쓰게 할 것인지를 결정하고 각각의 사용 방법에 대해
Extract Interface 사용하는 것이다.
이것은 클래스를 어떻게 분해할 지에 대한 아이디어를 줄 것이다. 큰 클래스가 GUI클래스 라면 데이터와 동작을 별도의 도메인 객체로 옮길 필요가 있다. 이것은 양쪽에 약간의 중복된 데이터를 유지하게 하고, 데이터를 동기화 해 주는 것이 필요하다.
Duplicate Observed Data가 이 작업을 어떻게 할 것인지 보여준다.
긴 파라미터 리스트 (Long Parameter List)
긴 파라미터 리스트는 이해하기도 어렵고, 일관성이 없거나 사용하기 어려울 뿐만 아니라 다른 데이터가 필요할 때마다 계속 고쳐야 하기 때문에 파라미터 리스트는 짧은 것이 좋다.
객체에 요청하여 파라미터의 데이터를 얻을 수 있으면
Replace Parameter with Method 사용하기
이 객체는 필드일 수도 있고 다른 파라미터일 수도 있다. 한 객체로 부터 주워 모은 데이터 뭉치를 그 객체 자체로 바꾸기 위해 Preserve Whole Object를 사용하기
객체와 관계 없는 여러 개의 데이터 아이템이 있으면 Introduce Parameter Object를 사용하라
이것을 적용할 때 한가지 중요한 예외사항이 있다. 그것은 호출하는 객체와 큰 객체 사이의 종속성(dependency)을 만들고 싶은 않을 때이다. 이런 경우 데이터를 하나씩 풀어서 파라미터로 넘기는 것이 현명하나 고통이 따른다는 것에 주의해야 한다. 파라미터 리스트가 지나치게 길거나 자주 변경된다면, 종속성 구조(dependency structure)를 다시 한번 생각해볼 필요가 있다
확산적 변경 (Divergent Change)
소프트웨어를 변경할 때 우리는 명확한 한 곳을 집어 변경할 수 있기를 이렇게 할 수 없을 때는 밀접한 관계를 가지는 두 가지 지독한 냄새 중 하나를 맡을 수 있을 것이다. 확산적 변경은 한 클래스가 다른 이유로 인해 다른 방법으로 자주 변경되는 경우에 발생한다.
만약 어떤 클래스를 보고는 "새로운 데이터베이스를 추가할 때마다 항상 이 세 개의 메소드를 수정해야 하는 군" 또는 "새로운 어음이 있을 때마다 항상 이 네 개의 메소드를 변경해야 하는 군" 하고 말한다면하나보다는 두개의 객체로 만드는 것이 더 좋은 상황에 있는 것이다.
하나의 클래스를 수정하는 선에서 끝나야 하며, 새로운 클래스는 그 변화를 반영하는 것이어야 한다. 특정 원인에 대해 변해야 하는 것을 모두 찾은 다음, Extract Class를 사용하여 따로 하나로 묶어야 한다.
산탄총 수술 (Shotgun Surgery)
변경을 할 때마다 많은 클래스를 조금씩 수정해야 한다면 산탄총 수술의 냄새를 풍기고 있는 것이다.
Move Method와 Move Field를 사용하여 변경해야 할 부분을 모두 하나의 클래스로 몰아넣고 싶을 것이다.
기존의 클래스 중에서 메소드나 필드가 옮겨갈 적절한 후보가 없다면 새로 하나를 만들어라 Inline Class를 사용하여 모든 동작을 하나로 모을 수도 있다.
기능에 대한 욕심 (Feature Envy)
객체의 가장 중요한 요점은 데이터와 데이터를 사용하는 프로세스를 하나로 묶는 기술이다. 한가지 고전적인 냄새가 있는데 그것은 메소드가 자신이 속한 클래스보다 다른 클래스에 관심을 가지고 있는 경우이다. 가장 흔한 욕심이 데이터에 대한 욕심이다.
어떤 값을 계산하기 위해 다른 객체의 get메소드를 호출하는 경우는 수도 없이 많다
Move Method를 사용한다.
메소드의 특정 부분만 이런 욕심으로 고통 받는데 이럴 때는 욕심이 많은 부분에 대해서
Extract Method 사용한 다음 Move Method를 사용한다.
한 메소드가 여러개의 클래스의 기능을 사용하는 경우가 있는데 이럴때는 어느 클래스 메소드로 옮겨야 하는가?
경험적인 방법은 어떤 클래스에 있는 데이터를 가장 많이 사용하는 가를 보고 메소드를 그 클래스로 옮기는 것이다
물론 이런 규칙이 깨지는 몇몇 복잡한 패턴도 있다. 디자인 패턴에서 Strategy와 Visitor 가 당장 떠오른다. Kent Beck의 Self Delegation도 그 중 하나다.
확산적 변경과 싸우기 위해 이런 것들을 이용해야 한다. 가장 기본적인 규칙은 같이 변하는 것을 모으는 것이다.
데이터와 그 데이터를 이용하는 동작은 보통 같이 변하지만 예외도 있다.
이런 예외의 경우는 동작을 옮겨서 한 곳에서만 변하도록 한다. Strategy와 Visitor는 인디렉션을 사용하여 오버라이드 되어야 하는 약간의 기능을 독립시킴으로써 동작을 쉽게 변경할 수 있다.
데이터 덩어리 (Data Clump)
데이터 아이템은 아이들과 같아서 몰려다니기를 좋아한다. 함께 몰려다니는 데이터의 무리는 그들 자신의 객체로 만들어져야 한다.
첫번째 단계는 필드로 나타나는 덩어리들이 있는 곳 찾는 것이다. 이 덩어리를 객체로 바꾸기 위해 이 필드들에 대해 Extract Class를 사용한다.
그런 다음 관심을 메소드 시그니처로 돌려 Introduce Parameter Object나 Preserve Whole Object를 사용하여 파라미터 리스트를 단순하게 한다.
기본 타입에 대한 강박관념 (Primitive Obsession)
레코드 타입은 데이터를 구조화 하여 의미 있는 그룹으로 만들수 있게 한다. 레코드는 항상 어느 정도의 오버헤드를 가지고 있다. 레코드는 데이터베이스의 테이블을 의미할수도 있고, 한두가지를 위해 생성하려 할때는 좀 거북할 수도 있다. 객체의 유용한 점 중의 하나는 이런 기본 타입과 레코드 타입의 경계를 흐리게 하거나 아예 없애버린다는 것이다. 우리는 언어의 기본 타입과 구별할 수 없는 작은 클래스를 쉽게 작성할 수 있다.
자바는 수(number)에 대한 기본 타입은 있지만 다른 환경에서는 기본 타입인 문자열(String)과 날짜(Date) 클래스이다.
객체를 처음 접하는 사람은 보통 수와 화폐 단위를 묶는 Money클래스나 상한이나 하한을 가지는 Range 클래스, 전화번호나 우편번호 같은 특별한 문자열을 위한 클래스와 같이 작은 작업을 위해 작은 객체를 사용하는 것을 꺼린다.
각각의 데이터 값에 대해 Replace Data Value with Object를 사용한다.
데이터 값이 타입 코드이고 값이 동작에 영향을 미치지 않는다면
Replace Type Code with Class 사용한다. 또는 Replace Type Code with State/Strategy를 이용하라
항상 몰려다녀야 할 필드 그룹이 있다면 Extract Class를 사용한다.
파라미터 리스트에서 이런 기본 타입을 보면 Introduce Parameter Object를 사용한다.
배열을 쪼개서 쓰고 있는 자신을 발견하거든 Replace Array with Object를 사용한다.
Switch 문 (Switch Statements)
객체지향 코드의 가장 명확한 특징 중 하나는 switch문이 비교적 적제 쓰인다는 것이다. switch문의 문제는 본질적으로 중복이 된다는 것이다. 객체지향 개념중 다형성(polymorphism)이 이런 문제를 다루는 훌륭한 방법을 제공한다. switch문을 볼 때면 항상 다형성을 생각해야 한다
Extract Method 를 사용해여 switch문을 뽑아내고 Move Method를 사용하여 다형성이 필요한 클래스로 옮긴다
이 시점에서 Replace Type Code with Subclasses를 사용할 것인지 Replace Type Code with State/Strategy를 사용할 것인지 결정한다.
상속구조를 결정했으면 Replace Conditional with Polymorphism을 사용할 수 있다.
만약 하나의 메소드에만 영향을 미치는 몇 개의 경우가 있다면 굳이 바꿀 필요가 없다. 이런 경우 다형성은 과하다 이런 경우예는 Replace Parameter with Explicit Methods가 좋은 선택이다.
조건중 null이 있는 경우가 있으면 Introduce Null Object 를 사용해라
평행 상속 구조 (Parallel Ingeritance Hierarchies)
평행 상속 구조는 실제로 산탄총 수술의 특별한 경우이다. 이런 경우 한 클래스의 서브클래스를 만들면, 다른 곳에도 모두 서브 클래스를 만들어주어야 한다.
한쪽 상속구조에서 클래스 이름의 접두어가 다른 쪽 상속구조의 클래스 이름의 접두어와 같은 경우에 냄새를 인식할 수 있다.
중복을 제거하는 일반적인 전략은 한쪽 상속 구조의 인스턴스가 다른 쪽 구조의 인스턴스를 참조하도록 만드는 것이다.
Move Method와 Move Field를 사용하면 참조하는 쪽의 상속구조가 사라질 것이다.
게으른 클래스 (Lazy Class)
클래스를 생성할 때마다 그것을 유지하고, 이해하기 위한 비용이 발생한다. 이 비용을 감당할 만큼 충분한 일을 하지 않는 클래스는 삭제되어야 한다.
별로 하는 일도 없는 클래스의 서브클래스가 있다면 Collapse Hierarchy를 사용하라
거의 필요 없는 클래스에 대해서는 Inline Class를 적용해야 한다.
추측성 일반화 (Speculative Generality)
Brian Foote가 제안한 이 냄새의 이름. 사람들이 "언젠가 이런 종류의 일을 처리하기 위한 기능이 필요하다고 생각하는데" 라고 말하면서 필요하지도 않는 것을 처리하기 위해 모든 종류의 갈고리와 특별한 상자를 원할 때 냄새를 맡을 수 있다.
별로 하는 일이 없는 추상 클래스가 있으면 Collapse Hierarchy를 사용하라
불필요한 위임(delegation)은 Inline Class로 제거될 수 있다.
메소드에 사용되지 않는 파라미터가 있다면 Remove Parameter를 적용해야 한다.
메소드 이름이 이상하고 추상적일때는 Rename Method를 이용하여 구체적인 이름으로 바꾸어야 한다.
만약 어떤 클래스나 메소드가 적절한 기능을 이용하는 테스트 케이스에 대한 헬퍼(helper)인 경우에는 물론 남겨두어야 한다.
임시 필드 (Temporary Field)
때로는 어떤 객체 안의 인스턴스 변수가 특정 상황에서만 세팅되는 경우가 있다. 이런 코드는 이해하기 어려운데 왜냐하면 보통은 객체의 모든 변수가 값을 가지고 있을 거라고 기대하기 때문이다
고아 변수들을 위한 집을 만들기 위해 Extract Class를 사용한다. 그 변수를 사용하는 모든 코드를 새로 만든 클래스에 넣는다.
변수의 값이 유효하지 않는 경우에 대한 대체 컴포넌트(alternative component)를 만들기 위해 Introduce Null Object를 이용하여 조건문이 포함된 코드를 제거할 수 있다.
임시 필드는 복잡한 알고리즘이 여러 변수를 필요로 할 때 흔히 나타난다. 이런 경우 필요한 변수와 메소드를 묶어 Extract Class를 사용할 수 있다. 세로운 객체는 메소드 객체(method object)가 된다.
메시지 체인 (Message Chains)
클라이언트가 어떤 객체를 얻기 위해 다른 객체에 물어보고 다른 객체는 또 다시 다른 객체에 물어보고 그 객체는 다시 다른 객체에 물어보고 ... 이런 경우 메시지 체인을 볼 수 있다. 이것은 긴 줄의 getThis 메소드 또는 임시변수의 시퀀스로 볼 수 있다. 이런 식으로 진행된다는 것은 클라이언트가 클래스 구조와 결합되어 있다는 것을 뜻한다. 중간에 어떤 관계가 변한다면 클라이언트 코드도 변경되어야 한다.
이 경우 Hide Delegate를 사용할 수 있다. 체인의 여러 지점에서 이것을 적용할 수 있다.
원칙적으로는 체인 내의 모든 객체에 적용할 수 있지만, 이 경우 종종 중간에 있는 모든 객체를 미들 맨(middle man)으로 만드는 결과를 초래할 수 있다. 그것을 사용하는 코드의 조각을 취해 Extract Method를 사용할 수 있는지 보고 Move Method로 그것을 체인의 밑으로 밀어넣는다.
미들 맨 (Middle Man)
객체의 주요 특징하나가 캡슐화(encapsulation - 내부의 상세사항을 외부로부터 숨기는 것)이다. 캡슐화는 보통 위임(delegation)과 함께 사용된다.
클래스의 인터페이스를 보니 메소드의 태반이 다른 클래스로 위임하고 있다면 Remove Middle Man을 사용해여 그 객체에 실제로 뭐가 어떻게 되어가고 있는지 알게 해줄 때이다.
몇몇 메소드가 많은 일을 하지 않는다면 Inline Method를 사용하여 호출하는 곳에 코드를 삽입할 수 있다.
추가 동작이 있다면 Replace Delegation with Inheritance을 사용하여 미들맨을 실제 객체의 서브클래스로 바꿀 수도 있다.
부적절한 친밀 (Inappropriate Intimacy)
때로는 클래스가 지나치게 친밀하게 되어 서로 사적인 부분을 파고드느라 너무 많은 시간을 소모할 수 있다. 지나치게 친밀한 클래스는 옛날 연인을 갈라 놓은 것처럼 서로 떼어 놓아야 한다.
Move Method와 Move Field를 사용하여 조각으로 나누고 친밀함을 줄어야 한다.
Change Bidirectional Association to Unidirectional이 적용 가능한지를 보라
이들 클래스에 공통 관심사가 있다면 Extract Class를 사용하여 공통된 부분을 안전한 곳으로 빼내서 별도의 클래스를 만들어라
Hide Delegate를 사용하여 다른 클래스가 중개하도록 하라
상속은 종종 과도한 친밀을 유도할 수 있다. 서브클래스는 항상 그 슈퍼클래스가 알려주고 싶은 것보다 많은 것을 알려고 한다. 만약 출가(서브클래스를 부모클래스에서 분리하는 것) 할 때라면 Replace Inheritance with Delegation을 적용하라
다른 인터페이스를 가진 대체 클래스 (Alternative Classes with Different Interface)
같은 작업을 하지만 다른 시그니처(signature)를 가지는 메소드에 대해서는 Rename Method를 사용하라 종종 이것만으로는 부족할 때도 있다. 이럴 때는 클래스가 여전히 충분한 작업을 하지 않는 경우이다.
프로토콜이 같아질 때 까지 Move Method를 이용하여 동작을 이동시켜라
너무 많은 코드를 옮겨야할때에는 목적을 이루기 위해 Extract Superclass를 사용할 수 있다.
불완전한 라이브러리 클래스 (Incomplete Library Class)
문제는 라이브러리가 종종 나쁜형태이고 원하는 것을 하기 위해 라이브러리 클래스를 수정하는 것은 거의 불가능하다는 것이다.
라이브러리 클래스가 가지고 있었으면 하는 메소드가 몇개 있다면 Introduce Foreign Method를 사용하라
별도의 동작이 잔뜩 있다면 Introduce Local Extension이 필요하다.
데이터 클래스 (Data Class)
필드와 각 필드에 대한 get/set 메소드만 가지고 다른 것은 아무것도 없는 클래스가 있다. 초기 단계이 이런 클래스는 public 필드만 가질 것이다.
Encapsulate Field를 적용해야 한다.
만약 컬렉션(Collection)필드를 가지고 있다면 적절히 캡슐화되어 있는지 확인하고 적절히 캡슐화가 되어 있지 않으면 Encapsulate Collection을 적용한다.
값이 변경되면 안되는 필드에 대해서는 Remove setting Mothod를 사용한다.
get/set 메소드가 다른 클래스에서 사용되는 지 찾아보고 동작을 데이터 클래스로 옮기기 위해 Move Method를 시도한다.
메소드 전체를 옮길 수 없을 때는 Extract Method를 사용해서 옮길 수 있는 메소드를 만들어라 잠시후에 get/set 메소드에 대해 Hide Method를 사용할 수 있다.
거부된 유산 (Refused Bequest)
서브클래스는 메소드와 데이터를 그 부모클래스로부터 상속 받는다 만약 서브클래스가 그들에게 주어진 것을 원하지 않는다거나 필요하지 않는다면 어떻게 될까? 전통적인 관점에서 이것은 클래스 상속 구조가 잘못 되었다는 것을 뜻한다.
새로운 형제 클래스를 만들고, Push Down Method와 Push Down Field를 사용해서 사용되지 않는 메소드를 모두 형제 클래스로 옮겨야 한다.
만약 서브클래스가 동작은 재사용하지만 수퍼클래스의 인터페이스를 지원하는 것은 원치 않는다면 거부된 유산의 냄새가 훨씬 더 강하다 구현을 거부하는 것은 상관 하지 않지만, 인터페이스를 거부하는 것은 심각한 문제이다. * 이런경우에도 클래스 구조를 손보는것보다는 것보다는 Replace Inheritance 적용하여 해결하는 것이 낫다
주석 (Comments)
우리의 후각에 의하면 주석은 나쁜 냄새가 아니다. 사실은 달콤한 향기다. 여기서 주석에 대해 말하는 이유는 주석이 종종 탈취제로 사용되기 때문이다
코드블록이 무슨 작업을 하는지 설명하기 위해 주석이 필요하다면 Extract Method를 시도해보라
만약 메소드가 이미 추출되어 있는데도 여전히 주석이 필요하다면 Rename Mehod를 사용하라
시스템의 필요한 상태에 대한 이떤 규칙 같은 것을 설명할 필요가 있다면 Introduce Assertion을 사용하라
주석을 써야 할 것 같은 생각이 들면 먼저 코드를 리팩토링하여 주석이 불필요하도록 하라
주석을 사용하기 좋을 때는 무엇을 해야 할지 모를 때이다. 저석은 무엇이 진행되고 있는지를 설명할 뿐아니라. 불확실한 부분을 표시할 수도 있다.
[4] 테스트 만들기
리펙토링을 하고자 할 때 견고한 테스트는 없어서는 안 될 필수조건이다
(테스트 및 JUnit에 대한 설명이 들어가는 부분이나
제가 자바와 JUnit을 해본적이 없기 때문에 나중에 정리하는 것으로 보류 합니다)
[5] 리펙터링 카탈로그
리팩터링 형식
이름 - 리팩터링의 어휘를 구성하게 될 키(Key) 로써 중요하다. 리팩터링을 책의 다른 부분에서 참조할때 이 이름을 사용한다.
요약 - 어떤 경우에 해당 리팩터링이 필요한 지, 그리고 리팩터링이 하는 작업이 무엇인지 간단하게 설명한다. 리팩터링을 빨리 찾을 수 있도록 도와준다.
동기 - 왜 이 리팩터링이 필요한 지에 대해 그리고 이 리팩터링을 사용하면 안되는 상황 등을 설명한다.
절차 - 이 리팩터링을 어떻게 진행하는 지 한단계 씩 간단하게 설명한다.
예제 - 해당 리팩터링이 어떻게 동작하는 지를 보이기 위해 간단한 예제로 설명한다.
리팩터링은 얼마나 완벽한가?
이것들이 단일 프로세스 소프트웨어에 대한 고려만 하고 있다는 것이다. 컨커런트 및 분산 프로그래밍에서도 사용할 수 있는 리팩터링을 볼 수 있기를 바란다. 패턴과 리팩터링에는 자연스러운 관계가 있다. 패턴은 우리가 있고 싶은 곳이고, 리팩터링은 그곳에 이르는 방법이다.
[6] 메소드 정리(Composing Method)
리펙토링의 많은 부분이 메소드를 정리해서 코드를 적절하게 포장하는 것이다.
대부분의 경우 문제는 지나치게 긴 메소드에서 나온다.
가장 중요한 리펙토링은 Extract Method 인데 코드 덩어리를 별도의 메소드로 뽑아내는 것이다.
Inline Method는 Extract Method의 반대 개념으로 메소드 호출 부분을 해당 메소드의 몸체로 바꾸는 것이다.
여러 코드를 추출하여 메소드를 만들었는데 그 중 몇몇이 제 몫을 못하거나 또는 메소드를 쪼갠 방법을
재구성 해야 할 필요가 있을 때 Inline Method를 사용한다.
Extract Method를 사용할 때 가장 큰 문제는 지역 변수를 다루는 것이고 임시 변수 또한 그러하다.
가능한 모든 임시 변수를 제거하기 위해 Replace Temp with Query를 즐겨 사용한다.
하나의 임시변수가 여러 목적으로 사용된다면 먼저 Split Temporary Variable을 사용하여 쉽게 바꿀 수 있도록 만든다.
임시변수가 너무 꼬여 있어 제거하기가 어려운 경우도 있다. Replace Method with Method Object가 필요하다
새로운 클래스를 만들어야 하지만 아주 복잡하게 꼬여 있는 메소드라도 분해할 수 있다.
파라미터에 값을 대입하고 있다면 Remove Assignment to Parameters가 필요하다
코드를 좀 더 명확하게 하기 위해서 알고리즘이 개선될 수 있다는 것을 알아낼 수 있을 지도 모른다.
Substitute Algorithm을 사용하여 더 명확한 알고리즘을 도입한다.
1) Extract Method
그룹으로 함께 묶을 수 있는 코드 조각이 있으면
코드의 목적이 잘 드러나도록 메소드의 이름을 지어 별도의 메소드로 뽑아낸다.
Extract Method
void printOwing(double amount)
{
printBanner();
//상세 정보 표시
System.out.println( "name:" + _name );
System.out.println( "amount:" + amount );
}
===========================================
void printOwing(double amount)
{
printBanner();
printDetails(amount);
}
void printDetails(double amount)
{
System.out.println( "name:" + _name );
System.out.println( "amount:" + amount );
}
동기
첫째 메소드가 잘 쪼개져 있을 때 다른 메소드에서 사용될 확률이 높아진다
둘째 고수준(high-level)의 메소드를 볼 때 일련의 주석을 읽는 것 같은 느낌을 들도록 할 수 있다.
절차
메소드를 새로 만들고 의도를 잘 나타낼 수 있도록 이름을 정한다. 어떻게 하는지 나타내는 방식으로 이름을 정하지 말고 무엇을 하는 지를 나타내게 이름을 정한다.
뽑아내고자 하는 부분이 한 줄의 메시지나 함수 호출과 같이 아주 간단한 경우에는 새로운 메소드의 이름이 그 코드의 의도를 더 잘 나타낼 수 있을 때만 뽑아낸다. 더 이해하기 쉬운 이름을 지을 수 없다면 코드를 뽑아내지 않는 것이 낫다.
원래 메소드에서 뽑아내고자 하는 부분의 코드를 복사하여 새 메소드로 옮긴다
원래 메소드에서 사용되고 있는 지역 변수가 뽑아낸 코드에 있는 지 확인한다. 이런 지역변수는 새로운 메소드의 지역변수나 파라미터가 된다.
뽑아낸 코드 내에서만 사용되는 임시변수가 있는 지 본다. 있다면 새로운 메소드의 임시변수를 선언한다.
뽑아낸 코드 내에서 지역변수의 값이 수정되는 지 본다. 하나의 지역변수만 수정된다면, 뽑아낸 코드를 질의(query)로 보고 수정된 결과를 관련된 변수에 대입 할 수 있는 지 본다. 이렇게 하는 것이 이상하거나 값이 수정되는 지역 변수가 두개 이상있다면 쉽게 메소드로 추출할 수 없는 경우이다. 이럴때는 Split Temporary Variable 사용한 다음 다시 시도해야 한다. 임시변수는 Replace Temp with Query로 제거할 수 있다.
뽑아낸 코드에서 읽기만 하는 변수는 새 메소드의 파라미터로 넘긴다
지역변수와 관련된 사항을 다룬 후에는 컴파일을 한다.
원래 메소드에서 뽑아낸 코드 부분은 새로 만든 메소드를 호출하도록 바꾼다.
새로 만든 메소드로 옮긴 임시변수가 있는 경우 그 임시변수가 원래 메소드의 밖에서 선언되었는 지를 확인한다. 만약 그렇다면 새로 만들 메소드에서는 선언을 해줄 필요가 없다.
컴파일과 테스트를 한다.
예제 : 지역변수가 없는 경우
이런 단순한 경우 Exract Method는 아주 쉽다
void printOwing()
{
Enumeration e = _orders.elements();
double outstanding = 0.0;
//배너표시
System.out.println("***********************");
System.out.println("**** Customer Owes ****");
System.out.println("***********************");
//outstanding 계산
while( e.hasMoreElements() )
{
Order each = (Order)e.NextElement();
outstanding += each.getAmount();
}
//상세정보표시
System.out.println("name:" + _name);
System.out.println("amount:" + outstanding);
}
void printOwing()
{
Enumeration e = _orders.elements();
double outstanding = 0.0;
printBanner();
//outstanding 계산
while( e.hasMoreElements() )
{
Order each = (Order)e.NextElement();
outstanding += each.getAmount();
}
//상세정보표시
System.out.println("name:" + _name);
System.out.println("amount:" + outstanding);
}
void printBanner()
{
//배너표시
System.out.println("***********************");
System.out.println("**** Customer Owes ****");
System.out.println("***********************");
}
잘라서 새 메소드로 붙여넣고 원래 코드는 새 메소드를 호출하도록 바꾸기만 하면 된다.
예제 : 지역변수가 포함되어 있는 경우
지역변수가 포함된 경우에서 가장 쉬운것은 변수가 읽히기만 하고 값이 변하지 않는 경우이다
이런 경우는 변수를 파라미터로 넘길 수 있다.
void printOwing()
{
Enumeration e = _orders.elements();
double outstanding = 0.0;
printBanner();
//outstanding 계산
while( e.hasMoreElements() )
{
Order each = (Order)e.NextElement();
outstanding += each.getAmount();
}
//상세정보표시
System.out.println("name:" + _name);
System.out.println("amount:" + outstanding);
}
void printOwing()
{
Enumeration e = _orders.elements();
double outstanding = 0.0;
printBanner();
//outstanding 계산
while( e.hasMoreElements() )
{
Order each = (Order)e.NextElement();
outstanding += each.getAmount();
}
printDetails(outstanding);
}
void printDetails(double outstanding)
{
//상세정보표시
System.out.println("name:" + _name);
System.out.println("amount:" + outstanding);
}
파라미터를 가진 메소드로 뽑아낼 수 있다.
이 메소드는 필요에 따라 여러 지역변수와 사용될 수 있다.
지역변수가 객체이고 그 객체의 속성을 바꾸는 메소드를 호출하는 경우도
위의 경우와 마찬가지다. 이 경우도 그 객체를 파라미터로 넘길 수 있다.
실제로 지역변수에 다른 값을 대입하는 경우에만 부가적인 작업이 필요하다.
예제 : 지역변수에 다른 값을 여러 번 대입하는 경우
지역변수에 다른 값을 대입하는 코드가 있는 경우에는 문제가 복잡해진다.
이런 경우 중 임시변수에 대해서만 생각해 보자
파라미터에 다른 값을 대입하는 코드가 있다면 즉시 Remove Assignments to Parameters를 적용해야 한다.
임시변수에 값을 대입하는 경우는 두가지가 있다.
임시변수가 뽑아낸 코드 안에서만 사용될 때와 임시변수가 뽑아낸 코드 외부에서 사용되는 경우이다
첫번째 경우는 뽑아낸 코드로 임시변수를 옮길 수 있다.
두번째 경우는 그 변수가 뽑아낸 코드 이후의 부분에서 사용되지 않는다면 앞에서의 경우와 같이 바꿀 수 있다.
뽑아낸 코드 이후 부분에서 사용된다면 뽑아낸 코드에서 임시변수의 바뀐 값을 리턴하도록 수정해야 한다.
void printOwing()
{
Enumeration e = _orders.elements();
double outstanding = 0.0;
printBanner();
//outstanding 계산
while( e.hasMoreElements() )
{
Order each = (Order)e.NextElement();
outstanding += each.getAmount();
}
printDetails(outstanding);
}
void printOwing()
{
printBanner();
double outstanding = getOutstanding();
printDetails(outstanding);
}
double getOutstanding()
{
Enumeration e = _orders.elements();
double outstanding = 0.0;
while( e.hasMoreElements() )
{
Order each = (Order)e.NextElement();
outstanding += each.getAmount();
}
return outstanding;
}
좌의 코드에서 계산 부분의 코드를 뽑아낸다
변수 e(Enumeration)는 뽑아낸 코드 안에서만 사용되기 때문에 새 메소드로 옮길 수 있다.
변수 outstanding은 양쪽에서 모두 사용되므로 뽑아낸 메소드에서 그 값을 리턴하게 할 필요가 있다.
이 수정에 대해 컴파일과 테스트를 한 다음, 변수 이름을 보통 자신이 쓰는 관례대로 바꾼다.
double getOutstanding()
{
Enumeration e = _orders.elements();
double result = 0.0;
while( e.hasMoreElements() )
{
Order each = (Order)e.NextElement();
result += each.getAmount();
}
return result;
}
이 경우 변수 outstanding은 명확한 초기값으로 초기화하기 때문에, 새로 뽑아낸 메소드 안에서 초기화 할 수 있다.
만약 변수와 관련된 다른 코드가 있다면, 이전의 값을 파라미터로 넘겨야 한다.
void printOwing(double previousAmount)
{
Enumeration e = _orders.elements();
double outstanding = previousAmount * 1.2;
printBanner();
//outstanding 계산
while( e.hasMoreElements() )
{
Order each = (Order)e.NextElement();
outstanding += each.getAmount();
}
printDetails(outstanding);
}
void printOwing(double previousAmount)
{
double outstanding = previousAmount * 1.2;
printBanner();
double outstanding = getOutstanding(outstanding);
printDetails(outstanding);
}
double getOutstanding(double initialValue)
{
double result = initialValue;
Enumeration e = _orders.elements();
while( e.hasMoreElements() )
{
Order each = (Order)e.NextElement();
result += each.getAmount();
}
return result;
}
이런 경우 다음과 같이 바뀔 것이다
컴파일과 테스트를 한 후에 outstanding 변수를 초기화하는 방법을 바꾼다.
void printOwing(double previousAmount)
{
printBanner();
outstanding = getOutstanding(previousAmount * 1.2);
printDetails(outstanding);
}
만약 두개 이상의 값이 리턴되어야 한다면 어떻게 될까
가장 좋은 선택은 뽑아낼 코드를 다르게 선택하는 것이다.
메소드가 하나의 값을 리턴하기 위해 잘 정리해서 각각의 다른 값에 대한 메소드를 따로 만들어야 할것이다
사용중인 언어가 출력 파라미터(output paraneter)를 지원한다면 이것을 이용할 수도 있다.
어떤 경우는 임시변수가 너무 많아 코드를 뽑아내기가 어려운 경우도 있다.
이런 경우에는 Replace Temp with Query를 사용하여 임시변수를 줄인다.
어떻게 해봐도 여전히 난처한 경우에는 Replace Method with Method Object에 도움을 청한다.
2) Inline Method
메소드 몸체가 메소드의 이름 만큼이나 명확할 때는
호출하는 곳에 메소드의 몸체를 넣고, 메소드를 삭제하라
Inline Method
int getRating()
{
return (moreThanFiveLateDeliveries()) ? 2: 1;
}
boolean moreThanFiveLateDeliveries()
{
return _numberOfLateDeliveries > 5;
}
int getRating()
{
return (_numberOfLateDeliveries > 5) ? 2 : 1;
}
동기
때로는 메소드의 몸체가 메소드의 이름 만큼이나 명확할 때가 있다.
또는 메소드의 몸체를 메소드의 이름 만큼 명확하게 리펙토링 할 수도 있다.
Inline Method는 메소드가 잘못 나누어져 있을 때에도 사용할 수도 있다.
Replace Method with Method Object를 사용하기 전에 이 리펙토링을 사용한다면 좋다는 것을 알아냈다.
모든 메소드가 단순히 다른 메소드에 위임을 하고 있어 그 인디렉션 속에서 길을 잃을 염려가 있을 때도 Inline Method를 사용한다.
절차
메소드가 다형성을 가지고 있지 않은지 확인한다.
서브클래스에서 오버라이드하고 있는 메소드에는 적용하지 않는다. 수퍼클래스에 없는 메소드를 서브클래스에서 오버라이드 할 수 없다.
메소드를 호출하고 있는 부분을 모두 찾는다.
각각의 메소드 호출을 메소드 몸체로 바꾼다.
컴파일과 테스트를 한다.
메소드 정의를 제거한다.
Inline Method는 간단하게 보인다.
재귀가 사용되는 경우나 리턴 포인트가 여러 곳인 경우에 대해 어떻게 하고,
접근자(accessor)가 없을 때는 어떻게 다른 객체로 인라인화 하는 지 등에 대해 여러 페이지에 걸쳐 설명할 수도 있다.
이런 설명을 하지 않는 이유는 이런 복잡한 경우에는 이 리펙토링을 하지 않는 것이 좋기 때문이다.
3) Inline Temp
간단한 수식의 결과값을 가지는 임시변수가 있고 그 임시변수가 다른 리펙토링을 하는데 방해가 된다면,
이 임시변수를 참조하는 부분을 모두 원래의 수식으로 바꾸라
Inline Temp
double basePrice = anOrder.basePrice();
return (basePrice > 1000);
return (anOrder.basePrice() > 1000);
동기
Inline Temp는 Replace Temp with Query의 한부분으로 사용된다.
따라서 진짜 동기는 그 쪽에 있다.
Inline Temp가 자신의 목적으로 사용되는 유일한 경우는 메소드 호출의 결과값이 임시변수에 대입되는 경우이다.
Extract Method와 같은 다른 리펙토링에 방해가 된다면, 인라인화 하는 것이 좋다.
절차
임시변수를 final로 선언한 다음 컴파일 한다. (C++에서 const이다)
이것은 임시변수에 값이 단 한번만 대입되고 있는지를 확인하기 위한것이다.
임시변수를 참조하고 있는 곳을 모두 찾아 대입문(assignment)의 우변에 있는 수식으로 바꾼다.
각각의 변경에 대해 컴파일과 테스트 한다.
임시변수의 선언과 대입문을 제거한다.
컴파일과 테스트 한다.
4) Replace Temp with Query
어떤 수식의 결과값을 저장하기 위해서 임시변수를 사용하고 있다면
수식을 뽑아내서 메소드로 만들고, 임시변수를 참조하는 곳을 찾아 모두 메소드 호출로 바꾼다.
새로 만든 메소드는 다른 메소드에서도 사용될 수 있다.
Replace Temp with Query
double basePrice = _quantity * _itemPrice;
if( basePrice > 1000 )
return basePrice * 0.95;
else
return basePrice * 0.98;
===========================================
if( basePrice() > 1000 )
return basePrice * 0.95;
else
return basePrice * 0.98;
...
double basePrice()
{
return _quantity * _itemPrice;
}
동기
임시변수는 임시로 사용되고 특정 부분에서만 의미를 가지므로 문제가 된다.
임시변수는 그것이 사용되는 메소드의 컨텍스트 안에서만 볼 수 있으므로
임시변수가 사용되는 메소드는 보통 길이가 길어지는 경향이 있다.
임시변수를 질의 메소드(query method)로 바꿈으로써
클래스 내의 어떤 메소드도 임시변수에 사용될 정보를 얻을 수 있다.
Replace Temp with Query는 Extract Method를 적용하기 전의 필수 단계이다.
지역변수는 메소드의 추출을 어렵게 하기 때문에 가능한 많은 지역변수를 질의 메소드로 바꾸는 것이 좋다.
이 리팩토링을 적용하는 데 있어 가장 간단한 경우는 임시변수에 값이 한번만 대입되고,
대입문(assignment)을 만드는 수식이 부작용을 초래하지 않는 경우이다.
Split Temporary Variable이나 Separate Query from Modifier를 먼저 적용하는 것이 쉬울 것이다.
만약 임시변수가 어떤 결과를 모으는 경우(루프를 돌면서 덧셈을 하는 경우와 같이)
질의 메소드 안으로 몇몇 로직을 복사할 필요가 있다.
절차
임시변수가 값이 한번만 대입되는지를 확인한다.
임시변수에 값이 여러번 대입되는 경우에는 Split Temporary Variable을 먼저 적용한다.
임시변수를 final로 선언(C++에서 const) 한다
컴파일한다.
이렇게 하여 임시변수에 값이 한번만 대입되는 지 확인한다.
대입문의 우변을 메소드로 추출한다.
처음에는 메소드를 private로 선언한다. 나중에 다른 곳에서도 사용하는 것이 좋을 것 같으면 그 때 쉽게 접근 권한을 바꿀 수 있다.
추출된 메소드에 부작용이 없는지(어느 객체의 속성을 바꾸거나 하면 안된다.)확인한다. 만약 부작용이 있는 경우에는 Separate Query from Modifier를 사용한다.
컴파일과 테스트를 한다.
Inline Temp를 적용한다.
예제
먼저 간단한 메소드로 시작한다.
double getPrice()
{
int basePrice = _quantity * _itemPrice;
double discountFactor;
if( basePrice > 1000 ) discountFactor = 0.95;
else discountFactor = 0.98;
return basePrice * discountFactor;
}
두개의 임시변수를 하나씩해서 모두 제거할 예정이다.
임시변수를 final로 선언하여 테스트할 수 있다.
double getPrice()
{
final int basePrice = _quantity * _itemPrice;
final double discountFactor;
if( basePrice > 1000 ) discountFactor = 0.95;
else discountFactor = 0.98;
return basePrice * discountFactor;
}
먼저 확인하는 이유는 만약 문제가 있다면 이 리팩토링을 하면 안되기 때문이다.
임시변수를 하나씩 바꾼다. 먼저 대입문의 우변을 메소드로 뽑아낸다.
double getPrice()
{
final int basePrice = basePrice();
final double discountFactor;
if( basePrice > 1000 ) discountFactor = 0.95;
else discountFactor = 0.98;
return basePrice * discountFactor;
}
private int basePrice()
{
return _quantity * _itemPrice;
}
다시 컴파일과 테스트를 한 다음 Inline Temp를 사용한다.
먼저 임시변수를 참조하는 첫번째 부분을 바꾼다.
double getPrice()
{
final int basePrice = basePrice();
final double discountFactor;
if( basePrice() > 1000 ) discountFactor = 0.95;
else discountFactor = 0.98;
return basePrice * discountFactor;
}
private int basePrice()
{
return _quantity * _itemPrice;
}
컴파일 테스트를 한 다음 임시변수를 두 번째 참조하는 곳도 바꾼다.
basePrice를 참조하는 곳은 이게 마지막이므로, 임시변수의 선언도 삭제한다.
double getPrice()
{
final double discountFactor;
if( basePrice() > 1000 ) discountFactor = 0.95;
else discountFactor = 0.98;
return **basePrice()** * discountFactor;
}
private int basePrice()
{
return _quantity * _itemPrice;
}
작업이 끝난 후 discountFactor에 대해서도 위와 같은 작업을 한다.
double getPrice()
{
final double discountFactor = discountFactor();
return basePrice() * discountFactor();
}
private int basePrice()
{
return _quantity * _itemPrice;
}
private double discountFactor()
{
if( basePrice() > 1000 ) discountFactor = 0.95;
else discountFactor = 0.98;
return discountFactor;
}
basePrice를 질의 메소드로 바꾸지 않았더라면 discountFactor를 뽑아내는 데 얼마나 어려웠을지 생각해보라
getPrice 메소드는 결국 다음과 같이 될 수 있다.
double getPrice() { return basePrice() * discountFactor(); }
private int basePrice()
{
return _quantity * _itemPrice;
}
private double discountFactor()
{
double result = 0.0;
if( basePrice() > 1000 ) result = 0.95;
else result = 0.98;
return result;
}
5) Introduce Explaining Variable
복잡한 수식이 있는 경우에는
수식의 결과나 또는 수식의 일부에 자신의 목적을 달성하는 이름으로 된 임시 변수를 사용하라
Introduce Explaining Variable
if( ( platform.toUpperCase().indexOf("MAC") > -1 ) &&
( browser.toUpperCase().indexOf("IE") > -1 ) &&
( wasInitialized() && resized > 0 )
{
// 작업...
}
final boolean isMasOS = platform.toUpperCase().indexOf("MAC") > -1;
final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1;
final boolean wasResized = 0;
if( isMacOS && isIEBrowser && wasInitialized() && wasResized )
{
// 작업...
}
동기
수식은 매우 복잡해져 알아보기가 어려워질 수 있다.
이런 경우 임시변수가 수식을 좀더 다루기 쉽게 나누는데 도움이 될 수 있다.
Introduce Explaining Variable은 특히 조건문에서
각각의 조건의 뜻을 잘 설명하는 이름의 변수로 만들어 사용할 때 유용하다.
다른 경우로 긴 알고리즘에서 각 단계의 계산 결과를 잘 지어진 이름의 임시변수로 설명할 수 있다.
Introduce Explaining Variable은 매우 일반적인 리팩토링이지만 저자는 이것을 그렇게 많이 사용하지 않는다.
Extract Method를 사용한다. 임시변수는 한 메소드의 컨텍스트 내에서만 유용하다.
그러나 메소드는 객첵의 모든 부분에서 뿐만 아니라 다른 객체에서도 유용하다.
그러나 때로는 지역변수 때문에 Extract Method를 사용하기가 어려운 경우도 있다.
Introduce Explaining Variable을 사용하는 것은 이럴때이다.
절차
final 변수를 선언하고 복잡한 수식의 일부를 이변수에 대입한다.
원래 복잡한 수식에서 임시 변수에 대입한 수식을 임시변수로 바꾼다.
만약 이부분이 반복된다면 반복되는 부분을 하나식 바꿀 수 있다.
컴파일과 테스트를 한다.
수식의 다른 부분에 대해서도 위의 작업을 반복한다.
예제
double price() {
//price = (base price) - (quantity discount) + (shipping)
return _quantity * _itemPrice -
Math.max(0, _quantity - 500) * _itempPrice * 0.05 +
Math.min(_quantity * itemPrice * 0.1, 100.0 );
}
quantity와 itemPrice의 곱이 basePrice라는 것을 알아낸다.
double price() {
//price = (base price) - (quantity discount) + (shipping)
final double basePrice = _quantity * _itemPrice;
return basePrice -
Math.max(0, _quantity - 500) * _itempPrice * 0.05 +
Math.min(_quantity * itemPrice * 0.1, 100.0 );
}
quantity와 itemPrice의 곱은 코드의 뒷부분에서도 사용된다.
double price() {
//price = (base price) - (quantity discount) + (shipping)
final double basePrice = _quantity * _itemPrice;
return basePrice -
Math.max(0, _quantity - 500) * _itempPrice * 0.05 +
Math.min( basePrice * 0.1, 100.0 );
}
다음에는 quantityDiscount를 도입한다.
double price() {
//price = (base price) - (quantity discount) + (shipping)
final double basePrice = _quantity * _itemPrice;
final double quantityDiscount = Math.max( 0, _quantity-500) * _itemPrice * 0.05;
return basePrice - quantityDiscount +
Math.min( basePrice * 0.1, 100.0 );
}
shipping에 대해서도 같은 작업을 한다.
double price() {
//price = (base price) - (quantity discount) + (shipping)
final double basePrice = _quantity * _itemPrice;
final double quantityDiscount = Math.max( 0, _quantity-500) * _itemPrice * 0.05;
final double shipping = Math.min( basePrice * 0.1, 100.0 );
return basePrice - quantityDiscount + shipping;
}
예제 Extract Method를 사용하는 경우
Extract Method를 적용해보자
double price() {
//price = (base price) - (quantity discount) + (shipping)
return _quantity * _itemPrice -
Math.max(0, _quantity - 500) * _itempPrice * 0.05 +
Math.min(_quantity * itemPrice * 0.1, 100.0 );
}
basePrice를 메소드로 뽑아낸다
double price() {
//price = (base price) - (quantity discount) + (shipping)
return basePrice() -
Math.max(0, _quantity - 500) * _itempPrice * 0.05 +
Math.min( basePrice() * 0.1, 100.0 );
}
private double basePrice() {
return _quantity * _itemPrice;
}
다른 부분에 대해서도 계속 작업을 진행하면 다음과 같이 된다.
double price() {
//price = (base price) - (quantity discount) + (shipping)
return basePrice() - quantityDiscount() + shiiping();
}
private double quantityDiscount() {
return Math.max(0, _quantity - 500) * _itempPrice * 0.05;
}
private double shiiping() {
return Math.min( basePrice() * 0.1, 100.0 );
}
private double basePrice() {
return _quantity * _itemPrice;
}
Introduce Explaining Variable는 언제 사용되는가? Extract Method를 사용하기가 더 어려울 때이다.
만약 수만은 지역변수를 사용하는 알고리즘을 손보고 있다면 Extract Method를 쉽게 사용할 수는 없을 것이다.
코드를 이해하기 위해 Introduce Explaining Variable를 사용한다.
꼬였던 로직이 좀 풀리면 나중에 Replace Temp with Query를 적용한다.
만약 Replace Method with Method Object를 사용한다면 임시변수 또한 유용하다.
6) Split Temporary Variable
루프 안에 있는 변수나 collecting temporary variable도 아닌
임시 변수에 값을 여러번 대입하는 경우에는
각각의 대입에 대해서 따로따로 임시변수를 만들어라
Split Temporary Variable
double temp = 2 * (_height * _width);
System.out.println(temp);
temp = _height * _width;
System.out.println(temp);
============================================
double perimeter = 2 * (_height * _width);
System.out.println(perimeter);
final double area = _height * _width;
System.out.println(area);
동기
임시변수는 여러 곳에서 다양하게 쓰일 수 있다.
어떤 경우에는 임시변수에 여러번 값을 대입하게 된다. 루프에 사용되는 변수는 한번 돌때 마다 값이 바뀐다.
collecting temporary variable은 메소드를 실행하는 동안 모이는 어떤값을 모으는 변수이다.
다른 많은 임시변수는 주로 긴 코드에서 계산한 결과값을 나중에 쉽게 참조하기 위해서
보관하는 용도로 사용된다. 이런 종류의 변수는 값이 한번만 설정되어야 한다.
만약 여러번 설정된다면 그 변수는 메소드 안에서 여러가지 용도로 사용되고 있다는 뜻이다.
어떤 변수든 여러가지 용도로 사용되는 경우에는 각각의 용도에 대해 따로 변수를 사용하도록 바꾸어야 한다.
하나의 임시변수를 두가지 용도로 사용하면 코드를 보는 사람은 매우 혼란스러울 수 있다.
절차
임시변수가 처음 선언된 곳과 임시변수에 값이 처음 대입된 곳에서 변수의 이름을 바꾼다.
만약 임시변수에 값을 대입할 때 i = i + (수식) 과 같은 형태라면, 이것은 이 변수가 collecting temporary variable이라는 뜻으로 분리하면 안된다. collecting temporary variable에 대한 연산은 보통 더하기, 문자열 연결(string concatenation), 스트림 쓰기, 컬렉션 요소(element)를 추가하기 등이다.
새로 만든 임시 변수를 final로 선언한다.
임시변수에 두 번째로 대입하는 곳의 직전까지 원래 임시변수를 참조하는 곳을 모두 바꾼다.
임시변수에 두번째로 대입하는 곳에서 변수를 선언한다.
컴파일과 테스트를 한다.
각 단계 (임시변수가 선언 된는 곳에서 부터 시작하여) 반복한다. 그리고 임시변수에 다음으로 대입하는 곳까지 참조를 바꾼다.
예제
이 예제는 해기스(haggis)(역자주: 양의 내장을 잘게 썰어 위장에 넣고 끊인 스코틀랜드 요리)가 이동한 거리를 계산하는 코드다. 서있는 상태에서 해기스는 첫번째 힘을 받는다. 잠시후 두번째 힘이 해기스를 다시 가속한다. 일반적인 운동법칙을 사용하여 해기스가 이동한 거리를 다음과 같이 구할 수 있다.
double getDistanceTravelled(int time) {
double result;
double acc = _primaryForce / _mess;
int primaryTime = Math.min(time, _delay);
result = 0.5 * acc * primaryTime * primaryTime;
int secondaryTime = time - _delay;
if( secondaryTime > 0 ) {
double primaryVel = acc * _delay;
acc = (_primaryForce * _secondaryTime ) / _mass;
result += primaryVel * secondaryTime + 0.5 * acc * secondaryTime * secondaryTime)
}
return result;
}
약간 어색해 보이는 함수다. 이 예제에서 흥미로은 것은 변수 acc 에 값이 두번 설정된다는 것이다.
즉 두가지 용도로 사용된다는 말이다. 그 중 하나는 첫번째 힘으로 인한 초기 가속을 저장하는 것이고
다른하나는 첫번째 힘과 두번째 힘 모두에 의하 가속을 저장하는 것이다.
먼저 임시변수의 이름을 바꾸고 새로운 이름의 변수를 final로 선언한다.
그리고 나서 변수를 처음 선언한 곳부터 두번째로 값을 대입하기 전까지의 부분에서
해당 임시변수를 참조하는 곳을 모두 수정한다.
두번째로 값을 대입하는 부분에는 다시 선언을 한다.
double getDistanceTravelled(int time) {
double result;
final double primaryAcc = _primaryForce / _mess;
int primaryTime = Math.min(time, _delay);
result = 0.5 * primaryAcc * primaryTime * primaryTime;
int secondaryTime = time - _delay;
if( secondaryTime > 0 ) {
double primaryVel = primaryAcc * _delay;
double acc = (_primaryForce * _secondaryTime ) / _mass;
result += primaryVel * secondaryTime + 0.5 * acc * secondaryTime * secondaryTime)
}
return result;
}
나는 변수의 새로운 이름이 임시변수의 첫번째 용도를 나타내도록 하였다.
또한 값이 한번만 설정되는지 확인하기 위해 final로 선언하였다.
이제 컴파일과 테스트를 할 수 있다. 모든것이 제대로 작동 해야 한다.
이제 임시 변수의 두번째 대입에 대해서도 작업을 계속한다.
두번째 임시변수의 용도에 대해서도 새로운 이름을 지정하여 원래의 임시변수 이름을 완전히 제거한다.
double getDistanceTravelled(int time) {
double result;
final double primaryAcc = _primaryForce / _mess;
int primaryTime = Math.min(time, _delay);
result = 0.5 * primaryAcc * primaryTime * primaryTime;
int secondaryTime = time - _delay;
if( secondaryTime > 0 ) {
double primaryVel = primaryAcc * _delay;
final double secondaryAcc = (_primaryForce * _secondaryTime ) / _mass;
result += primaryVel * secondaryTime + 0.5 * secondaryAcc * secondaryTime * secondaryTime)
}
return result;
}
여기에도 아직 많은 리팩토링이 적용될수 있다. 마음껏 리팩토링을 즐기기 바란다.
아마 해기스를 먹는 것보다는 휠씬 나을 것이다. - 해기스에 무엇이 들어가는지 아는가?
7) Remove Assignments to Parameters
파라미터에 값을 대입하는 코드가 있으면 대신 임시변수를 사용하도록 하라
Remove Assignments to Parameters
int discount(int inputVal, int quantity, int yearToDate) {
if( inputVal > 50 ) inputVal -= 2;
}
int discount(int inputVal, int quantity, int yearToDate) {
int result = inputVal;
if( inputVal > 50 ) result -= 2;
}
동기
먼저 파라미터에 값을 대입한다는 말의 뜻을 명확히 하자. 만약 파라미터로 객체를 넘긴 다음
파라미터에 다른 값을 대입하는 것은 파라미터가 다른 객체를 참조하게 하는 것을 뜻한다.
파라미터로 넘겨진 객체로 어떤 작업을 하는 것은 아무런 문제가 없다.
파라미터가 완전히 다른 객체를 참조하도록 하는 것에는 반대한다.
void aMethod(Object foo) {
foo.modifiInSomeWay(); // 아무런 문제 없음
foo = anotherObject; // 문제가 됨
}
명확하지 않고, 값에 의한 전달(pass by value)과
참조에 의한 전달(pass by reference)을 혼동하게 하기 때문이다.
자바에서는 값에 의한 전달만 사용되고 여기서의 논의는 이것을 바탕으로 한다.
값에 의한 전달에서는 파라미터의 어떤 변경을 가하더라도 호출하는 루틴 쪽에서는 반영되지 않는다.
참조에 의한 전달을 사용하던 사람들에게는 아마도 이것이 헷갈릴 것이다.
또한 메소드 몸체 안의 코드 자체에서도 혼돈이 된다. 따라서 파라미터는 전달된 그대로 쓰는 것이
일관적인 사용법이고 휠씬 명확하다 자바에서는 파라미터에 값을 대입해서는 안되고
이런 코드를 보면 Remove Assignments to Parameters를 적용해야 한다.
절차
파라미터를 위한 임시 변수를 만든다.
파라미터에 값을 대입한 코드 이후에서 파라미터에 대한 참조를 임시변수로 바꾼다.
파라미터에 대입하는 값을 임시변수에 대임하도록 바꾼다.
컴파일과 테스트를 한다.
만약 의미체계(semantics)가 참조에 의한 호출(call by reference)인 경우 메소드를 호출하는 부분에서 파라미터가 계속해서 사용되는 지 살펴 보라 또한 거기서 얼마나 많은 참조에 의한 호출 파라미터에 값이 대입되고 사용되는 지도 본다. 리턴값으로 하나의 값을 넘길수 있도록 하라. 만약 하나 이상이 있다면 데이터 덩어리를 객체로 만들 수 있는지 또는 별도의 메소드로 만들수 있는지 보라
예제
먼저 다음과 같은 간단한 루틴에서 시작한다.
int discount (int inputVal, int quantity, int yearToDate) {
if(inputVal > 50) inputVal -=2;
if(quantity > 100) inputVal -=1;
if(yearToDate > 100000) inputVal -=4;
return inputVal;
}
파라미터를 임시변수로 바꾸면 다음과 같이 된다.
int discount (int inputVal, int quantity, int yearToDate) {
int result = inputVal;
if(inputVal > 50) result -=2;
if(quantity > 100) result -=1;
if(yearToDate > 100000) result -=4;
return result;
}
파라미터를 final로 선언하여 이 관례를 따르도록 강제할 수 있다.
int discount (final int inputVal,final int quantity,final int yearToDate) {
int result = (inputVal;
if(inputVal > 50) result -=2;
if(quantity > 100) result -=1;
if(yearToDate > 100000) result -=4;
return result;
}
저자는 실제로 파라미터에서 final을 많이 쓰지는 않는다.
왜나하면 짧은 메소드에 대해서는 코드를 명확하게 하는 데
별로 큰 도움이 안되기 때문이다. 메소드가 길 경우에는 다른 코드가
파라미터를 변경하는 지 보기 위해 쓰기도 한다.
자바에서 값에 의한 전달(pass by value)
자바에서 값에 의한 전달의 사용은 종종 혼동의 원인이 되기도 한다.
자바는 엄격하게 모든 경우에 값에 의한 전달을 사용한다.
class Param {
public static void main(String[] args) {
int x = 5;
triple(x);
System.out.println("X after triple" + x );
}
private static void triple(int arg) {
arg = arg * 3;
System.out.println("arg in triple:" + arg);
}
}
따라서 위의 프로그램을 실행시키면 결과가 다음과 같이 나온다.
arg in triple : 15
X after triple : 5
이런 혼동은 객체에 대해서도 존재한다.
class Param {
public static void main(String[] args) {
Date d1 = new Date("1 Apr 98");
nextDateUpdate(d1);
System.out.println("d1 after nextDay:" + d1);
Date d2 = new Date("1 Apr 98");
nextDateReplace(d2);
System.out.println("d2 after nextDay:" + d2);
}
private static void nextDateUpdate(Date arg) {
arg.setDate(arg.getDate()+1);
System.out.println("arg in nextDay:" + arg);
}
private static void nextDateReplace(Date arg) {
arg = new Date(arg.getYear(), arg.getMonth(), arg.getDate()+1);
System.out.println("arg in nextDay:" + arg);
}
}
이번엔 Date 객체를 사용하였는데 결과는 다음과 같다.
arg in nextDay: Thu Apr 02 00:00:00 EST 1998
d1 after nextDay: Thu Apr 02 00:00:00 EST 1998
arg in nextDay: Thu Apr 02 00:00:00 EST 1998
d2 after nextDay: Wed Apr 01 00:00:00 EST 1998
본질적으로 객체 잠조가 값으로 넘겨졌다. 이것은 객체의 속성을 변경하는 것은 가능하게 하지만,
파라미터에 다름 값을 대입하는 것은 고려하지 않는다.
8) Replace Method with Method Object
긴 메소드가 있는데 지역변수 때문에 Extract Method를 적용할 수 없는 경우에는
메소드를 그 자신을 위한 객체로 바꿔서 모든 지역변수가 그 객체의 필드가 되도록 한다.
이렇게 하면 메소드를 같은 객체 안의 여러메소드로 분해할 수 있다.
Repalce Method with Method Object
동기
이책에서 저자는 작은 메소드의 아름다움을 강조했다.
거대한 메소드에서 작은 부분을 뽑아냄으로써 코드를 더 이해하기 쉽게 만든다.
지역변수는 메소드를 분해할 때 어려움을 준다. 즉 지역변수가 많으면 분해가 어려워질 수 있다.
Replace Temp with Query는 이런 짐을 덜도록 도와주지만
때로는 쪼개야 하는 메소드를 쪼갤수 없는 경우가 생길수 있다.
이런 경우에는 도구상자의 깊숙한 부분에서 메소드 객체를 꺼내 사용한다.
Replace Method with Method Object를 사용하는 것은
이런 모든 지역변수를 메소드 객체의 필드로 바꿔버린다.
그런 다음에 이 새로운 객체에 Extract Method를 사용하여 원래의 메소드를 분해할 수 있다.
절차
뻔뻔스럽게도 Beck에서 훔쳐옴
메소드의 이름을 따서 새로운 클래스를 만든다.
새로운 클래스에 원래 메소드가 있던 객체(소스 객체)를 보관하기 위한 final 필드를 하나 만들고 메소드에서 사용되는 임시변수와 파라미터를 위한 필드를 만들어준다.
새로운 클래스에 소스 객체와 파라미터를 취하는 생성자를 만들어준다.
새로운 클래스에 compute라는 이름의 메소드를 만들어 준다.
원래의 메소드를 compute 메소드로 복사한다. 원래의 객체이 있는 메소드를 사용하는 경우 소스 객체 필드를 사용하도록 바꾼다.
컴파일 한다.
새로운 클래스의 객체를 만들고 원래 메소드를 새로 만든 객체의 compute메소드를 호출하도록 바꾼다.
예제
이 리펙토링을 적절히 설명하기 위해서는 아주 긴 예제가 필요하기 때문에
여기서는 이 리펙토링을 실제로 필요하지 않는 간단한 메소드로 설명하고자 한다.
이 메소드는 뭘하는거냐고 묻지 말길 바란다. 그냥 생각나는 대로 만든것이다.
class Account {
int gamma(int inputVal, int quantity, int yearToDate ) {
int importantVaule1 = (inputVal * quantity) + delta();
int importantValue2 = (inputVal * yearToDate) + 100;
if(( yearToDate - importantValue1 ) > 100 )
importantValue2 -= 20;
int importantValue3 = importantValue2 *7;
return importantValue3 - 2 * importantVaule1;
}
}
이 메소드를 메소드 객체로 바꾸기 위해서 새로운 클래스를 만든다.
원래의 객체를 위한 final 필드와 메소드의 파라미터와 임시변수를 위한 필드를 넣는다.
class Gamma
{
private final Account _account;
private int inputVal;
private int quantity;
private int importantValue1;
private int importantValue2;
private int importantValue3;
}
저자는 보통 필드 이름앞에 언더스코어를 붙여 표시한다.
그러나 지금은 각 단계를 작게 하기 위해 이름을 그대로 남겨두었다.
다음과 같이 생성자를 추가한다.
Gamma( Account source, int inputValArg, int quantityArg, int yearToDateArg ) {
_account = source;
inputVal = inputValArg;
quantity = quantityArg;
yearToDate = yearToDateArg;
}
이제 원래의 메소드를 옮길수 있게 되었다.
Account에 있는 메소드를 호출하는 경우에는 _account 필드를 사용하도록 수정해야 한다.
int compute() {
int importantVaule1 = (inputVal * quantity) + _account.delta();
int importantValue2 = (inputVal * yearToDate) + 100;
if(( yearToDate - importantValue1 ) > 100 )
importantValue2 -= 20;
int importantValue3 = importantValue2 *7;
return importantValue3 - 2 * importantVaule1;
}
이렇게 한다음 예전의 메소드가 메소드 객체를 위임하도록 수정한다.
int gamma(int inputVal, int quantity, int yearToDate ){
return new Gamma(inputVal,quantity,yearToDate).compute();
}
이것은 아주 중요한 리팩토링이다. 이 리팩토링은 이점은 compute메소드에서
파라미터를 넘겨주는 것에 대한 걱정이 없이 쉽게 Extract Method를 사용할 수 있다는 것이다
int compute() {
int importantVaule1 = (inputVal * quantity) + _account.delta();
int importantValue2 = (inputVal * yearToDate) + 100;
importantThing();
int importantValue3 = importantValue2 *7;
return importantValue3 - 2 * importantVaule1;
}
void importantThing() {
if(( yearToDate - importantValue1 ) > 100 )
importantValue2 -= 20;
}
9) Substitue Algorithm
알고리즘을 보다 명확한것으로 바꾸고 싶을때는 메소드의 몸체를 새로운 알고리즘으로 바꾼다.
Substitue Algorithm
String foundPerson(String[] people) {
for( int i=0; i< people.length; ++i ) {
if( people[i].equals("Don") ) {
return "Don";
}
if( people[i].equals("John") ) {
return "John";
}
if( people[i].equals("Kent") ) {
return "Kent";
}
}
return " ";
}
=========================================================================
String foundPerson( String[] people ) {
List candidates = Array.asList( new String[]{"Don","John","Kent"} );
for( int i=0; i< people.length; ++i ) {
if(candidates.contains.(people[i]))
return people[i];
}
return " ";
}
동기
어떤 것을 할 때건 한가지 이상의 방법이 있게 마련이다. 그 중 어떤것은 분명 다른 것보다 쉬울 것이다.
알고리즘에서도 마찬가지 이다. 어떤 것을 할 때 더 명확한 방법을 찾게 되면
복잡한 것을 명확한 것으로 바꾸어야 한다.
리팩토링은 복잡한 것을 간단한 조각으로 분해하지만
때로는 전체 알고리즘을 간단한 것으로 바꾸어야 하는 시점에 도달하게 된다.
이런 상황은 문제에 대해서 더 많이 알게 되고 그것을 하기 위해
더 쉬운 방법이 있다는 것을 깨닫게 될 때 발생한다.
또한 여러분의 코드와 중복되는 기능지원하는 라이브러리를 사용하기 시작할 때에도 발생한다.
어떤 때에는 어떤일을 조금 다르게 처리하기 위해 알고리즘을
바꾸고 싶을 때가 있는데 원하는 변경을 하기 위해 먼저 간단한 것으로 치환하는 것이 더 쉽다.
이 단계를 거져야 할 때 가능한 많이 메소드를 분해해 두어야 한다.
아주 크고 복잡한 알고리즘을 치환 하는 것은 매우 어렵다.
따라서 알고리즘을 간단하게 해야 치환을 쉽게 할수 있다.
절차
대체 알고리즘을 준비한다. 적용하여 컴파일 한다.
알고리즘을 테스트 한다. 만약 결과가 같다면 작업은 끝난 것이다.
만약 결과가 같지 않다면 테스트에서 비교하기 위해 예전의 알고리즘을 사용하여 디버깅한다.
예전 알고리즘과 새 알고리즘에 대해 각각의 테스트 케이스를 실행시키고 두 결과를 본다. 이것은 어떤 테스트 케이스가 어떻게 문제를 일으키는지 찾는데 도움을 줄것이다.
[7] 객체간의 기능 이동
객체 디자인에서 가장 기본이 되는 것 중의 하나는 책임을 어디에 둘지를 결정하는 것이다.
이런 문제를 Move Method와 Move Field를 사용해서 동작을 옮김으로써 간단히 해결할 수 있다
둘다 사용해야 할 필요가 있다면 먼저 Move Field를 사용하고 그 다음에 Meve Method를 사용한다.
클래스는 종종 너무 많은 책임으로 비대해진다. 이런 경우에 Extract Class를 사용하여 책임의 일부를 떼어 놓는다.
클래스가 하는 일이 별로 없다면 Inline Class를 사용하여 다른 클래스로 통합시킨다.
실제로는 다른 클래스가 사용되고 있다면 Hide Delegate를 써서 이사실을 숨기는 것이 유용하다.
때로는 위임 크래스를 숨기는 것은 계속해서 소유 클래스의 인터페이스를 변경하는 결과를 초래한다.
Remove Middle Man를 사용할 필요가 있다.
이장의 마지막 두 리팩터링인 Introduce Foreign method와 Introduce Local Extension은 특별한 경우 이다.
어떤 클래스의 소스코드에 접근할 수 있지만 수정할 수 없는 클래스로 책임을 옮기고 싶은 때만 이 방법을 사용한다.
옮기려는 것이 단지 한두개의 메소드 뿐이라면 Introduce Foreign Method를 사용하고 옮기는 것이 많다면
Introduce Local Extension을 사용한다.
1) Move Method
메소드가 자신이 정의된 클래스보다 다른 클래스의 기능을 더 많이 사용하고 있다면
이 메소드를 가장 많이 사용하고 있는 클래스에 비슷한 몸체를 가진 새로운 메소드를 만들어라
그리고 이전 메소드는 간단한 위임으로 바꾸거나 완전히 제거하라
Move Method
동기
메소드를 옮기는 것은 리패고링에서 가장 중요하고 기본이 되는 것이다.
클래스가 너무 많은 동작을 가지고 있거나, 다른 클래스와 공동으로 일하는
부분이 너무 많아서 단단히 결합되어 있을 때 메소드를 옮긴다.
메소드를 옮김으로써 클래스를 더 간단하게 할 수 있고 클래스는
맡고 있는 책임에 대해 더욱 명확한 구현을 가질 수 있게 된다.
옮길만한 메소드를 발견하면, 이 메소드를 호출하는 메소드, 이 메소드가 호출하는 메소드,
그리고 상속 계층에서 이 메소드를 재정의하고 있는 메소드를 살펴본다.
그리고 옮기려고 하는 메소드와 상호작용을 더 많이 하고 있는 것처럼
보이는 클래스를 기초로 하여 계속 진행할지를 평가한다.
절차
소스 클래스에 정의된 소스 메소드에 의해 사용되는 모든 부분을 조사한다
어떤 부분이 지금 옮기려는 메소드에서만 사용된다면 그 부분 또한 같이 옮기는 것이 낫다. 그 부분이 다른 메소드에 의해서도 사용된다면 그 메소드도 같이 옮기는 것을 고려하라. 때로는 한번에 하나씩 옮기는 것보다 관련된 여러 메소드를 한꺼번에 옮기는 것이 더 쉽다
소스 클래스의 서브클래스나 수퍼클래스에서 옮기려고 하는 메소드에 대한 다른 선언이 있는지 확인한다.
다른 선언이 있다면 다형성이 타겟클래스에서도 역시 표현될수 있는 경우에만 해당클래스를 옮길 수 있을 것이다
타겟 클래스에 메소드를 정의한다
타겟 클래스에서 좀더 이해하기 쉬운 다른 이름을 사용하여 메소드를 만들어도 된다
소스 메소드에서 타겟 메소드로 코드를 복사한다. 그리고 그 메소드가 타겟 클래스에서 동작하도록 적절히 수정한다
타겟 메소드가 원래 메소드의 코드를 사용한다면 타겟 메소드에서 소스 객체를 참조하는 방법을 결정할 필요가 있다 타겟 클래스에서 소스 객체를 참조할 방법이 없다면 새로운 메소드에 소스 객체의 참조를 파라미터로 넘겨야 한다
메소드가 예외처리를 포함하고 있다면 논리상 어느 클래스가 예외처리를 해야 하는지를 결정하라
타겟 클래스를 컴파일 한다
소스 클래스에서 적절한 타겟 객체를 참조하는 방법을 결정한다
타겟 클래스를 넘겨주는 필드나 메소드가 소스 클래스에 이미 존재할지도 모른다. 만약 존재하지 않는다면 타겟 클래스를 넘겨주는 메소드를 쉽게 만들 수 있는지 살펴보라. 이것이 쉽지 않으면 타겟 클래스를 저장할 수 있는 필드를 소스 클래스에 만들 필요가 있다. 이 필드는 계속 존재할 수도 있지만 이 필드가 없어져도 될 만큼 충분히 리팩토링 될 때까지 임시로 존재할 수도 있다.
소스 메소드를 위임 메소드로 바꾼다.
컴파일 테스트를 한다.
소스 메소드를 제거할지 위임 메소드로 남겨둘지 결정한다
만약 소스 메소드를 참조하는 부분이 많다면 소스 메소드를 위임메소드로 남겨두는 것이 더 쉬운 방법이다
소스 메소드를 제거한다면 소스 메소드를 참조하고 있는 부분을 타겟 메소드를 참조하도록 수정한다
각각의 참조를 변경한 후에 컴파일과 테스트할 수 있다. 그러나 보통 찾기/바꾸기로 한꺼번에 바꾸는 것이 더 쉽다
컴파일, 테스트를 한다
예제
Account 클래스로 이 리팩토링을 설명한다
class Account {
double overdraftCharge() {
if (_type.isPremium()) {
double result = 10;
if (_daysOverdrawn > 7 ) result += (_daysOverdrawn - 7) * 0.85;
return result;
}
else return _daysOverdrawn * 1.75;
}
double bankCharge() {
double result = 4.5;
if (_daysOverdrawn > 0) result += overdraftCharge();
return result;
}
private AccountType _type;
private int _daysOverdrawn;
}
새로운 계좌타입이 몇가지 추가될 예정이고 이들은 당좌 대월액을 계산하는 각각의 규칙을 가지고 있다고 생각해보자
overdraftCharge 메소드를 AccountType 클래스로 옮기고 싶다
첫번째 단계는 overdraftCharge 메소드가 사용하고 있는 부분들을 살펴보고
관련된 여러개의 메소드를 같이 옮길만한 가치가 있는지 고려하는 것이다
class AccountType {
double overdraftCharge(int daysOverdrawn) {
if (isPremium()) {
double result = 10;
if (daysOverdrawn > 7) result += (daysOverdrawn - 7) * 0.85;
return result;
}
else return daysOverdrawn * 1.75;
}
}
이 경우에 알맞게 고친다는 것은 AccountType클래스의 overdraftCharge 메소드에서 사용하는 _type을 제거하고 여전히 필요로 하는 계좌의 특성을 얻기 위해 어떤일을 한다는 것을 의미한다. 소스 클래스의 일부분을 사용하려 할 때 다음 네 가지 중 한가지를 선택 할 수 있다
(1)이 부분 또한 타겟 클래스로 옮긴다 (2)타겟 클래스에서 소스 클래스를 참조하는 부분을 새로 만들거나 사용한다 (3)메소드에 소스 객체를 파라미터로 넘긴다 (4)사용하려는 부분이 변수라면 파라미터로 넘긴다
이 경우에 변수를 파라미터로 넘겼다
메소드를 타겟 클래스에 알맞게 고치고 타겟클래스를 컴파일 하고 난 후, 소스 메소드 몸체를 간단한 위임으로 대체할 수 있다
class Account {
double overdraftCharge() {
return _type.overdraftCharge(_dayOverdrawn);
}
}
이 시점에서 컴파일, 테스트를 할 수 있다.
이와 같이 메소드를 소스 클래스에 남겨놓았거나 또는 소스 클래스에서 이 메소드를 제거할 수 있다.
메소드를 제거하기 위해서는 이 메소드를 호출하는 모든 부분을 찾아서 AccountType 클래스에 있는
새로운 메소드를 호출하도록 재지정(redirect)해야 한다
class Account {
double bankCharge() {
double result = 4.5;
if(_dayOverdrawn > 0) result += _type.overdraftCharge(_daysOverdrawn);
return result;
}
}
호출하는 부분을 모두 바꾸고 나서 Account 클래스에 있는 메소드 정의를 제거할 수 있다.
하나씩 제거한 후에 컴파일, 테스트를 할 수도 있고, 혹은 한 번에 작업할 수도 있다.
제거한 메소드가 private로 선언된 메소드가 아니라면 다른 클래스에서 이 메소드를 사용하고 있는 지를 찾아야 한다.
엄격한 타입 체크 언어에서는 소스 클래스에서 메소드를 제거한 후 컴파일을 해보면 내가 놓친 모든 부분을 찾을 수 있다.
이 예제와 같은 경우에 메소드는 단지 하나의 필드만 참조했기 때문에 나는 단지 그 필드를 변수로 넘길 수 있었다.
만약 그 메소드가 Account 클래스에 있는 다른 메소드를 호출 했다면 파라미터로 소스 객체를 넘겨야 한다
class AccountType {
double overdraftCharge(Account account) {
if(isPremium()) {
double result = 10;
if (account.getDaysOverdrawn() > 7)
result += (account.getDaysOverdrawn() - 7) * 0.85;
return result;
}
else return account.getDaysOverdrawn() * 1.75;
}
}
또한 소스 클래스의 변수나 메소드를 몇 개 사용할 필요가 있다면 파라미터로 소스 객체를 넘긴다.
타겟 클래스의 새로운 메소드에서 소스 객체를 사용하는 부분이 너무 많아서 그 이상의 리팩토링이 필요하다
할지라도 말이다. 일반적으로는 분해해서 그 일부를 원래대로 옮겨 놔야 한다.
2) Move Field
필드가 자신이 정의된 클래스보다 다른 클래스에 의해 더 많이 사용되고 있다면
타겟 클래스(target class)에 새로운 필드를 만들고 기존 필드를 사용하는 모든 부분을 변경하라
Move Field
동기
어떤 필드가 자신이 속한 클래스보다 다른 클래스의 메소드에서 더 많이 사용되고
있는 것을 보면 그 필드를 옮기는 것을 고려한다
그러는 한편 다른 클래스가 get/set메소드를 통해서 이 필드를 간접적으로
많이 사용하고 있을지도 모른다는 생각도 한다
절차
필드 public으로 선언되어 있으면 Encapsulate Field를 사용한다.
필드에 자주 접근하는 메소드를 옮기려고 하거나 많은 메소드가 이 필드로 접근하고있다면 Self Encapsulate를 사용하는 것이 유용하다는 것이 알게 될 것이다.
컴파일, 테스트를 한다.
타겟 클래스에 필드와 그 필드에 대한 get/set 메소드를 만든다.
타켓 클래스를 컴파일 한다.
소스 클래스에서 타겟 객체를 참조하는 방법을 결정한다
타겟 클래스를 넘겨주는 필드나 메소드가 소스 클래스에 이미 존재할지도 모른다. 만약 존재하지 않는다면 타겟 클래스를 넘겨 주는 메소드를 쉽게 만들 수 있는지 살펴본다. 이것이 실패한다면 소스 클래스에 타겟 클래스를 저장할 수 있는 필드를 만들 필요가 있다. 이 필드는 계속 존재할지도 모르지만 없어져도 될만큼 충분히 리팩토링 될 때까지 임시로 존재할 수도 있다.
소스 클래스에 있는 필드를 제거한다.
소스 필드를 참조하고 있는 모든 부분을 타겟 클래스에 있는 적당한 메소드를 참조하도록 바꾼다
변수에 접근하는 코드는 타겟 객체의 get 메소드를 호출하게 바꾼다. 변수(또는 필드)에 할당하는 코드는 타겟 객체의 set 메소드를 호출하게 한다.
필드가 private로 선언되어 있지 않다면, 필드를 참조하는 부분을 찾기 위해서 소스 클래스의 모든 서브 클래스를 살펴봐야 한다.
컴파일, 테스트를 한다.
예제
여기에 Account 클래스의 일부분이 있다
class Account {
private AccountType _type;
private double _interestRate;
double interestForAmount_days(double amount, int days) {
return _interestRate * amount * days / 365;
}
}
interestRate 필드를 AccountType 클래스로 옮기고 싶다. 이 필드를 참조하고 있는 몇 개의 메소드가 있는데 interestForAmount_days 메소드도 그 중 하나이다.
AccountType 클래스에 필드에 접근자를 만든다
class AccountType {
private double _interestRate;
void setInterestRate (double arg) {
_interestRate = arg;
}
double getInterestRate() {
return _interestRate;
}
}
이 시점에서 새로운 클래스를 컴파일 할 수 있다.
이제 Account 클래스에 있는 메소드가 AccountType 클래스에 있는 메소드를 사용하도록 재지정하고
Account 클래스에 있는 _interestRate 필드를 제거한다. 재지정이 실제로 일어나고 있다는 것을 확실히 하기 위해서
원래 필드를 제거해야 한다. 필드를 제거하고 나서 컴파일을 해 보는 것을 재지정하지 못한 메소드를 찾는데 도움이 된다.
private double _interestRate;
double interestForAmount_days (double amount, int days) {
return _type.getInterestRate() * amount * days / 365;
}
예제 : 자체 캡슐화(self-encapsulation) 사용
많은 메소드가 Account 클래스의 _interestRate 필드를 사용하고 나면 나는 Self Encapsulate Field
를 사용할 것이다.
class Account {
private AccountType _type;
private double _interestRate;
double interestForAmount_days(double amount, int days) {
return getInterestRate() * amount * days / 365;
}
private void setInterestRate (double arg) {
_interestRate = arg;
}
private double getInerestRate () {
return _interestRate;
}
}
이런 방법을 사용하면 접근자를 재지정 하면 된다
double interestForAmount_days(double amount, int days) {
return getInterestRate() * amount * days / 365;
}
private void setInterestRate (double arg) {
_type.setInterestRate(arg);
}
private double getInerestRate () {
return _type.getInerestRate();
}
3) Extract Class
두 개의 클래스가 해야 할 일을 하나의 클래스가 하고 있는 경우
새로운 클래스를 만들어서 관련 있는 필드와 메소드를 예전 클래스에서 새로운 클래스로 옮겨라
Extract Class
동기
클래스는 분명하게 추상화되어야 하고, 몇 가지 명확한 책임을 가져야 한다는 말 또는 이와 비슷한 지침을
들었을 것이다. 실제로 클래스는 점점 커진다. 어떤 동작을 추가할 때도 있고 약간의 데이터를 추가할 때도 있다.
우리는 별도의 클래스로 만들 만한 가치가 없다고 느끼는 책임을 기존 클래스에 추가한다.
클래스는 많은 메소드와 데이터를 가지고 있고 너무 커서 쉽게 이해할 수도 없다.
이제 우리는 그 클래스를 분리할 방법을 생각하고 클래스를 분리해야 한다
데이터의 부분 집합과 메소드의 부분 집합이 같이 몰려다니는 것은
별도의 클래스로 분리할 수 있다는 좋은 신호이다
보통 같이 변하거나 특별히 서로에게 의존적인 데이터의 부분 집합
또한 별도의 클래스로 분리할 수 있다는 좋은 신호이다. 만약 일부 데이터나 메소드를
제거한다면 다른 필드나 메소드가 의미없는 것이 될지를 자신에게 물어보는 것은 편리한 테스트 방법이다
개발의 후반부에 종종 나타나는 신호중의 하나는 클래스가 서브타입이 되는 방법이다.
서브타이핑이 단지 몇몇 기능에만 영향에 미친다는 것을 알게 되거나
또는 어떤 부분은 이런 식으로 서브타입이 되어야 하고
다른 부분은 또 다른 방법으로 서브타입이 되어야 한다는 것을 알게 될 것이다
절차
클래스의 책임을 어떻게 나눌지를 결정하라.
분리된 책임을 떠맡을 새로운 클래스를 만든다.
책임을 분리한 후 이전 클래스의 책임이 이름과 더 이상 맞지 않는다면, 이전 클래스의 이름을 변경한다.
이전 클래스에서 새로 만든 클래스에 대한 링크를 만든다.
양방향 링크가 필요할지도 모른다. 그러나 필요해지기 전에는 새로 만든 클래스에서 이전 클래스로 가는 링크(back link)를 만들지 말라
옮기고가 하는 각각의 필드에 대해 Move Field를 사용한다
각각의 필드를 옮길 때마다 컴파일, 테스트를 한다.
Move Method를 사용해서 이전 클래스에서 새로 만든 클래스로 메소드를 옮긴다. 저수준 메소드(호출하기 보다는 호출되는 메소드)부터 시작해서 점점 고수준의 메소드에 적용한다.
각각의 메소드를 옮길 때마다 컴파일, 테스트 한다.
각 클래스를 검토하고, 인터페이스를 줄인다.
양방향 링크를 가지고 있다면, 단방향 링크로 만들 수 있는 지 알아본다
새로운 클래스를 공개할지 결정한다. 새로운 클래스를 공개하기로 결정했다면, 참조 객체로 드러낼지 또는 불변성 값 객체(immutable value object)로 드러낼지를 결정한다.
예제
간단한 Person 클래스를 가지고 시작한다.
class Person {
public String getName() {
return _name;
}
public String getTelephoneNumber() {
return ("(" + _officeAreaCode + ")" + _officeNumber);
}
String getOfficeAreaCode() {
return _officeAreaCode;
}
void setOfficeAreaCode(String arg) {
_officeAreaCode = arg;
}
String getOfficeNumber() {
return _officeNumber;
}
void setOfficeNumber(String arg) {
_officeNumber = arg;
}
private String _name;
private String _officeAreaCode;
private String _officeNumber;
}
이 경우에 전화번호와 관련된 동작을 별도의 클래스로 분리할 수 있다. TelephoneNumber클래스를 정의한다.
class TelephoneNumber {
}
그리고 Person 클래스에서 TelephoneNumber 클래스에 대한 링크를 만든다.
class Person {
private TelephoneNumber _officeTelephone = new TelephoneNumber();
}
이제 필드중의 하나에 Move Field를 사용한다.
class TelephoneNumber{
String getAreaCode() {
return _areaCode;
}
void setAreaCode(String arg) {
_areaCode = arg;
}
private String _areaCode;
}
class Person {
public String getTelephoneNumber() {
return ("(" + getOfficeAreaCode() + ")" + _officeNumber);
}
String getOfficeAreaCode() {
return _officeTelephone.getAreaCode();
}
void setOfficeAreaCode(String arg) {
_officeTelephone.setAreaCode(arg);
}
}
그런 다음 다른 필드로 옮길 수 있고 Move Method를 사용해서 메소드를 Telephone Number 클래스로 옮길 수 있다
class Person {
public String getName() {
return _name;
}
public String getTelephoneNumber() {
return _officeTelephone.getTelephoneNumber();
}
TelephoneNumber getOfficeTelephone() {
return _officeTelephone;
}
private String _name;
private TelephoneNumber _officeTelephone = new TelephoneNumber();
}
class TelephoneNumber {
public String getTelephoneNumber() {
return ("(" + _areaCode + ") " + _number);
}
String getAreaCode() {
return _areaCode;
}
void setAreaCode(String arg) {
_areaCode = arg;
}
String getNumber() {
return _number;
}
void setNumber(String arg) {
_number = arg;
}
private String _number;
private String _areaCode;
}
이제 새로운 클래스를 클라이언트에게 얼마나 공개할지를 결정해야 한다.
새로운 클래스의 인터페이스에 위임 메소드를 제공함으로써 새로운 클래스를 완전히 숨길 수 있고
또 새로운 클래스를 공개할 수도 있다. 나는 클래스를 같은 패키지에 있는 일부 클라이언트에게만
공개하고 다른 클라이언트에게는 공개하지 않는 것을 택할지도 모른다.
클래스를 공개하려 한다면 aliasing의 위험을 고려할 필요가 있다.
전화번호 객체를 공개하고 있고 클라이언트가 전화번호 객체에서 지역 코드를 변경한다면?
변경해야 하는 주체가 이 메소드를 직접 호출하는 클라이언트가 아닐지도 모른다.
클라이언트의 클라이언트의 클라이언트가 호출할지도 모른다
다음중 하나를 선택할 수 있다
임의의 객체가 TelephoneNumber의 모든 부분을 변경할 수 있다는 사실을 받아들인다. 이것은 TelephoneNumber를 참조객체로 만들어야 하므로 Change Value to Reference를 고려해야 한다. 이 경우 Person 객체가 전화번호에 대한 접근점(access point)이 될것이다.
어느 누구도 Person 객체를 거치지 않고 TelephoneNumber의 속성을 변경하는 것을 원하지 않는다. TelephoneNumber를 불변성(immutable)으로 만들거나 또는 TelephoneNumber에 대한 불변성 인터페이스(immutable interface)만을 제공할 수 있다.
또 다른 방법은 TelephoneNumber를 전달하기 전에 복사하는 것이다. 그러나 이렇게 하면 값을 변경할 수 있다고 생각하기 때문에 혼란을 일으킬 수 있다. 또한 TelephoneNumber가 여러 곳으로 전달된다면 클라이언트 사이에 aliasing 문제를 일으킬지도 모른다.
Extract Class는 두 개의 결과 클래스가 독립된 락을 가지게 하기 때문에 컨커런트 프로그램의 가용성을 향상시키는 일반적인 기법이다. 만약 두개의 객체 모두에 락을 걸 필요가 없다면 그렇게 하지 않아도 된다.
4) Inline Class
클래스가 하는 일이 많지 않은 경우에는
그 클래스에 있는 모든 변수와 메소드를 다른 클래스로 옮기고 그 클래스를 제거하라
Inline Class
동기
Inline Class는 Cxtract Class의 반대이다. 클래스가 더 이상 제 몫을 하지 못하고 더 이상 존재할 필요가 없다면
Inline Class를 사용한다.
절차
흡수하는 클래스에 소스 클래스의 public 필드와 메소드를 선언한다.
소스 클래스 메소드에 대한 인터페이스를 분리하는 것이 이치에 맞다면, 인라인화 하기 전에 Extract Interface 를 사용하라
소스 클래스를 참조하고 있는 모든 부분을 흡수하는 클래스를 참조하도록 변경한다.
패키지 밖에서 참조하는 부분(out-of-package 참조)을 없애기 위해서 소스 클래스를 private로 선언하라. 또한 컴파일러가 소스 클래스에 대한 모든 죽은 참조(dangling reference) 찾도록 소스 클래스의 이름을 변경한다.
컴파일, 테스트 한다.
Move Method와 Move Field를 사용하여, 소스 클래스에 있는 모든 변수와 메소드를 흡수하는 클래스로 옮긴다.
짧고 간단한 장례식을 거행한다.
예제
class Person {
public String getName() {
return _name;
}
public String getTelephoneNumber() {
return _officeTelephone.getTelephoneNumber();
}
TelephoneNumber getOfficeTelephone() {
return _officeTelephone;
}
private String _name;
private TelephoneNumber _officeTelephone = new TelephoneNumber();
}
class TelephoneNumber {
public String getTelephoneNumber() {
return ("(" + _areaCode + ") " + _number);
}
String getAreaCode() {
return _areaCode;
}
void setAreaCode(String arg) {
_areaCode = arg;
}
String getNumber() {
return _number;
}
void setNumber(String arg) {
_number = arg;
}
private String _number;
private String _areaCode;
}
Person 클래스에다가 TelephoneNumber 클래스에 있는 눈에 보이는(TelephoneNumber 밖에서 볼 수 있는)
모든 메소드를 정의한다.
class Person {
String getAreaCode() {
return _officeTelephone.getAreaCode();
}
void setAreaCode(String arg) {
_officeTelephone.SetAreaCode(arg);
}
String getNumber() {
return _officeTelephone.getNumber();
}
void setNumber(String arg) {
_officeTelephone.setNumber(arg);
}
}
TelephoneNumber 클래스의 클라이언트를 찾아서 Person 클래스의 인터페이스를 사용하도록 바꾼다
Person martin = new Person();
martin.getOfficeTelephone().setAreaCode("781");
는 다음과 같이 된다.
Person martin = new Person();
martin.setAreaCode("781");
이제 TelephoneNumber 클래스에 아무것도 남지 않을 때까지 Move Method와 Move Field를 사용할 수 있다.
5) Hide Delegate
클라이언트가 객체의 위임 클래스를 직접 호출하고 있는 경우
서버에 메소드를 만들어서 대리객체(delegate)를 숨겨라
Hide Delegate
동기
캡슐화는 객체에서 가장 중요한 개념 가운데 하나이다. 캡슐화는 객체가 시스템의 다른 부분에 대해
좀 적게 알아도 된다는 것을 의미한다. 캡슐화가 되어 있는 경우에는 어떤 것이 변경되었을때 시스템의
다른 부분이 영향을 덜 받으므로 결과적으로 변경을 좀 더 쉽게 할 수 있게 한다.
자바는 필드가 public으로 선언되는 것을 허용하지만, 객체를 다루는 사람이라면
필드는 숨겨져야 한다는 것을 알고 있다. 여러분은 점점 세련되어질수록 캡슐화 할 수 있는 것이
더 많아진다는 것을 알게 된다.
클라이언트가 서버 객체의 필드에 들어있는 객체에 정의된 메소드를 호출한다면
클라이언트는 대리객체(delegate)에 대해서 알아야 한다
이와 같은 경우에 서버 객체에 간단한 위임 메소드를 두어 위임을 숨김으로서 이런 종속성을 제거할 수 있다.
서버의 일부 또는 모든 클라이언트에 대해서 Extract Class를 사용할 가치 있다는 것을 발견할지도 모른다.
만약 모든 클라이언트에게 실제로 일을 처리하는 부분을 숨기고 있다면
서버의 인터페이스에서 위임과 관련된 모든 부분을 제거할 수 있다.
절차
대리객체의 각각의 메소드에 대해, 서버에서 간단한 위임 메소드를 만든다.
클라이언트가 서버를 호출하도록 바꾼다
클라이언트가 서버와 같은 패키지에 있지 않다면 실제로 일을 처리하는 메소드의 접근 권한을 package로 변경하는 것을 고려하라
각각의 메소드를 알맞게 바꾸고 나서 컴파일, 테스트를 한다.
어떤 클라이언트도 더 이상 대리객체에 접근할 필요가 없다면, 서버 클래스에서 대리객체에 대한 접근자를 제거한다.
컴파일, 테스트를 한다.
예제
Person 클래스와 Department 클래스로 시작한다.
class Person {
Department _department;
public Department getDepartment() {
return _department;
}
public void setDepartment(Department arg) {
_department = arg;
}
}
class Department {
private String _chargeCode;
private Person _manager;
public Department (Person manager) {
_manager = manager;
}
public Person getManager() {
return _manager;
}
}
클라이언트가 어떤 사람의 매니저를 알려고 한다면 먼저 그 사람이 속해 있는 부서를 알 필요가 있다
manager = john.getDepartment().getManager();
이는 Department 클래스가 작동하는 방법과 Department 클래스가 매니저 정보를 관리하는 책임이 있다는
것을 클라이언트에게 드러낸다 Department 클라이언트에게 숨김으로써 이러한 결합을 줄일 수 있다.
Department 클래스를 클라이언트에게 숨김으로써 이러한 결합을 줄일 수 있다. Person 클래스에 간단한 위임 메소드를 만들어서 이와 같은 일을 한다
public Person getManager() {
return _department.getManager();
}
Person 클래스의 모든 클라이언트가 이 새로 만든 메소드를 사용하도록 변경해야 한다
manager = john.getManager();
Department 클래스의 모든 메소드와 Person 클래스의 모든 클라이언트에 대해서 변경한 후에
Person에 있는 getDepartment 접근자를 제거할 수 있다.
6) Remove Middle Man
클래스가 간단한 위임을 너무 많이 하고 있는 경우에는
클라이언트가 대리객체(delegate)를 직접 호출하도록 하라
Remove Middle Man
동기
Hide Delegate를 사용하는 동기를 이야기할 때 대리객체 사용을 캡슐화 하는 것의 장점에 대해서 이야기 했다.
그러나 여기에는 그만한 대가를 치러야 한다. 클라이언트 대리객체의 새로운 메소드를 사용하려 할 때 마다 서버 클래스는 간단한 위임 메소드를 추가해야 하는 것이다. 새로운 메소드를 추가하려면 추가 비용이 들게 된다.
서버 클래스는 단지 미들맨(middle man)에 지나지 않게 되는데 아마도 이때가 클라이언트로 하여금 대리객체를 직접 호출하도록 해야 할 때일 것이다.
어느 정도를 숨기는 것이 적절한지 판단하는 것은 어렵다. 다행히 Hide Delegate와 Remove Middle Man에서는 이것이 별로 중요하지 않다. 시간이 지남에 따라 시스템을 조절할 수 있다. 시스템이 변할수록 얼마나 숨겨야 하는지에 대한 원칙 또한 변경된다. 여섯 달 전에 잘된 것 같은 캡슐화가 지금 와서는 서툴러 보일지도 모른다. 리팩토링은 미리 예상하지 못했던 것에 대해 미안하다고 말할 필요가 없이, 그냥 지금 고치며 된다는 것을 의미한다.
절차
대리객체에 대한 접근자를 만든다.
서버 클래스에 있는 위임 메소드를 사용하는 각각의 클라이언트에 대해 클라이언트가 대리객체의 메소드를 호출하도록 바꾸고 서버 클래스에 있는 메소드를 제거한다
각각의 메소드에 대한 작업을 마칠 때 마다 컴파일, 테스트 한다.
예제
여기서는 Hide Delegate에서의 예제를 반대로 해보자. 먼저 Department 클래스를 감추고 있는 Person 클래스를 살펴보자
class Person {
Department _department;
public Person getManager() {
return _department.getManager();
}
}
class Department {
private Person _manager;
public Department (Person manager) {
_manager = manager;
}
}
클라이언트는 어떤 사람의 매니저를 찾기 위해서 다음과 같이 요청한다
manager = john.getManager();
많은 메소드가 이와 같이 하고 있다면 Person 클래스에 간단한 위임 메소드가 너무 많이 생기게 된다
이때 미들맨을 제거할 좋은 시점이다. 우선 대리객체에 대한 접근자를 만든다
class Person {
public Department getDepartment() {
return _department;
}
}
한번에 하나씩 메소드를 취해 작업한다. Person 클래스에 있는 위임 메소드를 사용하는 클라이언트를 찾아 대리객체를 얻어서 사용하도록 수정한다.
manager = john.getDepartment().getManager();
이제 Person 클래스에서 getManager 메소드를 제거할 수 있다. 컴파일을 해보면 어느 부분을 빠뜨렸는지를 알 수 있다. 편의상 몇몇 위임 메소드를 남겨두고 싶을지도 모른다. 또한 어떤 클라이언트에게는 대리객체를 감추고 다른 클라이언트에게는 대리객체를 보이게 하고 싶을지도 모른다. 그렇다면 일부 위임 메소드를 그대로 남겨두어도 상관없다.
7) Introduce Foreign Method
사용하고 있는 서버 클래스에 부가적인 메소드가 필요하지만 클래스를 수정할 수 없는 경우에는
첫 번째 인자로 서버 클래스의 인스턴스를 받는 메소드를 클라이언트에 만들어라
Introduce Foreign Method
Date newStart = new Date (previousEnd.getYear(), previousEnd.getMonth(), previousEnd.getDate() + 1);
===============================================
Date newStart = nextDay(previousEnd);
private static Date newDay(Date arg) {
return new Date (arg.getYear(),arg.getMonth(),arg.getDate() + 1);
}
동기
모든 서비스를 제공하는 정말로 멋진 클래스를 사용하고 있다. 그러나 꼭 필요하지만
그 클래스가 제공하지 않는 서비스가 하나 있다. 소스 코드를 변경할 수 없다면
부족한 메소드를 클라이언트 쪽에 만들어야 한다.
클라이언트 클래스에서 필요한 메소드를 단지 한 번만 사용한다면 추가 코딩은 큰 문제가 아니고
이런 경우에는 아마도 서버 클래스에 메소드를 추가할 필요가 없을 것이다.
새로 만드는 메소드를 외래 메소드(foreign method)로 만들어서
이 메소드가 실제로는 서버 클래스에 있어야 하는 메소드라는 것을 명확하게 나타낼 수 있다.
만약 서버 클래스의 외래 메소드를 많이 만들어야 한다는 것을 깨닫게 되거나
많은 클래스가 동일한 외래 메소드를 필요로 한다는 것을 알게 된다면
Introduce Local Extension을 대신 사용해야 한다
외래 메소드는 임시 방편이라는 것을 잊지 마라. 만약 할 수 있다면,
외래 메소드를 그들이 원래 있어야 하는 위치로 옮기는 것을 시도해 봐라
코드 소유권이 문제가 된다면 외래 메소드를 서버 클래스의 소유자에게 보내고 그 소유자에게
그 메소드를 구현해 달라고 요청하라
절차
필요한 작업을 하는 메소드를 클라이언트 클래스에 만든다
그 메소드는 클라이언트 클래스의 어떤 부분에도 접근해서는 안된다. 값이 필요하다면 값을 파라미터로 넘겨야 한다.
첫 번째 파라미터로 서버 클래스의 인스턴스를 받도록 한다.
메소드에 '외래 메소드, 원래는 서버 클래스에 있어야 한다.' 와 같은 주석을 달아 놓는다.
이렇게 해두면 나중에 이들 메소드를 옮길 기회가 생겼을 때 텍스트 검색을 이용하여 외래 메소드를 쉽게 찾을 수 있다.
예제
대금 결제일을 연기해주는 코드가 있다. 원래 코드는 다음과 같다
Date newStart = new Date (previousEnd.getYear(), previousEnd.getMonth(), previousEnd.getDate() + 1);
대입문의 우변에 있는 코드를 메소드로 뽑아낼 수 있다. 이 메소드는 Date 클래스의 외래 메소드이다.
Date newStart = nextDay(previousEnd);
private static Date newDay(Date arg) {
//외래 메소드(foreign method), Date에 있어야
return new Date (arg.getYear(),arg.getMonth(),arg.getDate() + 1);
}
8) Introduce Local Extension
사용하고 있는 서버 클래스에 여러 개의 메소드를 추가할 필요가 있지만 서버 클래스를 수정할 수 없는 경우
필요한 추가 메소드를 포함하는 새로운 클래스를 만들어라
이 확장 클래스를 원래 클래스의 서브클래스 또한 래퍼(wrapper) 클래스로 만들어라
Introduce Local Extension
동기
때로는 소스 코드를 수정할 수 없는 경우가 있다. 한두 개의 메소드가 필요하다면
Introduce Foreign Method룰 사용할 수 있다.
객체지향 기술인 서브클래싱과 래핑(wrapping)은 이런 작업을 하는 명확한 방법이다
서브 클래스 또는 래퍼 클래스를 Local Extension이라 부른다.
Local Extension을 사용함으로써 메소드와 데이터가 잘 정의된 단위로 묶어야 한다는 원칙을 지키는 것이다.
서브 클래스와 래퍼 중 하나를 선택해야 할 때 보통 할 일이 적은 서브클래스가 선택한다
서브 클래스를 만들 때 가장 큰 장애물은 객체를 생성할 때에 적용해야 한다는 것이다
서브 클래싱은 그 서브클래스의 새로운 객체를 만들도록 한다. 다른 객체가 예전 객체에 접근하고 있다면
원래의 데이터를 가진 두 개의 객체를 가지고 있는 것이 된다.
원래 객체가 불변성(immutable)이라면 문제가 없다. 안전하게 복사할 수 있다.
원래 객체가 가변성(mutable)이라면 문제가 있는데, 왜냐하면 한 객체에서의
변화가 다른 객체를 변경하지 않기 때문이다. 이런 경우 래퍼를 사용해야 한다. 래퍼를 사용하는 것은 Local Extension을 통해 변경된 사항이 원래 객체에 영향을 미칠 수 있게 하고
원래 객체를 통해 변경된 사항은 래퍼에 영향을 미치게 한다.
절차
원래 클래스의 서브클래스나 래퍼 클래스로 확장 클래스를 만든다
변환 생성자(converting constructor)를 확장 클래스에 추가한다
생성자는 원래 클래스를 인자로 받는다. 서브 클래스 버전은 적당한 수퍼클래스 생성자를 호출한다. 래퍼를 사용할 경우에는 대리객체에 대한 필드를 파라미터로 설정한다.
새로운 기능을 확장 클래스에 추가한다.
필요한 곳에서 원래 클래스를 확장 클래스로 대체한다.
이 클래스에 대해 정의된 외래 메소드를 모두 확장 클래스로 옮기라
예제
JDK 1.0.1과 Date클래스를 가지고 이런 종류의 작업을 꽤 많이 해야 했다
예제 : 서브클래스를 사용하는 경우
class MfDateSub extends Date {
// 생성자 정의
public MfDateSub (String dateString) {
super(dateString);
}
// 변환 생성자 정의
public MfDateSub (Date arg) {
super(arg.getTime())
}
//필요한 외래 메소드 정의
Date nextDay() {
return new Date (getYear(), getMonth(), getDate() + 1);
}
}
예제 : 래퍼 클래스를 사용하는 경우
class MfDateWrap {
private Date _original;
//생성자 정의
public MfDateWrap(String dateString) {
_original = new Date(dateString);
}
//변환 생성자 정의
public MfDateWrap(Date arg) {
_original = arg;
}
//기존 기능 위임 메소드
public int getYear() {
return _original.getYear();
}
//기존 기능 위임 메소드
public boolean equals (MfDateWrap arg) {
return (toDate().equals(arg.toDate()));
}
//필요한 외래 메소드 정의
Date nextDay() {
return new Date (getYear(), getMonth(), getDate() + 1);
}
}
래퍼 클래스 사용할 때 생기는 특별한 문제는 원래 클래스를
인자로 받는 메소드를 다루는 방법이다
원래 클래스와 래퍼 클래스를 똑같이 다룰 수 있어야 해야 하지만 이런 정보를 완전히 숨길 수는 없다
[1] 이 책을 쓴 이유
소프트웨어 패턴이 대단한 점은 그로부터 설계에 대한 여러 유용한 아이디어를 얻을 수 있다는 데 있다
따라서 여러 종류의 패턴을 배우면 훌륭한 소프트웨어 설계자가 될 수 있을 것럼 보인다
과연 그럴까?
패턴은 융통성 있는 프레임 워크와 견고하면서도 확장성 있는 소프트웨어를 개발하는 데 도움이 됐다
그러나 몇 년이 지난후 패턴에 대한 지식이나 패턴을 사용하는 방식이 때로는 일을 지나치게 복잡하게 만들기도 한다는 것을 깨달았다
설계 기술이 향상되면서 나는 패턴을 다른 방식으로 사용하게 됐다. 사전 설계에서 패턴을 사용하거나 또는 패턴을 너무 일찍 코드에 도입하는대 신 특정 패턴이 꼭 필요한 시기에 리패터링을 통해 도입하는 방법을 취했다
과도한 설계
코드를 필요 이상으로 융통성있게 또는 정교하게 만들 때 이를 과도한 설계라고 한다 어떤 프로그래머들은 시스템에 대한 미래의 요구사항을 알고 있다고 믿기 때문에 과도하게 설계한다 현재 설계를 융통성 있고 정교하게 해두어 미래의 요구사항을 수용할 수있게 하는 것이 최선이라고 생각하는 것이다 그럴싸하게 들린다. 여러분이 무당이라고 된다면 말이다.
패턴 만능주의
패턴을 처음 배우기 시작했을 때, 패턴은 객체지향 설계를 위한 융통성 있고 정교하며 심지어 우아하기까지 한 방법으로 보였다. 그래서 나는 패턴을 완전히 정복하고 싶었다. 많은 패턴과 패턴 언어를 철저히 공부한 다음 내가 이미 구축한 시스템을 개선하거나 구축하려는 시스템의 설계를 공식화하는 데 사용했다.
시간이 지나면서 패턴의 강력함에 빠져 간단하게 작성할 수 있는 코드조차 괜히 복잡하게 만들게 되었다
미진한 설계
과도한 설계보다는 미진한 설계가 훨씬 흔하다. 형편없이 설계된 소프트웨어를 만들때 이것을 미진한 설계라 한다
시간도 없고 시간을 내기도 어렵고 리팩터링할 시간도 주어지지 않을 때
어떤 것이 훌륭한 설계인지 모를 때
기존 시스템에 새로운 기능을 급하게 추가해야 할 때
한꺼번에 너무 많은 프로젝트에 참여하야 할 때
테스트 주도 개발과 지속적인 리팩터링
테스트 주도 개발과 지속적인 리팩터링은 프로그래밍을 다음과 같은 대화로 바꿔, 기존 코드를 효율적으로 발전시킬수 있도록 한다
질문. 테스트를 작성함으로써 시스템에 질문한다
대답. 테스트를 통과하는 코드를 작성해 질문에 대답한다
정제. 아이디어를 통합하고, 불필요한 것은 제거하고, 모호한 것은 명확히 해서 대답을 정제한다.
반복. 다음 질문을 물어 대화를 계속한다.
TDD와 지속적인 리팩터링에 대해 켄트 벡이 내건 슬로건 빨강, 초록, 리팩터링 이다.
빨강. 코드가 해야 할 일을 예상하고 이것을 나타내는 테스트를 작성한다. 테스트를 통과하는 코드를 아직 작성하지 않았기 때문에 테스트는 실패할것이다.
초록. 테스트를 통과하도록 임시방편으로라도 프로그램을 작성한다. 이 단계에서는 코드 중복, 단순함, 명확한 설계 같은 것을 고민할 필요가 없다. 그런 설계는 나중에 모든 테스트를 통과한 후 더 좋은 설계를 맘 편하게 테스트할 수 있는 단계가 되면 그 때에 가서 생각할 일 이다.
리팩터링. 테스트를 통과한 코드의 설계를 개선한다.
리팩터링과 패턴
디자인 패턴의 모든 패턴에는 의도라는 절이 있는데 다음 질문에 대답하는 간단한 문장이다. 이 디자인 패턴이 무엇을 하는 가? 근본적 이유와 의도는 무엇인가? 이 패턴이 설계 이슈나 문제는 무엇인가?
디자인 패턴에 대한 의도 절은 패턴이 해결하는 주된 문제를 그저 암시할 뿐인 경우가 많다 대신 패턴이 하는 일에 초점이 맞춰져 있다
Template Method 패턴의 의도
하나의 오퍼레이션에 알고리즘의 뼈대를 정의하고, 몇몇 단계는 서브클래스로 미룬다. Template Method 패턴을 이용하면, 알고리즘의 구조를 바꾸지 않고도 서브클래스에서 알고리즘의 특정 단계를 재정의할 수 있게 된다.
State 패턴의 의도
내부 상태가 변했을 때 객체 자신이 그 동작을 바꿀 수 있도록 한다. 객체가 자신의 클래스를 바꾼 것처럼 보일 것이다
의도를 읽으며 특정 패턴이 주어진 상황에 적절한지 궁리한다. 이런 방법은 패턴을 선택하는 데 별 도움이 되지 않는다. 주어진 설계 문제를 각 패턴이 다루는 문제와 대응시켜 보는 것이 낫다. 왜 그럴까? 패턴은 문제를 풀기 위해 존재하며 주어진 상황에서 패턴이 정말 도움이 되는지 이해하는 것과 관계가 깊기 때문이다
[Design Patterns]
우리의 디자인 패턴은 리팩터링의 결과로 나온 구조를 반영한다
따라서 디자인 패턴은 리팩터링의 목표점이 되는 것이다
[Refactoring]
패턴과 리팩터링 사이에는 자연스런 관계가 있다.
패턴은 도달하고 싶은 곳이고 리팩터링은 그곳으로 가는 방법이다
발전적 설계
'리팩터링의 결과로 나온 구조'인 패턴에 많이 익숙한 지금, 나는 패턴의 최종 결과나 그 결과의 구현이 의미하는 바를 이해하는 것보다 패턴을 목표로 한 또는 패턴을 지향한 리팩터링을 하는 이유를 이해하는 것이 훨씬 가치 있다고 생각한다.
[2] 리팩터링
마틴 파울러의 리팩토링의 원리와 함께 읽으면 좋다
리팩터링이란?
리팩터링을 하는 이유
새로운 코드를 더 쉽게 추가할 수 있도록 하기 위해
기존 코드의 설계를 개선하기 위해
기존 코드를 더 잘 이해하기 위해
덜 짜증나는 코드로 만들기 위해
많은 눈
미국 독립선언서 초안이었을때 토마스 제퍼슨 옆에 앉아 있던 벤저민 프랭클린이 제퍼슨의
'우리는 이런 진실이 신성하고 부인할 수 없다고 믿는다' 문구를
'우리는 이런 진실이 자명하다고 믿는다' 라고 교정 했다
가게 간판의 구도
john Thompson, 모자 제작자.
현금을 받고 모자를 만들거나 판매함
친구 1 : 모자를 만들거나, 모자 제작자 중복 '모자 제작자' 문구 삭제
천구 2 : 고객은 누가 모자를 만들든 상관하지 않을것 '만들거나' 문구 삭제
친구 3 : 모자를 신용 판매할 것은 아닐테니 '현금을 받고' 문구 삭제
친구 4 : 모자를 나눠줄거라 생각하지 않아 '모자를 판매함' 문구 삭제
코드에서도 마찬가지다. 최상의 리팩터링 결과를 얻으려면 많은 눈의 도움을 받는 것이 좋다
eXtreme Progamming가 짝 프로그래밍과 코드의 공동소유를 주장하는 것도 같은 이유
사람이 읽기 쉬운 코드
Ward Cunningham의 코드 중에서
november( 20, 2005 );
public void Date november(int day, int year)
단순하고 이해하기 쉽게 작성하는 것이 신경 쓰지 않는다면...
java.util.Calendar c = java.util.Calendar.getInstance();
c.set(2005,java.util.Calendar.NOVEMBER, 20);
c.getTime();
구어처럼 읽힌다 //영어에서는 월 일 연도 순으로 쓴다
복잡한 코드로 부터 중요한 부분을 분리했다.
Wall Street의 대형 은행에서 대출 위험을 계산하는, 스파게티 처럼 얽혀있는 Turbo Pascal 코드에서 w44()함수를 발견했다
w => with
44 => 쉼표의 아스키 코드
숫자를 쉼표 포매팅을 해서 리턴하는 함수
깔끔하게 유지하기
작은 단계
설계 부채
새로운 아키텍처 발전시키기
복함 리팩터링과 테스트 주도 리팩터링
복합 리팩터링의 장점
리팩터링 도구
[3] 패턴
패턴이란?
건축가, 교수이자 사회 평론가인 크리스토퍼 알렉산더(Christopher Alexander)는 그의 두 대표작 [A Timeless Way of Buiding]와 [A Pattern Language]로 소프트웨어 패턴 운동에 영감을 불어넣었다 1980년대 말 부터, 다년간의 개발 경험이 있는 소프트웨어 실무자들은 알렉산더의 저작을 공부했고 패턴과 패턴언어(복잡하게 얽혀있는 패턴 네트워크) 형태로 지식을 공유하기 시작했다
패턴중독
패턴을 구현하는 다양한 방법
패턴 목표, 패턴 지향, 패턴 제거 리팩터링
[2] 의미 있는 이름
소프트웨어에서 이름은 어디서나 쓰인다. 변수, 함수, 인수와 클래스, 패키지, 원시파일, 원시파일이 담긴 디렉토리...
이렇듯 많이 사용하므로 이름을 잘 지으면 여러모로 편하다. 이름을 잘 짓는 간단한 규칙 몇 가지를 소개 한다.
의도를 분명히 밝혀라
변수, 함수, 클래스 이름은 다음 질문에 답해야 한다.
변수(함수,클래스) 존재 이유는? 수행 기능은? 사용 방법은?
주석이 필요한다면 의도를 분명히 드러내지 못했다는 소리다.
int d; // 경과 시간(단위: 날짜 수)
측정하려는 값과 단위를 표현하는 이름이 필요하다.
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;
다음 코드는 목적이 무엇일까?
public List<int[]> getThem() {
List<int[]> list1 = new ArrayList<int[]>();
for (int[] x : theList)
if (x[0] == 4)
list1.add(x);
return list1;
}
코드가 하는 일을 짐작하기 어렵다. 왜일까? 문제는 코드의 단순성이 아니라 코드의 함축성이다.
코드 맥락이 코드 자체에 명시적으로 드러나지 않는다. 위 코드는 독자가 다음 정보를 안다고 명시적으로 가정한다.
theList에 무엇이 들었는가?
theList에서 0번째 값이 어째서 중요한가?
값4는 무슨 의미인가?
함수가 반환하는 리스트는 list1을 어떻게 사용하는가?
지뢰찾기 게임으로 가정하자
theList가 게임판이라는 사실을 안다 theList를 gameBoard로 바꿔보자
게임판에서 각 칸은 단순 배열로 표현한다. 배열에서 0번째 값은 칸 상태를 뜻한다.
값4는 깃발이 꽂힌 상태를 가리킨다
public List<int[]> getFlaggedCells() {
List<int[]> flaggedCells = new ArrayList<int[]>();
for (int[] cell : gameBoard)
if (cell[STATUS_VALUE] == FLAGGED)
flaggedCells.add(cell);
return flaggedCells;
}
코드는 명확해졌다
int 배열을 사용하는 대신 칸들 간단한 클래스로 만들어도 되겠다
isFlagged라는 좀 더 명시적인 함수를 이용해 FLAGGED라는 상수를 감추어도 좋겠다
public List<Cell> getFlaggedCells() {
List<Cell> flaggedCells = new ArrayList<Cell>();
for (Cell cell : gameBoard)
if (cell.isFlagged())
flaggedCells.add(cell);
return flaggedCells;
}
그릇된 정보를 피하라
나름대로 널리 쓰이는 의미가 있는 단어를 다른 의미로 사용해도 안된다.
예를 들어 hp,aix,sco는 변수 이름으로 적합하지 못하다.
유닉스 플랫폼이나 유닉스 변종을 가리키는 이름이기 때문이다
직각 삼각형의 사변hypotenuse을 구현할때 hp가 훌륭한 약어로 보일지라도
hp라는 변수는 독자에게 그릇된 정보를 제공한다.
여러 계정을 묶을때 실제로 List가 아니라면 accountList라고 명명하지 않는다.
프로그래머에게 List라는 단어는 특수한 의미이다.
서로 흡사한 이름을 사용하지 않도록 주의 한다.
한 모듈에서 XYZControllerForEfficientHandlingOfStrings 라는 이름을 사용하고
조금 떨어진 모듈에서 XYZControllerForEfficientStorageOfStrings 라는 이름을 사용한다면?
차이를 알아챘는가? 두단어는 겁나게 비슷하다
그릇된 정보를 제공하는 끔찍한 예가 소문자L 이나 대문자O 변수다
두 변수를 한꺼번에 사용하면 더욱 끔찍해진다.
int a = l;
if( O == l)
a = O1;
else
l = Ol;
의미 있게 구분하라
컴파일러를 통과할지라도 연속된 숫자를 덧붙이거나 불용어(noise word)를 추가하는 방식은 적절하지 못하다.
이름이 달라야 한다면 의미도 달라져야 한다.
public static void copyChars(char a1[], char a2[])
{
for( int i =0; i < a1.length; i++ )
{
a2[i] = a1[i];
}
}
함수 인수 이름으로 source와 destination을 사용한다면 코드를 읽기가 훨씬 쉬워진다.
Product라는 클래스가 있다고 해서 다른 클래스를 ProductInfo혹은 ProductData라 부른다면 개념을 구분하지 않은채 이름만 달리한 경우다. Info나 Data는 a, an, the 와 마찬가지로 의미가 불분명한 불용어다.
a나 the와 같은 접두어를 사용하지 말라는 소리가 아니다. 의미가 분명히 다르다면 사용해도 무방하다.
예를 들어 모든 지역 변수는 a를 사용하고 모든 함수 인수 the를사용해도 되겠다.
그러나 zork라는 변수가 있다는 이유만으로 theZork이라 이름을 지어서는 안된다는 소리다
불용어는 중복이다 변수 이름에 variable이라는 단어는 단연코 금물
표 이름에 table이라는 단어도 마찬가지다
NameString이 Name보다 뭐가 나은가? Name이 부동소수가 될 가능성이 있던가?
Customer 라는 클래스와 CustomerObject라는 클래스를 발견했다면 차이를 알겠는가?
발음하기 쉬운 이름을 사용해라
사람들은 단어에 능숙하다. 우리 두뇌에서 상당 부분은 단어라는 개념만 전적으로 처리한다.
그리고 정의상으로 단어는 발음이 가능하다. 말을 처리하려고 발달한 두뇌를 활용하지 않는다면 안타까운 손해다.
그러므로 발음하기 쉬운 이름을 선택한다. 발음하기 어려운 이름은 토론하기도 어렵다.
검색하기 쉬운 이름으로 사용해라
문자 하나를 사용하는 이름과 상수는 텍스트 코드에서 쉽게 눈에 띄지 않는다는 문제점이 있다.
MAX_CLASSES_PER_STUDENT는 찾기 쉽지만 숫자 7은 은근히 까다롭다.
인코딩을 피하라
유형이나 범위정보까지 인코딩에 넣으면 그만큼 해독하기 어려워진다.
헝가리식 표기법
과거 윈도우 C API는 헝가리식 표기법을 매우 중요하게 여겼다. 윈도우 C API는 모든 변수가
정수 핸들, long 포인터, void 포인터, (속성과 용도가 다른)여러'문자열'중 하나였다.
당시에는 컴파일러가 타입을 점검하지 않았으므로 프로그래머는 타입을 기억할 단서가 필요했다.
현대 언어는 훨씬 많은 타입을 지원한다. 또한 컴파일러는 타입을 기억하고 강제한다.
게다가 클래스와 함수는 점차 작아지는 추세다. 변수 선언 위치와 사용하는 위치가 멀지 않다.
멤버 변수 접두어
멤버변수에 m_ 이라는 접두어를 붙일 필요도 없다. 멤버변수를 다른 색상으로
표시하거나 눈에 띄게 보여주는 IDE를 사용해야 마땅하다
사람들은 접두어(또는 접미어)를 무시하고 이름을 해독하는 방식을 재빨리 익힌다.
코드를 읽을 수 록 접두어는 관심 밖으로 밀려난다.
결국은 접두어는 옛날에 작성한 구닥다리 코드라는 징표가 되어 버린다.
인터페이스 클래스와 구현 클래스
때로는 인코딩이 필요할 경우도 있다. 예를 들어 도형을 생성하는 추상 팩토리(Abstract Factory)를
구현한다고 가정하자. 이 팩토리는 인터페이스 클래스(Interface Class)다.
구현은 실제 클래스(Concrete Class)에서 한다.
인터페이스 접두어 I는 잘해봤자 주의를 흩트리고 나쁘게는 과도한 정보를 제공한다.
클린코드 필자는 인터페이스라는 사실을 남에게 알리고 싶지 않다.
인터페이스 클래스느느 ShapeFactory 실제 클래스는 ShapeFactoryImp 라고 명명한다.
자신의 기억력을 자랑하지 마라
독자가 코드를 읽으면서 변수 이름을 자신이 아는 이름으로 변환해야 한다면 그 변수 이름은 바람직하지 못하다.
이는 일반적으로 문제 영역이나 해법 영역에서 사용하지 않는 이름을 선택했기 때문에 생기는 문제다
문자 하나를 사용하는 변수 이름은 문제가 있다. 루프에서 반복 횟수를 세는 변수 i,j,k는 괜찮다(l은 안된다)
단, 루프 범위가 아주 작고 다른 이름과 충돌하지 않을 때만 괜찮다.
일반적으로 프로그래머들은 아주 똑똑하다. 때때로 똑똑한 사람은 자신의 정신적 능력을 과시하고 싶어 한다.
똑똑한 프로그래머와 전문가 프로그래머 사이에서 다른 점 하나를 들자면 전문가 프로그래머는 명료함이 최고라는 사실을 이해한다. 전문가 프로그래머는 자신의 능력을 좋은 방향으로 사용해 남들이 이해하는 코드를 내놓는다.
클래스 이름
클래스 이름과 객채 이름은 명사나 명사구가 적합하다
Customer, WikiPage, Account, AddressParser 등이 좋은 예다.
Manager, Processor, Data, Info 등과 같은 단어는 피하고 동사는 사용하지 않는다.
메소드 이름
메소드 이름은 동사나 동사구가 적합하다.
postPayment, deletePage, save등이 좋은 예이다.
접근자Accessor, 변경자Mutator, 조건자Predicate, 자바 빈javabean 표준에 따라 값 앞에 get, set, is를 붙인다.
생성자Constructor를 중복해 정의overload 할때 정적 팩토리 메소드를 사용한다.
메소드는 인수를 설명하는 이름을 사용한다.
정적 펙토리 메소드 (생성자 보다 낫다)
생성자
Complex fulcrumPoint = Complex.FromRealNumber(23.0);
Complex fulcrumPoint = new Complex(23.0);
생성자 사용을 제한하려면 해당 생성자를 private로 선언한다.
기발한 이름은 피하라
간혹 프로그래머가 재치를 발휘해 구어체나 속어를 이름으로 사용하는 사례가 있다.
예를 들어 kill()대신 whack()이라고 부르거나 abort()대신 eatMyShort() 라고 부른다.
특정 문화에서만 사용하는 농담은 피하는 편이 좋다.
의도를 분명하고 솔직하게 표현하라
개변 하나에 단어 하나를 사용해라
추상적인 개념 하나에 단어 하나를 선택해 이를 고수 한다.
예를 들어 똑같은 메소드를 fetch, retrieve, get이라고 제각각 부르면 혼란스럽다.
이클립스, IntelliJ등 과 같은 신형 IDE는 문맥에 맞는 단서를 제공한다.
메소드 이름은 독자적이고 일관적이여야 한다.
그래야 주석을 뒤져보지 않고도 프로그래머가 올바른 메소드를 선택할 수 있다.
동일한 코드 기반에 controller, manager, driver를 섞어 쓰면 혼란스럽다.
말장난을 하지 마라
한단어를 두가지 목적으로 사용하지 마라.
다른 개념에 같은 단어를 사용한다면 그것은 말장난에 불과하다.
예를 들어 지금까지 구현한 add 메소드는 모두가 기존 값 두개를 더하거나 이어서 새로운 값을 만든다고 가정하자
새롭게 작서하는 메소드는 집합에 값을 하나를 추가한다.
이 메소드의 이름은 insert나 append라는 이름이 적당하다
해법 영역에서 사용하는 이름을 사용하라
코드를 읽을 사람도 프로그래머라는 사실을 명심한다.
전산용어, 알고리즘 이름, 패턴이름, 수학용어 등을 사용해도 괜찮다.
무조건 문제 영역에서 모든 이름을 가져오면 같은 개념을 다른 이름으로 이해하던
동료들이 매번 고객에게 의미를 물어야 하므로 현명하지 못하다.
비지터Vistor 패턴에 친숙한 프로그래머는 AccountVisitor라는 이름을 금방 이해한다.
JobQueue 등 기술적인 개념에는 기술적인 이름이 가장 적합한 선택이다.
문제 영역과 관련 있는 이름을 사용하라
적절한 '프로그래머 용어'가 없다면 문제 영역에서 이름을 가져온다.
그러면 코드를 유지 보수하는 프로그래머가 분야 전문가에게 의미를 물어 파악할 수 있다.
우수한 프로그래머와 설계자라면 해법 영역과 문제 영역을 구분할 줄 알아야 한다.
의미있는 맥락을 추가하라
스스로 의미가 분명한 이름이 없지 않다. 하지만 대다수 이름은 그렇지 못하다.
그래서 클래스, 함수, 이름공간에 넣어 맥락을 부여한다.
모든 방법이 실패하면 마지막수단으로 접두어를 붙인다.
예를 들어 firstName, lastName, street, houseNumber, city, state, zipcode라는 변수가 있다.
변수를 훓어보면 주소라는 사실이 금방 알아챈다.
하지만 어느 메소드가 state라는 변수 하나만 사용한다면? 변수 state가 주소 일부라는 사실을 금방 알아챌까?
addr 접두어를 추가해 addrFirstName, addrLastName, addrState라고 쓰면 맥락이 분명해진다.
불필요한 맥락을 없애라
고급 휘발유 충전소Gas Station Deluxe 라는 응용 프로그램을 짠다고 가정하자
모든 클래스의 이름을 GSD로 시작하겠다는 생각은 전혀 바람직하지 못하다.
IDE에서 G를 입력하고 자동완성키를 누르면 IDE는 모든 클래스를 열거 한다.
IDE는 개발자를 도우려는 도구다. IDE를 방해할 이유가 없다.
[3] 함수
프로그래밍 초창기에는 시스템을 루틴과 하위 루틴으로 나눴다.
포트란과 PL/1 시절에는 시스템을 프로그램, 하위 프로그램, 함수로 나눴다.
지금은 함수만 남았다. 이 장은 함수를 잘 만드는 법을 소개한다.
우선 샘플 코드 부터 살펴보자
길이가 길고, 중복된 코드에 괴상한 문자열에 낯설고 애매한 자료 유형과 API가 많다
//HtmlUtil.java (FitNesse 20070619)
public static String testableHtml( PageData pageData, boolean includeSuiteSetup ) throws Exception
{
WikiPage wikipage = pageData.getWikiPage();
StringBuffer buffer = new StringBuffer();
if( pageData.hasAttribute("Test") )
{
if( includeSuiteSetup )
{
WikiPage suiteSetup =
PageCrawlerImpl.getInheritedPage( suiteResponder.SUITE_SETUP_NAME, wikiPage );
if( suiteSetup != null )
{
WikiPagePath pagePath = suiteSetup.getPageCrawler().getFullPath( suiteSetup );
String pagePathName = PathParser.render( pagePath );
buffer.append("!include -setup .").append(pagePathName).append("\n");
}
}
WikiPage setup = PageCrawlerImpl.getInheritedPage( "Setup", wikiPage );
if( setup != null )
{
WikiPagePath setupPath = suiteSetup.getPageCrawler().getFullPath( setup );
String setupPathName = PathParser.render( setupPath );
buffer.append("!include -setup .").append(pagePathName).append("\n");
}
}
buffer.append( pageData.getContent() );
if( pageData.hasAttribute("Test") )
{
WikiPage teardown = PageCrawlerImpl.getInheritedPage( "TearDown", wikiPage );
if( teardown != null )
{
WikiPagePath tearDownPath = wikiPage.getPageCrawler().getFullPath( teardown );
String tearDownPathName = PathParser.render( tearDownPath );
buffer.append("\n").append("!include -teardown .")
.append(tearDownPathName).append("\n");
}
if( includeSuiteSetup )
{
WikiPage suiteTeardown =
PageCrawlerImpl.getInheritedPage( SuiteResponder.SUITE_TEARDOWN_NAME, wikiPage );
if( suiteTeardown != null )
{
WikiPagePath pagePath = suiteSetup.getPageCrawler().getFullPath( suiteTeardown );
String pagePathName = PathParser.render( pagePath );
buffer.append("!include -teardown .").append(pagePathName).append("\n");
}
}
}
pageData.setContent( buffer.toString() );
return pageData.getHtml();
}
두겹으로 중첩된 if문, 이상한 플래그, 이상한 문자열, 이상한 함수를 사용한다.
WikiPage 만들고 WikiPagePath 만들고 buffer에 문자열 추가되는 로직이 중복된다
메소드 몇 개를 추출하고 이름 몇개 변경하고 구조를 조금 변경 한 코드를 살펴보자
public static String renderPageWithSetupsAndTeardowns
( PageData pageData, boolean isSuite ) throws Exception
{
boolean isTestPage = pageData.hasAttribute("Test");
if( isTestPage )
{
WikiPage testPage = PageData.getWikiPage();
StringBuffer newPageContent = new StringBuffer();
includeSetupPages( testPage, newPageContentm isSuite );
newPageContent.append( pageData.getContent() );
includeTeardownPages(newPageContent.toString() );
}
return pageData.getHtml();
}
첫번째 코드에서 파악하기 어려웠던 정보가 두번째코드에서 쉽게 드러낸다
두번째 함수가 읽기 쉽고 이해하기 쉬운 이유는 무엇일까?
의도를 분명히 표현하는 함수는 어떻게 구현할 수 있을까?
함수에 어떤 속성을 부여해야 처음 읽는 사람이 프로그램 내부를 직관적으로 파악할 수 있을 까?
작게 만들어라
함수를 만드는 첫번째 규칙은 '작게!'다
한 화면에 가로 150자 넘어서는 안된다 세로 20도 길다 더 작게 만들어라
함수가 얼마나 짧아야 하냐고?
서두에서 제시했던 두번째 코드를 다시 줄였다 이런 크기가 좋다
public static String renderPageWithSetupsAndTeardowns
( PageData pageData, boolean isSuite ) throws Exception
{
if( isTestPage( pageData ) )
includeSetupAndTeardownPages( pageData, isSuite );
return pageData.getHtml();
}
다시말해 if문/else문/while문 등에 들어가는 블록은 한줄이여야 한다는 의미다.
그래야 함수는 읽고 이해하기 쉬워진다.
한가지만 해라
다음은 지난 30여 년 동안 여러가지 다양한 표현으로 프로그래머들에게 주어진 충고다.
함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한가지만 해야 한다.
함수가 지정된 함수 이름 아래에서 추상화 수준이 하나이 단계만 수행 한다면 그 함수는 한가지 작업만 한다.
어쨌거나 우리가 함수를 만드는 이유는 큰 개념을 (다시말해, 함수 이름을) 다음 추상화 수준에서 여러 단계로
나눠 수행하기 위해서가 아니던가
함수가 '한 가지'만 하는지 판단하는 방법이 하나 더 있다. 단순히 다른 표현이 아니라
의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러가지 작업을 하는 셈이다.
함수당 추상화 수준은 하나로
함수가 확실히 '한가지' 작업만 하려면 함수 내 모든 문장이 동일한 추상화 수준에 있어야 한다.
한 함수 내에서 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다. 특정 표현이 근본 개념인지 세부 사항인지
구분하기 어려운 탓이다.
위에서 아래로 코드 읽기 : 내려가기 규칙
코드는 위에서 아래로 이야기처럼 읽혀야 좋다. 한 함수는 다음에는 추상화 수준이 낮은 함수가 온다.
즉, 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한번에 한단계씩 낮아진다.
필자는 이것을 내려가기 규칙이라 부른다.
Switch 문
switch문은 작게 만들기 어렵다. case 분기가 단 두개인 switch 문도 필자의 취향에는 너무 길며
단일 블록이나 함수를 선호한다.
4. 클린 코드
각 챕터마다 분량이 짧습니다 그래서 먼저 정리하려고 합니다
여기는 자바코드를 예제로 합니다
(저는 C++를 하기에 개인적으로 아쉽습니다)
Clean Code 클린 코드 : 애자일 소프트웨어 장인 정신 / 케이앤피북스 / 로버트 C. 마틴 저, 박재호 이해영 역
댓글 없음:
댓글 쓰기