OOP パラダイムの導入により、継承、ポリモーフィズム、抽象化、カプセル化などの主要なプログラミング概念が普及しました。 OOP はすぐに広く受け入れられるプログラミング パラダイムとなり、Java、C、C#、JavaScript などのいくつかの言語で実装されました。 OOP システムは時間の経過とともに複雑になってきましたが、そのソフトウェアは依然として変化に強いものでした。ソフトウェアの拡張性を向上させ、コードの剛性を下げるために、Robert C. Martin (別名 Uncle Bob) は 2000 年代初頭に SOLID 原則を導入しました。
SOLID は、単一責任原則、オープンクローズ原則、リスコフ置換原則、インターフェース分離原則、依存関係逆転原則といった一連の原則で構成される頭字語であり、ソフトウェア エンジニアが保守可能、スケーラブル、および柔軟なソフトウェアを設計および作成するのに役立ちます。コード。その目的は?オブジェクト指向プログラミング (OOP) パラダイムに従って開発されたソフトウェアの品質を向上させるため。
この記事では、SOLID の原則をすべて詳しく説明し、最も人気のある Web プログラミング言語の 1 つである JavaScript を使用してそれらがどのように実装されるかを説明します。
SOLID の最初の文字は、単一責任の原則を表します。この原則は、クラスまたはモジュールが 1 つの役割だけを実行する必要があることを示唆しています。
簡単に言うと、クラスには変更する単一の責任または単一の理由が必要です。クラスが複数の機能を処理する場合、他の機能に影響を与えずに 1 つの機能を更新するのは困難になります。その後の複雑な問題により、ソフトウェアのパフォーマンスに障害が発生する可能性があります。このような問題を回避するには、関心事が分離されたモジュール型ソフトウェアを作成するように最善を尽くす必要があります。
クラスの責任や機能が多すぎると、変更するのが面倒になります。単一責任の原則を使用すると、モジュール式で保守が容易で、エラーが発生しにくいコードを作成できます。たとえば、人物モデルを考えてみましょう。
class Person { constructor(name, age, height, country){ this.name = name this.age = age this.height = height this.country = country } getPersonCountry(){ console.log(this.country) } greetPerson(){ console.log("Hi " + this.name) } static calculateAge(dob) { const today = new Date(); const birthDate = new Date(dob); let age = today.getFullYear() - birthDate.getFullYear(); const monthDiff = today.getMonth() - birthDate.getMonth(); if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { age--; } return age; } }
上記のコードは問題ないようですよね?完全ではありません。サンプル コードは単一責任の原則に違反しています。 Person クラスは、他の Person インスタンスを作成できる唯一のモデルであるだけでなく、calculateAge、greetperson、getPerson Country などの他の役割も担っています。
これらの追加の責任が Person クラスによって処理されるため、コードの 1 つの側面だけを変更することが困難になります。たとえば、calculateAge をリファクタリングしようとすると、Person モデルのリファクタリングも強制される可能性があります。コード ベースがどれほどコンパクトで複雑かによっては、エラーを発生させずにコードを再構成することが難しい場合があります。
間違いを修正してみましょう。次のように、責任をさまざまなクラスに分けることができます。
class Person { constructor(name, age, height, country){ this.name = name this.age = age this.height = height this.country = country } getPersonCountry(){ console.log(this.country) } greetPerson(){ console.log("Hi " + this.name) } static calculateAge(dob) { const today = new Date(); const birthDate = new Date(dob); let age = today.getFullYear() - birthDate.getFullYear(); const monthDiff = today.getMonth() - birthDate.getMonth(); if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { age--; } return age; } }
上記のサンプル コードからわかるように、私たちは責任を分離しました。 Person クラスは、新しい person オブジェクトを作成できるモデルになりました。そして、PersonUtils クラスの役割は 1 つだけです。それは、人の年齢を計算することです。 PersonService クラスは挨拶を処理し、各人の国を表示します。
必要に応じて、このプロセスをさらに減らすこともできます。 SRP に従って、クラスの責任を最小限に分離して、問題が発生したときに、それほど手間をかけずにリファクタリングとデバッグを実行できるようにしたいと考えています。
機能を個別のクラスに分割することで、単一責任の原則を遵守し、各クラスがアプリケーションの特定の側面を担当するようにしています。
次の原則に進む前に、SRP に準拠することは、各クラスに単一のメソッドまたは機能を厳密に含める必要があるという意味ではないことに注意してください。
ただし、単一責任の原則に従うということは、クラスへの機能の割り当てについて意図的に行う必要があることを意味します。クラスで実行されるすべてのことは、あらゆる意味で密接に関連している必要があります。複数のクラスがあちこちに散在しないように注意する必要があり、コード ベース内のクラスが肥大化することは絶対に避けるべきです。
オープンクローズの原則では、ソフトウェア コンポーネント (クラス、関数、モジュールなど) は拡張に対してオープンであり、変更に対してクローズである必要があると規定されています。あなたが何を考えているかはわかります。はい、この考えは最初は矛盾しているように思えるかもしれません。しかしOCPは単に、ソースコードを必ずしも変更せずに拡張できる方法でソフトウェアを設計することを求めているだけです。
OCP は大規模なコード ベースを維持するために非常に重要です。このガイドラインにより、コードを破損するリスクをほとんどまたはまったく発生させずに新機能を導入できるようになります。新しい要件が発生したときに既存のクラスやモジュールを変更するのではなく、新しいコンポーネントを追加して関連するクラスを拡張する必要があります。これを行うときは、新しいコンポーネントがシステムにバグを引き起こさないことを必ず確認してください。
OC 原則は、ES6 クラスの継承機能を使用して JavaScript で実現できます。
次のコード スニペットは、前述の ES6 クラス キーワードを使用して、JavaScript で Open-Closed 原則を実装する方法を示しています。
class Person { constructor(name, dateOfBirth, height, country){ this.name = name this.dateOfBirth = dateOfBirth this.height = height this.country = country } } class PersonUtils { static calculateAge(dob) { const today = new Date(); const birthDate = new Date(dob); let age = today.getFullYear() - birthDate.getFullYear(); const monthDiff = today.getMonth() - birthDate.getMonth(); if(monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { age--; } return age; } } const person = new Person("John", new Date(1994, 11, 23), "6ft", "USA"); console.log("Age: " + PersonUtils.calculateAge(person.dateOfBirth)); class PersonService { getPersonCountry(){ console.log(this.country) } greetPerson(){ console.log("Hi " + this.name) } }
上記のコードは正常に動作しますが、計算できるのは長方形の面積のみに限定されます。ここで、計算するための新しい要件があると想像してください。たとえば、円の面積を計算する必要があるとします。これに対応するには、shapeProcessor クラスを変更する必要があります。ただし、JavaScript ES6 標準に従って、shapeProcessor クラスを必ずしも変更せずに、新しい形状の領域を考慮してこの機能を拡張できます。
それは次のように行うことができます:
class Person { constructor(name, age, height, country){ this.name = name this.age = age this.height = height this.country = country } getPersonCountry(){ console.log(this.country) } greetPerson(){ console.log("Hi " + this.name) } static calculateAge(dob) { const today = new Date(); const birthDate = new Date(dob); let age = today.getFullYear() - birthDate.getFullYear(); const monthDiff = today.getMonth() - birthDate.getMonth(); if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { age--; } return age; } }
上記のコード スニペットでは、extends キーワードを使用して Shape クラスの機能を拡張しました。各サブクラスで、 area() メソッドの実装をオーバーライドします。この原則に従って、ShapeProcessor クラスの機能を変更することなく、シェイプと処理領域を追加できます。
リスコフ置換原則では、コードを壊すことなくサブクラスのオブジェクトがスーパークラスのオブジェクトを置換できる必要があると述べています。これがどのように機能するかを例で詳しく見てみましょう。L が P のサブクラスである場合、システムを壊すことなく L のオブジェクトが P のオブジェクトを置き換える必要があります。これは、システムを壊さない方法でサブクラスがスーパークラスのメソッドをオーバーライドできる必要があることを意味します。
実際には、リスコフ置換原則により、次の条件が確実に遵守されるようになります。
JavaScript コードサンプルを使用してリスコフ置換原理を説明します。見てみましょう:
class Person { constructor(name, dateOfBirth, height, country){ this.name = name this.dateOfBirth = dateOfBirth this.height = height this.country = country } } class PersonUtils { static calculateAge(dob) { const today = new Date(); const birthDate = new Date(dob); let age = today.getFullYear() - birthDate.getFullYear(); const monthDiff = today.getMonth() - birthDate.getMonth(); if(monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { age--; } return age; } } const person = new Person("John", new Date(1994, 11, 23), "6ft", "USA"); console.log("Age: " + PersonUtils.calculateAge(person.dateOfBirth)); class PersonService { getPersonCountry(){ console.log(this.country) } greetPerson(){ console.log("Hi " + this.name) } }
上記のコード スニペットでは、2 つのサブクラス (Bicycle と Car) と 1 つのスーパークラス (Vehicle) を作成しました。この記事の目的のために、スーパークラスに単一のメソッド (OnEngine) を実装しました。
LSP の中核となる条件の 1 つは、サブクラスがコードを壊さずに親クラスの機能をオーバーライドする必要があるということです。このことを念頭に置いて、先ほど見たコード スニペットがリスコフ置換原則にどのように違反しているかを見てみましょう。実際、車にはエンジンがあり、エンジンをかけることができますが、自転車にはエンジンがないため、エンジンをかけることができません。したがって、Bicycle は、コードを壊さずに Vehicle クラスの OnEngine メソッドをオーバーライドすることはできません。
リスコフ置換原則に違反するコードのセクションを特定しました。 Car クラスは、スーパークラスの OnEngine 機能をオーバーライドして、他の乗り物 (飛行機など) と区別できる方法で実装でき、コードが壊れることはありません。 Car クラスはリスコフ置換原則を満たします。
以下のコード スニペットでは、リスコフ置換原則に準拠するようにコードを構造化する方法を示します。
class Person { constructor(name, age, height, country){ this.name = name this.age = age this.height = height this.country = country } getPersonCountry(){ console.log(this.country) } greetPerson(){ console.log("Hi " + this.name) } static calculateAge(dob) { const today = new Date(); const birthDate = new Date(dob); let age = today.getFullYear() - birthDate.getFullYear(); const monthDiff = today.getMonth() - birthDate.getMonth(); if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { age--; } return age; } }
これは、一般的な機能である移動を備えた Vehicle クラスの基本的な例です。すべての乗り物は動く、というのが一般的な考えです。それらは異なるメカニズムを介して移動しているだけです。 LSP を説明する 1 つの方法は、move() メソッドをオーバーライドし、特定の車両 (たとえば、自動車) がどのように移動するかを示す方法で実装することです。
そのために、次のように、Vehicle クラスを拡張し、車の動きに合わせて move メソッドをオーバーライドする Car クラスを作成します。
class Person { constructor(name, dateOfBirth, height, country){ this.name = name this.dateOfBirth = dateOfBirth this.height = height this.country = country } } class PersonUtils { static calculateAge(dob) { const today = new Date(); const birthDate = new Date(dob); let age = today.getFullYear() - birthDate.getFullYear(); const monthDiff = today.getMonth() - birthDate.getMonth(); if(monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { age--; } return age; } } const person = new Person("John", new Date(1994, 11, 23), "6ft", "USA"); console.log("Age: " + PersonUtils.calculateAge(person.dateOfBirth)); class PersonService { getPersonCountry(){ console.log(this.country) } greetPerson(){ console.log("Hi " + this.name) } }
別のサブビークル クラス (飛行機など) に move メソッドを実装することもできます。
その方法は次のとおりです:
class Rectangle { constructor(width, height) { this.width = width; this.height = height; } area() { return this.width * this.height; } } class ShapeProcessor { calculateArea(shape) { if (shape instanceof Rectangle) { return shape.area(); } } } const rectangle = new Rectangle(10, 20); const shapeProcessor = new ShapeProcessor(); console.log(shapeProcessor.calculateArea(rectangle));
上記の 2 つの例では、継承やメソッドのオーバーライドなどの重要な概念を説明しました。
注意: 親クラスですでに定義されているメソッドをサブクラスで実装できるようにするプログラミング機能は、メソッド オーバーライドと呼ばれます。
ハウスキーピングをして、次のようにすべてをまとめてみましょう:
class Shape { area() { console.log("Override method area in subclass"); } } class Rectangle extends Shape { constructor(width, height) { super(); this.width = width; this.height = height; } area() { return this.width * this.height; } } class Circle extends Shape { constructor(radius) { super(); this.radius = radius; } area() { return Math.PI * this.radius * this.radius; } } class ShapeProcessor { calculateArea(shape) { return shape.area(); } } const rectangle = new Rectangle(20, 10); const circle = new Circle(2); const shapeProcessor = new ShapeProcessor(); console.log(shapeProcessor.calculateArea(rectangle)); console.log(shapeProcessor.calculateArea(circle));
これで、親クラスから 1 つの機能を継承およびオーバーライドし、要件に従って実装する 2 つのサブクラスができました。この新しい実装はコードを壊しません。
インターフェース分離の原則では、クライアントは使用しないインターフェースに強制的に依存すべきではないと述べています。クライアントに必要のないメソッドの実装を強いる大規模でモノリシックなインターフェイスではなく、特定のクライアントに関連する、より小さく、より具体的なインターフェイスを作成することを望んでいます。
インターフェースをコンパクトに保つことで、コードベースのデバッグ、保守、テスト、拡張が容易になります。 ISP がなければ、大規模なインターフェイスの一部を変更すると、コードベースの無関係な部分の変更が強制される可能性があり、コードのリファクタリングを実行することになりますが、コード ベースのサイズによっては、ほとんどの場合、困難な作業になる可能性があります。
JavaScript は、Java などの C ベースのプログラミング言語とは異なり、インターフェースのサポートを組み込みません。ただし、インターフェースを JavaScript で実装する手法もあります。
インターフェイスは、クラスが実装する必要があるメソッド シグネチャのセットです。
JavaScript では、次のようにメソッドと関数のシグネチャの名前を持つオブジェクトとしてインターフェイスを定義します。
class Person { constructor(name, age, height, country){ this.name = name this.age = age this.height = height this.country = country } getPersonCountry(){ console.log(this.country) } greetPerson(){ console.log("Hi " + this.name) } static calculateAge(dob) { const today = new Date(); const birthDate = new Date(dob); let age = today.getFullYear() - birthDate.getFullYear(); const monthDiff = today.getMonth() - birthDate.getMonth(); if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { age--; } return age; } }
JavaScript でインターフェースを実装するには、クラスを作成し、インターフェースで指定されているのと同じ名前とシグネチャを持つメソッドがクラスに含まれていることを確認します。
class Person { constructor(name, dateOfBirth, height, country){ this.name = name this.dateOfBirth = dateOfBirth this.height = height this.country = country } } class PersonUtils { static calculateAge(dob) { const today = new Date(); const birthDate = new Date(dob); let age = today.getFullYear() - birthDate.getFullYear(); const monthDiff = today.getMonth() - birthDate.getMonth(); if(monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { age--; } return age; } } const person = new Person("John", new Date(1994, 11, 23), "6ft", "USA"); console.log("Age: " + PersonUtils.calculateAge(person.dateOfBirth)); class PersonService { getPersonCountry(){ console.log(this.country) } greetPerson(){ console.log("Hi " + this.name) } }
これで、JavaScript でインターフェースを作成して使用する方法がわかりました。次に行う必要があるのは、JavaScript でインターフェイスを分離する方法を示し、すべてがどのように連携してコードの保守が容易になるかを理解できるようにすることです。
次の例では、プリンターを使用してインターフェース分離の原理を説明します。
プリンター、スキャナー、ファックスがあると仮定して、これらのオブジェクトの機能を定義するインターフェイスを作成しましょう:
class Rectangle { constructor(width, height) { this.width = width; this.height = height; } area() { return this.width * this.height; } } class ShapeProcessor { calculateArea(shape) { if (shape instanceof Rectangle) { return shape.area(); } } } const rectangle = new Rectangle(10, 20); const shapeProcessor = new ShapeProcessor(); console.log(shapeProcessor.calculateArea(rectangle));
上記のコードでは、これらすべての機能を定義する 1 つの大きなインターフェイスに対して、分離または分離されたインターフェイスのリストを作成しました。これらの機能をより小さな部分とより具体的なインターフェイスに分割することで、さまざまなクライアントが必要なメソッドのみを実装できるようになり、他の部分はすべて排除されます。
次のステップでは、これらのインターフェースを実装するクラスを作成します。インターフェイス分離の原則に従い、各クラスは必要なメソッドのみを実装します。
ドキュメントの印刷のみが可能な基本的なプリンタを実装したい場合は、次のように、printerInterface を介して print() メソッドを実装するだけです。
class Shape { area() { console.log("Override method area in subclass"); } } class Rectangle extends Shape { constructor(width, height) { super(); this.width = width; this.height = height; } area() { return this.width * this.height; } } class Circle extends Shape { constructor(radius) { super(); this.radius = radius; } area() { return Math.PI * this.radius * this.radius; } } class ShapeProcessor { calculateArea(shape) { return shape.area(); } } const rectangle = new Rectangle(20, 10); const circle = new Circle(2); const shapeProcessor = new ShapeProcessor(); console.log(shapeProcessor.calculateArea(rectangle)); console.log(shapeProcessor.calculateArea(circle));
このクラスは PrinterInterface のみを実装します。スキャンまたは FAX メソッドは実装されていません。インターフェイス分離の原則に従うことで、クライアント (この場合は Printer クラス) の複雑さが軽減され、ソフトウェアのパフォーマンスが向上しました。
さて、最後の原則である依存関係逆転の原則について説明します。この原則は、上位レベルのモジュール (ビジネス ロジック) が下位レベルのモジュール (具象) に直接依存するのではなく、抽象化に依存する必要があることを示しています。これにより、コードの依存関係が軽減され、開発者は複雑な問題を引き起こすことなく、より高いレベルでアプリケーションを変更および拡張できる柔軟性が得られます。
なぜ依存関係逆転の原則は直接的な依存関係よりも抽象化を優先するのでしょうか?これは、抽象化の導入により、変更による潜在的な影響が軽減され、テスト容易性が向上し (具体的な実装ではなく抽象化がモックされる)、コードの柔軟性が向上するためです。このルールにより、モジュール化アプローチを通じてソフトウェア コンポーネントを拡張することが容易になり、高レベルのロジックに影響を与えることなく低レベルのコンポーネントを変更することもできます。
DIP に準拠すると、コードの保守、拡張、拡張が容易になり、コードの変更によって発生する可能性のあるバグを防ぐことができます。開発者はクラス間の密結合ではなく疎結合を使用することを推奨しています。一般に、直接的な依存関係よりも抽象化を優先する考え方を採用することで、チームは波及的な混乱を引き起こすことなく、新しい機能を適応して追加したり、古いコンポーネントを変更したりする機敏性を得ることができます。 JavaScript では、次のように依存関係注入アプローチを使用して DIP を実装できます。
class Person { constructor(name, age, height, country){ this.name = name this.age = age this.height = height this.country = country } getPersonCountry(){ console.log(this.country) } greetPerson(){ console.log("Hi " + this.name) } static calculateAge(dob) { const today = new Date(); const birthDate = new Date(dob); let age = today.getFullYear() - birthDate.getFullYear(); const monthDiff = today.getMonth() - birthDate.getMonth(); if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { age--; } return age; } }
上記の基本的な例では、Application クラスはデータベースの抽象化に依存する高レベルのモジュールです。 MySQLDatabase と MongoDBDatabase という 2 つのデータベース クラスを作成しました。データベースは低レベルのモジュールであり、そのインスタンスはアプリケーション自体を変更せずにアプリケーション ランタイムに挿入されます。
SOLID 原則は、拡張性、保守性、堅牢性を備えたソフトウェア設計の基本的な構成要素です。この一連の原則は、開発者がクリーンでモジュール化された適応性のあるコードを作成するのに役立ちます。
SOLID 原則は、統合された機能、変更なしの拡張性、オブジェクトの置換、インターフェイスの分離、具体的な依存関係の抽象化を促進します。バグを防ぎ、その利点をすべて享受できるように、必ず SOLID 原則をコードに組み込んでください。
コードのデバッグは常に面倒な作業です。しかし、間違いを理解すればするほど、修正が容易になります。
LogRocket を使用すると、これらのエラーを新しい独自の方法で理解できます。当社のフロントエンド監視ソリューションは、JavaScript フロントエンドに対するユーザーの関与を追跡し、エラーを引き起こしたユーザーの行動を正確に確認できるようにします。
LogRocket は、コンソール ログ、ページの読み込み時間、スタック トレース、ヘッダー本体を含む遅いネットワーク リクエスト/レスポンス、ブラウザーのメタデータ、カスタム ログを記録します。 JavaScript コードの影響を理解するのがこれまでになく簡単になります!
無料でお試しください。
以上がJavaScript の確かな原則の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。