paint-brush
クリーンなコード: 単一責任、オープン/クローズ、リスコフ置換 TS における SOLID 原則 [パート 4]@alenaananich
4,731 測定値
4,731 測定値

クリーンなコード: 単一責任、オープン/クローズ、リスコフ置換 TS における SOLID 原則 [パート 4]

Alena Ananich7m2023/11/09
Read on Terminal Reader

長すぎる; 読むには

この記事では、最初の 3 つの SOLID 原則、単一責任原則、オープン/クローズド原則、リスコフ置換原則を実際の例で見ていきます。
featured image - クリーンなコード: 単一責任、オープン/クローズ、リスコフ置換 TS における SOLID 原則 [パート 4]
Alena Ananich HackerNoon profile picture


前の部分:


私たちはコードをクリーンで柔軟性があり、保守しやすいものにするためのアプローチを引き続き検討しています。それでは、クリーンなアーキテクチャ ソリューションの調査を始めましょう。


SOLID 原則は Robert C. Martin によって導入され、オブジェクト指向プログラミングおよびソフトウェア設計のベスト プラクティスとして広く認められています。


この記事では、最初の 3 つの SOLID 原則、単一責任原則、オープン/クローズド原則、リスコフ置換原則を実際の例に基づいて説明します。

目次:

  1. 単一責任の原則
  2. オープン/クローズの原則
  3. リスコフ置換原理

1. 単一責任原則 (SRP)

クラスを変更する理由は 1 つだけである必要があります


言い換えれば、クラスは単一の責任または仕事を持つ必要があります。単一の責任を持つクラスは、理解、変更、保守が容易であるため、これは重要です。コードの 1 つの領域の変更がシステム全体に波及することがないため、バグが発生するリスクが軽減されます。


実際の例と、単一責任の原則に従う方法を見てみましょう。

 // 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を作成する必要があります。特定の実現は、アナナスとバナナというクラスを継承します。彼らは自分自身の要求を実現します。


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クラスに問題があります。 DesignerProgrammer 、およびSellerすべて Worker であり、親クラスWorkerから継承されます。しかし同時に、設計者は従業員ではなく請負業者であるため、閉じた境界にアクセスすることはできません。そして、 access方法をオーバーライドし、リスコフ置換原則を破りました。


この原則は、スーパークラスWorkerそのサブクラス (たとえばDesignerクラス) に置き換えた場合でも、機能が壊れるべきではないことを示しています。しかし、それを行うと、 Programmerクラスの機能が壊れてしまい、 accessメソッドにDesignerクラスからの予期しない認識が発生することになります。


リスコフ置換原則に従って、サブクラスの実現を書き換えるべきではなく、抽象化の種類ごとに特定の実現を定義する新しい抽象化層を作成する必要があります。


それを修正しましょう:

 // 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 の原則について説明します。