자바로 배우는 리팩토링 입문, 유키 히로시 지음, 서수환 옮김, 길벗 출판
리팩토링이란 외부에서 보는 프로그램 동작은 바꾸지 않고 프로그램의 내부 구조를 개선하는 것입니다.
- 28p
31p
소스 코드를 보다가 이 말이 입에서 튀어나오면 '리팩토링이 필요하겠구나'라고 생각하면 됩니다.
- 겹치잖아!
- 너무 길어!
- 너무 많아!
- 이름이 안 맞잖아!
- 너무 공개적이잖아!
- 객체 지향답지 않아!
- 34p
스텝 바이 스텝 : 두 가지 수정을 한꺼번에 하지 않기
스텝 바이 스텝 : 되돌리기 쉽게 하기
- 39p
실제 리팩토링은 설계 개선으로도 볼 수 있습니다.
- 43p
매직 넘버를 기호 상수로 치환
상수 의존 관계
public static final int MAX_INPUT_LENGTH = 100;
public static final int WORK_AREA_LENGTH = MAX_INPUT_LENGTH * 2;
- 55p
명령어를 나타내는 기호 상수로 WALK, STOP, JUMP라는 enum을 선언합니다.
public class Robot {
private final String _name;
public enum Command {
WALK,
STOP,
JUMP
};
- 60p
제어 플래그 삭제
제어 플래그 할당을 break나 return으로 바꿔 쓰면 프로그램 가독성이 좋아지기도 합니다. 그 이유는 break나 return을 본 순간 이후에 오는 코드를 읽지 않아도 되는 경우가 많기 때문입니다.
- 79p
제어 플래그가 public 인스턴스 필드라면 더욱 문제입니다. 제어 플래그를 변경하는 코드가 프로그램 전체에 등장할 가능성이 있기 때문입니다.
- 80p
정규 표현식 패키지 사용
private Map<String,String> _map = new HashMap<String,String>();
private static Pattern _pattern = Pattern.compile("([^=]+)=(.*)");
public SimpleDatabase(Reader r) throws IOException {
BufferedReader reader = new BufferedReader(r);
while (true) {
String line = reader.readLine();
if (line == null) {
break;
}
Matcher matcher = _pattern.matcher(line);
if (matcher.matches()) {
String key = matcher.group(1);
String value = matcher.group(2);
_map.put(key, value);
}
패턴을 Pattern.compile 메서드로 컴파일한 후 Matcher 클래스로 패턴이 일치하는지 확인합니다.
-83p
어서션 도입
예를 들어 프로그램의 어떤 부분에서 변수 value 값이 참이어야 한다고 합시다. 만약 value 값이 참이 아니라면 프로그래머의 의도와는 다른 동작이 됩니다. 자바에서는 'value' 값이 참이어야 한다'라는 건 다음과 같이 작성합니다.
assert value > 0;
- 88p
널 객체 도입
1) 널 객체 클래스 작성
public class NullLabel extends Label {
}
2) isNull 메서드 작성
public class Label {
...
public boolean isNull() {
return false;
}
}
- 114p
public static Label newNull() {
return new Nullable();
}
이렇게 하고 나면
new Nullable()
위의 표현식 대신에 다음과 같은 코드를 작성할 수 있습니다.
Label.newNull()
...
팩터리 메서드 패턴과 싱글톤 패턴을 조합해서 NullLabel 클래스를 Label 클래스 안에 중첩 클래스로 구현한 코드
public class Label {
private final String _label;
public Label(String label) {
_label = label;
}
public void display() {
System.out.println("display: " + _label);
}
public String toString() {
return "\"" + _label + "\"";
}
public boolean isNull() {
return false;
}
// 팩토리 메서드
public static Label newNull() {
return NullLabel.getInstance();
}
// 널 객체
private static class NullLabel extends Label {
// 싱글톤
private static final NullLabel singleton = new NullLabel();
private static NullLabel getInstance() {
return singleton;
}
private NullLabel() {
super("(none)");
}
@Override public void display() {
}
@Override public boolean isNull() {
return true;
}
}
}
- 120 ~ 122p
메서드 추출
역 리팩토링
메서드 추출 <-> 메서드 인라인화
- 140p
클래스 추출
불변 인터페이스라는 방법을 사용하면 일반 인스턴스 내용을 읽기 전용으로 만들 수 있습니다.
public interface ImmutableAuthor {
public String getName();
public String getMail();
}
- 163p
저자 정보를 변경하면 곤란한 상황일 경우에는 Author 가 아닌 Immutable Author 객체로 _author 필드값을 넘깁니다.
public class Book {
...
private Author _author;
...
public ImmutableAuthor getAuthor() {
return _author;
}
- 163p
분류 코드를 클래스로 치환
public class Item {
public static final int TYPECODE_BOOK = 0;
public static final int TYPECODE_DVD = 1;
public static final int TYPECODE_SOFTWARE = 2;
private final int _typecode;
private final String _title;
private final int _price;
... 상품 분류 코드를 가리킵니다.
- 174p
public class ItemType {
public static final ItemType BOOK = new ItemType(0);
public static final ItemType DVD = new ItemType(1);
public static ...
이건 int 타입이 아닌 ItemType입니다. BOOK, DVD, SOFTWARE라는 인스턴스가 분류 코드를 대신합니다.
private ItemType(int typecode) {
_typecode = typecode;
}
ItemType 클래스 인스턴스에서 분류 코드를 얻는 게터 메서드를 만듭니다.
- 177p
분류 코드를 하위 클래스로 치환
public static void main(String[] args) {
Shape line = Shape.createShape(Shape.TYPECODE_LINE, 0, 0, 100, 200);
Shape rectangle = Shape.createShape(Shape.TYPECODE_RECTANGLE, 10, 20, 30, 40);
Shape oval = Shape.createShape(Shape.TYPECODE_OVAL, 100, 200, 300, 400);
Shape[] shape = {
line, rectangle, oval
};
for (Shape s : shape) {
s.draw();
}
public static Shape createShape(int typecode, int startx, int starty, int endx, int endy) {
switch (typecode) {
case TYPECODE_LINE:
return new ShapeLine(startx, starty, endx, endy);
case TYPECODE_RECTANGLE:
return new ShapeRectangle(startx, starty, endx, endy);
case TYPECODE_OVAL:
return new ShapeOval(startx, starty, endx, endy);
default:
throw new IllegalArgumentException("typecode = " + typecode);
}
}
- 202 ~ 203p
switch 문과 instanceof 연산자가 풍기는 악취
- 203p
팩토리 메서드로 switch 문을 없애기 위해 좀 더 간단한 방법을 생각해 봅시다.
createShape라는 팩토리 메서드 하나로 다 하려고 하지 말고, 처음부터 클래스에 따른 팩토리 메서드를 다음과 같이 만들면 됩니다.
ShapeLine -> createShapeLine
ShapeRectangle -> createShapeRectangle
ShapeOval -> createShapeOval
public static ShapeLine createShapeLine(int startx, int starty, int endx, int endy) {
return new ShapeLine(startx, starty, endx, endy);
}
- 206p
분류 코드를 상태/전략 패턴으로 치환
public class Logger {
public static final int STATE_STOPPED = 0;
public static final int STATE_LOGGING = 1;
private int _state;
public Logger() {
_state = STATE_STOPPED;
}
public void start() {
switch (_state) {
case STATE_STOPPED:
=> 분류 코드를 상태/전략 패턴으로 치환
int로 구현한 분류 코드를 상태 객체로 치환
public abstarc class State {
public abstrac int getTypeCode();
}
public class StateStopped extends State {
@Override public int getTypeCode() {
return Logger.STATE_STOPPED;
}
}
- 216p
public class Logger {
private enum State {
STOPPED {
@Override public void start() {
System.out.println("** START LOGGING **");
}
@Override public void stop() {
/* 아무것도 하지 않음 */
}
@Override public void log(String info) {
System.out.println("Ignoring: " + info);
}
},
LOGGING {
@Override public void start() {
/* 아무것도 하지 않음 */
}
@Override public void stop() {
System.out.println("** STOP LOGGING **");
}
@Override public void log(String info) {
System.out.println("Logging: " + info);
}
};
public abstract void start();
public abstract void stop();
public abstract void log(String info);
}
private State _state;
public Logger() {
setState(State.STOPPED);
}
public void setState(State state) {
_state = state;
}
public void start() {
_state.start();
setState(State.LOGGING);
}
public void stop() {
_state.stop();
setState(State.STOPPED);
}
public void log(String info) {
_state.log(info);
}
}
switch 문이 없어지고 _state.start(), _state.stop(), .state.log()처럼 _state에 작업을 위임하는 형태가 되었습니다.
왜 이렇게 되는 걸까요? 그건 다형성의 전형적인 예이기 때문입니다. _state필드는 State 타입으로 상태에 따라 STATE_STOPPED 인스턴스 또는 STATE_LOGGING 인스턴스가 할당됩니다. 즉, _state.start()라는 표현식은 상태에 따라 STATE_STOPPED의 start 메서드 또는 STATE_LOGGING의 start 메서드 호출이 됩니다. 분기하는 코드가 switch 문처럼 눈에 보이는 건 아니지만 _state 값에 따라 호출되는 메서드가 다르다는 점을 이해하기 바랍니다.
-229 ~ 231p
상태 패턴과 전략 패턴의 차이
리팩토링 이름에 등장하는 상태 패턴과 전략 패턴은 둘다 GoF 디자인 패턴입니다.
상태 패턴은 예제 프로그램에서 소개한 것처럼 프로그램 상태를 객체로 표현하고 상태에 의존하는 코드를 하위 클래스 메서드에 작성하는 것입니다. 이걸로 상태에 따른 switch 문이 없어져서 상태 전이도 깔끔하게 작성할 수 있습니다.
전략 패턴은 하나로 정리된 처리를 하는 알고리즘을 조용히 전환할 때 사용하는 패턴입니다. 알고리즘 입출력을 인터페이스(API)로 규정해 두고 그 인터페이스(API)를 만족하는 구체적인 클래스를 선업합니다. 이렇게 하면 요구에 따른 알고리즘을 선택할 수 있어서 한 알고리즘을 다른 알고리즘으로 바궈서 재계산하는 일이 편해집니다.
-234p
에러 코드를 예외로 치환
return null
=>
throw new InvalidCommandException(name);
- 243p
인스턴스를 하나만 가지므로 다음처럼 익명 클래스를 쓰면 좀 더 단순하게 작성할 수 있습니다.
public abstract class Command {
private static class Forward extends Command {
public Forward() {
super("forward");
}
@Override public void execute(Robot robot) {
robot.forward();
}
}
private static class Backward extends Command {
public Backward() {
super("backward");
}
@Override public void execute(Robot robot) {
robot.backward();
}
}
private static class Right extends Command {
public Right() {
super("right");
}
@Override public void execute(Robot robot) {
robot.right();
}
}
private static class Left extends Command {
public Left() {
super("left");
}
@Override public void execute(Robot robot) {
robot.left();
}
}
이는 익명클래스로 싱글톤 패턴을 구현한 것입니다.
- 259 ~ 260p
생성자를 팩토리 메서드로 치환
new 연산자를 사용해 생성자를 직접 호출하는 대신에 인스턴스를 생성하는 팩토리 메서드를 제공합니다.
- 271p
public static void main(String[] args) {
Shape line = Shape.create(Shape.TYPECODE_LINE, 0, 0, 100, 200);
Shape rectangle = Shape.create(Shape.TYPECODE_RECTANGLE, 10, 20, 30, 40);
Shape oval = Shape.create(Shape.TYPECODE_OVAL, 100, 200, 300, 400);
Shape[] shape = {
line,
rectangle,
oval,
};
for (Shape s : shape) {
s.draw();
}
}
public static Shape create(int typecode, int startx, int starty, int endx, int endy) {
return new Shape(typecode, startx, starty, endx, endy);
}
private Shape(int typecode, int startx, int starty, int endx, int endy) {
_typecode = typecode;
_startx = startx;
_endx = endx;
_starty = starty;
_endy = endy;
}
- 285 ~ 287p
<패턴을 활용한 리팩터링>에서는 생성 메서드라는 용어를 새로 제안했습니다.
- 생성 메서드 = 인스턴스를 생성하는 메서드를 총칭
- 팩토리 메서드 = GoF의 디자인 패턴과 의미가 같은 인스턴스 생성 메서드
- 287p
private final Printer _printer;
public Client(boolean graphical) {
if (graphical) {
_printer = new GraphPrinter();
} else {
_printer = new DigitPrinter();
}
}
=> Client가 Printer, GraphPrinter, DogitPrinter에 모두 의존하고 있다는 문제
=> 해결
private final Printer _printer;
public Client(boolean graphical) {
_printer = Printer.create(graphical);
}
public void execute() {
int[] table = { 3, 1, 4, 1, 5, 9 };
for (int n : table) {
_printer.println(n);
}
}
Printer에만 의존하도록 함
private final Printer _printer;
public Client(Printer printer) {
_printer = printer;
}
public void execute() {
int[] table = { 3, 1, 4, 1, 5, 9 };
for (int n : table) {
_printer.println(n);
}
}
= 의존성 주입
Client 클래스의 생성자 매개변수를 변경해서 Printer 하위 클래스의 인스턴스를 직접 넘기도록 수정합니다. 그러면 Client 클래스는 DigitPrinter 클래스와 GraphPrinter 클래스에 의존하지 않습니다.
public static void main(String[] args) {
new Client(new DigitPrinter()).execute();
new Client(new GraphPrinter()).execute();
}
}
Client 인스턴스를 만들 때 Printer의 하위 클래스의 인스턴스를 넘긴다는 건 Client 인스턴스를 생성할 때 의존 관계를 만들어 낸다는 것이 됩니다. 이런 기법을 일반적으로 의존성주입이라고 부릅니다. DI라고 줄여 부르기도 합니다. 의존성 주입을 사용하면 클래스 의존 관계를 줄일 수 있으므로 클래스를 부품으로 다루기 쉬워집니다.
의존성 주입으로 Client 클래스는 DigitPrinter 클래스나 GraphPrinter 클래스에 의존하지 않게 됩니다. 대신에 이번에는 Main 클래스가 양쪽 클래스에 존재합니다. 이렇듯 클래스 사이의 의존 관계 취급은 번거롭습니다.
- 289 ~ 292p
관측 데이터 복제
이 리팩토링은 혼재하는 모델과 뷰를 분리합니다. 단지 분리할 뿐이라면 서로 동기화되지 않으므로 관찰자 패턴이나 이벤트 리스너를 사용해서 모델 내용이 변하면 그 사실을 뷰에 알리고 모델과 뷰를 동기화합니다. 이것이 관측 데이터 복제입니다.
- 294p
뷰에서 모델 참조
private int _value = 0;
=> private Value _value = new Value(0);
모델을 메서드로 조작
_value.setValue(value);
통지 내용을 나타내는 이벤트 선언
public class ValueChangeEvent {
private final Value _source;
public ValueChangeEvent(Value source) {
_source = source;
}
통지 관련 인터페이스 선언
통지 메서드(valueChanged)를 포함한 인터페이스 (ValueListener)를 선언합니다. valueChanged 메서드에는 앞에서 선언한 ValueChangeEvent를 넘깁니다.
public interface ValueListener {
public void valueChanged(ValueChangeEvent e);
}
통지를 받는 메서드를 뷰에 선언
public class IntegerDisplay extends Frame implements ActionListener, ValueListener {
...
public void valueChanged(ValueChangeEvent e) {
}
}
- 303 ~ 304p
모델 : 뷰를 모델에 등록 가능하게 만듬
public class Value {
...
private final List<ValueListener> _listeners = new Array...
public void addValueListener(ValueListener listener) {
뷰 : 뷰를 모델에 등록
IntegerDisplay 생성자에서 앞의 addValueListner를 호출합니다.
public IntegerDisplay() {
...
_value.addValueListener(this);
...
모델 : 모델을 변경하면 뷰에 통지하는 코드 작성
notiyfyToListners();
private void notifyToListeners() {
for (ValueListener listener : _listeners) {
listner.valueChanged(new ValueChangeEvent(this));
뷰 : 통지를 받는 메서드 안으로 표시 갱신 처리를 이동
if (e.getSource() == _value) {
_ocalLable.setText(Integer.tostaring...)
- 304 ~ 305p
상속을 위임으로 치환
class Another {
void method() ...
class Something extends Another {
...
=>
class Something {
Another _delegate = new Another();
...
void method() {
_delegate.method();
}
}
- 323p
public class Dice {
private final Random _random;
public Dice() {
_random = new Random(314159L);
}
public Dice(long seed) {
_random = new Random(seed);
}
public int nextInt() {
return _random.nextInt(6) + 1;
}
public void setSeed(long seed) {
_random.setSeed(seed);
}
}
기존에 상속받던 것을 생성자 호출로 전환
- 332p
상속은 최후의 무기
- 335p
IS - A 관계가 상속과 관련 있듯이 HAS - A 관계는 위임과 관계 있습니다. 그렇다면 ㅇㅇ 클래스의 인스턴스 필드가 ㅁㅁ 클래스의 인스턴스를 저장하고 있다면 ㅇㅇ은 ㅁㅁ를 가지고 있다 (ㅇㅇ has a ㅁㅁ)라는 HAS - A 관곅 ㅏ성립합니다.
- 337p
대리자 은폐
server.getDelegate().handle();
클라이언트 클래스가 대리 클래스를 이용하고 있음 (대리 클래스에 의존하는 상태)
server.handle();
클라리언트 클래스가 대리 클래스를 이용하지 않게 됨 (대리 클래스에 의존하지 않는 상태)
- 344p
'Book' 카테고리의 다른 글
[독서 기록] 개발자의 글쓰기, 저자 김철수, 출판 위키북스, 2019 (1) | 2022.11.26 |
---|---|
[독서 기록] 나는 주니어 개발자다 (0) | 2022.11.22 |
[독서 기록] 엘레강트 오브젝트 - 새로운 관점에서 바라본 객체 지향 3장 - 2 (2) | 2022.11.19 |
[독서 기록] 엘레강트 오브젝트 - 새로운 관점에서 바라본 객체 지향 3장 -1 (1) | 2022.11.19 |
[독서 기록] 엘레강트 오브젝트 - 새로운 관점에서 바라본 객체 지향 2장 (0) | 2022.11.16 |