高品質のフロントエンド コードを作成して結合を軽減し、直交性を向上させます。

結合と直交性

カップリングとは何ですか

百度百科ではカップリングの説明

カップリングとは、2 つ以上のシステムまたは 2 つの運動形式が相互作用を通じて相互に影響し合い、さらには結合する現象を指します。

リモコンの飛行機のおもちゃを買ったことがあるのですが、前進レバーを押すと飛行機が前に進むだけでなく、左右にも動きます。また、上ボタンを押すと上昇するだけでなく、不規則な前進や左右旋回も伴います。

飛行機の操縦姿勢は私のコントロールの範疇ではなく、理想的な状態では前進は前進で、左右の揺れやリフトがないはずであり、左右の操縦やリフト操作も同様であるはずです。システムの操作が簡単であると、すべての操作が他の操作に影響を及ぼし、単純なタスクを正常に完了できなくなります。

コード内の結合についても同様です。明らかに関数 A のみを変更したいのですが、それが誤って関数 B に影響を及ぼします。関数 B を修復すると、関数 C にも影響します。このように 1 つのコードを変更すると、別のコードにも影響します。と呼ばれるこれら 2 つのコードは結合されています。

ただし、結合の概念は少し曖昧です。たとえば、モジュール A とモジュール B が結合されています。モジュール A がモジュール B を呼び出すのか、モジュール B がモジュール A を呼び出すのかはわかりません。そのため、モジュール間の関係をより正確に把握できます。 「依存性」で説明され、A が B に依存するということは、A が B の公開 API を呼び出す必要があることを意味します。

結合は完全に悪いわけではありません。モジュールを他の人が使用したい場合、結合が存在する必要があります。したがって、一般的な議論は低結合についてです。不合理な結合を排除するように努める必要があります。つまり、不合理な依存関係を排除する必要があります。システムの保守が容易になります。

一般的なカップリングの種類

1. システムレベルの結合

テクノロジー選択の組み合わせ

モノリシック モードとマイクロサービス モードについては誰もが聞いたことがあると思いますが、モノリシック モードでは、複数のプロジェクトで同じ開発言語、同じさまざまなサードパーティ モジュール バージョンなどを使用する必要があります。たとえば、元のプロジェクトは Vue2+ で開発されました。 ElementUI2 、現在は新しい Vue3 と Element Plus がありますが、対照的に、プロジェクト全体をアップグレードしない限り、これらの新しいテクノロジを使用することはできません。アーキテクチャ レベルで、シングル モードでは、新しいプロジェクトがオリジナルで使用されているテクノロジに依存する必要があるためです。プロジェクトは一種のアーキテクチャカップリングです。マイクロサービス アーキテクチャでは、この問題は存在しません。各プロジェクトは独立しており、サブプロジェクト間の依存関係はありません。Vue2、Vue3、さらには React を使用できます。

バックエンドサービスも同様で、マイクロサービスアーキテクチャを採用することで、特定の言語に基づくメソッドを直接呼び出すのではなく、http経由で各モジュールを呼び出すことで、サービス間の技術選択の分離を実現します。

導入環境の結合

最近では、Docker ベースのデプロイ方法を選択する企業が増えています。Docker テクノロジーを使用して、サービスごとに個別のコンテナを作成します。各コンテナには、独自の必要なデプロイ環境が含まれています。Docker を使用しない場合、サーバーにデプロイする場合は、 PHP、Java、Python などの複数の言語アプリケーションを同時にデプロイしたり、同じ言語で異なるバージョンのアプリケーションを同時にデプロイしたりすることは非常に困難です。競合の問題がよく発生します。Docker を使用すると、この結合関係が解消されます。 . これが、Docker が非常に人気がある理由の 1 つです。

2. システムとサードパーティモジュールの結合

フロントエンド プロジェクトでは、基本的に axios や fetch などのネットワーク リクエスト用のサードパーティ ライブラリが使用されます。ページ上で axios メソッドを直接呼び出してネットワーク リクエストを行うと、いつかネットワークを置き換えたい場合に、リクエスト ライブラリのバージョンに関して、新しいリクエスト ライブラリのメソッドとパラメータの順序が以前のものと矛盾している場合、多大な修正作業が発生します。

さらに考えると、実際に必要なのは axios ではなく、ネットワーク リクエストです。ネットワーク リクエストの作成方法は、axios が提供するインターフェイスに依存すべきではありません。代わりに、ネットワーク リクエストのインターフェイスを定義し、axios を使用してそれを実装します。

たとえば、get、post、およびその他のリクエストを提供するネットワーク リクエスト モジュール request を定義できます。request によって提供されるメソッドはページ上で呼び出されます。いつか axios を変更したい場合は、メソッドを変更するだけで済みます。リクエスト。

変換前のページ内のネットワーク リクエスト

//页面中直接调用axios
import axios from 'axios'
function getData(url, params){axios.get(url, params)
} 

request.js を刷新しました

import axios from 'axios'
function request(url, options){return axios(url, options).then(res =>{}).catch(e =>{})
}

function get(url, params){return request(url, {method: 'GET',params})
}

function post(url, body){return request(url, {method: 'POST',body})
}
export default {get,post
} 

変換されたページ内のネットワーク リクエスト

import request from '@/utils/request'
function getData(url, params){request.get(url, params)
} 

この変換により、将来 axios のインターフェースパラメータ転送方法が変更されたり、axios を直接 fetch に置き換えたりした場合でも、プロジェクトとサードパーティコンポーネントのソリューションが完了しているため、非常に簡単に行うことができます。この依存関係逆転メソッド。

同様の問題はバックエンドにも存在します。たとえば、バックエンドがログを保存するときに、Log4j などのログ フレームワークに直接依存している場合、その後のログ フレームワークの置き換えにより多くの変更が発生します。同様に、ログをカプセル化することもできます。クラスを作成し、サードパーティの Log4j を使用してそれを実装し、プロジェクトと特定のログ フレームワークの分離を完了します。

3. 循環依存関係

適切に設計されていないシステムでは、循環依存関係の問題がよく発生します。たとえば、utils の 3 つのモジュール A、B、および C のうち、A が B のメソッドを呼び出し、次に B が A のメソッドを呼び出すか、A が B を呼び出します。 B は C を呼び出し、C は A を呼び出します。これも循環依存関係を形成します。

循環依存関係のあるシステムは、モジュール間で十分な結合が発生し、変更が非常に不便です。複数のビジネス モジュール ユーティリティの共通部分を階層的にベース レイヤに抽出できます。ビジネス レイヤ ユーティリティは、ベース レイヤ ユーティリティに依存できます。ただし、ベース層はビジネス層に依存できないため、循環依存関係が排除されます。

4. グローバルデータ結合

現在、フロントエンド プロジェクトでは、グローバル変数が使用されることは比較的まれですが、グローバル データはますます増えています。vuex と redux の使用により、グローバル状態を通じて複数の機能モジュールをリンクするモジュールがますます増えています。この結合を利用する必要がありますが、この結合に関与するモジュールの数を制御するようにしてください。

グローバル データを結合すると、必然的にモジュールやコンポーネントのカプセル化が破壊され、コンポーネントの再利用が困難になります。同時に、グローバル データを結合するとテスト容易性も損なわれるため、単一のテストを記述するときにグローバル データ コードをシミュレートする必要があります。テスト自体はさらに長くなります。

たとえば、以下に示すように、2 つのオブジェクト間の差分を比較する diff 関数があります。渡されるパラメータはターゲットであり、比較されるソース データはグローバル状態から取得されます。

//utils中的diff函数
function diff(target){let source = store.state.appInfo//对比source和target
} 

これを実装すると、次のような問題が発生します。

  • ビジネスでコードを読む場合、diff が誰と比較されているのか分かりませんし、例えば diff 関数を深く掘り下げて真実を理解する必要があり、読む際の認知的負荷は確実に増加します。
//业务中的代码
import {diff} from 'utils'

function getDiffResult(info){return diff(info);//当阅读这块代码时,你知道是和谁对比吗?
} 
  • diff メソッドを他のプロジェクトに移行したり、他のモジュールで使用したりすることはできません。これは、他の場所で使用する場合、必ずしもこのグローバル変数との差分を取る必要がなく、再利用性が低下するためです。
  • diff メソッドをテストするときは、グローバル変数をモックする必要があるため、テスト容易性が低下します。

この結合を切り離すために必要なのは、グローバル変数をパラメーターとして渡すという単純な変換だけです。これにより、少なくとも diff 関数が再利用可能でテスト可能であることが保証されます。

//utils中的diff函数
function diff(source, target){//对比source和target//不在依赖任何全局变量,可以随处使用
} 
//业务中的代码
import {diff} from 'utils'

function getDiffResult(info){return diff(store.state.appInfo, info);//清晰的看出谁和谁进行比较
} 

グローバル データは、カプセル化、再利用性、テスト容易性を結合および破壊するだけでなく、可読性も低下させます。特定のグローバル データがどこから来たのか、いつ変更されるのかは、ユーザーが詳細に理解していない限りわかりません。これは最小値の原則に違反します。コード行を変更するには、まず多くの知識を追加する必要がありますが、それは間違いなく恐ろしいことです。

必要な場合を除き、グローバル変数を追加しないでください。

5. データ構造の結合

関数のパラメータがオブジェクトである場合、関数はオブジェクトの構造を理解する必要があることを意味し、これも一種の結合です。

オブジェクトのデータ構造が変わると、関数内の実装も変更する必要があり、この変更は避けられない場合もありますが、オブジェクトの内部構造を深く理解すること、つまり、オブジェクトの全体的なデータを取得することは避けるべきです。オブジェクト。

たとえば、カプセル化したテーブルで削除操作を実行するには、データの特定の行のオブジェクト listProps を渡す必要がありますが、処理関数で実際の行データを取得したい場合は、listProps を通じてそれを読み取る必要があります。 .rowData.row、これは処理関数です。テーブルから送信されるデータ構造について明確にしておく必要があります。いつかテーブルのデータ構造を変更したい場合、内部実装に大量の変更が発生することになります。機能の。

<template><tn-table><templateslot="op-buttons"slot-scope="listProps"><el-buttontype="text" size="mini"@click="deleteApp(listProps)">删除</el-button></template></tn-table>
</template>

<script>
export default {methods: {deleteApp(listProps) {//必须清楚表格传递过来的数据结构let rowData = listProps.rowData.row; let id = rowData.id;//调用接口删除数据}}
}
</script> 

削除関数 deleteApp がテーブル コンポーネントの実装と結合していることがわかります。テーブル コンポーネントによって渡されるデータ構造がどのようなものであるかを知る必要があります。これは不合理です。削除関数の場合、テーブル コンポーネントの特定のデータである限り、削除する行を入力するか、削除するデータのIDを渡すだけです。

変更された実装は次のとおりです。削除関数を呼び出すときに、データ行の実際の内容が渡され、削除関数はテーブルの実装の詳細を知る必要がなくなりました。

<template><tn-table><templateslot="op-buttons"slot-scope="listProps"><el-buttontype="text" size="mini"@click="deleteApp(listProps.rowData.row)">删除</el-button></template></tn-table>
</template>

<script>
export default {methods: {deleteApp(row) {//不在需要知道表格的特殊行数据结构,传递过来的就直接是改行数据let id = row.id;//调用接口删除数据}}
}
</script> 

または、完全なオブジェクトの分解を渡す必要がなく、使用される特定のパラメータだけを渡す必要がある場合もあります。

前者の変換:

function deleteNamespace(row){let namespace = row.content.metadata.namespace;//删除namesoace
} 

変換後:

//删除函数不再需要知道具体数据结构
function deleteNamespace(namespace){//删除namesoace
}

//调用方把需要的值直接取出来传递过去
deleteNamespace(row.content.metadata.namespace) 

この方法は、パラメータが比較的少ない状況にのみ適しています。パラメータが多すぎると、認知と記憶の負担がさらに増大するため、状況に応じてバランスをとってください。

6. 機能とAPIインターフェースの連携

一般に、関数と API インターフェイスには何らかの結合があります。結局のところ、データはインターフェイスから取得する必要があります。インターフェイスの変更により、フロントエンドも変更されます。たとえば、バックエンドが作成時刻フィールドを createTime から変更する場合フィールド マッピングを使用するとコストが高くなり、一部のフィールドしか分離できなくなります。

例 1: たとえば、バックエンドは状態を定義します。

  • 0: 同期済み
  • 1: 同期されていません
  • 2: 対立

コード内で直接使用すると、まず可読性が悪く、次にバックエンドがステータスと値の対応関係を後から変更すると、それに合わせてフロントエンドも変更されてカップリングが発生します。

function deleteHandle(rowData) {if (rowData.status === 0 || rowData.status === 2) {}
} 

状態定数を定義することにより、バックエンド インターフェイスの状態と特定の値の間の対応関係が切り離されます。

export const K8S_RESOURCE_STATUS = {synchronized: 0,unSynchronized: 1,inconsistent: 2
};

function deleteHandle(rowData) {if (rowData.status === K8S_RESOURCE_STATUS.synchronized|| rowData.status === K8S_RESOURCE_STATUS.inconsistent) {}
} 

API と組み合わせたシナリオには、バックエンド インターフェイス アドレスの変更によってもたらされる変更も含まれるため、前に紹介したように、ネットワーク リクエストについては、API アドレスを構成ファイルに作成する必要があり、ネットワーク リクエストは構成ファイルにカプセル化されます。サービス層を使用して、フロントエンドの API インターフェイスの必要性を軽減します。

7. コンポーネント/モジュール間の結合

フロントエンドの学生は基本的に、サブコンポーネント間の通信は相互に直接呼び出すことができないため、サブコンポーネント間のカップリングが発生することを知っており、特定のサブコンポーネントに何かが発生した場合は、イベントをスローするだけで済みます。 ? その後の処理は、リーダー (親コンポーネント) が検討および設計します。将来の変更も親コンポーネントによって決定されます。子コンポーネントには、他のコンポーネントに干渉する権利はなく、ましてや他のコンポーネントに指示したり制御したりする権利はありません。

ビジネスプロセスの制御は親コンポーネントに与えられるべきであり、サブコンポーネントは自らの責任の範囲内で物事を完了するだけの責任を負い、その権限を超えてはなりません。

これと同様に、子孫コンポーネント間には、イベント バス EventBus などを介した通信があります。イベントが発生すると、イベントがスローされます。このイベントを気にする人は誰でもリッスンします。イベントをスローする側は、イベントを受け取る側から切り離されます。イベント。

メッセージ キュー、オブザーバー モード、サブスクリプション モードなどの同様のアイデアもあります。データやイベントのプロデューサーは、後でそれを処理する方法を気にしません。この登録と監視のモードを通じて、プロデューサーとコンシューマーは分離されます。

直交および分離

直交系

幾何学では、2 本の直線が直角に交差することを「直交」といいますが、ベクトルの場合、2 本の直線は互いに独立しており、まったく異なる方向に展開します。

コンピューターでは、直交性は独立性または分離を象徴し、2 つ以上の事柄について、1 つの変更が他のものに影響を与えない場合、これらの事柄は直交していると言われます。たとえば、データベース関連のコードはユーザー インターフェイスと直交している必要があり、インターフェイスを変更してもデータベースには影響せず、データベースを切り替えてもインターフェイスには影響しません。

私たちは、デザインのコンポーネントが自己完結型であること、つまり自律的であり、明確に定義された単一の意図を持っていることを望んでいます。コンポーネントが相互に分離されている場合、他のコンポーネントへの影響を心配することなく、コンポーネントの 1 つを変更できることがわかります。コンポーネントの外部インターフェイスが変更されない限り、システムの他の部分に影響を与えることはありませんのでご安心ください。

直交システムの利点

直交システムは、生産性の向上とリスクの軽減という 2 つの主な利点をもたらすと同時に、システムのテスト容易性も向上します。

1. 生産性の向上

  • 変更が局所的に行われると、開発時間とテスト時間の両方が短縮されます。完全なコード ブロックを記述するよりも、比較的小さな自己完結型コンポーネントを記述する方が簡単です。
  • 直交的なアプローチは再利用も促進し、コンポーネントの役割が明確に定義され単一であれば、さまざまな方法で他のコンポーネントと組み合わせることができます。
  • 1 つのコンポーネントが M 個の固有のことを実行でき、もう 1 つが N 個のことを実行できる場合、それらが直交していれば、組み合わせると M*N のことができます。2 つのコンポーネントが直交していない場合、組み合わせるのは困難になります。

2. リスクを軽減する

  • コードの異常部分が隔離されているため、あるモジュールで問題が発生しても、システムの他の部分に症状が波及する可能性が低く、また、異常部分を切り取って新しいものに置き換えることも簡単です。
  • このシステムは、特定の領域に小さな変更や修正が加えられた場合に、その結​​果として生じる問題がその領域に局在するほど脆弱ではありません。
  • 直交システムはテストしやすく、テストしやすい
  • 特定のサードパーティ製コンポーネントに関連付けられることはなく、状況に応じていつでも交換できます。

3. テスト容易性の向上

  • 単体テストが難しいコードを見つけた場合、それはおそらく結合されすぎていることを意味します。
  • 結合されたコードをテストするには、多くの準備作業が必要であり、コード自体をテストするよりもコードの準備に時間がかかる場合があります。
  • 直交システムはテスト ケースを構築するのに非常に便利で、テスト容易性は直交性と正の関係があります。

デカップリングのいくつかの方法

ブラックボックス開発

複数のモジュールを呼び出す場合、内部実装の詳細は分からないまま、モジュールが提供する API を通じて呼び出されるため、各モジュールは互いにブラックボックスのようなものです。いわゆるブラック ボックスとは、モジュールの内部データ構造や呼び出しシーケンスなど、内部実装の詳細を知る必要がないことを意味します。

たとえば、あるコンポーネント クラス Component 内にイベント クラス Event があり、Event はイベント プロキシのデリゲート メソッドを提供します。

let component = new Component();
component.event.delegate('click', callback); 

この例では、イベント委任を実行する場合は、コンポーネント内のイベント インスタンスを通じてそれを呼び出す必要があります。つまり、コンポーネント クラスの内部構造を知る必要があります。コンポーネント クラスがイベント プロキシ メソッドを提供しなくなった場合イベント クラスに基づいている場合、またはコンポーネント クラスが内部実装で変更されている場合は、イベント プロキシを呼び出すすべてのビジネス コードを変更する必要があります。つまり、ビジネス モジュールとコンポーネント クラスの実装は深く結合されています。

より良い実装は、コンポーネント クラスが外部に提供する API を直接呼び出すことです。たとえば、コンポーネント クラスがイベント プロキシ メソッドを外部に直接提供し、内部のデリゲート メソッドが Event クラスのデリゲート メソッドを呼び出します。 。

class Component {delegate(eventName, callback){this.event.delegate(eventName, callback)}
}

let component = new Component()
component.delegate('click', callback) 

後続のコンポーネントクラスはデリゲートメソッドを調整するため、APIの構造が変わらない限り業務に影響はなく、業務にとってはブラックボックスのようなものになります。

層状の

デカップリングの本質は懸念事項の分離であり、階層化は懸念事項を分離するための一般的かつ重要な方法です。

フロントエンドの開発の歴史は階層化開発の歴史であり、当初はフロントエンドとバックエンドが混在して開発され、UIページのコードとデータベース関連のコードが混在していましたが、後にMVC階層化モデルが誕生しました。このようにして、デカップリング後、ようやくフロントエンドとバックエンドの開発がスムーズに並行して開発できるようになり、フロントエンドの開発もますます進んでいきます。近年、VueやReactといったMVVMパターンフレームワークの登場により、フロントエンドも史上最高の時代を迎えています。

MVC を例にとると、階層化後、ビュー層とモデル層が分離されます。モデル層のクエリ データベースはページに影響を与えずに変更され、ビュー層の変更はモデル層のコードには影響しません。この階層化方法により、次のことが可能になります。次の 2 つのメリットがあります。

  • 並行開発: インタラクティブなデータ構造が合意されている限り、フロントエンドとバックエンドのエンジニアは独自のコードを並行して開発できます。
  • 再利用の向上: フロントエンドとバックエンドのロジックを含むページは、別のプロジェクトで使用するのが困難です。まったく同じフロントエンドとバックエンドを持つページはほとんどありません。ただし、階層化した後は、たとえビュー レイヤーが完全に同じではありませんが、モデル層とコントローラー層はおそらく同じままです。

フロントエンド開発のあらゆる場面において、実は階層化という考え方を使って開発を行っており、例えばCSS開発ではテーマ変数、グローバルCSS、ページCSS、コンポーネントCSS、utilsライブラリも基本的なものに分けることができます。一般的なユーティリティとビジネス ユーティリティ、基本的なユーティリティは、よく見かける固有の文字列 uuid を生成する機能など、ビジネスとは関係のないユーティリティであり、コンポーネントも基本コンポーネントとビジネス コンポーネントに分けることができます。一般的なテーブル コンポーネント、ポップアップ コンポーネントなどの特定のビジネス。

CSS、JS、ベースレイヤーのコンポーネントが業務から切り離されるため、再利用性が大幅に向上 基本的に新規プロジェクトを開いた後は、元のベースレイヤーのコードが利用可能 毎回新規プロジェクトを開く場合スクラッチは、ビジネスから切り離されていないことを意味します。

私たちの目標は、デカップリングによってベース レイヤを大きく、厚くすることです。ベース レイヤは安定したコードであり、ビジネス レイヤは機密コードです。ベース レイヤが完成した後は、多くの変更はほとんど必要ありません。

依存関係の逆転

依存関係逆転の原則とは、プログラムは特定の実装ではなく抽象インターフェイスに依存する必要があることを意味します。簡単に言うと、実装ではなく抽象化をプログラミングする必要があるため、クライアントと実装モジュールの間の結合が軽減されます。

例えば、同社はフォードとホンダ向けに自動運転システムを開発しており、両車に搭載されていれば自動運転機能を実現できる。自動運転システム クラス AutoSystem、フォード車クラス FordCar、ホンダ車クラス HondaCar を定義しました。フォードとホンダでは発進方法が異なり、一方は走行、もう一方はドライバーであるとすると、自動運転システムで車を発進させるには、両者を区別する必要がある。

class HondaCar{run(){console.log("本田开始启动了");}
}
class FordCar{driver(){console.log("福特开始启动了");}
}

class AutoSystem {run(car){if(car instanceof HondaCar){car.run()}else if(car instanceof FordCar){car.driver()}}
} 

同社のビジネスが成長した現在、BMW 車に自動運転システムを導入しようとしています。BMW 車の始動方法は startCar であるため、自動運転システムを BMW 車に対応するように変更する必要があります。

class HondaCar{run(){console.log("本田开始启动了");}
}
class FordCar{driver(){console.log("福特开始启动了");}
}
class BmwCar {startCar(){console.log("宝马开始启动了");}
}

class AutoSystem {run(car){if(car instanceof HondaCar){car.run()}else if(car instanceof FordCar){car.driver()}else if(car instanceof BmwCar){car.startCar()}}
} 

その後のビジネスが成長するにつれて、自動運転システムにはさまざまな「if-else」が発生します。これはまだ車を起動するための 1 つの方法の例にすぎません。実際の状況は間違いなくもっと複雑です。パートナーと交渉するたびに、自動運転システムは自動運転システムと特定のモデルの間には深刻な関係があります。これはまさに、高レベルのアプリケーションが基礎となる実装に依存しているためです。すべての自動車にバックエンドのインターフェイスと呼ばれる固定メソッドを持たせる必要があるとします。そうすれば、自動運転システムを頻繁に変更する必要はなくなり、新しいメソッドが追加されます。車種、対応する車のインターフェイスを追加するだけです。

class HondaCar{run(){console.log("本田开始启动了");}
}
class FordCar{run(){console.log("福特开始启动了");}
}
class BmwCar {run(){console.log("宝马开始启动了");}
}

class AutoSystem {run(car){car.run()}
} 

自動運転 AutoSystem は大幅に簡素化されており、将来的には特定のモデルに結合されなくなることがわかります。

次のステップは常に次のとおりです。

1. 上位層のアプリケーションは下位層のモジュールに依存すべきではなく、すべて抽象化に依存する必要があります。

2. 抽象化は具体性に依存すべきではなく、具体性は抽象化に依存すべきです。

中間層マッピング、アダプテーション層

A は B に依存します。A と B を分離したい場合は、A と B の間に中間層 C を追加して、依存関係を A は B に依存し、B は C に依存するように変更できます。これにより、A と B の間の直接的な接続が回避されます。カップリングです。

後で B を変更する場合は、アダプテーション層 C を変更するだけでよく、A 層のコードを変更する必要はありません。

レイヤーを追加すると、B の変更はまだ修正する必要があるが、A の変更は C に移動するという人もいるかもしれません。

A が B のメソッドを 5 回呼び出した場合、B がメソッドのパラメーターの受け渡し順序を変更すると、A はそれを 5 回変更する必要があります。中間層のマッピングが使用されている場合は、B を変更した後、中間層 C のみを変更する必要があります。修正が必要な場合には、中間層やアダプテーション層を追加することで、依存部分変更後のシステム全体の修正回数が削減されることがわかる。

中間層を追加すると、サードパーティ モジュールへの依存が減り、交換が容易になり、システムが固定サプライヤーに縛られなくなります。

生成と消費、パブリッシュとサブスクライブ、イベント、メッセージ キュー

既知のイベントを例にとると、プロデューサーはイベント バスにイベントをスローし、コンシューマーはイベントをリッスンしてアクションを実行します。イベント プロデューサーは誰がイベントを処理するかは気にせず、イベント コンシューマーはイベントを処理するかどうかを気にしません。イベントは誰がそれを生成するかによって決まります。このようにして、プロデューサーとコンシューマーの分離が達成されます。

たとえば、プロデューサー製品の場合、イベント メソッドが使用されない場合、一般的な操作は次のようになります。プロデューサー コードでコンシューマーを呼び出します。

function product(){custom1()custom2()
}

function custom1(){}
function custom2(){

} 

需要の変化に応じてコンシューマ カスタム 3 を追加し、コンシューマ カスタム 1 を削除する必要がある場合は、それに応じて製品を変更する必要があります。つまり、プロデューサ コードとコンシューマ コードが結合されます。

function product(){//去除custom1()custom2()//新增custom3()
} 

イベント バスではそのような問題はなく、プロデューサーはイベントと関連データをスローするだけでよく、どのように処理されてもプロデューサー コードは変更されません。

function product(){event.emit('someEvent', data)
} 

これらに似ているのは、メッセージ キュー、オブザーバー モード、生成および消費モードなどです。違いはありますが、一般的な考え方は、生成と消費の分離を実現することです。

純粋関数

純粋関数は、すべての関数型プログラミング言語で使用される概念です。純粋関数は、数学における関数 y = f(x) に似ています。y の値は、パラメーター x にのみ依存します。つまり、パラメーター x が指定されている限り、y の値はパラメーター x にのみ依存します。決定されると、出力値 y は一定になります。コード内の純粋関数には通常、次の特性があります。

  • 関数の戻り値はパラメータのみに依存し、パラメータが変化しない場合、戻り値は変化しません。
  • 関数の実行中に副作用はありません

例 1: 非純粋関数への各呼び出しの結果は異なる場合があります

//非纯函数,每次调用add返回值不同
let count = 0
function add(){return count++
}

//纯函数
function add(a, b){return a + b
} 

例 2: 不純な関数はグローバル変数などの外部変数に依存する可能性がある

//非纯函数, 会依赖全局变量
function add(a){return window.count + a
} 

いわゆる副作用とは、外部での関数実行の影響を指します。この影響には次のものが含まれますが、これらに限定されません。

  • パラメータ値を変更する
  • ネットワークリクエストを行う
  • ドムを操作する
//非纯函数, 副作用:改变参数结构
function format(data){data.value = data.value + ''return data
}

//非纯函数, 副作用:网络请求
function getData(data){return axios.get(url)
}

//非纯函数, 副作用:存储
function setData(data){ localStorage.setItem('a', data)
} 

純粋関数のこれらの特性により、純粋関数は通常の関数よりも当然結合度が低くなります (たとえば、外部のグローバル データに依存せず、副作用も生成せず、もちろん外部世界と密結合していません)。

したがって、開発時には可能な限り純粋関数を使用する必要があり、不純関数はもちろん避けられませんが、副作用のある一部の業務コードから、副作用のある大きなコードである純粋関数を抽出する必要があります。多くの場合、副作用のある小さな関数といくつかの小さな純粋な関数に分割できますが、結合部分を非常に小さなコード領域に制限します。

たとえば、パラメーターが target である utils 関数 diff は、target とグローバル変数を比較します。これは明らかに純粋な関数ではなく、グローバル変数との結合もあります。純粋な関数になるには、単純な変更を行うだけで済みます。 。

//非纯函数
function diff(target){let source = window.source//比对
} 

変身後

//utils中的纯函数
function diff(source, target){//比对
}

//业务中调用
diff(window.source, target) 

単純な変換後、コードの結合部分はビジネス コードに限定され、基本的な utils 関数によってこの結合の問題が解決されます。

コード内にできるだけ多くの純粋な関数を記述することをお勧めします。

戦略パターン

一部のインターフェイスの特定の属性値が中国語に変換されてページに表示される状況がよく発生します。たとえば、ユーザー タイプの場合、インターフェイスは通常英語であり、ページに表示されるときに英語に変換する必要があります。

function getLabel(type){if(type === 'normal'){return '普通类型'}else if(type === 'vip'){return '会员'}else {return '未知'}
} 

ビジネスが変更され、新しいメンバーシップ タイプが追加された場合、変換コードをあらゆる場所で変更する必要があるため、ビジネス ルールと UI 表示の間には強い結びつきが存在します。

この問題の解決策も非常に簡単です。ベア メタル ビジネスを構成ファイルに抽出し、戦略として使用します。その後のビジネス変更では、戦略を更新するだけで済みます。

//配置文件config.js
export const userType = {normal: '普通类型',vip: '会员',svip: '超级会员',default: '未知'
}

//业务中使用
function getLabel(type){return userType[type] || userType['default']
} 

ビジネス コードには煩わしい if-else がなくなり、ビジネスが変更された後でもコードを変更する必要はなく、開始と終了の原則に準拠したポリシー構成を変更するだけであることがわかります。

もちろん、これは戦略モデルを使用する最も単純な方法にすぎませんが、原理は同じであり、いくつかの複雑なロジック処理も可能です。中国、世界、現在の都市など様々な国のデータが表示されますが、タブページを切り替える際にはそれぞれ異なるデータをリクエストする必要があります。

<template><div><tabs v-model="activeTab" @change="tabChange"><tab-pannel name="china"><ChinaData /></tab-pannel><tab-pannel name="world"><WorldData /></tab-pannel><tab-pannel name="beijing"><BeijingData /></tab-pannel></tabs></div>
</template>
<script>
export default {data() {return {activeTab: 'china'}},methods: {tabChange(tab) {if (tab === 'china') {//请求中国数据} else if(tab === 'world') {//世界数据}else if(tab === 'beijing'){//北京数据}}}
}
</script> 

プロダクト マネージャーが比較のために米国のデータを追加したい場合は、多くのロジックを変更する必要がありますが、戦略モデルを使用してこの結合を切り離すことができます。

2 つのフィールドを含む構成を追加します。

  • getData: さまざまな都市に対応するデータ関数を取得します。
  • コンポーネント: さまざまな都市に対応する UI 表示コンポーネント
<template><div><tabs v-model="activeTab" @change="tabChange"><tab-pannel v-for="item, type in config" :key="type"><component :is="item.component" /></tab-pannel></tabs></div>
</template>
<script>
export default {data() {return {activeTab: 'china',//增加策略配置configconfig:{china: {getData: this.getChinaData,component: ChinaData},world:{getData: this.getWorldData,component: WorldData},beijing:{getData: this.getBeijingData,component: BeijingData},}}},methods: {tabChange(tab) {//切换页签的逻辑变的异常简单 this.config[tab].getData()},getChinaData(){},getWorldData(){},getBeijingData(){}}
}
</script> 

変換後に US データを追加する場合は、config で設定するだけで済み、他のロジックを変更する必要はありません。

責任の連鎖

もう 1 つの結合関係は、モジュール間の呼び出し順序の結合です。たとえば、関数 A は関数 B を呼び出し、関数 B は関数 C を呼び出します。ある日呼び出し順序が変更された場合、それに応じて関連する処理コードを調整する必要があります。

フォーム検証を例に挙げると、名前検証に 3 つの検証方法があるとします。つまり、空かどうか、長さが正当かどうか、特殊文字が含まれているかどうかのチェックです。

function checkEmpty(){//校验逻辑checkLength()
}
function checkLength(){//校验逻辑checkSpecialCharacter()
}
function checkSpecialCharacter(){//校验逻辑
} 

たとえば、新しい検証ロジックを追加する場合、文字で始めることができず、2 番目の検証位置に配置される場合は、他の検証ロジックのコードを変更する必要があります。

この状況に対応して、責任の連鎖モデルを使用して書き直すことができます。いわゆる責任の連鎖とは、単に各ステップが実行された後に何をすべきかを意味します。それはもはやハードコード化されず、動的に構成され、それぞれのステップが実行された後に何をすべきかを意味します。処理機能 チェーンに登録された責任チェーンは、実行シーケンス全体を制御する責任を負います。

実装にはさまざまな方法があります。たとえば、バックエンドでは統合ベースの実装が使用されます。各責任チェーンの処理関数には、次の関数を指す next 属性があります。

class CheckChainHandler{constructor() {this.nextHandler = null;}setNextHandler(nextHandler) {this.nextHandler = nextHandler;}doValidate(params){throw new Error('必须由子类重写的验证方法');}executeValidateChain(params){let validateResult = this.doValidate(params);if(!validateResult) return;if (this.nextHandler){this.nextHandler.executeValidateChain.call(this.nextHandler,params);}}
}
class EmptyCheckHandler extends CheckChainHandler {
}
class LengthCheckHandler extends CheckChainHandler {
}
class SpecialCharacterCheckHandler extends CheckChainHandler {
}

function checkName(name){const emptyCheckHandler = new EmptyCheckHandler()const lengthCheckHandler = new LengthCheckHandler()const specialCharacterCheckHandler = new SpecialCharacterCheckHandler()emptyCheckHandler.setNextHandler(lengthCheckHandler)lengthCheckHandler.setNextHandler(specialCharacterCheckHandler)emptyCheckHandler.executeValidateChain(name)
} 

フロントエンドでは、通常、use メソッドを使用して責任の連鎖を作成します。これは比較的簡単に使用できます。

const checkMiddleware = new CheckMiddleware()
checkMiddleware.use(checkEmpty).use(checkLength).use(checkSpecialCharacter) 

パイプを一気に飲み込む:

gulp.src('client/templates/*.jade').pipe(jade()).pipe(minify()).pipe(gulp.dest('build/minified_templates')); 

koa のミドルウェア:

const Koa = require('koa');
const app = new Koa();
// logger
app.use(async (ctx, next) => {await next();const rt = ctx.response.get('X-Response-Time');console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});
// response
app.use(async ctx => {ctx.body = 'Hello World';
});
app.listen(3000); 

グローバル データの使用には注意が必要です

グローバル データは、複数の異なるモジュールを自然に結合します。関連する内容についても上で説明されているため、詳細は説明しません。グローバル データについては、使用をできるだけ少なくし、注意してください。場合によっては、多数のモジュールの結合を回避できることがありますパラメータを渡すことでグローバル データを使用し、結合されるモジュールの数を減らします。

章のまとめ

  • 結合は複数のモジュール間の相互依存関係であり、結合をなくすことはできず、軽減することしかできません。
  • システムレベルの結合、システムとサードパーティモジュール間の結合、循環依存関係、グローバルデータによって引き起こされる結合、データ構造の結合、フロントエンド API とバックエンド API 間の結合、結合など、結合が発生する状況は数多くあります。モジュール間の結合など
  • 直交システムを構築すると、生産性が向上し、リスクが軽減されます。モジュールのテスト容易性は直交性と正の関係があります。テスト可能かどうかは、結合の程度を判断するために使用できます。
  • 分離するには、ブラック ボックス開発、階層化、依存関係の逆転、アダプテーション レイヤーの追加、生産と消費、パブリッシュとサブスクライブ、オブザーバー、イベント、メッセージ キュー、戦略、責任チェーン、その他のモードの使用など、多くの方法があります。グローバル変数を使用する

やっと

HTML、CSS、JavaScript、HTTP、TCPプロトコル、ブラウザ、VUE、React、データ構造、アルゴリズムなどをまとめた「フロントエンド大手企業向け面接ガイド」を作成しました。面接の質問は全部で201問あり、各質問に対する回答を作成しました。回答と分析。

必要としている友人は、記事の最後にあるカードをクリックしてこの文書を受け取り、無料で共有できます。

ドキュメント表示の一部:



記事の長さに制限があるため、以下の内容を 1 つずつ表示することはできません。

困っている友達は下のカードをクリックして無料で入手してください

おすすめ

転載: blog.csdn.net/web2022050901/article/details/129204763