티스토리 뷰

728x90
반응형

이론적으로 OOP(객체지향프로그래밍)을 배울때 항상 함께 배우는 이론적 지식이 SOLID 원칙입니다

 

객체지향설계라고 불리는 SOLID 원칙은 프로그래밍할때의 아주 중요한 가이드라고 할수 있습니다

 

 

S - SRP
단일 책임 원칙 (Single responsibility principle)
한 클래스는 하나의 책임만 가져야 한다.


O - OCP
개방-폐쇄 원칙 (Open/closed principle)
“소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.”


L - LSP
리스코프 치환 원칙 (Liskov substitution principle)
“프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.” 계약에 의한 설계를 참고하라.


I - ISP
인터페이스 분리 원칙 (Interface segregation principle)
“특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.”


D - DIP
의존관계 역전 원칙 (Dependency inversion principle)
프로그래머는 “추상화에 의존해야지, 구체화에 의존하면 안된다.” 의존성 주입은 이 원칙을 따르는 방법 중 하나다.

 

 

위키에서 가져온 SOLID 원칙에 대한 설명입니다

 

누가 javascript로는 solid 원칙을 지킬수 없다고 했던가요

 

동적언어인 javascript로 어떻게 하면 SOLID 원칙을 준수하여 훌륭한 코드를 작성할수 있는지 함께 경험해봅시다

 

 

S - 단일 책임원칙

 

한 클래스는 하나의 책임만 가져야 한다.

당연합니다 하나의 클래스는 한가지 일만 하도록 하여 재사용을 높이고 테스트를 쉽게 합니다

 

class UserService {
    updateProfile() {}
    
    login() {}
    
    saveUser() {}
}

BAD

하나의 클래스가 많은 역할을 하고 있음

 

class User {
}

class UserCredential {
	verify() {}
}

class RegisterUseCase {
}

GOOD

하나의 클래스에는 단일 책임만을 부여

 

 

O - 개방-폐쇄 원칙

확장에 열려있고, 변경에는 닫혀있다

이 문장의 표현은 다시 해석해보면 이렇게 해석할수 있습니다
확장에는 기존 소스코드를 수정하지 않아야 한다
내부에 변경이 되어야할 때는 외부의 코드 변화가 없어야 한다

 

 

getTop10RankItem() : any[] {
	// 평점순
}

getTop10RankItem() : Promise<any[]> {
	// 평점 + 북마크
}

BAD

함수 안의 동작 방식이 변경되었을뿐인데 리턴 타입이 달라졌습니다

이렇게 되면 외부의 코드도 모두 바뀌어야 하는 연쇄적 사이드 이펙트가 발생합니다

내부가 변경되더라도 외부에는 혼란을 일으키지 말아야 합니다

 

 

const saveRecordAndNotify = async (record, notifyType, notifyTo) {
    await recordManager.save(record);

    if (notifyType === "email") {
        emailManager.send(notifyTo, record);
    } else if (notifyType === "phone") {
        smsManager.send(notifyTo, record);
    }
}

BAD

확장에 닫혀있음

새로운 notifyType이 추가될때마다 saveRecordAndNotify 내부를 변경해야함

 

 

const saveRecord = async (record, callback) {
    await recordManager.save(record);

    callback(record);
}

// 이메일로 보내기
saveRecord(record, (record) => {
    emailManager.send(notifyTo, record)
})

// SMS로 보내기
saveRecord(record, (record) => {
    smsManager.send(notifyTo, record)
})

// 슬랙으로 보내기
saveRecord(record, (record) => {
    slackManager.send(notifyTo, record)
})

GOOD

확장에 열려있음

콜백형태로 분리하여 notifyType이 늘어나더라도 saveRecord에는 영향을 주지 않음

여기서 좀 더 발전하면 notifyManager를 만들어서 slack, sms, email같은 notify방법을 알고 있는 인스턴스를 전달하도록 하면 더 좋을것 같네요

 

 

 

L - 리스코프 치환 원칙

하위 타입의 인스턴스를 교체해도 동작해야한다

 

class Car {
    getNumberOfTire() : number
}

class Tesla extends Car {
    getNumberOfTier() {
        return 4;
    }
}

class Sonata extends Car {
    getNumberOfTier() {
        return 4;
    }
}

class TireCostCalculator {
    getTireCost(car: Car) {
        return car.getNumberOfTier() * 10000;
    }
}

const cal = new TireCostCalculator();
cal.getTireCost(new Tesla());
cal.getTireCost(new Sonata());

GOOD

Car 하위 타입의 클래스가 생성되어도 TireCostCalculator는 잘 동작합니다

예제처럼 해도 되지만 javascript는 덕타이핑을 지원하기 때문에 굳이 Car를 상속받지 않더라도

getNumberOfTier()를 구현했다면 Car로 간주됩니다

 

 

I - 인터페이스 분리 원칙

사용하지 않는 인터페이스에 의존하지 말아라

 

interface Phone {
    call(number) {

    }

    takePhoto() {

    }

    connectToWifi() {

    }
}

class IPhone extends Phone {
    call(number) {

    }

    takePhoto() {

    }

    connectToWifi() {

    }
}

class Lollipop extends Phone {
    call(number) {

    }

    takePhoto() {

    }

    connectToWifi() {
        ???
    }
}

BAD

사용하지 않는 인터페이스에 의존되어있음

너무 많은 기능을 담은 인터페이스를 상속했기때문에 불필요한 메소드까지 구현이 필요함

 

 

interface Phone {
	call()
}

interface Camera {
	takePhoto()
}

interface Network {
	connectToWifi()
}

class IPhone {
	call() {
	}

	takePhoto() {
	}

	connectToWifi() {
	}
}

GOOD

인터페이스를 분리하여 실제 사용하는 메소드만을 구현(javascript의 덕타이핑을 활용)

 

 

D - 의존관계 역전 원칙

추상화에 의존해야지 구체화에 의존하면 안된다

 

class FileSystem {
  writeToFile(data) {
    // Implementation
  }
}

class ExternalDB {
  writeToDatabase(data) {
    // Implementation
  }
}

class LocalPersistance {
  push(data) {
    // Implementation
  }
}

class PersistanceManager {
  saveData(db, data) {
    if (db instanceof FileSystem) {
      db.writeToFile(data)
    }

    if (db instanceof ExternalDB) {
      db.writeToDatabase(data)
    }

    if (db instanceof LocalPersistance) {
      db.push(data)
    }
  }
}

BAD

서로 다른 스토리지의 저장방법이 각각 달라 전달받는 인스턴스의 종류에 따라 다른 메소드를 호출해야함(구체화에 의존되어있음)

 

 

interface Storage {
  save(data)
}

class FileSystem {
  save(data) {
    // Implementation
    ..writeToFile(data)
  }
}

class ExternalDB {
  save(data) {
    // Implementation
    ..writeToDatabase(data)
  }
}

class LocalPersistance {
  save(data) {
    // Implementation
    ..push(data)
  }
}

class PersistanceManager {
  saveData(storage, data) {
    storage.save(data)
  }
}

GOOD

각각의 스토리지를 Storage interface 시그니처를 갖도록 하여 PersistanceManager는 구체화에 의존하는것이 아닌 추상화된 storage에 의존하도록함

그래서 앞으로 다른 어떤종류의 저장타입이 생기더라도 PersistanceManager는 호환가능함

 

 

 

javascript로도 SOLID 가능하다구요!

 

728x90
반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함