디자인 패턴 with Typescript
역할 & 책임
1.책임 (Responsibility)
- 책임은 객체나 모듈이 수행해야 하는 구체적인 작업이나 기능을 의미.
- 맴버 변수 혹은 매소드로 표현
- 정보 보유 책임: 객체가 어떤 데이터를 가지고 있어야 하는가에 대한 책임입니다.
- 예를 들어,
User
클래스는 사용자의 이름과 이메일을 보유할 책임이 있습니다.
- 예를 들어,
- 행동 책임: 객체가 수행해야 할 작업에 대한 책임입니다.
- 예를 들어,
UserService
클래스는 사용자를 저장하거나 검색하는 작업을 수행할 책임이 있을 수 있습니다.
- 예를 들어,
2.역할 (Role)
- 역할은 책임의 집합
- 역할은 보통 인터페이스나 추상 클래스로 표현
- 예)
- 사용자 데이터를 데이터베이스에 저장하는 책임
- 사용자 입력을 유효성 검사하는 책임
- 특정 비즈니스 규칙을 적용하는 책임
*단일 책임 원칙(Single Responsibility Principle, SRP)을 지킨다는 것은 하나의 클래스가 하나의 역할만 담당.
SOLID
1. 단일 책임 원칙 (Single Responsibility Principle, SRP)
클래스에 너무 많은 책임을 넣으면 안된다.
- 정의: 클래스는 하나의 책임만 가져야 하며, 변경의 이유는 오직 하나여야 합니다.
// SRP 위반 예시
class UserService {
// 사용자의 데이터를 저장
saveUser(user: User): void {
// 데이터베이스에 사용자 정보를 저장
console.log("Saving user to the database");
}
// 사용자의 이메일을 발송
sendEmail(user: User, message: string): void {
// 이메일 발송 로직
console.log(`Sending email to ${user.email}: ${message}`);
}
// 사용자의 데이터 유효성 검사
validateUserData(user: User): boolean {
// 사용자 데이터가 유효한지 확인하는 로직
if (user.name && user.email) {
console.log("User data is valid");
return true;
}
console.log("User data is invalid");
return false;
}
}
// SRP 준수 예시
// 사용자 데이터를 저장하는 책임
class UserRepository {
saveUser(user: User): void {
// 데이터베이스에 사용자 정보를 저장
console.log("Saving user to the database");
}
}
// 사용자에게 이메일을 발송하는 책임
class EmailService {
sendEmail(user: User, message: string): void {
// 이메일 발송 로직
console.log(`Sending email to ${user.email}: ${message}`);
}
}
// 사용자 데이터의 유효성을 검사하는 책임
class UserValidator {
validateUserData(user: User): boolean {
// 사용자 데이터가 유효한지 확인하는 로직
if (user.name && user.email) {
console.log("User data is valid");
return true;
}
console.log("User data is invalid");
return false;
}
}
2. 개방-폐쇄 원칙 (Open/Closed Principle, OCP)
부모 클래스를 자꾸 수정하는건 잘못되었다. 자식을 확장해라.
정의: 소프트웨어 엔터티(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하지만, 수정에는 닫혀 있어야 합니다.
사례: 만약 새로운 할인 정책을 추가해야 한다고 가정할 때, 기존 코드를 수정하지 않고 새로운 클래스를 추가하여 기능을 확장할 수 있어야 합니다.
- 예를 들어,
Discount
클래스를 확장하는 새로운HolidayDiscount
클래스를 추가할 수 있습니다.
// OCP 위반 예시
class Discount {
calculate(price: number, type: string): number {
if (type === 'holiday') {
return price * 0.9;
} else if (type === 'member') {
return price * 0.8;
}
return price;
}
}
// OCP 준수 예시
abstract class Discount {
abstract calculate(price: number): number;
}
class HolidayDiscount extends Discount {
calculate(price: number): number {
return price * 0.9;
}
}
class MemberDiscount extends Discount {
calculate(price: number): number {
return price * 0.8;
}
}- 예를 들어,
3. 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
부모의 기능을 잘쓰다가, 자식에와서 그 기능이 예상과 다르게 동작하게 하지 말라
- 정의: 자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 합니다.
- 사례: 예를 들어,
Rectangle
클래스를 상속받은Square
클래스가 있다면,- 어떤 함수도
Square
객체를Rectangle
처럼 사용할 수 있어야 합니다. 그렇지 않다면 LSP를 위반한 것입니다. - *인지적인 측면이 있다. getArea 함수는 WxH 를 연산하는 줄 알았다.(부모), 하지만 자식에서는 WxW 혹은 HxH 으로 바뀌어 있다.
- *다형성을 가지는것은 필요한데, 부모의 동작을 예상과 크게 벗어나면 안된다.
- 어떤 함수도
// LSP 위반 예시
class Rectangle {
constructor(protected width: number, protected height: number) {}
setWidth(width: number) {
this.width = width;
}
setHeight(height: number) {
this.height = height;
}
getArea(): number {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width: number) {
this.width = width;
this.height = width;
}
setHeight(height: number) {
this.height = height;
this.width = height;
}
}
function printArea(rectangle: Rectangle) {
rectangle.setWidth(5);
rectangle.setHeight(10);
console.log(rectangle.getArea()); // Rectangle에서는 50을 기대하지만, Square에서는 100이 출력됨
}
// LSP 준수 예시
class Shape {
getArea(): number {
throw new Error("Method not implemented.");
}
}
class Rectangle extends Shape {
constructor(private width: number, private height: number) {
super();
}
getArea(): number {
return this.width * this.height;
}
}
class Square extends Shape {
constructor(private size: number) {
super();
}
getArea(): number {
return this.size * this.size;
}
}
다른 예시) 부모클래스의 fly는 잘 동작하였으나, 자식클래스는 예외를 던진다.
// 부모 클래스: 새(Bird)
class Bird {
fly() {
console.log("I can fly!");
}
}
// 자식 클래스: 펭귄(Penguin)
class Penguin extends Bird {
fly() {
throw new Error("Penguins can't fly.");
}
}
4. 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)
모듈에서 미사용 인터페이스가 있다면 더 분리가 가능하다. 너무 한곳에 몰아둔것.
- 정의: 클라이언트는 자신이 사용하지 않는 인터페이스에 의존하지 않아야 합니다.
- 사례:
Robot
클래스는eat()
메서드를 사용하지 않을 것입니다. 이는 ISP를 위반하는 것이므로,Worker
인터페이스를Workable
과Eatable
로 분리하는 것이 좋습니다.
// ISP 위반 예시
interface Worker {
work(): void;
eat(): void;
}
class HumanWorker implements Worker {
work() {
console.log('Working');
}
eat() {
console.log('Eating');
}
}
class RobotWorker implements Worker {
work() {
console.log('Working');
}
eat() {
// 로봇은 먹지 않음
}
}
// ISP 준수 예시
interface Workable {
work(): void;
}
interface Eatable {
eat(): void;
}
class HumanWorker implements Workable, Eatable {
work() {
console.log('Working');
}
eat() {
console.log('Eating');
}
}
class RobotWorker implements Workable {
work() {
console.log('Working');
}
}
5. 의존 역전 원칙 (Dependency Inversion Principle, DIP)
고수준의 모듈(부모)에 의존해야, 저수준 모듈(자식)을 많이 수용 할 수 있다.
- 정의: 고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 합니다.
- 사례: 예를 들어,
Light
클래스가Button
클래스에 의존하고 있다면,Button
이 변경될 때마다Light
도 변경해야 할 수 있습니다.- 이를 방지하기 위해,
Light
클래스는Switchable
인터페이스에 의존하고,Button
클래스가 이 인터페이스를 구현하도록 해야 합니다.
- 이를 방지하기 위해,
// DIP 위반 예시
class Button {
turnOn() {
console.log('Button turned on');
}
turnOff() {
console.log('Button turned off');
}
}
class Light {
private button: Button;
constructor(button: Button) {
this.button = button;
}
toggle() {
this.button.turnOn();
}
}
// DIP 준수 예시
interface Switchable {
turnOn(): void;
turnOff(): void;
}
class Button implements Switchable {
turnOn() {
console.log('Button turned on');
}
turnOff() {
console.log('Button turned off');
}
}
class Light {
private device: Switchable;
constructor(device: Switchable) {
// DI를 사용한 의존성 주입
this.device = device;
}
toggle() {
this.device.turnOn();
}
}
5.1 의존성 주입 (Dependency Injection, DI)
- DIP를 구현하는 방법 중 하나이다.
- DI를 통해서 의존성을 클래스 내부 프로퍼티가 아닌 생성자를 통해서 받도록 한다.
SOLID ( OOP vs Functional Programming )
1. 단일 책임 원칙 (Single Responsibility Principle, SRP)
- OOP: 클래스는 하나의 책임만 가져야 합니다.
- FP: 함수는 하나의 일만 수행
- 순수 함수(pure function) : 외부 상태를 변경하지 않습니다. 이로써 함수의 책임이 명확해집니다.
2. 개방-폐쇄 원칙 (Open/Closed Principle, OCP)
- OOP: 클래스는 확장 가능해야 하지만, 수정하지 않고도 기존 코드를 변경할 수 있어야 합니다.
- FP: 함수형 프로그래밍에서는 함수들이 불변성(immutability)을 가진다.
- 기존의 함수를 수정하는 대신, 새로운 함수를 작성하여 기존 함수를 조합하거나 확장.
- 함수의 조합(composition)이나 고차 함수(higher-order function)를 사용
3. 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
- OOP: 자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 합니다.
- FP: 함수형 프로그래밍에서는 주로 다형성을 고차 함수나 함수 합성(composition)을 통해 구현합니다.
4. 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)
- OOP: 클라이언트는 자신이 사용하지 않는 인터페이스에 의존하지 않아야 합니다.
- FP: 함수형 프로그래밍에서 인터페이스는 함수의 시그니처로 표현.
- ISP의 개념은 함수가 불필요하게 많은 인자를 받지 않도록 설계하는 것과 관련이 있습니다.
- 작은 함수로 나누기
5. 의존 역전 원칙 (Dependency Inversion Principle, DIP)
- OOP: 고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 합니다.
- FP:
- DIP와 유사 : 의존성을 함수로 전달하여, 구체적인 구현 대신 추상적인 행동을 의존성으로 사용할 수 있다는 점
- DI와 유사 : 의존성을 주입하는 대신, 의존성 자체를 함수의 인자로 전달하여 구현
- 함수형 프로그래밍은 주로 불변성, 순수 함수, 함수 합성 등의 개념을 강조하기 때문에, 구현 방식이 다르다.