한 클래스는 하나의 책임을 가져야 한다.
클래스를 변경하는 이유는 단 하나여야 한다.
이를 지키지 않으면, 한 책임의 변경에 의해 다른 책임과 관련된 코드에 영향을 미칠 수 있음
// 잘못된 예제 -> '한 클래스는 하나의 책임을 가져야 한다' 위반
interface Employee_X {
// 경리팀 업무
void calculatePay();
// 인사팀 업무
void reportHours();
// DB 관리자 업무
void save();
}
// 올바른 예제 -> '한 클래스는 하나의 책임을 가져야 한다'
interface Employee_O {
void do_something();
}
// 인사 업무 클래스
class HourReporter implements Employee_O {
@Override
public void do_something() {
System.out.println("HourReporter.do_something");
}
}
// 경리 업무 클래스
class PayCalculator implements Employee_O {
@Override
public void do_something() {
System.out.println("PayCalculator.do_something");
}
}
// DB 관리자 업무 클래스
class EmployeeSaver implements Employee_O {
@Override
public void do_something() {
System.out.println("EmployeeSaver.do_something");
}
}
클래스 확장에는 개방적이어야 하고, 변경에는 폐쇄적이어야 한다.
즉, 기존 코드를 변경하지 않고 기능을 수정하거나 추가할 수 있도록 설계해야 함.
이를 지키지 않으면,instanceof
와 같은 연산자를 사용하거나, 다운 캐스팅이 발생한다.
class BadDeveloper {
public void eatSleepDevelopRest() {
System.out.println("eating");
System.out.println("sleeping");
System.out.println("developing");
System.out.println("restring");
}
}
// 만약 다른 일을 추가해야 한다면?
// -> 기존 코드를 변경하면서 기능을 추가해야한다.
class BadDeveloper {
public void eatSleepDevelopSomeWorksRest() {
System.out.println("eating");
System.out.println("sleeping");
System.out.println("developing");
System.out.println("some job 1");
System.out.println("some job 2");
System.out.println("restring");
}
}
class GoodDeveloper {
void eat() {
System.out.println("eating");
}
void sleep() {
System.out.println("sleeping");
}
void develop() {
System.out.println("developing");
}
void rest() {
System.out.println("resting");
}
}
// 만약 다른 일을 추가해야 한다면?
// -> 기존 코드를 변경하지 않고 기능을 추가할 수 있다.
class GoodDeveloper {
void eat() {
System.out.println("eating");
}
void sleep() {
System.out.println("sleeping");
}
void develop() {
System.out.println("developing");
}
void some_job_1() {
System.out.println("some job 1");
}
void some_job_2() {
System.out.println("some job 2");
}
void rest() {
System.out.println("resting");
}
}
하위 타입 객체는 상위 타입 객체에서 가능한 행위를 수행할 수 있어야 함.
즉, 상위 타입 객체를 하위 타입 객체로 치환해도 정상적으로 동작해야 함.
상속관계에서는 꼭 일반화 관계 (IS-A
)가 성립해야 한다는 의미 (일관성 있는 관계인지)
상속관계가 아닌 클래스들을 상속관계로 설정하면 이 원칙이 위배됨. (재사용 목적으로 사용하는 경우)
리스코프 치환 원칙을 지키지 않으면 개방 폐쇄 원칙을 위반하게 된다.
따라서 상속 관계를 잘 정의하여 LSP 원칙이 위배되지 않도록 설계해야 한다.
// LSP를 위반한 예제
class Rectangle {
private int width;
private int height;
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
class Square extends Rectangle {
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(getWidth());
}
@Override
public void setHeight(int height) {
super.setHeight(height);
super.setWidth(getHeight());
}
}
public class Main {
public static void main(String[] args) {
// 하위 타입 객체는 상위 타입 객체에서 가능한 행위를 수행할 수 없음!
Rectangle rectangle = new Rectangle();
rectangle.setWidth(10);
rectangle.setHeight(5);
System.out.println("rectangle = " + rectangle.getArea());
Rectangle square = new Square();
square.setWidth(10);
square.setHeight(5);
System.out.println("rectangle = " + square.getArea()); // 25 출력!
}
}
// LSP를 준수한 코드
// Shape 클래스
class Shape {
private int width;
private int height;
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
// 직사각형 클래스
class Rectangle extends Shape {
public Rectangle(int width, int height) {
setWidth(width);
setHeight(height);
}
}
// 정사각형 클래스
class Square extends Shape {
public Square(int length) {
setWidth(length);
setHeight(length);
}
}
public class Main {
public static void main(String[] args) {
// 이제 더이상 Rectangle과 Square가 상속 관계가 아니므로, 리스코프 치환 원칙의 영향에서 벗어남.
Shape rectangle = new Rectangle(10, 5);
Shape square = new Square(5);
System.out.println(rectangle.getArea());
System.out.println(square.getArea());
}
}
클라이언트는 자신이 사용하는 메소드에만 의존해야 한다는 원칙.
한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 않아야 함.
하나의 통상적인 인터페이스보다는 차라리 여러 개의 세부적인 인터페이스가 나음.
인터페이스는 해당 인터페이스를 사용하는 클라이언트를 기준으로 잘게 분리되어야함.
// ISP을 위반한 예제 -> 인터페이스 분리가 안되어있음.
interface AllInOneDevice {
void print();
void copy();
void fax();
}
class SmartMachine implements AllInOneDevice {
@Override
public void print() {
System.out.println("print");
}
@Override
public void copy() {
System.out.println("copy");
}
@Override
public void fax() {
System.out.println("fax");
}
}
class PrinterMachine implements AllInOneDevice {
@Override
public void print() {
System.out.println("print");
}
@Override
public void copy() {
throw new UnsupportedOperationException();
}
@Override
public void fax() {
throw new UnsupportedOperationException();
}
}
// ISP를 준수한 예제 -> 인터페이스를 분리
interface PrinterDevice {
void print();
}
interface CopyDevice {
void copy();
}
interface FaxDevice {
void fax();
}
class SmartMachine implements PrinterDevice, CopyDevice, FaxDevice {
@Override
public void print() {
System.out.println("print");
}
@Override
public void copy() {
System.out.println("copy");
}
@Override
public void fax() {
System.out.println("fax");
}
}
class PrinterMachine implements PrinterDevice {
@Override
public void print() {
System.out.println("print");
}
}
의존 관계를 맺을 때, 변하기 쉬운 것 (구체적인 것) 보다는 변하기 어려운 것 (추상적인 것)에 의존해야함.
구체화된 클래스에 의존하기 보다는 추상 클래스나 인터페이스에 의존해야 한다는 뜻.
즉, 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 됨.
저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 함.
저수준 모듈이 변경되어도 고수준 모듈은 변경이 필요없는 형태가 이상적
// DIP를 위반한 예제
// 한손검 객체
class OneHandSword {
private final String name;
private final int damage;
public OneHandSword(String name, int damage) {
this.name = name;
this.damage = damage;
}
public int attack() {
return damage;
}
@Override
public String toString() {
return "OneHandSword{" +
"name='" + name + '\'' +
'}';
}
}
// 한손검을 제외한 다른 무기를 사용하려면 Character의 코드를 변경해야 한다. OneHandSword에 의존성을 가진다.
class Character {
private final String name;
private int health;
private OneHandSword weapon;
public Character(String name, int health, OneHandSword weapon) {
this.name = name;
this.health = health;
this.weapon = weapon;
}
public int attack() {
return weapon.attack();
}
public void damaged(int amount) {
health -= amount;
}
public void changeWeapon(OneHandSword weapon) {
this.weapon = weapon;
}
@Override
public String toString() {
return "Character{" +
"name='" + name + '\'' +
'}';
}
}
// DIP를 준수한 예제
// Attackable라는 고수준 모듈을 추가함으로써 Character가 OneHandSword를 받는게 아닌 Attackable을 받게 수정
// 공격 인터페이스
interface Attackable {
int attack();
@Override
String toString();
}
class OneHandSword implements Attackable {
private final String name;
private final int damage;
public OneHandSword(String name, int damage) {
this.name = name;
this.damage = damage;
}
@Override
public int attack() {
return damage;
}
@Override
public String toString() {
return "OneHandSword{" +
"name='" + name + '\'' +
'}';
}
}
class Character {
private final String name;
private int health;
private Attackable weapon;
public Character(String name, int health, Attackable weapon) {
this.name = name;
this.health = health;
this.weapon = weapon;
}
public int attack() {
return weapon.attack();
}
public void damaged(int amount) {
health -= amount;
}
public void changeWeapon(Attackable weapon) {
this.weapon = weapon;
}
@Override
public String toString() {
return "Character{" +
"name='" + name + '\'' +
'}';
}
}