노드심화 3-3

·2022년 12월 27일
0

study

목록 보기
32/81
post-thumbnail

객체 지향 설계

  • 좋은 설계요구하는 기능온전히 수행하면서 추후의 변경을 매끄럽게 수용할 수 있는 설계입니다.

  • 변경 가능한 코드이해하기 쉬운 코드입니다. 만약 코드를 변경해야 하는데 그 코드를 이해할 수 없다면 변경에 유용하더라도 코드를 수정하겠다는 마음이 선뜻 들지는 않을 것입니다.

  • 변경하기 쉬운 설계는 한 번에 하나의 클래스만 변경할 수 있는 설계입니다.

  • 훌륭한 객체지향 설계의 핵심은 캡슐화를 이용해 의존성을 적절히 관리함으로써 객체 사이의 결합도를 낮추는 것이다.

객체 지향 설계 5원칙 (SOLID)

객체 지향 프로그래밍 및 설계의 다섯가지 기본원칙을 SOLID라고 부른다.

프로그래머가 시간이 지나도 유지 보수와 확장이 쉬운 시스템을 만들고자 할 때 사용합니다.

SOLID의 종류

  • 단일 책임의 원칙 (Single Responsibility Principle, SRP)
  • 개방-폐쇄 원칙 (Open-Closed Principle, OCP)
  • 리스코프 치환 원칙 (Liskov substitution principle, LSP)
  • 인터페이스 분리 원칙 (Interface segregation principle, ISP)
  • 의존성 역전 원칙 (Dependency Inversion Principle, DIP)

단일 책임의 원칙(SRP)

하나의 객체는 단 하나의 책임을 가져야 한다.
=> 클래스마다 단 1개의 책임
=> 클래스나 모듈을 변경할 이유가 단 하나 뿐이어야 한다는 원칙입니다.

/** SRP Before **/
class UserSettings {
  constructor(user) { // UserSettings 클래스 생성자
    this.user = user;
  }

  changeSettings(userSettings) { // 사용자의 설정을 변경하는 메소드
    if (this.verifyCredentials()) {
      //...
    }
  }

  verifyCredentials() { // 사용자의 인증을 검증하는 메소드
    //...
  }
}

=>

/** SRP After **/
class UserAuth {
  constructor(user) { // UserAuth 클래스 생성자
    this.user = user;
  }

  verifyCredentials() { // 사용자의 인증을 검증하는 메소드
    //...
  }
}

class UserSettings {
  constructor(user) { // UserSettings 클래스 생성자
    this.userAuth = new UserAuth(user); // UserAuth를 새로운 객체로 정의한다.
  }

  changeSettings(userSettings) { // 사용자의 설정을 변경하는 메소드
    if (this.userAuth.verifyCredentials()) { // 생성자에서 선언한 userAuth 객체의 메소드를 사용한다.
      //...
    }
  }
}

개방-폐쇄 원칙(OCP)

소프트웨어 엔티티 또는 개체(클래스, 모듈, 함수 등)는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
=> 소프트웨어 개체의 행위는 확장될 수 있어야 하지만, 개체를 변경해서는 안된다.
=> 기존 코드에 영향을 주지않고 소프트웨어에 새로운 기능이나 구성 요소를 추가할 수 있어야 한다

/** OCP Before **/
function calculator(nums, option) {
  let result = 0;
  for (const num of nums) {
    if (option === "add") result += num; // option이 add일 경우 덧셈 연산을 합니다.
    else if (option === "sub") result -= num; // option이 sub일 경우 뺄셈 연산을 합니다.
    // 새로운 연산(기능)을 추가 하기 위해서는 함수 내부에서 코드 수정이 필요합니다.
  }
  return result;
}

console.log(calculator([2, 3, 5], "add")); // 10
console.log(calculator([5, 2, 1], "sub")); // -8

=>

/** OCP After **/
function calculator(nums, callBackFunc) {
  // option을 CallbackFunc로 변경
  let result = 0;
  for (const num of nums) {
    result = callBackFunc(result, num); // option으로 분기하지 않고, Callback함수를 실행하도록 변경
  }
  return result;
}

const add = (a, b) => a + b; // 함수 변수를 정의합니다.
const sub = (a, b) => a - b;
const mul = (a, b) => a * b;
const div = (a, b) => a / b;
console.log(calculator([2, 3, 5], add)); // add 함수 변수를 Callback 함수로 전달합니다.
console.log(calculator([5, 2, 1], sub)); // sub 함수 변수를 Callback 함수로 전달합니다.

리스코프 치환 원칙(LSP)

프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
=> 만약 부모 클래스와 자식 클래스가 있는 경우 서로를 바꾸더라도 해당 프로그램에서 잘못된 결과를 도출하지 않는 것입니다.

/** LSP Before **/
class Rectangle {
  constructor(width = 0, height = 0) { // 직사각형의 생성자
    this.width = width;
    this.height = height;
  }

  setWidth(width) { // 직사각형은 높이와 너비를 독립적으로 정의한다.
    this.width = width;
    return this;
  }

  setHeight(height) { // 직사각형은 높이와 너비를 독립적으로 정의한다.
    this.height = height;
    return this;
  }

  getArea() { // 사각형의 높이와 너비의 결과값을 조회하는 메소드
    return this.width * this.height;
  }
}

class Square extends Rectangle { // 정사각형은 직사각형을 상속받습니다.
  setWidth(width) { // 정사각형은 높이와 너비가 동일하게 정의된다.
    this.width = width;
    this.height = width;
    return this;
  }

  setHeight(height) { // 정사각형은 높이와 너비가 동일하게 정의된다.
    this.width = height;
    this.height = height;
    return this;
  }
}

const rectangleArea = new Rectangle() // 35
  .setWidth(5) // 너비 5
  .setHeight(7) // 높이 7
  .getArea(); // 5 * 7 = 35
const squareArea = new Square() // 49
  .setWidth(5) // 너비 5
  .setHeight(7) // 높이를 7로 정의하였지만, 정사각형은 높이와 너비를 동일하게 정의합니다.
  .getArea(); // 7 * 7 = 49

=>

/** LSP After **/
class Shape { // Rectangle과 Square의 부모 클래스를 정의합니다.
  getArea() { // getArea는 빈 메소드로 정의
  }
}

class Rectangle extends Shape { // Rectangle은 Shape를 상속받습니다.
  constructor(width = 0, height = 0) { // 직사각형의 생성자
    super();
    this.width = width;
    this.height = height;
  }

  getArea() { // 직사각형의 높이와 너비의 결과값을 조회하는 메소드
    return this.width * this.height;
  }
}

class Square extends Shape { // Square는 Shape를 상속받습니다.
  constructor(length = 0) { // 정사각형의 생성자
    super();
    this.length = length; // 정사각형은 너비와 높이가 같이 깨문에 width와 height 대신 length를 사용합니다.
  }

  getArea() { // 정사각형의 높이와 너비의 결과값을 조회하는 메소드
    return this.length * this.length;
  }
}

const rectangleArea = new Rectangle(7, 7) // 49
  .getArea(); // 7 * 7 = 49
const squareArea = new Square(7) // 49
  .getArea(); // 7 * 7 = 49

인터페이스 분리 원칙(ISP)

사용자가 필요하지 않은 것들에 의존하지 않도록, 인터페이스를 작게 유지해야 한다.

인터페이스(interface)는 대표적으로 Java, C++ 그리고 Typescript에서 사용하는 문법

Javascript에서는 interface 기능을 제공하지 않으므로 이번 예제는 Typescript로 진행

/** ISP Before **/
interface SmartPrinter { // SmartPrinter가 사용할 수 있는 기능들을 정의한 인터페이스 
  print();

  fax();

  scan();
}

// SmartPrinter 인터페이스를 상속받은 AllInOnePrinter 클래스
class AllInOnePrinter implements SmartPrinter {
  print() { // AllInOnePrinter 클래스는 print, fax, scan 기능을 지원한다.
    // ...
  }

  fax() { // AllInOnePrinter 클래스는 print, fax, scan 기능을 지원한다.
    // ...
  }

  scan() { // AllInOnePrinter 클래스는 print, fax, scan 기능을 지원한다.
    // ...
  }
}

// SmartPrinter 인터페이스를 상속받은 EconomicPrinter 클래스
class EconomicPrinter implements SmartPrinter {
  print() { // EconomicPrinter 클래스는 print 기능만 지원한다.
    // ...
  }

  fax() { // EconomicPrinter 클래스는 fax 기능을 지원하지 않는다.
    throw new Error('팩스 기능을 지원하지 않습니다.');
  }

  scan() { // EconomicPrinter 클래스는 scan 기능을 지원하지 않는다.
    throw new Error('Scan 기능을 지원하지 않습니다.');
  }
}

=>

/** ISP After **/
interface Printer { // print 기능을 하는 Printer 인터페이스
  print();
}

interface Fax { // fax 기능을 하는 Fax 인터페이스
  fax();
}

interface Scanner { // scan 기능을 하는 Scanner 인터페이스
  scan();
}


// AllInOnePrinter클래스는 print, fax, scan 기능을 지원하는 Printer, Fax, Scanner 인터페이스를 상속받았다.
class AllInOnePrinter implements Printer, Fax, Scanner {
  print() { // Printer 인터페이스를 상속받아 print 기능을 지원한다.
    // ...
  }

  fax() { // Fax 인터페이스를 상속받아 fax 기능을 지원한다.
    // ...
  }

  scan() { // Scanner 인터페이스를 상속받아 scan 기능을 지원한다.
    // ...
  }
}

// EconomicPrinter클래스는 print 기능을 지원하는 Printer 인터페이스를 상속받았다.
class EconomicPrinter implements Printer {
  print() { // EconomicPrinter 클래스는 print 기능만 지원한다.
    // ...
  }
}

// FacsimilePrinter클래스는 print, fax 기능을 지원하는 Printer, Fax 인터페이스를 상속받았다.
class FacsimilePrinter implements Printer, Fax {
  print() { // FacsimilePrinter 클래스는 print, fax 기능을 지원한다.
    // ...
  }

  fax() { // FacsimilePrinter 클래스는 print, fax 기능을 지원한다.
    // ...
  }
}

SmartPrinter 인터페이스에 정의된 기능들을 Printer, Fax, Scanner 인터페이스로 분리하여 ISP 원칙에서 “클라이언트가 필요하지 않는 기능을 가진 인터페이스에 의존해서는 안 되고, 최대한 인터페이스를 작게 유지해야한다.”에 해당하는 원칙을 수행하는 코드로 개선할 수 있습니다

의존성 역전 원칙(DIP)

  1. 고수준 계층의 모듈(도메인)은 저수준 계층의 모듈(하부구조)에 의존해서는 안된다. 둘 다 추상화에 의존해야 한다.

  2. 추상화는 세부 사항에 의존해서는 안 된다. 세부 사항은 추상화에 의존해야 한다.

만약 추상화를 하지 않고 고수준 계층의 모듈이 저수준 계층의 모듈을 의존하고 있다면 어떤 상황이 발생할까요? 사소한 코드 변경에도 고수준 계층의 코드를 변경해야할 것이고, 소모되는 개발 코스트또한 엄청나게 증가할 것입니다.

/** DIP Before **/
const readFile = require('fs').readFile;

class XmlFormatter {
  parseXml(content) {
    // Xml 파일을 String 형식으로 변환합니다.
  }
}

class JsonFormatter {
  parseJson(content) {
    // JSON 파일을 String 형식으로 변환합니다.
  }
}

class ReportReader {

  async read(path) {
    const fileExtension = path.split('.').pop(); // 파일 확장자

    if (fileExtension === 'xml') {
      const formatter = new XmlFormatter(); // xml 파일 확장자일 경우 XmlFormatter를 사용한다.

      const text = await readFile(path, (err, data) => data);
      return formatter.parseXml(text); // xmlFormatter클래스로 파싱을 할 때 parseXml 메소드를 사용한다.

    } else if (fileExtension === 'json') {
      const formatter = new JsonFormatter(); // json 파일 확장자일 경우 JsonFormatter를 사용한다.

      const text = await readFile(path, (err, data) => data);
      return formatter.parseJson(text); // JsonFormatter클래스로 파싱을 할 때 parseJson 메소드를 사용한다.
    }
  }
}

const reader = new ReportReader();
const report = await reader.read('report.xml');
// or
// const report = await reader.read('report.json');

=>

/** DIP After **/
const readFile = require('fs').readFile;

class Formatter { // 인터페이스지만, Javascript로 구현하기 위해 클래스로 선언합니다.
  parse() {  }
}

class XmlFormatter extends Formatter {
  parse(content) {
    // Xml 파일을 String 형식으로 변환합니다.
  }
}

class JsonFormatter extends Formatter {
  parse(content) {
    // JSON 파일을 String 형식으로 변환합니다.
  }
}

class ReportReader {
  constructor(formatter) { // 생성자에서 Formatter 인터페이스를 상속받은 XmlFormatter, JsonFormatter를 전달받습니다.
    this.formatter = formatter;
  }

  async read(path) {
    const text = await readFile(path, (err, data) => data);
    return this.formatter.parse(text); // 추상화된 formatter로 데이터를 파싱합니다.
  }
}

const reader = new ReportReader(new XmlFormatter());
const report = await reader.read('report.xml');
// or
// const reader = new ReportReader(new JsonFormatter());
// const report = await reader.read('report.json');
profile
개발자 꿈나무

0개의 댓글