SRP (Single Responsibility Principle) ‘클래스를 변경하는 이유가 한 가지 이상 있어서는 안 된다.’는 말이 있다. 한 개의 클래스가 한 가지 일만 하도록 하는 것은 매우 중요하다. 클래스의 기능이 많아질수록 역할이 불분명해지고 의존하고 있는 모듈들에 어떤 영향을 끼칠지 알 수 없기 때문이다.
Bad 1 2 3 4 5 6 7 8 9 10 11 12 13 14 class UserSettings { constructor (private readonly user: User ) { } changeSettings (settings: UserSettings ) { if (this .verifyCredentials()) { } } verifyCredentials ( ) { } }
Good 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class UserAuth { constructor (private readonly user: User ) { } verifyCredentials ( ) { } } class UserSettings { private readonly auth: UserAuth; constructor (private readonly user: User ) { this .auth = new UserAuth(user); } changeSettings (settings: UserSettings ) { if (this .auth.verifyCredentials()) { } } }
OCP (Open Closed Principle) 소프트웨어 개체(클래스, 모듈, 함수, 등)는 확장에 열려있지만 변경에는 닫혀있어야 한다. 기존 코드를 수정하지 않으면서 새로운 기능을 추가할 수 있어야 한다는 말이다.
Bad 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 class AjaxAdapter extends Adapter { constructor ( ) { super (); } } class NodeAdapter extends Adapter { constructor ( ) { 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 .adapter instanceof NodeAdapter) { const response = await makeHttpCall<T>(url); } } } function makeAjaxCall <T >(url: string ): Promise <T > { } function makeHttpCall <T >(url: string ): Promise <T > { }
Good 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 abstract class Adapter { abstract async request<T>(url: string ): Promise <T>; } class AjaxAdapter extends Adapter { constructor ( ) { super (); } async request<T>(url: string ): Promise <T>{ } } class NodeAdapter extends Adapter { constructor ( ) { super (); } async request<T>(url: string ): Promise <T>{ } } class HttpRequester { constructor (private readonly adapter: Adapter ) { } async fetch<T>(url: string ): Promise <T> { const response = await this .adapter.request<T>(url); } }
LSP (Liskov Substitution Principle) type S가 type T의 서브 타입이라면 type T는 아무 수정 없이도 type S로 대체될 수 있어야 한다. 다시 말하면 부모 클래스와 자식 클래스가 있을 때 그 둘은 대체 가능해야 한다는 것이다. ‘is-A’에 맞다고 하더라도 상위 타입에서 정의한 조건중 부합하지 않는게 있다면 이를 상속받아서는 안된다.
Bad 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 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(); rectangle.render(area); }); } const rectangles = [new Rectangle(), new Rectangle(), new Square()];renderLargeRectangles(rectangles);
Good 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 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) 클라이언트에게 굳이 필요없는 인터페이스를 노출시키지 말아야 한다는 뜻이다. SRP와도 연관이 있다. 클라이언트에게 전체 클래스가 아닌 필요한 메소드만 노출시켜 사용할 수 있도록 추상화를 잘 설계해야 한다.
Bad 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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.' ); } }
Good 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 interface Printer { print(); } interface Fax { fax(); } interface Scanner { scan(); } class AllInOnePrinter implements Printer , Fax , Scanner { print ( ) { } fax ( ) { } scan ( ) { } } class EconomicPrinter implements Printer { print ( ) { } }
Dependency Inversion Principle (DIP) 의존성 역전 원칙은 두 가지 중요한 특징이 있다.
상위레벨 모듈은 하위 레벨 모듈에 의존해서는 안된다. 두 모듈 모두 추상적으로 의존해야 한다.
추상화가 구체적이어서는 안된다. 구현이 추상화에 의존해야 한다.
의존성 역전 원칙에 따라 설계했을 시의 가장 큰 장점은 모듈간의 결합도(coupling)이 떨어진다는 점이다. DIP는 보통 IoC(Inversion of Control) 컨테이너를 사용하면 달성된다.
Bad 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import { readFile as readFileCb } from 'fs' ;import { promisify } from 'util' ;const readFile = promisify(readFileCb);type ReportData = { } class XmlFormatter { parse<T>(content: string ): T { } } class 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();const report = await reader.read('report.xml' );
Good 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 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 { } } class JsonFormatter implements Formatter { parse<T>(content: string ): 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' );const reader = new ReportReader(new JsonFormatter());const report = await reader.read('report.json' );
참고