SOLID 원칙

soonniee·2021년 5월 7일
0

클린코딩

목록 보기
5/10
post-thumbnail

1. 단일책임원칙

단일책임원칙에 대한 사전적 의미는 다음과 같다.

클래스는 단일책임을 맡는다.

쉽게 와닿지 않는다.
빠른 이해를 위해서 맥도날드 매점의 분업화를 떠올려보자.

테이블이 더럽다. 누구의 책임인가?
패티가 덜 익혀졌다. 누구의 책임인가?
계산 오류가 발생했다. 누구의 책임인가?

책임소재가 불분명하면 작은 오류에도 시스템 전체가 흔들릴 수 있다.

이는 프로그래밍에서도 마찬가지로 하나의 클래스에 모든 동작을 집어 넣으면 시스템은 비대해지고, 한 부분의 오류로 전체가 무너지게된다.

예시를 통해 살펴보자.

//Bad👎

class UserSettings {
  constructor(user) {
    this.user = user;
  }

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

  verifyCredentials() {
    // ...
  }
}

UserSettings 클래스 안에 changeSettings 메소드와 verifyCredentials 메소드가 함께 존재한다.

//Good👍

class UserAuth {
  constructor(user) {
    this.user = user;
  }

  verifyCredentials() {
    // ...
  }
}


class UserSettings {
  constructor(user) {
    this.user = user;
    this.auth = new UserAuth(user);
  }

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

UserAuth 클래스에 verifyCredentials 메소드, UserSettings 클래스에 changeSettings 메소드로 나누어 클래스의 단일책임원칙을 지키도록 바꾼다.

2. 개방/폐쇄 원칙

개방/폐쇄 원칙을 더 구체적으로 말하자면, "확장은 개방, 수정은 폐쇄" 원칙이다.

개방/폐쇄 원칙을 한 마디로 표현하자면 "아웃소싱" 개념에 가깝다. (Airbnb VS 힐튼 호텔)

변화하는 부분을 추상화함으로써 기존 코드를 수정하지 않고도, 확장을 할 수 있게 만들어 준다.

//Bad
class AjaxAdapter extends Adapter {
  constructor() {
    super();
    this.name = 'ajaxAdapter';
  }
}

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

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

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

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

function makeHttpCall(url) {
  // request and return promise
}
//Good👍

class AjaxAdapter extends Adapter {
  constructor() {
    super();
    this.name = 'ajaxAdapter';
  }

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

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

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

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

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

3. 리스코프 치환 원칙

객체지향 프로그래밍에서 리스코프 치환 원칙을 사용해야하는 이유는 "복제"와 관련이 깊다.

객체 지향에서 "복제"를 간단하게 하는 방법은 "상속"으로 상속은 extends 를 사용한다.

그리고 super()라는 메소드로 상위 클래스의 특징과 기능을 복제할 수 있다.

//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(); // 정사각형일때 25를 리턴합니다. 하지만 20이어야 하는게 맞습니다.
    rectangle.render(area);
  });
}

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

class Shape {
  setColor(color) {
    // ...
  }

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

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }

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

class Square extends Shape {
  constructor(length) {
    super();
    this.length = length;
  }

  getArea() {
    return this.length * this.length;
  }
}

function renderLargeShapes(shapes) {
  shapes.forEach((shape) => {
      const area = shape.getArea();
      shape.render(area);
    });
  }

const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeShapes(shapes);

Shape 클래스를 상속받은 후 getArea 메소드는 Rectangle, Square 클래스 별로 따로 정의하여 문제 해결.

4. 인터페이스 분리 원칙.

Javascript에서 인터페이스 분리 원칙을 보여주는 가장 좋은 예는 방대한 양의 설정 객체가 필요한 클래스입니다.

설정을 선택적으로 할 수 있다면 무거운 인터페이스를 만드는 것을 방지할 수 있습니다.

//Bad
class DOMTraverser {
  constructor(settings) {
    this.settings = settings;
    this.setup();
  }

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

  traverse() {
    // ...
  }
}

const $ = new DOMTraverser({
  rootNode: document.getElementsByTagName('body'),
  animationModule() {} // 우리는 대부분의 경우 DOM을 탐색할 때 애니메이션이 필요하지 않습니다.
  // ...
});
//Good
class DOMTraverser {
  constructor(settings) {
    this.settings = settings;
    this.options = settings.options;
    this.setup();
  }

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

  setupOptions() {
    if (this.options.animationModule) {
      // ...
    }
  }

  traverse() {
    // ...
  }
}

const $ = new DOMTraverser({
  rootNode: document.getElementsByTagName('body'),
  options: {
    animationModule() {}
  }
});

setupOptions 메소드를 추가하여 설정을 선택적으로 할 수 있도록 함.

5. 의존성 역전 원칙.

고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 된다. 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다."를 의미합니다. 이것을 아주 쉽게 말하면, "자신보다 변하기 쉬운 것에 의존하지 마라"라고 할 수 있다.

의존 역전 원칙은 그 중에서도 추상화를 이용한다. (스노우 타이어나 일반 타이어를 '타이어' 자체로 추상화)

//Bad
class InventoryRequester {
  constructor() {
    this.REQ_METHODS = ['HTTP'];
  }

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

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

    // 안좋은 이유: 특정 요청방법 구현에 대한 의존성을 만들었습니다.
    // requestItems는 한가지 요청방법을 필요로 합니다.
    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);
    });
  }
}

class InventoryRequesterV1 {
  constructor() {
    this.REQ_METHODS = ['HTTP'];
  }

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

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

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

// 의존성을 외부에서 만들어 주입해줌으로써,
// 요청 모듈을 새롭게 만든 웹소켓 사용 모듈로 쉽게 바꿔 끼울 수 있게 되었습니다.
const inventoryTracker = new InventoryTracker(['apples', 'bananas'], new InventoryRequesterV2());
inventoryTracker.requestItems();

0개의 댓글

관련 채용 정보