WEB2.0/프로그래밍

누가 javascript로 SOLID 지키는 소리를 내었는가

나를찾는아이 2022. 5. 6. 12:34
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
반응형