예를들어 아래는 캐릭터 클래스가 공격, 방어, 이동 3개의 기능을 가지고있다.
class Character {
attack(target) {
console.log(`${this.name}이 ${target.name}을(를) 공격.`);
}
move(position) {
console.log(`${this.name}이 ${position}으로 이동.`);
}
defence(target) {
console.log(`${this.name}이 ${target.name}의 공격을 방어.`);
}
}
이걸 아래처럼 한가지 책임만 가지게 수정한다.
/* 수정 */
class Character {
constructor(name) {
this.name = name;
}
attack(target) {
console.log(`${this.name}이 ${target.name}을(를) 공격.`);
}
defence(target) {
console.log(`${this.name}이 ${target.name}의 공격을 방어.`);
}
}
class Movement {
move(character, position) {
console.log(`${character.name}이 ${position}으로 이동.`);
}
}
어? 뭔가 이상한거같은데요?
=> 아니오
공격과 방어는전투
라는 책임에 포함된 기능들 이므로 책임은 하나임
기능과 책임의 차이를 구분해야함.
예시로 아래는 캐릭터 클래스의 공격 메서드에서 새로운 무기(예로 지팡이)를 추가하려면 클래스 내부 코드를 건드려야함
class Character {
...
attack(weaponType, target) {
if (weaponType === 'sword') {
console.log(`${this.name}이 ${target.name}을 칼로 공격.`);
} else if (weaponType === 'bow') {
console.log(`${this.name}이 ${target.name}을 활로 공격.`);
}
}
}
아래처럼 수정하면 새로운 무기만 만들고 다른 코드를 건드릴 필요가 없어짐
/* 수정 */
class Character {
...
attack(target) {
this.weapon.attack(this, target);
}
}
class Sword extends Weapon {
attack(character, target) {
console.log(`${character.name}이 ${target.name}을 칼로 공격.`);
}
}
class Bow extends Weapon {
attack(character, target) {
console.log(`${character.name}이 ${target.name}을 활로 공격.`);
}
}
예를들어 아래는 새라는 카테고리에 포함된 펭귄을 예시로 들었다.
새는 하늘을 나는 것을 전재로 하는 bird클래스와 bird클래스를 상속받고 있지만 날지못하는 펭귄의 안타까운 사연을 보자
펭귄은 bird클래스의 기본기능을 제대로 작동시키지 않으므로 LSP위반이다.
class Bird {
fly() {
console.log('새가 하늘을 날아갑니다.');
}
}
class Penguin extends Bird {
fly() {
throw new Error('펭귄은 날 수 없습니다.');
}
}
애초에 부모클래스에서 새는 '이동을한다'로 바꿔보자
/* 수정 */
class Bird {
move() {
console.log('새가 이동합니다.');
}
}
class FlyingBird extends Bird {
move() {
this.fly();
}
fly() {
console.log('하늘을 날아갑니다.');
}
}
class Penguin extends Bird {
move() {
console.log('펭귄이 이동을 합니다.');
}
}
이렇게 하면 날 수 있는 새는 날 수 있게 그냥 이동하는 새는 이동하게 바꿀 수 있다.
클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않도록 인터페이스를 작게 분리해야 한다
간단하게 말해서 쓸모없는 기능을 넣지 말라는 것 이지만... 글로만 설명하기가 애매하므로 예시를보자
아래는 쓸때없이 Character에 이것 저것 밖아놔서 크기만 커진 결과다.
class Character {
attack() { /* ... */ }
fly() { /* ... */ }
swim() { /* ... */ }
}
class GroundCharacter extends Character {
attack() {
console.log('지상 캐릭터가 공격.');
}
fly() {
throw new Error('지상 캐릭터는 날 수 없습니다.');
}
swim() {
throw new Error('지상 캐릭터는 수영할 수 없습니다.');
}
}
이걸 아래와같이 수정하면
/* 수정 */
class Attackable {
attack()
}
class Flyable {
fly()
}
class Swimmable {
swim()
}
class GroundCharacter extends Attackable {
attack() {
console.log('지상 캐릭터가 공격.');
}
}
class FlyingCharacter extends Attackable, Flyable {
attack() {
console.log('비행 캐릭터가 공격.');
}
fly() {
console.log('비행 캐릭터가 날아갑니다.');
}
}
지상 캐릭터는 지상캐릭터답게 나는 캐릭은 날게 할 수 있다.
뭔가 어디서 본거같다고? 정답!
SRP를 지키면 ISP는 '거의' 해결된다.
아래는 DIP를 위반한 예시
아래 코드는 DB를 만약 다른 DB로 연결한다고 하면 메인 클래스로직도 수정해야함
class MySQLDatabase {
connect() {
console.log("MySQL 데이터베이스에 연결.");
}
}
class UserManager {
constructor() {
this.database = new MySQLDatabase();
}
saveUser(user) {
this.database.connect();
console.log(`${user}을(를) 저장.`);
}
}
이를 메인 클래스를 수정하는 것이 아닌 외부에서만 수정을 거칠 수 있게 바꿈
// 데이터베이스 인터페이스 (추상화)
class Database {
connect()
}
// MySQL 데이터베이스 구현 (저수준 모듈)
class MySQLDatabase extends Database {
connect() {
console.log("MySQL 데이터베이스에 연결.");
}
}
// PostgreSQL 데이터베이스 구현 (저수준 모듈)
class PostgreSQLDatabase extends Database {
connect() {
console.log("PostgreSQL 데이터베이스에 연결.");
}
}
// UserManager (고수준 모듈)
class UserManager {
constructor(database) {
this.database = database; // 추상화에 의존
}
saveUser(user) {
this.database.connect();
console.log(`${user}을(를) 저장.`);
}
}
// 이제 UserManager는 구체적인 데이터베이스 구현이 아닌 추상화에 의존.
const mysqlDatabase = new MySQLDatabase();
const userManager = new UserManager(mysqlDatabase);
userManager.saveUser('Alice');
// 데이터베이스를 쉽게 변경.
const postgresDatabase = new PostgreSQLDatabase();
const userManager2 = new UserManager(postgresDatabase);
userManager2.saveUser('Bob');
즉, 추상화된 인터페이스나 추상 클래스를 중간다리로 사용한다.