web-dev-qa-db-ja.com

事実上すべての意思決定ロジックが多数の分散状態をチェックする必要がある場合、どうすれば密結合を回避できますか?

私たちの会社の上級開発者として、私は現在、商用のphp-mysql eコマースソリューション(特定のERPシステムからデータを取得する)を、ほとんどすべてにグローバルを使用し、ほとんどない手続き型スパゲッティコードから移行し始めていますOOPへの関心の分離。

タイトルは基本的な問題をかなりよく説明しています-ほとんどすべてのユーザーインタラクションはたくさんのこと(状態)をチェックし、それに基づいてさまざまなことを行う必要があります。コントローラーとサービスの肥大化や、クラスに関するすべての構造知識(コンストラクターの過剰注入、サービスロケーター、法則違反など)のコーディングを回避するにはどうすればよいですか?

現在の状況に関する詳細情報:

私はすでに基本的なアーキテクチャスタイルの開発に成功しており、この設計でRMAモジュールを実装しました。

簡単な要約:すべての基本的なビジネスエンティティ(mysqlテーブルの行)について、データの格納と取得のためのクラスと、そのオブジェクトのNULLバージョンを取得するためのクラス、情報を保持する構成クラスがあります。どのフィールドがdbからマッピングされるか、db-id以外の潜在的な代替の一意のキーは何か、データを取得するテーブル(またはビュー)の名前。さらに、データクラスのインスタンスのみを確実に含む特定のコレクションクラスと、基本的な取得と永続性、およびエンティティ固有のメソッドの両方を管理するIDマップパターンを実装する特定のリポジトリがあります。非永続的なビジネスエンティティが必要な場合、それらはスタンドアロンで、コレクション付きのダイアッドまたはコレクションとリポジトリ付きのトライアドで構成できます。

これに加えて、私は処理タスクのサービスを使用するモジュール用の準「フロントコントローラー」を持っています。マルチステッププロセス(現在はRMA、後でユーザーアカウント、注文など)の場合、モジュール「フロントコントローラー」が現在のステップ(アクション)とユーザー入力(妥当なサービスに検証を委任)をチェックします。

ビューで複数のアクションが可能で、アクションがビュー内のデータのみを変更し、どのビューを選択する必要があるかを変更する可能性がない場合、「フロントコントローラー」はそのアクションをビューのプレゼンターに渡します。アクションが表示されるページを変更できる場合(検証エラー時の再ルーティング、入力検証が成功すると別のビューに進む)、サービスの助けを借りてビジネスロジックがモジュール「フロントコントローラー」自体によって実行されます。サービス。

かなり標準的なphp-applicationとして、アプリケーションはURLを介したユーザー入力、POST&GETで作成され、最初からこの入力を持ち、応答を作成して出力し、その後破棄されます。ユーザー入力により、アプリケーション全体が新たに作成されます。ただし、Ajaxコードがアプリケーションによってサイトに配置される1つまたは2つの場所を除きます。これは、基本的にミニアプリケーションを作成し、その出力をブラウザーウィンドウに統合するだけです。は、ページを作成したアプリケーションインスタンスとの相互作用ではありません。したがって、アプリケーションはその存続期間中にビューを更新できません。UIで実際にビューを作成すると、アプリケーションの存続期間が終了します。つまり、本来の実際のMVC/MVVM/MVP実装考えられることは厳密には可能ではありません...部分的なマッピングのみが可能です。私は上記のような控えめな実装を試みました。

RMAは適切に機能しており、懸念の分離、つまり「デバッグ可能性」がすでに大幅に向上しています。さまざまなクラスが共通のインターフェース(ビジネスエンティティ固有のコレクション、構成、データオブジェクト、リポジトリ/アイデンティティマップ)を実現するために必要なフィールドとフィールド関連のメソッドの特性を使用して、コードの重複をきれいに回避できました。

ベストプラクティス、パターンとアンチパターン、アーキテクチャスタイルなどについてすべてを数か月読んだ後、さまざまなことを試してみましたが、まだかなり差し迫った問題が残っています。

単一のビジネスエンティティ(データオブジェクト、構成、コレクション、リポジトリ)に関連するクラス間の結合は許容可能であり、実際には非常に有益です。このテトラッドパターンと特性の使用により、新しいビジネスエンティティが追加されます(そしてそれらを処理するための非常に強力な方法を提供します)ほんの数分で。スキーマの変更では、データオブジェクトの各フィールドと構成の説明を変更する必要があるだけです(すべてのフィールドに2行のコードを追加し、non-idの一意のキーが変更された場合はさらに2行になる可能性があります...起こります)。

主に私を悩ませているのは、コンストラクターの過剰注入、サービスロケーターに加えて、デメッターの法則違反、および密結合(知識が多すぎる)に関連する悪い習慣のセットの少なくとも1つに食い込むのを避ける方法がないことです。コントローラーとサービスの一般。

これは主に、ほとんどすべてのコントローラーが大量の分散状態をチェックする必要があるためです...主に、特定のPOST&GETのセットを使用して特定のURLで実行および表示する必要があることデータは非常に多くの要素に依存します:ショップはb2c、b2b、または営業担当者ですか?訪問者はログインしていますか?アクションの対象となるレコードを識別するフォームデータは、レコードの会社コード、ショップコード、ショップ言語コードと一致していますか? ?b2bにいる場合、ユーザーはモジュールの権限を持っていますか?ユーザーはレコードの権限を持っていますか?ローカル言語で指定されたコードを含むテキストスニペットの内容は何ですか?

このすべての情報は、さまざまなモデルエンティティで配布されます。プロシージャがオブジェクト自体のデータのみに関係する場合は常に、データクラス内、または(メタ情報が必要な場合)データクラスとその構成を認識しているサービス内にあります。ビジネスエンティティの特定のコレクション/集合体に対する操作のみが含まれている場合、コレクションクラスにあります。取得に関する場合は、リポジトリ/アイデンティティマップにあります。

しかし、実質的にすべてのモジュールと、ユーザーがモジュールに対して実行できるほぼすべてのアクションは、非常に多くの異なるものに依存しているため、現在、オブジェクトとそのコンストラクターの肥大化、または使用を回避する方法を見つけることができないようです。 (今はRMAモジュールで行うように)現在のショップ、ショップの言語、ローカルテキスト定数のプロバイダー、現在の訪問者、ユーザー、顧客、および「神」を含む「CurrShopConfiguration」などの構成オブジェクトCurrConfig-Objectを持ち、アクション、モデル、ビューの選択とそれらのリンクを超えて「フロントコントローラー」が実行する必要のあるすべての手順を実装するコントローラーのヘルパーサービスのように。

SOLIDコード-もっと書きたいと思いますが、モジュールとアクションの本質的な意思決定ロジックには多くの必要な依存関係があるため、方法はわかりません。

おそらくあなたはこの問題に取り組む方法についていくつかのアイデアを持っていますか?

以下に、コード例を示します。RMA-「フロントコントローラー」の一部であり、返品可能な貨物があるフォームで「保存」アクションが呼び出されたときに実行されます。

//Check for existence of shipment-id in input
if (!($this->inputHeaderID > 0)) {
    //add user-faced error-message for view-models
    $this->errors[] = $this->templateEngine->getText('record_not_set');
    //go to search-view (where errors are displayed)
    $this->_showSearchPage();
} else {
    //Try to get shipment from Repository (extracted from helper), get either target shipment or NULL-object (unset fields)
    $shipment = $this->retShipmentRepository->findByID($this->inputHeaderID);
    //Check for NULL-Object
    if (!((int)$shipment->id) > 0) {
        $this->errors[] = $this->templateEngine->getText('record_not_set');         
        $this->_showSearchPage();
    } else {
        //We have valid shipment. Check for user permission on shipment
        if (!$this->helper->checkUserShipmentPermission($this->user,$shipment,$this->inputEMail,$this->inputPostCode)) {
            $this->errors[] = $this->templateEngine->getText('record_not_permitted');
            $this->_showSearchPage();
        } else {
            //We have permission, attempt saving (shop-context based switching and further validation inside service)
            if(!($this->helper->saveReturnOrder($shipment,$this->inputReference,$this->inputLines))) {
                $this->errors = $this->helper->getErrors();
                $this->_showShipmentPage($shipment);
            } else {
                //The Shipment could be appended with the return order information and saved.
                //Check if it was deemed to require verification via link with token in email to owner of shipment
                //Show either success view or confirmation-required view based
                $saveStatus = $this->helper->getSaveStatus($shipment->id);
                if($saveStatus == RMAOrderHelper::SAVE_STATUS_WITHOUT_TOKEN) {
                    $this->_showSuccessPage();
                } elseif($saveStatus == RMAOrderHelper::SAVE_STATUS_WITH_TOKEN) {
                    $this->_requestTokenConfirmation($shipment);
                }
            }
        }
    }
}

ユーザー入力変数は、filter_varを介してコントローラーの__construct(ヘルパーと共に注入されます-ヘルパーの「CurrShopConfig」にアクセスし、必要な情報を直接抽出します)に格納されます。

上記のコードは私にはかなり悪臭がします...しかし、ヘルパーサービスにあり、現在のショップ構成、ユーザー、ビジターなどにアクセスしてパブリックメソッドのタスクを実行する意思決定ロジックには、はるかに多くの機能があります。

ヘルパーサービスのsaveReturnOrder-Methodの最初の2/3は次のとおりです

public function saveReturnOrder( RetShipmentDocument $shipment, $yourReference, $inputLines ) {
        //Check if document isn't already marked with a return order request 
        //that hasn't been synchronized with the ERP-System (and thus cleared) yet
        if (!$shipment->isReturnOrderSettable()) {
            $this->errors[] = $this->templateEngine->getText('internal_error');
            return FALSE;
        }
        //Check if the input for the shipment-lines (id, return quantity and return-reason code)
        //belong to the shipment and are valid (e.g. return quantity <= un-returned quantity)
        if (!$this->_validateReturnOrderLineInput($shipment, $inputLines)) {
            $this->errors[] = $this->templateEngine->getText('internal_error');
            return FALSE;
        }

        //ValidatedLineRequests are stored for the shipment by the _validateReturnOrderLineInput-Method
        //that has just been executed
        $requestLines  = $this->validatedLineRequests[$shipment->id];
        $shipmentLines = $shipment->getLines();

        $token  = '';
        $markAsConfirmed = TRUE;

        //If visitor is not logged in and we have a content for a confirmation-request mail
        //we must not mark it as confirmed and generate a token
        if (!$this->shopConfiguration->visitorLoggedIn() && ($this->getConfirmMailTextModule()->id > 0)) {
            $token  = md5(uniqid(mt_Rand(), TRUE));
            $markAsConfirmed = FALSE;
        //Otherwise if visitor is not logged in and there is no such mail-content
        //we have an error and cannot proceed
        } elseif (!$this->shopConfiguration->visitorLoggedIn() && !($this->getConfirmMailTextModule()->id > 0)) {
            $this->errors[] = $this->templateEngine->getText('internal_error');
            return FALSE;
        }
        $lineRepository = $this->shipmentRepository->getLineRepository();
        $committedLineRequests = 0;
        foreach ($requestLines as $lineRequest) {
            $lineID           = $lineRequest['line_id'];
            $returnQuantity   = $lineRequest['return_quantity'];
            $returnReasonCode = $lineRequest['return_reason_code'];         
            foreach ($shipmentLines as $shipmentLine) {             
                if ($shipmentLine->id == $lineID) {
                    //The shipment has a line matching the request, attempt setting the return-order data
                    if (!$shipmentLine->setReturnOrder($markAsConfirmed, $returnQuantity, $returnReasonCode)) {
                        $this->errors[] = $this->templateEngine->getText('internal_error');
                        $this->_unsetReturnOrder($shipment);
                        return FALSE;
                    }
                    //Data could be set in object - now try persisting it
                    if (!$lineRepository->updateSingle($shipmentLine)) {
                        $this->errors[] = $this->templateEngine->getText('internal_error');
                        $this->_unsetReturnOrder($shipment);
                        return FALSE;
                    }
                    //Persistence succeeded, increment counter
                    ++$committedLineRequests;
                }
            }
        }

ご覧のとおり...私はすでに多くの個別のサブプロセスをメソッドにして、それらが属するオブジェクトとともに配置しましたが、それでも巨大な醜いネストされた条件と依存関係の爆発が残っています。このようなネストされた条件文のすべての部分(またはほとんどの部分)を異なるオブジェクトに分割することは、実行不可能です...実際にはすべての条件文が、それらが属するオブジェクトのメソッドをすでに使用しているため、このような分割によって再作成が増える可能性があるため、役に立ちません使いやすさはほとんどなく、数十のそのような状況で実行する必要があるため、クラスとファイルが爆発的に増加します。

6
Michael Bauer

渡される依存関係が多すぎる場合、一般的な手法は、決定が行われる順序を変更することにより、コールスタックの上位にある依存関係を排除することです。これは例で説明するのが最も簡単です:

_getPath(config, user) {
  if (config.isB2B())
    return b2bpath(config, user);
  else
    return b2cpath(config, user);
}

b2bpath(config, user) {
  if (!config.allowedToAccessPath(user))
    return accessDeniedPage();
  else
    return "My fancy b2b page";
}

b2cpath(config, user) {
  if (!config.allowedToAccessPath(user))
    return accessDeniedPage();
  else
    return "My fancy b2c page";
}
_

呼び出しスタックの最下位レベルで承認チェックダウンを繰り返しているため、上に移動します。

_getPath(config, user) {
  if (!config.allowedToAccessPath(user))
    return accessDeniedPage();

  if (config.isB2B())
    return b2bpath();
  else
    return b2cpath();
}

b2bpath() {
    return "My fancy b2b page";
}

b2cpath() {
    return "My fancy b2c page";
}
_

次に、getPathを呼び出すコードに決定の一部を移動できるかどうかを確認するために繰り返します。これは簡単な例ですが、以前の種類のコードがより多くのレイヤーで見られます。最下層での決定から始めて、それらを上に移動する方法を考えてみてください。時々これは継承の賢明な使用を必要とします、例えば:

_getPath(config, user) {
  module = config.isB2B() ? B2BModule() : B2CModule()
  if (!config.allowedToAccessPath(user))
    return module.accessDeniedPage();

  return module.getPath();
}
_

この方法で依存関係を単純化できないことはveryまれです。うまくいくものが見つかるまで、さまざまなアレンジを試してみるということです。

最初の例のように、データの依存関係だけでなくワークフロータイプの依存関係がある場合は、次のようなものを使用してそれらを分離できます。

_step1 = new ValidateHeaderId(inputHeaderId);
step2 = new FindShipment(retShipmentRepository);
step3 = new ValidateUserPermission(user, inputPostCode, inputEmail);
step4 = new SaveReturnOrder(inputLines, inputReference);
step5 = new CheckSaveStatus();
notSet = new NotSetPage(templateEngine, searchPage);
notPermitted = new NotPermittedPage(templateEngine, searchPage);
saveErrors = new SaveErrorsPage();
success = new SuccessPage();
requestTokenConfirmation = new RequestTokenConfirmation();

steps = [step1, [notSet, step2], [notSet, step3], [notPermitted, step4],
         [saveErrors, step5], [success, requestTokenConfirmation]];
executeSteps(steps);
_

これにより、一連のステップがあり、それぞれが何らかの結果を生成して次のステップを選択することがわかります。 executeStepsは、各ステップでのrun()の呼び出しの繰り返しを抽象化し、前のステップの出力を次のステップに渡します。これにより、関数の代わりにデータ構造にステップを保存できます。これは、ある種の登録プロセスや構成ファイルなど、いくつかの異なる方法で構築できます。各ステップオブジェクトが作成されると、その依存関係をその外部で追跡する必要がなくなります。 BobDalgleishの答え のルールエンジンは、これをより簡単に行うために、基本的に既存のライブラリであると思います。

7
Karl Bielefeldt

複雑なプログラムの一般的なルールは

データと制御の分離

これらの2つの側面を別々に保つことができるほど、ソフトウェアの保守が容易になります。これは、リファクタリングで実行しようとしていることです。

より抜本的な一歩を踏み出し、「ビジネスルールエンジン」を使用してプログラムを推進することをお勧めします。非プログラマ向けに設計されたこれらの豪華なツールの1つを意味するのではなく、意思決定プロセスを実際のデータの収集または変換から明確に区別するものです。

私は、Javaベースのアプリケーションには JBoss Drools をお勧めします。 PHPで使用できる同様のタイプのエンジンがあることがわかります。それを見ると、それはあなたがしようとしている多くのことを達成します。 -> 定規

3
BobDalgleish