체스 미션에서 전략패턴을 도입하면서 다음과 같은 클래스 상속 구조를 갖추게되었다.
여기서 한 가지 문제점이 있다.
public class Piece {
protected final Color color;
protected final PieceType type;
public Piece(Color color, PieceType type) {
validateColor(color);
this.color = color;
this.type = type;
}
/***/
public boolean canMove(Move move, Piece targetPiece) {
return type.getMoveStrategy().canMove(move);
}
/***/
}
public class Pawn extends Piece {
public Pawn(Color color) {
super(color, PieceType.PAWN);
}
@Override
public boolean canMove(Move move, Piece targetPiece) {
if (isAttack(targetPiece)) {
return canAttack(move);
}
if (isFirstMove(move)) {
return canInitialMove(move);
}
return canMove(move);
}
/***/
}
그것은 바로 Piece는 abstract 클래스도 아니고, final 클래스도 아니라는 점이다!
(Piece는 인스턴스도 존재하고, Piece를 상속받는 Pawn이 존재한다.)
좋은 객체의 7가지 덕목 중 하나로, 객체의 클래스가 Final 이나 Abstract다 라는 내용이 있다.
final이나 abstract 클래스가 아니면, 사용자가 마음대로 사용할 수 있는 가능성을 열어두는 것이다.
사용자가 마음대로 사용할 수 있다면, 어떤 문제가 일어날지 모른다.
문제가 일어난 예시를 보자.
public class NamePrinter {
public void print(String name) {
if (Objects.equals(name, "사기꾼")) {
throw new IllegalArgumentException("부적절한 이름입니다");
}
System.out.println("이름:" + name);
}
}
NamePrinter의 print 메서드는 name을 입력받아, name이 사기꾼이면 예외를 던지고 예외가 아닌 경우에만 출력한다.
public class Application {
public static void main(String[] args) {
NamePrinter namePrinter = new NamePrinter();
namePrinter.print("사기꾼");
}
}
하지만, 사기꾼을 이름으로 가지고싶은 악덕 사용자가 있다고 가정하자.
해당 사용자는 우리의 NamePrinter를 악용하기 위해 해당 클래스를 상속받는 SonNamePrinter를 만들어서 이름이 사기꾼인지 검증하는 로직을 없앴다.
public class SonNamePrinter extends NamePrinter {
@Override
public void print(String name) {
System.out.println("이름: " + name);
}
}
public class Application {
public static void main(String[] args) {
NamePrinter namePrinter = new SonNamePrinter();
namePrinter.print("사기꾼");
}
}
사기꾼이 입력됐는데, 예외를 던지지 않고 출력이되었다..
이러한 오사용을 막기 위해서는 class를 final로 만들어주면 된다.
class를 final로 만들어주면, 해당 클래스를 상속하지 못한다!
public final class NamePrinter {
/***/
}
이러한 이유때문에, 클래스는 final 혹은 abstract만 가능하다.
또한, 마찬가지의 이유로 abstract 클래스 내 메서드도 final 혹은 abstract만 가능하다.
(메서드에 final을 붙이지 않으면, 사용자가 원하는대로 오버라이딩 할 수 있다. -> 예측 가능성이 줄어든다.)
상속과 메서드 오버라이딩을 하고싶은 부분만 abstract로 만들고, 나머지는 전부 final 키워드를 붙여줘 사용자의 오사용을 막아주도록 하자.
public abstract class Piece {
/***/
public abstract boolean canMove(Move move, Piece targetPiece);
public final boolean isSameColor(Piece target) {
if (target == null) {
return false;
}
return color == target.color;
}
public final boolean isRightTurn(Color turn) {
return this.color == turn;
}
/***/
}
그러면 Piece를 final 혹은 abstract 클래스로 만들어주는 방법은 무엇일까?
Piece를 abstract 클래스로 만들기
해답은 바로 Piece를 abstract 클래스로 만들고, 전략패턴을 사용하는 기물에 대해서 Piece를 상속받는 StrategyPiece라는 final 클래스를 구현하는 것이다.
위에서 말한 사용자의 오사용을 막기 위해, Piece의 canMove 는 abstract 키워드를 붙여주고, 나머지 메서드들에는 전부 다 final 키워드를 붙여주도록 하자.
public abstract class Piece {
protected final Color color;
protected final PieceType type;
public Piece(Color color, PieceType type) {
validateColor(color);
this.color = color;
this.type = type;
}
/***/
public abstract boolean canMove(Move move, Piece targetPiece);
public final boolean isSameColor(Piece target) {
if (target == null) {
return false;
}
return color == target.color;
}
public final boolean isRightTurn(Color turn) {
return this.color == turn;
}
/***/
}
public final class StrategyPiece extends Piece {
public StrategyPiece(Color color, PieceType type) {
super(color, type);
}
@Override
public boolean canMove(Move move, Piece targetPiece) {
return type.getMoveStrategy().canMove(move);
}
}
나는 아직 클래스, 메서드, 변수 등에 final 키워드를 붙여주는 습관이 들지 않았다.
사용자는 어떻게 사용할 지 모른다. 사람을 믿지 마라!
final 키워드를 붙이는 습관을 들여, 오사용의 가능성을 줄이도록 하자.
'우테코 5기 > 레벨1' 카테고리의 다른 글
[레벨1] 정적 팩토리 메서드 활용법 총 정리 (4) | 2023.04.07 |
---|---|
[체스 미션] 상속 대신 전략패턴 도입하기 (4) | 2023.03.28 |
정적 팩토리 메서드로 생성자 다이어트 시키기 (4) | 2023.03.05 |
전략 패턴을 통한 테스트 (2) | 2023.02.17 |
View단에서 도메인 객체를 생성해 return하는게 좋을까, 아니면 입력값을 그대로 return하는게 좋을까? (3) | 2023.02.17 |