[TIL] Day 56 : OOP 설계의 5 Principles (SOLID)

Q·2024년 7월 5일

TIL

목록 보기
57/59

SOLID

Single Responsibility Principle, SRP (단일 책임)

SRP를 위반하는 예시

class UserSettings {
	constructor(user) {
    	this.user = user;
	}
    
    changeSettings(userSettings) {
    	if (this.verifyCredentials()) {
        	// ...
        }
    }
    
    verifyCredentials() {
    	// ...
    }
}
    

SRP를 따르도록 변경

class UserAuth {
	constructor(user) {
    	this.user = user;
    }
    
    verifyCredentials() {
    	// ...
    }
}

class UserSettings {
	constructor(user, userAuth) {
    	this.user = user;
        this.userAuth = userAuth;
    }
    
    changeSettings(userSettings) {
    	if (this.userAuth.verifyCredentials()) {
        	// ...
        }
    }
}

Open-Closed Principle, OCP (개방-폐쇄)

소프트웨어 엔티티 또는 객체는 확장에는 열려있으나 변경에는 닫혀있어야 한다.
기존 코드에 영향을 주지 않고 소프트웨어에 새로우 ㄴ기능이나 구성 요소를 추가할 수 있어야 한다는 의미

OCP 위반 예시

function calculator(nums, option) {
	let result = 0;
    for (const num of nums) {
    	if (option === 'add') result += num;
        else if (option === 'sub') result -= num;
    }
    return result;
}

OCP 따르도록 변경

function calculator(nums, callBackFunc) {
	let result = 0;
    for (const num of nums) {
    	result = callBackFunc(result, num);
    }
    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;

Liskov Substitution Principle (리스코프 치환)

어플리케이션에서 객체는 프로그램의 동작에 영향을 주지 않으면서, 하위 타입의 객체로 바꿀 수 있어야 한다.
즉, S가 T의 하위 유형이라면, 프로그램의 기능에 변화를 주지 않고서도 T 타입의 객체를 S 객체로 대체할 수 있어야 한다.

LSP 위반 예시

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()
	.setWidth(5)
    .setHeight(7)
    .getArea(); // 35
    
const squareArea = new Square()
	.setWidth(5)
    .setHeight(7)
    .getArea(); // 49

LSP 따르도록 변경

class Shape {
    getArea() {
    }
}

class Rectangle extends Shape {
	constructor(width = 0, height = 0) {
    	super();
    	this.width = width
    	this.height = width;
    }
    
    getArea() {
    	return this.width * this.height;
    }
}

class Square extends Shape {
	constructor(length = 0) {
    	super();
    	this.length = length;
    }
    
    getArea() {
    	return this.length * this.length;
    }
}


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

Interface Segregation Principle (인터페이스 분리)

특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다
사용자가 필요하지 않은 것들에 의존하지 않도록 인터페이스는 최대한 작고 구체적으로 유지

ISP 위반 예시

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

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

class EconomicPrinter implements SmartPrinter {
	print() {
    	// ...
    }
    
    fax() {
    	throw new Error('팩스 기능을 지원하지 않습니다.');
    }
    
    scan() {
    	throw new Error('스캔 기능을 지원하지 않습니다.');
    }
}

ISP 따르도록 변경

interface Printer {
	print();
}
    
interface Fax {
	fax();
}

interface Scanner {
	scan();
}

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

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

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

Dependency Inversion Principle (의존성 역전)

프로그래머는 추상화에 의존해야지 구체화에 의존하면 안 된다.
높은 계층의 모듈이 저수준의 모듈에 직접 의존하면 안 된다.

DIP 위반 예시

import { readFile } from 'node:fs/promises';

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

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

class ReportReader {

	async read(path) {
    	const fileExtension = path.split('.').pop();
        
        if (fileExtenstion === 'xml') {
        	const formatter = new XmlFormatter();
            
            const text = await readFile(path, (err, data) => data);
            return formatter.parseXml(text);
            
        } else if (fileExtension === 'json') {
        	const formatter = new JsonFormatter();
            
            const text = await readFile(path, (err, data) => data);
            return formatter.parseJson(text);
            
        }
}

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

DIP 따르도록 변경

import { readFile } from 'node:fs/promises';

class Formatter { 
	parse() { }
}

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

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

class ReportReader {
	constructor(formatter) {
    	this.formatter = formatter;
    }
    
    async read(path) {
    	const text = await readFile(path, (err, data) => data);
        return this.formatter.parse(text);
    }
}

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

0개의 댓글