프론트엔드/JavaScript

[JavaScript] 클린 코드 - 6. SOLID

S.O.L.I.D

객체지향 5대 원칙의 약자를 딴 것이다.

  1. Single Responsibility Principle
  2. Open Close Principle
  3. Liskov Substitution Principle
  4. Interface Seggregation Principle
  5. Dependency Inversion Principle

 

1. 단일 책임 원칙 (Single Responsibility Principle, SRP)

하나의 객체는 하나의 책임을 가진다.

 

클래스를 수정할 때 수정할 이유가 2개 이상 있으면 안 된다.

이는 하나의 클래스가 많은 기능을 가진 거나 다름없기 때문이다.

이러면 수정할 때 다른 모듈들에 어떤 영향을 끼치는지 이해하기 어려워진다.

// BAD - change, verify 2개의 기능 가짐
class UserSettings {
  constructor(user) {
    this.user = user;
  }

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

  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()) {
      // ...
    }
  }
}

 

2. 개방-폐쇄 원칙 (Open Close Principle, OCP)

소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에 개방적이고 변경엔 폐쇄적이어야 한다.

만약 하나의 모듈을 수정할 때 그 모듈을 이용하는 다른 모듈을 전부 고쳐야 한다면 수정하기 어려울 것이다.

이 원칙을 적용하면 기능을 추가, 변경할 때 이미 제대로 동작하고 있는 원래 코드를 변경하지 않아도 새로운 코드를 쉽게 추가할 수 있다.

// 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. 리스코프 치환 원칙(Liskov Substituion Priniciple)

매우 간단하지만 강력한 원칙이다.

상속 관계에서는 일반화 관계(IS-A) 관계가 성립해야 한다.

즉 자료형 S가 자료형 T의 하위형이면 변경 없이 T의 객체를 S의 객체로 치환할 수 있어야 한다.

객체 지향 프로그래밍에선 부모 클래스 대신 자식 클래스를 사용해도 문제가 없어야 한다.

// BAD - Square(정사각형)이 Rectangle(직사각형)을 대체하지 못함
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);

 

4. 인터페이스 분리 원칙(Interface Segregation Principle, ISP)

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() {},
  },
});

 

5. 의존성 역전 원칙 (Dependency Inversion Principle)

유연성이 극대화된 시스템이란 소스 코드가 추상에 의존하지 구체에 의존하면 안 된다.

즉 상위 모듈은 하위 모듈에 종속되어선 안된다. 둘 다 추상화에 의존해야 한다.

모듈 간의 의존성이 높을수록 리팩토링이 어려워지고 이는 안 좋은 습관이다.

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

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

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

    // BAD - 특정 요청방법 구현에 대한 의존성이 생김
    // 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) {
    // ...
  }
}

// GOOD - 의존성을 외부에서 만들어 주입해줌 -> 요청 모듈을 새 웹소켓 모듈로 쉽게 바꿔 끼울 수 있게 됨
const inventoryTracker = new InventoryTracker(["apples", "bananas"], new InventoryRequesterV2());
inventoryTracker.requestItems();

 

참조