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
'Programming > TypeScript' 카테고리의 다른 글
type과 interface, union의 차이점 (0) | 2023.11.07 |
---|---|
TypeScript에서의 enum (0) | 2023.11.07 |
타입스크립트 설치 및 초기세팅 (0) | 2023.07.07 |
ts-pattern을 활용한 선언적 분기 작성 (0) | 2023.05.29 |
타입스크립트의 유틸리티 (0) | 2023.04.30 |