320x100
320x100

SOLID

: 객체 지향 설계에서 지켜야할 5가지 소프트웨어 개발 원칙

: SRP / OCP / LSP / ISP / DIP

 

- 목표

: 코드 확장 및 유지보수의 용이

: 불필요한 복잡성 제거

: 프로젝트 개발 생산성 향상

 

 

 

 

 

 

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

: 클래스 (객체)는 단 하나의 책임 (기능)만 가져야 한다

: 클래스에 국한되는 것이 아니라 함수 등의 데이터에도 해당된다

 

- 고려 사항

: 재사용이나 변경의 여지가 있는가?

: 함수명이 표현식 보다 훨씬 가독성이 있는가?

: 함수 표현으로 전체 파이프라인을 왔다갔다 하게되어 이해하는데 방해가 되는가?

: 하나의 파이프 라인에 속한 함수들이 각각의 모듈로 쪼개져 있어 응집도가 떨어지는가?

 

- 나쁜 예시

function emailActiveClients(clients: Client[]) {
  clients.forEach((client) => {
    const clientRecord = database.lookup(client);
    if (clientRecord.isActive()) {
      email(client);
    }
  });
}

 

- 좋은 예시

function emailActiveClients(clients: Client[]) {
  clients.filter(isActiveClient).forEach(email);
}

function isActiveClient(client: Client) {
  const clientRecord = database.lookup(client);
  return clientRecord.isActive();
}

 

- 요점

: 순수 함수를 작성하자

: 가독성과 응집도를 기준으로 적절히 쪼개자

 

※ 순수함수

: 1개의 반환값이 반드시 존재한다

: 같은 인자를 넣었을때에는 항상 같은 값을 반환한다

: 함수 외부의 어떠한 값을 변화시켜서는 안된다

 

 

 

 

 

 

 

OCP (개방 폐쇄 원칙, Open Closed Principle)

: 확장에 열려있으나, 수정에는 닫혀있어야 한다

: 새로운 기능의 추가가 일어났을때에는 기존 코드의 수정 없이 추가가 가능해야 하고, 내부 매커니즘이 변경되어야 하는 경우에는 외부의 코드 변화가 없어야 한다

 

- 나쁜 예시

function getMutipledArray(array, option) {
  const result = []
  for (let i = 0; i < array.length; i++) {
    if (option === "doubled") {
      result[i] = array[i] * 2 // 새로운 방식으로 만들기 위해서는 수정이 필요하다.
    }
    if (option === "tripled") {
      result[i] = array[i] * 3 // 옵션으로 분기는 가능하나
    }
    if (option === "half") {
      result[i] = array[i] / 2 // 새로운 기능을 추가하려면 함수 내에서 변경이 되어야 한다.
    }
  }
  return result
}

 

- 좋은 예시

// option을 받는게 아니라 fn을 받아보자.
// 이제 새로운 array를 만든다는 매커니즘은 닫혀있으나 방식에 대해서는 열려있다.
function map(array, fn) {
  const result = []
  for (let i = 0; i < array.length; i++) {
    result[i] = fn(array[i], i, array) // 내부 값을 외부로 전달하고 결과를 받아서 사용한다.
  }
  return result
}

// 얼마든지 새로운 기능을 만들어도 map코드에는 영향이 없다.
const getDoubledArray = (array) => map(array, (x) => x * 2)
const getTripledArray = (array) => map(array, (x) => x * 3)
const getHalfArray = (array) => map(array, (x) => x / 2)

 

- 요점

: 하나의 함수 기능이 여러가지 옵션들로 인해 내부에서 분기가 많이 발생한다면 OCP와 SRP 원칙에 맞게 함수를 매개 변수로 받는 방법을 통해서 공통 매커니즘의 코드와 새로운 기능에 대한 코드를 분리해서 다룰 수 있게 하자

 

 

 

 

 

 

 

LSP (리스코프 치환원칙, Liskov Substitution Principle)

: 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다

: 상속을 받은 하위 타입과 상위 타입을 치환해도 프로그램에 문제가 없어야 한다

 

- 리스코프 치환 원칙 위반 예시

: 앵무새는 새다

: 펭귄은 새다

: 새를 상속받아서 각각을 만들었을때 앵무새는 날 수 있지만, 펭귄은 날지 못하기 때문에 새가 될 수 없다

 

- 요점

: 먼저 선언된 조건들과 나중에 선언된 조건들이 서로 충돌나는 것을 방지 해야한다

: 선언형 함수형 프로그래밍에서 발생하는 순환 종속성을 만들어내는 infinite Cycle을 만들지 않아야 한다

 

- 함수형 프로그래밍에서의 예시

: 상속을 기반하는 원칙이기 때문에 함수형 프로그래밍에서는 적용하기 어렵다

 

- 나쁜 예시

class Rectangle {
  constructor(
    protected width: number = 0,
    protected height: number = 0) {

  }

  setColor(color: string): this {
    // ...
  }

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

  setWidth(width: number): this {
    this.width = width;
    return this;
  }

  setHeight(height: number): this {
    this.height = height;
    return this;
  }

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

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

  setHeight(height: number): this {
    this.width = height;
    this.height = height;
    return this;
  }
}

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

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

 

- 좋은 예시

abstract class Shape {
  setColor(color: string): this {
    // ...
  }

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

  abstract getArea(): number;
}

class Rectangle extends Shape {
  constructor(
    private readonly width = 0,
    private readonly height = 0) {
    super();
  }

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

class Square extends Shape {
  constructor(private readonly length: number) {
    super();
  }

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

function renderLargeShapes(shapes: Shape[]) {
  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);

 

 

 

 

 

 

 

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

: 사용자가 필요하지 않은 것들에 의존하게 되지 않도록 인터페이스를 작게 유지하라

 

- 함수형 프로그래밍에서의 예시

: 함수는 사실 interface당 함수가 1:1의 관계이기 때문에 ISP의 원칙을 위배하기 어렵다

: 다만, 타입스크립트에서 interface에 ?:로 필드를 추가하는 경우 이를 상속받는 객체에서 문제가 될 수 있다 (ISP 위배)

 

- 예시

: All in One USB가 있지만, 모든 USB에 기기가 연결되어 있어야만 충전이 가능하다

- 요점

: 하나의 모듈에 너무 많은 기능을 넣어서 덩치를 키우지말자

 

- 나쁜 예시

interface SmartPrinter {
  print();
  fax();
  scan();
}

class AllInOnePrinter implements SmartPrinter {
  print() {
    // ...
  }  
  
  fax() {
    // ...
  }

  scan() {
    // ...
  }
}

class EconomicPrinter implements SmartPrinter {
  print() {
    // ...
  }  
  
  fax() {
    throw new Error('Fax not supported.');
  }

  scan() {
    throw new Error('Scan not supported.');
  }
}

 

- 좋은 예시

interface Printer {
  print();
}

interface Fax {
  fax();
}

interface Scanner {
  scan();
}

class AllInOnePrinter implements Printer, Fax, Scanner {
  print() {
    // ...
  }  
  
  fax() {
    // ...
  }

  scan() {
    // ...
  }
}

class EconomicPrinter implements Printer {
  print() {
    // ...
  }
}

 

 

 

 

 

 

 

DIP (의존 역전 원칙, Dependency Inversion Principle)

: 프로그래머는 추상화에 의존해야지 구체화에 의존하면 안된다

: 숭위 레벨 모듈이 하위 레벨 세부 사항에 의존해서는 안된다

 

- 예시

: 전기를 이용하기 위해서는 플러그를 꽂으면 된다 (추상화)

: 플러그에서 실제 전기 배선이 어떻게 되는지 (구체화) 는 몰라도 된다 

: 우리가 필요한 것은 전기이지, 실제로 전기를 얻기 위한 구체적인 방법이 아니다

 

- 나쁜 예시

import { readFile as readFileCb } from 'fs';
import { promisify } from 'util';

const readFile = promisify(readFileCb);

type ReportData = {
  // ..
}

class XmlFormatter {
  parse<T>(content: string): T {
    // Converts an XML string to an object T
  }
}

class ReportReader {

  // BAD: We have created a dependency on a specific request implementation.
  // We should just have ReportReader depend on a parse method: `parse`
  private readonly formatter = new XmlFormatter();

  async read(path: string): Promise<ReportData> {
    const text = await readFile(path, 'UTF8');
    return this.formatter.parse<ReportData>(text);
  }
}

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

 

- 좋은 예시

import { readFile as readFileCb } from 'fs';
import { promisify } from 'util';

const readFile = promisify(readFileCb);

type ReportData = {
  // ..
}

interface Formatter {
  parse<T>(content: string): T;
}

class XmlFormatter implements Formatter {
  parse<T>(content: string): T {
    // Converts an XML string to an object T
  }
}


class JsonFormatter implements Formatter {
  parse<T>(content: string): T {
    // Converts a JSON string to an object T
  }
}

class ReportReader {
  constructor(private readonly formatter: Formatter) {
  }

  async read(path: string): Promise<ReportData> {
    const text = await readFile(path, 'UTF8');
    return this.formatter.parse<ReportData>(text);
  }
}

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

// or if we had to read a json report
const reader = new ReportReader(new JsonFormatter());
const report = await reader.read('report.json');

 

 

 

 

 

 

 

 

Reference

 

GitHub - labs42io/clean-code-typescript: Clean Code concepts adapted for TypeScript

Clean Code concepts adapted for TypeScript. Contribute to labs42io/clean-code-typescript development by creating an account on GitHub.

github.com

 

Javascript에서도 SOLID 원칙이 통할까?

제가 며칠 전에 클린소프트웨어 책을 보니 SOLID 법칙이 나오던데요, 자바나 C++ 같은 클래스 구조로 객체를 만드는 언어에서는 쉽게 따라해볼 수 있겠는데, 함수 위주로 작성하는 js, ts를 사용하

velog.io

 

300x250
728x90