[클린코드 TS] SOLID

moonee·2021년 9월 12일
1

클린코드

목록 보기
5/5

📍단일 책임 원칙

  • 클래스의 응집도가 낮다 === 변경될 상황이 많다.
  • 많은 기능이 하나의 클래스에 있고, 해당 클래스의 기능을 수정한다면 다른 종속된 모듈에 미칠 영향을 파악하는 것이 어렵다.


📍개방 폐쇄 원칙

  • 소프트웨어 엔터티(클래스, 모듈, 함수 등)는 상속에 개방되어 있지만 수정에는 폐쇄 되어 있다.
  • 즉, 기존 코드를 변경하지 않고 새로운 기능을 추가 할 수 있도록 해야한다.

Bad

class AjaxAdapter extends Adapter{
	constructor(){
    	super();
    }
}

class NodeAdapter extends Adapter{
	consructor(){
      	super();
    }
}

class HttpRequester {
	constructor(private readonly adapter: Adapter){}
  	
  	async fetch<T>(url:string): Promise<T>{
    	if(this.adapter instanceof AjaxAdapter){
        	const response = await makeAjaxCall<T>(url);
        } else if(this.adaper instanceof NodeAdapter){
        	const response = await makeHttpCall<T>(url);
        }
 
    }
}

function makeAjaxCall<T>(url:string): Promise<T>{ /*서버로 요청보냄*/ };
function makeHttpCall<T>(url:string): Promise<T>{ /*서버로 요청보냄*/ };
  • 클래스 HttpRequester의 fetch 메서드 내부에서 현재 adapter가 어떤 클래스의 객체인지에 따라서 각각 다른 요청 함수를 호출하고 있다.
  • 요청 함수의 기능이 달라질 경우 여러 군데를 수정해야하는 문제점이 있고, 요청 함수가 수정될 경우 fetch 메서드 내부 구현이 수정될 수 있는 문제가 있다.

Good

abstract class Adapter {
	abstract async request<T>(url:string): Promise<T>;
}

class AjaxAdapter extends Adapter{
	constructor(){
    	super();
    }
  
  async reqeust<T>(url:string): Promise<T>{}
}

class NodeAdapter extends Adapter{
	consructor(){
      	super();
    }
   async reqeust<T>(url:string): Promise<T>{}
}

class HttpRequester{
	constructor(private reqdonly adapter:Adapter){}
  	
  	async fetch<T>(url:string):Promise<T>{
    	const response= await this.adapter.request<T>(url);
    }
}
  • 추상클래스를 사용하여 하위 클래스들과 공유하는 추상 메서드를 정의한 후 각 클래스에서 해당 request 메서드를 구현한다.
  • 이에 따라 HttpRequester 의 adapter 객체가 어떤 클래스의 인스턴스인지 검사하지 않고 바로 해당 adapter에 따른 request 메서드를 호출 할 수 있으며, Adapter 클래스를 상속한 각 자식 클래스에서 request 메서드를 관리 할 수 있다.


📍리스코프 치환 원칙

  • 만약 S가 T의 하위 타입이라면, T타입의 객체는 S타입의 객체로 대체 될 수 있다.
  • 즉, 만약 부모 클래스와 자식 클래스가 있다면 부모 클래스와 자식 클래스는 잘못된 결과 없이 서로 교환하여 사용 될 수 있다는 것이다.

Bad

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(); // Square 클래스에서는 25 반환 / Rectangle 클래스에서는 20 반환 
    rectangle.render(area);
  });
}

const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);
  • 수학적으로 정사각형은 직사각형이지만, 이를 상속으로 구현하여 위와 같이 정사각형이 직사각형을 상속되도록 구현하여, 부모-자식을 서로 치환한다면 정사각형의 넓이를 제대로 구하지 못한다.

Good

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);
  • 공통적으로 Shape이란 상위 클래스에 공통되는 메서드를 정의 한후 각 클래스에서 메서드를 구현한다.
  • 이를 통해 부모-자식이 치환되어도 각각 올바른 결과가 도출됨을 알 수 있다.


📍인터페이스 분리 원칙

  • 클라이언트는 사용하지 않는 인터페이스에 의존하지 않는다.
  • 즉, 클라이언트가 노출된 메서드를 사용하는 데신에 전체 파이를 얻지 않는 방식으로 추상화를 설계 해야한다.

Bad

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.');
  }
}
  • Econimicprinter 클래스 에는 fax와 scan 기능이 필요하지 않지만, SmartPrinter 라는 interface에 정의 되어 있어서 구현 후 Error를 throw 하고 있다. 즉 불필요한 인터페이스에 의존되고 있다.

Good

interface Printer {
  print();
}

interface Fax {
  fax();
}

interface Scanner {
  scan();
}

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

  scan() {
    // ...
  }
}

class EconomicPrinter implements Printer {
  print() {
    // ...
  }
}
  • SmartPrinter Interface를 각각 Printer, Fax 그리고 Scanner Interface로 분리하여 추상화한다.


📍의존성 역전 원칙

  • 상위 레벨의 모듈은 하위 레벨의 모듈에 의존하지 않아야 한다. 두 모듈은 추상화에 의존해야한다.
  • 추상화는 세부사항에 의존하지 않아야 한다. 세부사항은 추상화에 의존해야 한다.
  • 이를 통해 모듈 간 결합도를 줄일 수 있다.

Bad

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

const readFile = promisify(readFileCb);

type ReportData = {
  // ..
}

class XmlFormatter {
  parse<T>(content: string): T {
    // XML 문자열을 T 객체로 변환
  }
}

class ReportReader {

  // 특정 요청의 구현에 의존
  // parse 메서드에 의존하는 ReportReader를 만들어야 한다.
  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();
await report = await reader.read('report.xml');
  • 위 코드는 ReportReader 내부에서 fomatter 객체를 생성한다.
  • 해당 객체는 '특정 요청' (여기에서는 Xml 포맷팅) 의 구현에 의존하게 된다.
  • 이에 따라서 ReportReader 클래스와 XmlFomatter 클래스 간의 결합도가 높아지게 된다.

Good

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 {
    // XML 문자열을 T 객체로 변환
  }
}


class JsonFormatter implements Formatter {
  parse<T>(content: string): T {
    // JSON 문자열을 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());
await report = await reader.read('report.xml');

// 또는 json 보고서가 필요한 경우
const reader = new ReportReader(new JsonFormatter());
await report = await reader.read('report.json');
  • parse 메서드를 갖는 Fomatter 클래스를 상속받는 XmlFomatter와 JsonFomatter 클래스를 구현했다.
  • ReportReader 클래스 내부에서 직접 특정 객체를 생성하지 않고, 생성자의 매개변수로 받아옴으로써 특정 클래스에 의존하지 않고 Fomatter 라는 추상화에 의존하고 있다.

참고

Clean-Code-Typescript Solid

profile
기록

0개의 댓글