顧客ごとに異なる可能性のある1つのメソッドを持つクラスの適切な設計


12

顧客の支払いの処理に使用するクラスがあります。このクラスの1つを除くすべてのメソッドは、すべての顧客で同じです。ただし、顧客のユーザーが負う金額を(たとえば)計算するものを除きます。これは顧客ごとに大きく異なる可能性があり、カスタムファクタはいくつも存在する可能性があるため、計算のロジックをプロパティファイルのようなものにキャプチャする簡単な方法はありません。

customerIDに基づいて切り替えるいコードを書くことができます。

switch(customerID) {
 case 101:
  .. do calculations for customer 101
 case 102:
  .. do calculations for customer 102
 case 103:
  .. do calculations for customer 103
 etc
}

ただし、新しい顧客を獲得するたびにクラスを再構築する必要があります。より良い方法は何ですか?

[編集]「複製」の記事はまったく異なります。私はswitchステートメントを回避する方法を求めているのではなく、このケースに最も適したモダンなデザインを求めています-恐竜のコードを書きたい場合はswitchステートメントで解決できます。そこで提供されている例は一般的なものであり、本質的には「スイッチはある場合には非常にうまく機能し、他の場合には機能しない」と言っているため、役に立たない。


[編集]次の理由から、トップランクの回答(標準インターフェイスを実装する顧客ごとに個別の「顧客」クラスを作成する)を採用することにしました。

  1. 一貫性:他の開発者によって作成された場合でも、すべてのCustomerクラスが同じ出力を受け取って返すことを保証するインターフェイスを作成できます。

  2. 保守性:すべてのコードは同じ言語(Java)で記述されているため、デッドシンプルな機能を維持するために他の誰かが別のコーディング言語を学ぶ必要はありません。

  3. 再利用:コードで同様の問題が発生した場合、Customerクラスを再利用して、任意の数のメソッドを保持して「カスタム」ロジックを実装できます。

  4. 親しみやすさ:私はすでにこれを行う方法を知っているので、すぐにそれを完了させ、他のより差し迫った問題に進むことができます。

欠点:

  1. 新しい顧客ごとに、新しいCustomerクラスのコンパイルが必要です。これにより、変更をコンパイルおよびデプロイする方法が複雑になる場合があります。

  2. 新しい顧客はそれぞれ、開発者が追加する必要があります。サポート担当者は、プロパティファイルのようなものにロジックを追加することはできません。これは理想的ではありません...しかし、サポート担当者が必要なビジネスロジックをどのように書き出すことができるのか、特に多くの例外を伴う複雑な場合(そうである可能性が高い場合)もわかりませんでした。

  3. 多くの新しい顧客を追加した場合、うまく拡張できません。これは予期されていませんが、もしそうなった場合、コードの他の多くの部分とこの部分を再考する必要があります。

興味のある方は、Java Reflectionを使用して名前でクラスを呼び出すことができます。

Payment payment = getPaymentFromSomewhere();

try {
    String nameOfCustomClass = propertiesFile.get("customClassName");
    Class<?> cpp = Class.forName(nameOfCustomClass);
    CustomPaymentProcess pp = (CustomPaymentProcess) cpp.newInstance();

    payment = pp.processPayment(payment);
} catch (Exception e) {
    //handle the various exceptions
} 

doSomethingElseWithThePayment(payment);

1
たぶん、計算のタイプについて詳しく説明する必要があります。リベートが計算されるだけですか、それとも別のワークフローですか?
qwerty_so 16

@ThomasKilianこれはほんの一例です。Paymentオブジェクトを想像してください。計算は「Payment.percentにPayment.totalを掛けます。ただし、Payment.idが「R」で始まる場合は計算できません」のようになります。そのような粒度。すべての顧客には独自のルールがあります。
アンドリュー


あなたが何かしようとした持ち、これを。顧客ごとに異なる数式を設定します。
ライヴ

1
さまざまな回答から提案されたプラグインは、顧客ごとに専用の製品がある場合にのみ機能します。顧客IDを動的に確認するサーバーソリューションがある場合、それは機能しません。それで、あなたの環境は何ですか?
qwerty_so 16

回答:


14

顧客の支払いの処理に使用するクラスがあります。このクラスの1つを除くすべてのメソッドは、すべての顧客で同じです。ただし、顧客のユーザーが負う金額を(たとえば)計算するものを除きます。

2つのオプションが思い浮かびます。

オプション1:クラスを抽象クラスにします。この場合、顧客によって異なるメソッドは抽象メソッドです。次に、顧客ごとにサブクラスを作成します。

オプション2:すべての顧客依存のロジックを含むCustomerクラスまたはICustomerインターフェースを作成します。支払い処理クラスに顧客IDを受け入れさせる代わりに、CustomerまたはICustomerオブジェクトに受け入れさせます。顧客に依存する何かをする必要があるときはいつでも、適切なメソッドを呼び出します。


Customerクラスは悪い考えではありません。各顧客に対して何らかの種類のカスタムオブジェクトが必要になりますが、これがDBレコード、プロパティファイル(何らかの種類)、または別のクラスのいずれであるかは明確ではありませんでした。
アンドリュー

10

カスタム計算をアプリケーションの「プラグイン」として記述することを検討することをお勧めします。次に、構成ファイルを使用して、どの計算プラグインをどの顧客に使用するかをプログラムします。この方法では、新しい顧客ごとにメインアプリケーションを再コンパイルする必要はありません。構成ファイルを読み取り(または再読み取り)、新しいプラグインをロードするだけです。


ありがとう。たとえばJava環境でプラグインはどのように機能しますか?たとえば、「result = if(Payment.id.startsWith( "R"))?Payment.percentage * Payment.total:Payment.otherValue」という式を作成して、顧客のプロパティとして保存し、挿入します適切な方法で?
アンドリュー

@Andrew、プラグインが機能する1つの方法は、リフレクションを使用して名前でクラスをロードすることです。構成ファイルには、顧客のクラスの名前がリストされます。もちろん、どのプラグインがどの顧客向けであるかを特定する方法が必要です(たとえば、名前またはどこかに保存されているメタデータによって)。
キャット

@Katありがとう、編集された質問を読んでくれたなら、それが実際に私がやっている方法だ。欠点は、開発者がスケーラビリティを制限する新しいクラスを作成する必要があることです。サポート担当者がプロパティファイルを編集することを望んでいますが、複雑すぎると思います。
アンドリュー

@Andrew:数式をプロパティファイルに書き込むこともできますが、その場合は何らかの式パーサーが必要になります。また、ある日、プロパティファイルに1行の式として(簡単に)書き込むには複雑すぎる式に遭遇する場合があります。プログラマ以外の人が本当にこれらの作業を行えるようにしたい場合は、アプリケーション用のコードを生成するユーザーフレンドリーな式エディタを使用する必要があります。これは実行できますが(見たことがあります)、簡単ではありません。
FrustratedWithFormsDesigner

4

計算を説明するルールセットを使用します。これは任意の永続ストアに保持され、動的に変更できます。

代替としてこれを考慮してください:

customerOps = [oper1, oper2, ..., operN]; // array with customer specific operations
index = customerOpsIndex(customer);
customerOps[index](parms);

どこcustomerOpsIndexで正しい操作指標が計算されたか(どの顧客がどの治療を必要としているか知っている)。


包括的なルールセットを作成できる場合は、そうします。ただし、新しい顧客ごとに独自のルールセットがある場合があります。また、これを行うデザインパターンがある場合は、コード全体をコードに含めることをお勧めします。私のswitch {}の例はこれを非常にうまく行いますが、あまり柔軟ではありません。
アンドリュー

顧客に呼び出される操作を配列に保存し、顧客IDを使用してインデックスを作成できます。
qwerty_so

それはより有望に見えます。:)
アンドリュー

3

以下のようなもの:

リポジトリにまだswitchステートメントがあることに注意してください。しかし、これを実際に回避する方法はありません。ある時点で、顧客IDを必要なロジックにマップする必要があります。賢く取得して構成ファイルに移動したり、マッピングをディクショナリに入れたり、ロジックアセンブリに動的にロードしたりできますが、基本的には切り替えます。

public interface ICustomer
{
    int Calculate();
}
public class CustomerLogic101 : ICustomer
{
    public int Calculate() { return 101; }
}
public class CustomerLogic102 : ICustomer
{
    public int Calculate() { return 102; }
}

public class CustomerRepo
{
    public ICustomer GetCustomerById(
        string id)
    {
        var data;//get data from db
        if (data.logicType == "101")
        {
            return new CustomerLogic101();
        }
        if (data.logicType == "102")
        {
            return new CustomerLogic102();
        }
    }
}
public class Calculator
{
    public int CalculateCustomer(string custId)
    {
        CustomerRepo repo = new CustomerRepo();
        var cust = repo.GetCustomerById(custId);
        return cust.Calculate();
    }
}

2

顧客からカスタムコードへの1対1のマッピングがあるように聞こえます。また、コンパイルされた言語を使用しているため、新しい顧客を獲得するたびにシステムを再構築する必要があります

埋め込みスクリプト言語を試してください。

たとえば、システムがJavaである場合、JRubyを埋め込み、顧客ごとに対応するRubyコードのスニペットを格納できます。理想的には、同じか別のgitリポジトリのいずれかのバージョン管理下にあります。そして、そのスニペットをJavaアプリケーションのコンテキストで評価します。JRubyは、任意のJava関数を呼び出して、任意のJavaオブジェクトにアクセスできます。

package com.example;

import org.jruby.embed.LocalVariableBehavior;
import org.jruby.embed.ScriptingContainer;

public class Main {

    private ScriptingContainer ruby;

    public static void main(String[] args) {
        new Main().run();
    }

    public void run() {
        ruby = new ScriptingContainer(LocalVariableBehavior.PERSISTENT);
        // Assign the Java objects that you want to share
        ruby.put("main", this);
        // Execute a script (can be of any length, and taken from a file)
        Object result = ruby.runScriptlet("main.hello_world");
        // Use the result as if it were a Java object
        System.out.println(result);
    }

    public String getHelloWorld() {
        return "Hello, worlds!";
    }

}

これは非常に一般的なパターンです。たとえば、多くのコンピューターゲームはC ++で記述されていますが、埋め込みLuaスクリプトを使用して、ゲーム内の各対戦相手の顧客の行動を定義します。

一方、顧客からカスタムコードへの多対1マッピングがある場合は、既に提案されている「戦略」パターンを使用します。

マッピングがユーザーID makeに基づいていない場合、match各戦略オブジェクトに関数を追加し、使用する戦略の順序付けられた選択を行います。

ここにいくつかの擬似コードがあります

strategy = strategies.find { |strategy| strategy.match(customer) }
strategy.apply(customer, ...)

2
私はこのアプローチを避けます。これらのスクリプトスニペットをすべてdbに入れると、ソース管理、バージョン管理、およびテストが失われる傾向があります。
ユアン

けっこうだ。別々のgitリポジトリまたはどこにでも保存するのが最適です。スニペットは理想的にはバージョン管理下にあるべきだと言うように私の答えを更新しました。
akuhn 16

プロパティファイルなどに保存できますか?固定されたCustomerプロパティが複数の場所に存在することを避けようとしています。そうしないと、維持できないスパゲッティに簡単に移行してしまいます。
アンドリュー

2

流れに逆らって泳ぎます。

ANTLRを使用して独自の式言語を実装してみます。

これまでのところ、すべての答えはコードのカスタマイズに強く基づいています。顧客ごとに具体的なクラスを実装することは、将来のある時点でうまくスケールしないと思われます。メンテナンスは高価で苦痛になります。

したがって、Antlrのアイデアは、独自の言語を定義することです。ユーザー(または開発者)がそのような言語でビジネスルールを記述できるようにすることができます。

コメントを例として:

「result = if(Payment.id.startsWith( "R"))?Payment.percentage * Payment.total:Payment.otherValue」という式を書きたい

ELを使用すると、次のような文を記述できます。

If paymentID startWith 'R' then (paymentPercentage / paymentTotal) else paymentOther

その後...

それを顧客のプロパティとして保存し、適切なメソッドに挿入しますか?

あなたは出来る。これは文字列です。プロパティまたは属性として保存できます。

私は嘘をつきません。かなり複雑で難しいです。ビジネスルールも複雑な場合はさらに難しくなります。

ここであなたに興味があるかもしれないいくつかのSOの質問:


注: ANTLRはPythonおよびJavascriptのコードも生成します。これにより、オーバーヘッドをあまりかけずに概念実証を作成できます。

Antlrが難しすぎる場合は、Expr4J、JEval、Parsiiなどのライブラリを試してみてください。これらはより高い抽象度で機能します。


それは良い考えですが、次の男のメンテナンスの頭痛の種です。しかし、すべての変数を評価して重量を測定するまで、アイデアを破棄するつもりはありません。
アンドリュー

1
あなたが持っている多くのオプションが優れています。私はちょうどより多くのものを与えたいと思った
LAIV

これが有効なアプローチであることを否定するものではありません。単に「エンドユーザーがカスタマイズできる」Websphereまたはその他の市販のエンタープライズシステムを見てください。 /ソース管理/変更管理
ユアン

@ユアン申し訳ありませんが、私はあなたの議論を理解していなかったのが怖いです。言語記述子と自動生成されるクラスは、プロジェクトの一部である場合とそうでない場合があります。しかし、いずれにしても、バージョニング/ソースコード管理が失われる理由はわかりません。一方、2つの異なる言語があるように見えるかもしれませんが、それは一時的なものです。言語が完了したら、文字列のみを設定します。Cron式のように。長い目で見れば、心配することはほとんどありません。
LAIV

「if paymentID startWith 'R' then(paymentPercentage / paymentTotal)else paymentOther」これをどこに保存しますか?それはどのバージョンですか?それは何語で書かれていますか?どんなユニットテストがありますか?
ユアン

1

少なくともアルゴリズムを外部化して、戦略パターンと呼ばれるデザインパターン(ギャングオブフォーにある)を使用して、新しい顧客が追加されたときにCustomerクラスを変更する必要がないようにすることができます。

あなたが与えたスニペットから、戦略パターンの保守性が低いのか、保守性が高いのかは議論の余地がありますが、少なくとも、Customerクラスが何をする必要があるかについての知識を排除します(そして、スイッチケースを排除します)。

StrategyFactoryオブジェクトは、CustomerIDに基づいてStrategyIntfポインター(または参照)を作成します。Factoryは、特別ではない顧客にデフォルトの実装を返すことができます。

Customerクラスは、ファクトリに正しい戦略を要求してから呼び出すだけです。

これは、私が意味することを示すための非常に短い疑似C ++です。

class Customer
{
public:

    void doCalculations()
    {
        CalculationsStrategyIntf& strategy = CalculationsStrategyFactory::instance().getStrategy(*this);
        strategy.doCalculations();
    }
};


class CalculationsStrategyIntf
{
public:
    virtual void doCalculations() = 0;
};

このソリューションの欠点は、特別なロジックを必要とする新しい顧客ごとに、CalculationsStrategyIntfの新しい実装を作成し、ファクトリを更新して適切な顧客に返す必要があることです。これもコンパイルが必要です。しかし、少なくとも、顧客クラスで増え続けるスパゲッティコードは避けてください。


はい、誰かが別の回答で同様のパターンを言及し、顧客ごとにロジックをCustomerインターフェースを実装する別のクラスにカプセル化し、PaymentProcessorクラスからそのクラスを呼び出すと思います。有望ですが、新しい顧客を獲得するたびに新しいクラスを作成する必要があることを意味します-本当に大したことではありませんが、サポート担当者ができることではありません。それでもオプションです。
アンドリュー

-1

単一のメソッドでインターフェイスを作成し、各実装クラスでラムダを使用します。または、匿名クラスを使用して、さまざまなクライアントのメソッドを実装できます

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.