paint-brush
클린 코드: 단일 책임, 공개/폐쇄, Liskov 대체 TS의 SOLID 원칙 [4부]~에 의해@alenaananich
4,777 판독값
4,777 판독값

클린 코드: 단일 책임, 공개/폐쇄, Liskov 대체 TS의 SOLID 원칙 [4부]

~에 의해 Alena Ananich7m2023/11/09
Read on Terminal Reader

너무 오래; 읽다

이 기사에서는 세 가지 첫 번째 SOLID 원칙인 단일 책임 원칙, 개방/폐쇄 원칙, Liskov 대체 원칙을 실제 사례를 통해 살펴보겠습니다.
featured image - 클린 코드: 단일 책임, 공개/폐쇄, Liskov 대체 TS의 SOLID 원칙 [4부]
Alena Ananich HackerNoon profile picture


이전 부품:


우리는 코드를 깔끔하고 유연하며 유지 관리 가능하게 만들기 위한 접근 방식을 계속해서 고려하고 있습니다. 이제 깨끗한 아키텍처 솔루션을 조사해 보겠습니다.


SOLID 원칙은 Robert C. Martin이 도입했으며 객체 지향 프로그래밍 및 소프트웨어 설계 분야의 모범 사례로 널리 알려져 있습니다.


이 기사에서는 첫 번째 3가지 SOLID 원칙인 단일 책임 원칙, 개방/폐쇄 원칙, Liskov 대체 원칙을 실제 사례를 통해 살펴보겠습니다.

목차:

  1. 단일 책임 원칙
  2. 개방형/폐쇄형 원칙
  3. Liskov 대체 원칙

1. 단일 책임 원칙(SRP)

클래스를 변경해야 하는 이유는 단 하나여야 합니다.


즉, 클래스에는 단일 책임이나 작업이 있어야 합니다. 단일 책임을 갖는 클래스는 이해하고, 수정하고, 유지 관리하기가 더 쉽기 때문에 중요합니다. 코드 중 한 영역의 변경 사항이 전체 시스템에 영향을 미치지 않으므로 버그가 발생할 위험이 줄어듭니다.


단일 책임 원칙을 따르는 실제 예와 방법을 살펴보겠습니다.

 // Bad class UserSettingsService { constructor(user: IUser) { this.user = user; } changeSettings(settings: IUserSettings): void { if (this.isUserValidated()) { // ... } } getUserInfo(): Promise<IUserSettings> { // ... } async isUserValidated(): Promise<boolean> { const userInfo = await this.getUserInfo(); // ... } }

이 예에서 클래스는 컨텍스트를 설정하고, 변경하고, 유효성을 검사하는 등 다양한 방향으로 작업을 수행합니다.


SRP를 따르려면 이러한 다양한 책임을 나누어야 합니다.

 // Better class UserAuth { constructor(user: IUser) { this.user = user; } getUserInfo(): Promise<IUserSettings> { // ... } async isUserValidated(): boolean { const userInfo = await this.getUserInfo(); // ... } } class UserSettings { constructor(user: IUser) { this.user = user; this.auth = new UserAuth(user); } changeSettings(settings: IUserSettings): void { if (this.auth.isUserValidated()) { // ... } } }


왜 중요 함?

SRP는 상호 의존성으로 인해 발생하는 문제를 최소화하면서 코드 모듈성을 향상시키는 것을 목표로 합니다. 코드를 함수로 구성하고, 클래스를 분리하고, 모듈화를 촉진함으로써 재사용성이 높아지고 기존 기능을 다시 코딩하는 데 소요될 시간이 절약됩니다.

2. 개방/폐쇄 원칙(OCP)

소프트웨어 엔터티(클래스, 모듈, 기능)는 확장을 위해 열려 있어야 하지만 수정을 위해서는 닫혀 있어야 합니다.


기존 코드를 변경하지 않고도 새로운 기능을 추가할 수 있어야 합니다. 이 원칙을 준수하면 기존 코드의 안정성을 위협하지 않고 시스템에 새로운 기능이나 구성 요소를 도입할 수 있기 때문에 중요합니다.


이는 코드 재사용성을 촉진하고 기능 확장 시 광범위한 변경 필요성을 줄여줍니다.

 // Bad class Product { id: number; name: string[]; price: number; protected constructor(id: number, name: string[], price: number) { this.id = id; this.name = name; this.price = price; } } class Ananas extends Product { constructor(id: number, name: string[], price: number) { super(id, name, price); } } class Banana extends Product { constructor(id: number, name: string[], price: string) { super(id, name, price); } } class HttpRequestCost { constructor(product: Product) { this.product = product; } getDeliveryCost(): number { if (product instanceOf Ananas) { return requestAnanas(url).then(...); } if (product instanceOf Banana) { return requestBanana(url).then(...); } } } function requestAnanas(url: string): Promise<ICost> { // logic for ananas } function requestBanana(url: string): Promise<ICost> { // logic for bananas }

이 예에서 문제는 HttpRequestCost 클래스에 있습니다. 이 클래스의 getDeliveryCost 메소드에는 다양한 유형의 제품 계산을 위한 조건이 포함되어 있으며 각 제품 유형에 대해 별도의 메소드를 사용합니다. 따라서 새로운 유형의 제품을 추가해야 하는 경우 HttpRequestCost 클래스를 수정해야 하는데 이는 안전하지 않습니다. 예상치 못한 결과를 얻을 수도 있습니다.


이를 방지하려면 구현하지 않고 Product 클래스에 추상 메서드 request 생성해야 합니다. 특정 실현은 Ananas 및 Banana 클래스를 상속받게 됩니다. 그들은 스스로 요청을 깨닫게 될 것입니다.


HttpRequestCost Product 클래스 인터페이스에 따라 product 매개변수를 사용하며, HttpRequestCost 에서 특정 종속성을 전달할 때 이미 자체적으로 request 방법을 실현합니다.

 // Better abstract class Product { id: number; name: string[]; price: string; constructor(id: number, name: string[], price: string) { this.id = id; this.name = name; this.price = price; } abstract request(url: string): void; } class Ananas extends Product { constructor(id: number, name: string[], price: string) { super(id, name, price); } request(url: string): void { // logic for ananas } } class Banana extends Product { constructor(id: number, name: string[], price: string) { super(id, name, price); } request(url: string): void { // logic for bananas } } class HttpRequestCost { constructor(product: Product) { this.product = product; } request(): Promise<void> { return this.product.request(url).then(...); } }


왜 중요 함?

이 원칙을 따르면 코드 결합을 줄이고 예측할 수 없는 동작으로부터 시스템을 보호할 수 있습니다.

3. 리스코프 대체 원리(LSP)

슈퍼클래스의 객체는 애플리케이션을 중단하지 않고 서브클래스의 객체로 대체 가능해야 합니다.


이 원리를 이해하기 위해 다음 예를 살펴보겠습니다.

 // Bad class Worker { work(): void {/../} access(): void { console.log('Have an access to closed perimeter'); } } class Programmer extends Worker { createDatabase(): void {/../} } class Seller extends Worker { sale(): void {/../} } class Designer extends Worker { access(): void { throwError('No access'); } }

이 예에서는 Contractor 클래스에 문제가 있습니다. Designer , ProgrammerSeller 모두 Worker이며 상위 클래스 Worker 에서 상속됩니다. 그러나 동시에 설계자는 직원이 아닌 계약자이기 때문에 폐쇄된 경계에 접근할 수 없습니다. 그리고 우리는 access 방법을 재정의하고 Liskov 대체 원칙을 위반했습니다.


이 원칙은 Worker 슈퍼클래스를 Designer 클래스와 같은 하위 클래스로 대체해도 기능이 중단되어서는 안 된다는 것을 알려줍니다. 그러나 그렇게 하면 Programmer 클래스의 기능이 손상됩니다. access 방법은 Designer 클래스에서 예상치 못한 구현을 갖게 됩니다.


Liskov 대체 원칙에 따라 하위 클래스의 구현을 다시 작성해서는 안 되며 각 추상화 유형에 대한 특정 구현을 정의하는 새로운 추상화 계층을 만들어야 합니다.


이를 수정해 보겠습니다.

 // Better class Worker { work(): void {/../} } class Employee extends Worker { access(): void { console.log('Have an access to closed perimeter'); } } class Contractor extends Worker { addNewContract(): void {/../} } class Programmer extends Employee { createDatabase(): void {/../} } class Saler extends Employee { sale(): void {/../} } class Designer extends Contractor { makeDesign(): void {/../} }

우리는 추상화 EmployeeContractor 의 새로운 레이어를 생성하고 access 방법을 Employee 클래스로 이동하고 특정 구현을 정의했습니다. Worker 클래스를 Contractor 하위 클래스로 바꾸면 Worker 기능이 손상되지 않습니다.

왜 중요 함?

LSP를 준수하는 경우 기본 클래스 인스턴스가 필요한 모든 하위 클래스 인스턴스를 대체할 수 있으며 프로그램은 계속 의도한 대로 작동해야 합니다. 이는 코드 재사용, 모듈화를 촉진하고 변경 사항에 대한 코드 탄력성을 높여줍니다.


다음 기사에서는 인터페이스 분리 및 종속성 반전 SOLID 원칙을 살펴보겠습니다.