0%

SOLID

SOLID

  1. Single Responsibility Principle (SRP)

    단일 책임 원칙

    • “클래스가 수정되어야 하는 이유가 2개 이상으로 존재하면 안 된다.”
    • 하나의 클래스에는 개념적으로 응집된 기능만을 포함하는 것이 좋다.
    • 하나의 클래스에 여러 기능이 포함되어 있는 경우 작은 변화가 이와 연결된 다른 모듈들에 영향을 줄 수 있다.
    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
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    // bad
    class UserSettings {
    constructor(user) {
    this.user = user;
    }

    changeSettings(settings) {
    if (this.verifyCredentials()) {
    // ...
    }
    }

    verifyCredentials() {
    // ...
    }
    }

    // good
    // Authorization은 따로 클래스 정의
    class UserAuth {
    constructor(user) {
    this.user = user;
    }

    verifyCredentials() {
    // ...
    }
    }

    // Setting과 Authorization 분리
    class UserSettings {
    constructor(user) {
    this.user = user;
    this.auth = new UserAuth(user);
    }

    changeSettings(settings) {
    if (this.auth.verifyCredentials()) {
    // ...
    }
    }
    }
  1. Open / Closed Principle (OCP)

    개방 / 폐쇄 원칙

    • “소프트웨어 개체 (e.g. Class, Module, Function, etc.) 는 확장이 용이하지만 변경은 어려워야 한다.”
    • 즉, 사용자가 이미 존재하는 코드를 (직접적으로) 수정하지 않아도 새로운 기능을 추가할 수 있도록 설정해야 한다.
    1. 확장 에 대해 열려 있다.
      • 모듈의 동작 (기능) 을 확장할 수 있음
      • 요구 사항이 변경될 때, 이에 맞게 새로운 동작을 추가해 모듈을 확장함
      • 모듈이 하는 일을 변경하는 것
    2. 수정 에 대해 닫혀 있다.
      • 모듈의 소스 / 바이너리 코드를 수정하지 않아도 모듈의 기능을 확장하거나 변경할 수 있어야 함

앞서 설명했 듯,

많은 모듈 (클래스) 중 하나를 수정할 때 이에 의존하는 다른 모듈들 또한 수정해야 하는 코드는 좋지 않은 코드를 의미한다.

  • 시스템의 구조를 올바르게 리팩토링 하여 앞선 변경과 같은 유형의 수정 사항이 발생할 경우 더 이상의 수정을 요구하지 않고, 잘 동작하고 있던 기존 코드의 변경 없이 새로운 코드를 추가 하여 기능의 추가와 변경이 가능하도록 한다.

    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
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    class AjaxAdapter extends Adapter {
    constructor() {
    super();
    this.name = "ajaxAdapter";
    }

    // good
    // 즉, REFACTORING 을 잘한 것이 OCP를 적용한 것
    request(url) {
    // request and return promise
    }
    }

    class NodeAdapter extends Adapter {
    constructor() {
    super();
    this.name = "nodeAdapter";
    }

    // good
    request(url) {
    // request and return promise
    }
    }

    class HttpRequester {
    constructor(adapter) {
    this.adapter = adapter;
    }

    fetch(url) {
    // bad
    if (this.adapter.name === "ajaxAdapter") {
    return makeAjaxCall(url).then(response => {
    // transform response and return
    });
    } else if (this.adapter.name === "nodeAdapter") {
    return makeHttpCall(url).then(response => {
    // transform response and return
    });
    }

    // good
    return this.adapter.request(url).then(response => {});

    }
    }

    function makeAjaxCall(url) {
    // request and return promise
    }

    function makeHttpCall(url) {
    // request and return promise
    }
  1. Liskov Substitution Principle (LSP)

    리스코프 치환 원칙

    • “만약 S가 T의 하위유형이라면, T 객체는 (바람직한) 속성 (e.g. 정확도, 수행성 등) 을 변경하지 않아도 S 객체로 변경 가능해야 한다.”

    • 즉, 부모 클래스와 자식 클래스가 있을 때, 이들을 서로 바꾸어 적용해도 올바른 결과가 나와야 한다.

      • 정사각형 - 직사각형 클래스에서, 정사각형은 직사각형이지만, 이들을 is-a 관계로 정의한다면 문제가 발생한다.

        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
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        61
        62
        63
        64
        65
        66
        67
        68
        69
        70
        71
        72
        73
        74
        75
        76
        77
        78
        79
        80
        81
        82
        83
        84
        85
        86
        87
        88
        89
        90
        91
        92
        93
        94
        95
        96
        97
        98
        99
        100
        101
        // bad
        class Rectangle {
        constructor() {
        this.width = 0;
        this.height = 0;
        }

        setColor(color) {
        // ...
        }

        render(area) {
        // ...
        }

        setWidth(width) {
        this.width = width;
        }

        setHeight(height) {
        this.height = height;
        }

        getArea() {
        return this.width * this.height;
        }
        }

        class Square extends Rectangle {
        setWidth(width) {
        this.width = width;
        this.height = width;
        }

        setHeight(height) {
        this.width = height;
        this.height = height;
        }
        }

        function renderLargeRectangles(rectangles) {
        rectangles.forEach(rectangle => {
        rectangle.setWidth(4);
        rectangle.setHeight(5);
        const area = rectangle.getArea(); // BAD: Returns 25 for Square. Should be 20.
        rectangle.render(area);
        });
        }

        const rectangles = [new Rectangle(), new Rectangle(), new Square()];
        renderLargeRectangles(rectangles);

        // good
        // 1. Shape 관련 메소드는 분리하여 따로 클래스 정의
        class Shape {
        setColor(color) {
        // ...
        }

        render(area) {
        // ...
        }
        }

        // 2. 직사각형은 Shape 를 상속받는다.
        class Rectangle extends Shape {
        constructor(width, height) {
        super();
        this.width = width;
        this.height = height;
        }

        getArea() {
        return this.width * this.height;
        }
        }

        // 3. 정사각형도 Shape 를 상속받지만, 직사각형과 다른 특징을 가지므로 직사각형과 'is-a' 관계로 정의 (=== 상속) 하지 않는다.
        class Square extends Shape {
        constructor(length) {
        super();
        this.length = length;
        }

        // 차이점
        getArea() {
        return this.length * this.length;
        }
        }

        // 4. Shape 를 통해 속성값을 설정해놓아 이를 분리하여 동작을 할 수 있도록 한다. 이로써 여러 모듈 (클래스) 가 엉키지 않고 독립적으로 작용하여 에러를 줄일 수 있으며, 다른 속성을 가진 객체들을 유연하게 다룰 수 있다.
        function renderLargeShapes(shapes) {
        shapes.forEach(shape => {
        const area = shape.getArea();
        shape.render(area);
        });
        }

        // 속성값 constructor의 인자로 전달
        const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
        renderLargeShapes(shapes);
  1. Interface Segregation Principle (ISP)

    인터페이스 분리 원칙

    자바스크립트는 인터페이스 (i.e. type) 가 존재하지 않기 때문에 해당 원칙이 명확하게 적용되지는 않지만, type 시스템이 존재하지 않음에도 불구하고 중요하게 작용하는 원칙이다.

    • “클라이언트는 그가 사용하지 않는 인터페이스에 의존하도록 강요받지 않아야 한다.”
    • 자바스크립트는 duck typing 이므로 여기서 인터페이스는 암시적인 요소로 존재한다.
      • e.g. 큰 setting 객체들을 요구하는 클래스를 예시로 들면,
      • 클라이언트는 대게 모든 setting을 필요로 하지는 않기 때문에, 많은 양의 option을 설정하도록 요구하는 것은 좋지 않다.
      • 따라서, Setting을 optional하게 만드는 것은 “fat interface” 를 방지한다.
    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
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    // bad
    class DOMTraverser {
    constructor(settings) {
    this.settings = settings;
    this.setup();
    }

    setup() {
    this.rootNode = this.settings.rootNode;
    this.settings.animationModule.setup();
    }

    traverse() {
    // ...
    }
    }

    const $ = new DOMTraverser({
    rootNode: document.getElementsByTagName("body"),
    animationModule() {} // Most of the time, we won't need to animate when traversing.
    // ...
    });

    // good
    class DOMTraverser {
    constructor(settings) {
    this.settings = settings;
    // options 속성을 추가한다.
    this.options = settings.options;
    this.setup();
    }

    setup() {
    this.rootNode = this.settings.rootNode;
    // settings에 직접 접근하지 않고 setupOptions() 메소드를 이용한다.
    this.setupOptions();
    }

    // options를 세팅하는 메소드를 따로 작성한다.
    setupOptions() {
    // 이로써 options에 접근하도록 한다.
    // setter과 같은 역할
    if (this.options.animationModule) {
    // ...
    }
    }

    traverse() {
    // ...
    }
    }

    const $ = new DOMTraverser({
    rootNode: document.getElementsByTagName("body"),
    // 'Most of the time, we won't need to animate when traversing.' 이므로,
    // 이를 options로 설정하여 무조건 실행되지 않고 선택적으로 실행될 수 있도록 한다.
    options: {
    animationModule() {}
    }
    });
  1. Dependency Inversion Principle (DIP)

    의존성 역전 원칙

    1. 상위 모듈은 하위 모듈에 종속되어서는 안 된다. 둘 다 추상화에 의존해야 한다.
    2. 추상화는 세부사항에 의존하면 안 된다. 세부사항이 추상화에 의존해야 한다.
    • 의존성 ( Coupling ) 이 높을수록 코드를 리팩토링하기 어렵다. 👉 나쁜 개발 습관
    • 상위 모듈이 하위 모듈의 세부사항을 알지 못하므로, 의존성을 감소시킬 수 있다.
    • 자바스크립트는 인터페이스가 없어 추상화에 의존한다는 것은 암시적인 것 (약속) 이므로, 다른 객체나 클래스에 노출되는 메소드와 속성 자체가 이에 (암시적인 약속, 추상화) 해당한다고 볼 수 있다.
    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
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    class InventoryRequester {
    constructor() {
    this.REQ_METHODS = ["HTTP"];
    }

    // 암시적인 약속
    // InventoryTracker에 대한 모든 요청 모듈은 requestItems 메소드를 가질 것이다.
    requestItem(item) {
    // ...
    }
    }
    /*
    // Requester를 버전 별로 (e.g. V1, V2) 나누어서 클래스로 생성 가능하다.
    class InventoryRequesterV1 {
    constructor() {
    this.REQ_METHODS = ["HTTP"];
    }

    requestItem(item) {
    // ...
    }
    }

    class InventoryRequesterV2 {
    constructor() {
    this.REQ_METHODS = ["WS"];
    }

    requestItem(item) {
    // ...
    }
    }
    */


    // bad
    class InventoryTracker {
    constructor(items) {
    this.items = items;

    // BAD: We have created a dependency on a 'specific' request implementation.
    // We should just have requestItems 'depend on a request method': `request`
    this.requester = new InventoryRequester();
    }

    requestItems() {
    this.items.forEach(item => {
    this.requester.requestItem(item);
    });
    }
    }

    const inventoryTracker = new InventoryTracker(["apples", "bananas"]);
    inventoryTracker.requestItems();


    // good
    class InventoryTracker {
    constructor(items, requester) {
    this.items = items;
    this.requester = requester;
    }

    requestItems() {
    this.items.forEach(item => {
    this.requester.requestItem(item);
    });
    }
    }

    // By constructing our dependencies externally and injecting them, we can easily
    // substitute our request module for a fancy new one that uses WebSockets.
    const inventoryTracker = new InventoryTracker(
    ["apples", "bananas"],
    // requester를 인자로 전달하여 tracker의 default requester로 객체를 설정하(여 코드를 정의하)는 것이 아니라 tracker 생성 시 개별적으로 적용 (생성) 할 수 있게 설정한다.
    new InventoryRequesterV2()
    );
    inventoryTracker.requestItems();