前の部分:
私たちはコードをクリーンで柔軟性があり、保守しやすいものにするためのアプローチを引き続き検討しています。それでは、クリーンなアーキテクチャ ソリューションの調査を始めましょう。
SOLID 原則は Robert C. Martin によって導入され、オブジェクト指向プログラミングおよびソフトウェア設計のベスト プラクティスとして広く認められています。
この記事では、最初の 3 つの SOLID 原則、単一責任原則、オープン/クローズド原則、リスコフ置換原則を実際の例に基づいて説明します。
クラスを変更する理由は 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 はコードのモジュール性を強化し、相互依存関係から生じる課題を最小限に抑えることを目的としています。コードを関数に編成し、クラスを分離し、モジュール化を促進することにより、コードの再利用性が高まり、既存の機能の再コーディングに費やす時間を節約できます。
ソフトウェア エンティティ (クラス、モジュール、関数) は拡張に対してオープンである必要がありますが、変更に対してはクローズされている必要があります
既存のコードを変更せずに新しい機能を追加できる必要があります。この原則に従うことで、既存のコードの安定性を損なうことなく新しい機能やコンポーネントをシステムに導入できるため、これは重要です。
これにより、コードの再利用性が促進され、機能を拡張する際に大規模な変更を行う必要性が軽減されます。
// 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(...); } }
この原則に従って、コードの結合を減らし、システムを予期しない動作から守ります。
スーパークラスのオブジェクトは、アプリケーションを中断することなく、そのサブクラスのオブジェクトと置き換えることができる必要があります。
この原則を理解するために、次の例を見てみましょう。
// 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
、 Programmer
、および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 {/../} }
新しい抽象化レイヤーEmployee
とContractor
を作成し、 access
メソッドをEmployee
クラスに移動して、特定の実現を定義しました。 Worker
クラスをサブクラスContractor
に置き換えても、 Worker
機能は壊れません。
LSP に準拠している場合は、基本クラス インスタンスが必要な場合はどこでもサブクラス インスタンスを置き換えることができ、プログラムは意図したとおりに動作するはずです。これにより、コードの再利用とモジュール化が促進され、コードの変更に対する耐性が強化されます。
次の記事では、インターフェイスの分離と依存関係の反転 SOLID の原則について説明します。