누가 javascript로 SOLID 지키는 소리를 내었는가
이론적으로 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 가능하다구요!