このシナリオでは訪問者パターンは有効ですか?


9

私のタスクの目標は、スケジュールされた繰り返しタスクを実行できる小さなシステムを設計することです。定期的なタスクとは、「月曜日から金曜日の午前8時から午後5時まで、毎時間管理者にメールを送信する」のようなものです。

RecurringTaskという基本クラスがあります。

public abstract class RecurringTask{

    // I've already figured out this part
    public bool isOccuring(DateTime dateTime){
        // implementation
    }

    // run the task
    public abstract void Run(){

    }
}

また、RecurringTaskから継承されたクラスがいくつかあります。それらの1つはSendEmailTaskと呼ばれます。

public class SendEmailTask : RecurringTask{
    private Email email;

    public SendEmailTask(Email email){
        this.email = email;
    }

    public override void Run(){
        // need to send out email
    }
}

そして、私はメールを送信するのに役立つEmailServiceを持っています。

最後のクラスはRecurringTaskSchedulerで、キャッシュまたはデータベースからタスクをロードして実行します。

public class RecurringTaskScheduler{

    public void RunTasks(){
        // Every minute, load all tasks from cache or database
        foreach(RecuringTask task : tasks){
            if(task.isOccuring(Datetime.UtcNow)){
                task.run();
            }
        }
    }
}

これが私の問題です:EmailServiceをどこに置くべきですか?

オプション1:注入するEmailServiceSendEmailTask

public class SendEmailTask : RecurringTask{
    private Email email;

    public EmailService EmailService{ get; set;}

    public SendEmailTask (Email email, EmailService emailService){
        this.email = email;
        this.EmailService = emailService;
    }

    public override void Run(){
        this.EmailService.send(this.email);
    }
}

サービスをエンティティに注入する必要があるかどうかについてはすでにいくつかの議論があり、ほとんどの人はそれが良い習慣ではないことに同意しています。この記事を参照してください

オプション2:RecurringTaskSchedulerの If ... Else

public class RecurringTaskScheduler{
    public EmailService EmailService{get;set;}

    public class RecurringTaskScheduler(EmailService emailService){
        this.EmailService = emailService;
    }

    public void RunTasks(){
        // load all tasks from cache or database
        foreach(RecuringTask task : tasks){
            if(task.isOccuring(Datetime.UtcNow)){
                if(task is SendEmailTask){
                    EmailService.send(task.email); // also need to make email public in SendEmailTask
                }
            }
        }
    }
}

もし上記のようにElse and castがOOではないので、さらに問題が発生すると言われています。

オプション3:Runのシグネチャを変更してServiceBundleを作成します。

public class ServiceBundle{
    public EmailService EmailService{get;set}
    public CleanDiskService CleanDiskService{get;set;}
    // and other services for other recurring tasks

}

このクラスをRecurringTaskSchedulerに挿入します

public class RecurringTaskScheduler{
    public ServiceBundle ServiceBundle{get;set;}

    public class RecurringTaskScheduler(ServiceBundle serviceBundle){
        this.ServiceBundle = ServiceBundle;
    }

    public void RunTasks(){
        // load all tasks from cache or database
        foreach(RecuringTask task : tasks){
            if(task.isOccuring(Datetime.UtcNow)){
                task.run(serviceBundle);
            }
        }
    }
}

SendEmailTaskRunメソッドは次のようになります

public void Run(ServiceBundle serviceBundle){
    serviceBundle.EmailService.send(this.email);
}

このアプローチには大きな問題はありません。

オプション4:訪問者のパターン。
基本的な考え方は、ServiceBundleのようにサービスをカプセル化するビジターを作成することです。

public class RunTaskVisitor : RecurringTaskVisitor{
    public EmailService EmailService{get;set;}
    public CleanDiskService CleanDiskService{get;set;}

    public void Visit(SendEmailTask task){
        EmailService.send(task.email);
    }

    public void Visit(ClearDiskTask task){
        //
    }
}

また、Runメソッドのシグネチャも変更する必要がありますSendEmailTaskRunメソッドは

public void Run(RecurringTaskVisitor visitor){
    visitor.visit(this);
}

これは訪問者パターンの典型的な実装であり、訪問者はRecurringTaskSchedulerに注入されます。

要約すると、これらの4つのアプローチのうち、私のシナリオに最適なのはどれですか。また、この問題について、Option3とOption4の間に大きな違いはありますか?

それとも、この問題についてより良い考えがありますか?ありがとう!

2015年5月22更新:Andyの回答は私の意図を非常によく要約していると思います。問題自体についてまだ混乱している場合は、まず彼の投稿を読むことをお勧めします。

私の問題がメッセージディスパッチの問題と非常に似ていることがわかりました。これはOption5につながります。

オプション5:問題をメッセージディスパッチに変換します。
私の問題とメッセージディスパッチの問題の間には1対1のマッピングがあります。

メッセージディスパッチャ:受信IMessageがのとディスパッチサブクラスIMessageがをそれらに対応するハンドラに。→RecurringTaskScheduler

IMessage:インターフェースまたは抽象クラス。→RecurringTask

MessageAは:から延びIMessageがいくつかの追加情報を有します。→SendEmailTask

MessageBIMessageの別のサブクラス。→CleanDiskTask

MessageAHandler:受信MessageAを、→EmailServiceが含まれているSendEmailTask​​Handlerを、それを処理し、それがSendEmailTaskを受信したときに電子メールを送信します

MessageBHandlerMessageAHandlerと同じですが、代わりにMessageBを処理します。→CleanDiskTaskHandler

最も難しいのは、さまざまな種類のIMessageをさまざまなハンドラーにディスパッチする方法です。こちらが便利なリンクです。

私はこのアプローチが本当に好きです。サービスでエンティティを汚染することはなく、Godクラスもありません。


言語またはプラットフォームにタグを付けていませんが、cronを調べることをお勧めします。ご使用のプラットフォームには、同様に機能するライブラリーがある場合があります(例えば、機能しないように見えるjcronなど)。ジョブとタスクのスケジュールは、主に解決された問題です。独自のオプションを導入する前に、他のオプションを検討しましたか?それらを使用しない理由はありましたか?

@Snowman後で成熟したライブラリに切り替える可能性があります。それはすべて私のマネージャー次第です。この質問を投稿する理由は、この「種類の」問題を解決する方法を見つけたいからです。私はこの種の問題を2回以上見ましたが、エレガントな解決策を見つけることができませんでした。だから私は何か間違ったことをしたのではないかと思っています。
Sher10ck 2015年

十分に公平ですが、可能な場合は常にコードの再利用をお勧めします。

1
SendEmailTask私にとっては、実体というよりサービスのようです。私は迷わずにオプション1を選びます。
Bart van Ingen Schenau、2015年

3
Visitorに(私にとって)欠けているのは、visitsのクラス構造ですaccept。訪問者の動機は、訪問する必要があるいくつかの集合体に多くのクラス型があり、新しい機能(操作)ごとにコードを変更するのが不便であることです。これらの集約オブジェクトが何であるかはまだわかりません。また、Visitorは適切ではないと思います。その場合は、質問(訪問者を参照)を編集する必要があります。
フルマネーター

回答:


4

オプション1が最適なルートだと思います。あなたがそれを却下すべきではない理由は、それが実体でSendEmailTaskないということです。エンティティは、データと状態の保持に関係するオブジェクトです。クラスにはそれがほとんどありません。実際には、それは実体ではありませんが、それが保持しているエンティティを:Emailあなたが保管されているオブジェクト。つまり、それはEmailサービスを受けるべきではなく、#Sendメソッドを持つべきではありません。代わりに、などのエンティティを受け取るサービスが必要EmailServiceです。したがって、サービスをエンティティーから除外するという考えに従っています。

以来SendEmailTask実体ではない、電子メールやそれにサービスを注入するので、まったく問題あり、それは、コンストラクタを介して行われるべきです。コンストラクターインジェクションSendEmailTaskを実行することで、そのジョブを実行する準備が常に整っていることを確認できます。

ここで、他のオプション(特にSOLIDに関して)を実行しない理由を見てみましょう。

オプション2

あなたは、そのようなタイプで分岐することは、より多くの頭痛の種を道にもたらすであろうと正しく言われました。その理由を見てみましょう。まず、ifsはクラスター化して成長する傾向があります。今日、それはメールを送信するタスクです。明日、クラスの種類ごとに異なるサービスやその他の動作が必要です。そのif声明を管理することは悪夢になります。型(この場合は明示的な型)で分岐しているため、言語に組み込まれている型システムを破壊しています。

オプション2は単一責任(SRP)ではありません。以前は再利用可能RecurringTaskSchedulerでしたが、これらのさまざまなタイプのタスクすべて、およびそれらが必要とする可能性のあるすべてのさまざまな種類のサービスと動作について知る必要があるためです。そのクラスを再利用するのははるかに困難です。また、オープン/クローズ(OCP)でもありません。この種類のタスクまたはその1つ(またはこの種類のサービスまたはその1つ)について知る必要があるため、タスクまたはサービスに異なる変更を加えると、ここで変更が強制される可能性があります。新しいタスクを追加しますか?新しいサービスを追加しますか?メールの処理方法を変更しますか?変更しRecurringTaskSchedulerます。タスクのタイプが重要であるため、Liskov Substitution(LSP)に準拠していません。タスクを取得して完了するだけではありません。タイプを尋ねる必要があり、タイプに基づいてこれを行うか、それを行います。違いをタスクにカプセル化するのではなく、それらすべてをに取り込みますRecurringTaskScheduler

オプション3

オプション3にはいくつかの大きな問題があります。あなたがリンクしている記事でさえ、著者はこれをしないようにします:

  • 静的サービスロケーターを引き続き使用できます…
  • 特にサービスロケータを静的にする必要がある場合は、できる限りサービスロケータを避けます。

クラスを使用してサービスロケーターを作成していますServiceBundle。この場合、静的ではないように見えますが、サービスロケータに固有の問題の多くがまだあります。これで、依存関係はこの下に隠されますServiceBundle。私のクールな新しいタスクの次のAPIを提供するとします。

class MyCoolNewTask implements RecurringTask
{
    public bool isOccuring(DateTime dateTime) {
        return true; // It's always happenin' here!
    }

    public void Run(ServiceBundle bundle) {
        // yeah, some awesome stuff here
    }
}

私が使用しているサービスは何ですか?テストでモックアウトする必要があるサービスは何ですか?システム内のすべてのサービスを使用できないようにする理由は何ですか?

いくつかのタスクを実行するためにタスクシステムを使用したい場合は、システムのすべてのサービスに依存しています。

ServiceBundleそれは知っている必要があるため、実際にはSRPではないすべてのシステムでサービスを提供しています。また、OCPではありません。新しいサービスを追加するとに変更が加えられ、ServiceBundleに変更を加えると、ServiceBundle別の場所にあるタスクに異なる変更が加えられる可能性があります。ServiceBundleインターフェイス(ISP)を分離しません。これらのすべてのサービスの無数のインターフェースがあり、それらはそれらのサービスのプロバイダーにすぎないため、そのインターフェースも、提供するすべてのサービスのインターフェースを包含すると考えることができます。タスクの依存関係はの背後で難読化されているため、タスクは依存関係の逆転(DIP)に準拠していませんServiceBundle。また、物事は必要以上に多くのことを知っているため、これは最小知識原則(別名、デメテルの法則)に準拠していません。

オプション4

以前は、独立して動作できる多数の小さなオブジェクトがありました。オプション4は、これらのオブジェクトをすべて取り、それらを1つのVisitorオブジェクトにまとめます。このオブジェクトは、すべてのタスクで神オブジェクトとして機能します。それはあなたのRecurringTaskオブジェクトを単に訪問者に呼びかける貧弱な影に減らします。すべての動作がに移動しますVisitor。動作を変更する必要がありますか?新しいタスクを追加する必要がありますか?変更しVisitorます。

より難しい部分は、さまざまな動作がすべて1つのクラスにあるため、他のすべての動作に沿っていくつかの多態的なドラッグを変更することです。たとえば、電子メールを送信する2つの異なる方法が必要です(おそらく、異なるサーバーを使用する必要がありますか?)。どうやってやるの?IVisitorインターフェースを作成して実装し、#Visit(ClearDiskTask)元のビジターのようにコードを複製する可能性があります。次に、ディスクをクリアする新しい方法を考え出した場合は、再度実装して複製する必要があります。次に、両方の種類の変更が必要です。再度実装して複製します。これらの2つの異なる、異なる動作は密接に関連しています。

たぶん代わりに、単にサブクラス化できVisitorますか?新しいメール動作を持つサブクラス、新しいディスク動作を持つサブクラス。これまでのところ重複はありません!両方のサブクラス?ここで、どちらか一方を複製する必要があります(必要に応じて両方とも複製します)。

オプション1と比較してみましょう。新しい電子メールの動作が必要です。RecurringTask新しい動作を実行するnew を作成し、その依存関係を挿入して、のタスクのコレクションに追加できRecurringTaskSchedulerます。ディスクのクリアについて話す必要はありません。その責任は完全に別の場所にあるためです。また、自由に使用できるOOツールもすべて揃っています。たとえば、そのタスクをログで装飾できます。

オプション1は痛みが最も少なく、この状況を処理する最も適切な方法です。


Otion2、3、4に関するあなたの分析は素晴らしいです!本当に助かります。ただし、オプション1の場合、* SendEmailTask​​ *はエンティティであると主張します。ID、繰り返しパターン、およびdbに保存する必要があるその他の有用な情報があります。アンディは私の意図をうまくまとめていると思います。* EMailTask​​Definitions *のような名前の方が適切かもしれません。エンティティをサービスコードで汚染したくありません。幸福は私がエンティティにサービスを注入する場合いくつかの問題に言及します。私も質問を更新してOption5を含めます。これは、これまでのところ最良の解決策だと思います。
Sher10ck 2015年

@ Sher10ck SendEmailTaskデータベースの構成をプルする場合、その構成は別の構成クラスでなければならず、これもに挿入する必要がありますSendEmailTask。からデータを生成する場合はSendEmailTask、状態を保存するためのmementoオブジェクトを作成し、それをデータベースに配置する必要があります。
cbojar 2015年

データベースから設定をプルする必要があるので、両方EMailTaskDefinitionsEmailServiceに注入することをお勧めしSendEmailTaskますか?次に、で、定義とサービスをロードしてそれらをに注入する責任を持つRecurringTaskSchedulerようなものSendEmailTaskRepositoryを注入する必要がありSendEmailTaskます。しかし、私は今RecurringTaskScheduler、のようなすべてのタスクのリポジトリを知る必要があると主張しますCleanDiskTaskRepository。またRecurringTaskScheduler、新しいタスク(リポジトリをスケジューラに追加する)があるたびに変更する必要があります。
Sher10ck 2015年

@ Sher10ck RecurringTaskSchedulerは、一般化されたタスクリポジトリとの概念のみを認識している必要がありますRecurringTask。これを行うことにより、抽象化に依存できます。タスクリポジトリはのコンストラクタに挿入できますRecurringTaskScheduler。次に、異なるリポジトリRecurringTaskSchedulerは、インスタンス化されている場所を知るだけで済みます(または、ファクトリで非表示にして、そこから呼び出すことができます)。抽象化にのみ依存するため、RecurringTaskScheduler新しいタスクごとに変更する必要はありません。それが依存関係の逆転の本質です。
cbojar

3

スプリングクオーツやスプリングバッチなど、既存のライブラリを確認しましたか(ニーズに最も合うものはわかりません)。

あなたの質問に:

問題は、いくつかのメタデータをポリモーフィックな方法でタスクに永続化する必要があるため、電子メールタスクに電子メールアドレスが割り当てられ、ログタスクにログレベルなどが割り当てられることです。それらのリストをメモリまたはデータベースに保存できますが、懸念を分離するために、エンティティをservice-codeで汚染することは望ましくありません。

私の提案する解決策:

タスクの実行部分とデータ部分を分離して、eg TaskDefinitionとaを作成しTaskRunnerます。TaskDefinitionには、TaskRunnerまたはそれを作成するファクトリへの参照があります(たとえば、smtp-hostなどの設定が必要な場合)。ファクトリは特定のものです-はを処理することしかできEMailTaskDefinitionず、のインスタンスのみを返しますEMailTaskRunner。このようにすると、オブジェクト指向と変更の安全性が向上します。新しいタスクタイプを導入する場合、コンパイルできない場合は、新しい特定のファクトリを導入する(または再利用する)必要があります。

この方法では、依存関係が生じることになります。エンティティレイヤー->サービスレイヤー、そして再び戻ります。Runnerはエンティティに格納された情報を必要とし、おそらくDB内の状態を更新する必要があるためです。

あなたは取る汎用工場、使用して円を壊す可能性のTaskDefinitionをして返す特定 TaskRunnerを、それは、IFSの多くを必要とします。リフレクションを使用して、定義と同様に名前が付けられたランナーを見つけることができます、このアプローチではパフォーマンスが低下し、実行時エラーが発生する可能性があることに注意してください。

PSここではJavaを想定しています。.netでも同様だと思います。ここでの主な問題は、二重バインディングです。

お客様パターンへ

純粋な二重バインディングの目的ではなく、実行時にアルゴリズムをさまざまな種類のデータオブジェクトと交換するために使用することを意図していたと思います。たとえば、さまざまな種類の保険とそれらを計算するさまざまな種類がある場合(たとえば、さまざまな国がそれを必要とするため)。次に、特定の計算方法を選択し、それをいくつかの保険に適用します。

あなたのケースでは、特定のタスク戦略(メールなど)を選択して、それをすべてのタスクに適用します。

PS私はそれをテストしませんでしたが、オプション4もまた二重結合であるため機能しないと思います。


あなたは私の意図を本当によく要約しています、thx!サークルを壊したいのですが。TaskDefinitonTaskRunnerまたはファクトリへの参照を保持させるには、Option1と同じ問題があります。私はファクトリーまたはTaskRunnerをサービスとして扱います。場合のTaskDefinitionが必要とそれらへの参照を保持している、あなたはどちらかにサービスを注入しているのTaskDefinition、または私は避けるようにしようとしていますされ、いくつかの静的メソッドを使用します。
Sher10ck 2015年

1

私はその記事に完全に同意しません。サービス(具体的には「API」)はビジネスドメインの重要な当事者であり、ドメインモデル内に存在します。また、ビジネスドメイン内のエンティティが同じビジネスドメイン内の他のエンティティを参照することには問題はありません。

XがYにメールを送信するとき。

ビジネスルールです。そのためには、メールを送信するサービスが必要です。そして、処理When Xするエンティティはこのサービスについて知っている必要があります。

しかし、実装にはいくつかの問題があります。エンティティがサービスを使用していることは、エンティティのユーザーに対して透過的である必要があります。そのため、コンストラクタにサービスを追加することは好ましくありません。これは、エンティティのデータとサービスのインスタンスの両方を設定する必要があるため、データベースからエンティティを逆シリアル化するときにも問題になります。私が考えることができる最良の解決策は、エンティティが作成された後にプロパティインジェクションを使用することです。エンティティの新しく作成された各インスタンスに、エンティティが必要とするすべてのエンティティを挿入する「初期化」メソッドを実行するように強制する可能性があります。


あなたが同意しないことについて、どの記事を参照していますか?ただし、ドメインモデルに関する興味深い観点。おそらくあなたはそれをそのように見ることができますが、すぐに密結合が作成されるため、人々は通常、サービスをエンティティに混在させることを避けます。
アンディ

@Andy Sher10ckが彼の質問で言及した1つ。そして、それがどのようにして緊密な結合を生み出すかはわかりません。不適切に記述されたコードは、密結合を引き起こす可能性があります。
陶酔

1

それは素晴らしい質問であり、興味深い問題です。Chain of ResponsibilityDouble Dispatchパターンの組み合わせを使用することをお勧めします(パターン例はこちら)。

まず、タスク階層を定義します。runDouble Dispatchを実装する複数のメソッドがあることに注意してください。

public abstract class RecurringTask {

    public abstract boolean isOccuring(Date date);

    public boolean run(EmailService emailService) {
        return false;
    }

    public boolean run(ExecuteService executeService) {
        return false;
    }
}

public class SendEmailTask extends RecurringTask {

    private String email;

    public SendEmailTask(String email) {
        this.email = email;
    }

    @Override
    public boolean isOccuring(Date date) {
        return true;
    }

    @Override
    public boolean run(EmailService emailService) {
        emailService.runTask(this);
        return true;
    }

    public String getEmail() {
        return email;
    }
}

public class ExecuteTask extends RecurringTask {

    private String program;

    public ExecuteTask(String program) {
        this.program = program;
    }

    @Override
    public boolean isOccuring(Date date) {
        return true;
    }

    public String getName() {
        return program;
    }

    @Override
    public boolean run(ExecuteService executeService) {
        executeService.runTask(this);
        return true;
    }
}

次に、Service階層を定義します。Servicesを使用して責任の連鎖を形成します。

public abstract class Service {

    private Service next;

    public Service(Service next) {
        this.next = next;
    }

    public void handleRecurringTask(RecurringTask req) {
        if (next != null) {
            next.handleRecurringTask(req);
        }
    }
}

public class ExecuteService extends Service {

    public ExecuteService(Service next) {
        super(next);
    }

    void runTask(ExecuteTask task) {
        System.out.println(String.format("%s running %s with content '%s'", this.getClass().getSimpleName(),
                task.getClass().getSimpleName(), task.getName()));
    }

    public void handleRecurringTask(RecurringTask req) {
        if (!req.run(this)) {
            super.handleRecurringTask(req);
        }
    }
}

public class EmailService extends Service {

    public EmailService(Service next) {
        super(next);
    }

    public void runTask(SendEmailTask task) {
        System.out.println(String.format("%s running %s with content '%s'", this.getClass().getSimpleName(),
                task.getClass().getSimpleName(), task.getEmail()));
    }

    public void handleRecurringTask(RecurringTask req) {
        if (!req.run(this)) {
            super.handleRecurringTask(req);
        }
    }
}

最後の部分は、RecurringTaskSchedulerロードおよび実行プロセスを調整するものです。

public class RecurringTaskScheduler{

    private List<RecurringTask> tasks = new ArrayList<>();

    private Service chain;

    public RecurringTaskScheduler() {
        chain = new EmailService(new ExecuteService(null));
    }

    public void loadTasks() {
        tasks.add(new SendEmailTask("here comes the first email"));
        tasks.add(new SendEmailTask("here is the second email"));
        tasks.add(new ExecuteTask("/root/python"));
        tasks.add(new ExecuteTask("/bin/cat"));
        tasks.add(new SendEmailTask("here is the third email"));
        tasks.add(new ExecuteTask("/bin/grep"));
    }

    public void runTasks(){
        for (RecurringTask task : tasks) {
            if (task.isOccuring(new Date())) {
                chain.handleRecurringTask(task);
            }
        }
    }
}

ここで、システムを示すアプリケーションの例を示します。

public class App {

    public static void main(String[] args) {
        RecurringTaskScheduler scheduler = new RecurringTaskScheduler();
        scheduler.loadTasks();
        scheduler.runTasks();
    }
}

アプリケーション出力を実行すると:

「ここでの最初のメール来るのコンテンツでSendEmailTaskを実行しているEmailService
コンテンツとSendEmailTaskを実行しているEmailService「ここでは、第二の電子メールである」
コンテンツとExecuteTaskを実行しているExecuteService '/ルート/ Pythonの
ExecuteServiceコンテンツとExecuteTaskを実行している' / binに/猫
EmailServiceはとSendEmailTaskを実行していますコンテンツ 'ここに3番目の電子メールがあります'
ExecuteServiceはコンテンツ '/ bin / grep'でExecuteTaskを実行しています


Taskがたくさんあるかもしれません。新しいTaskを追加するたびに、RecurringTaskを変更する必要があり、そのすべてのサブクラスも変更する必要があります。これは、public abstract boolean run(OtherService otherService)のような新しい関数を追加する必要があるため です。ダブルディスパッチを実装するビジターパターンであるOption4にも同じ問題があると思います。
Sher10ck 2015年

いい視点ね。run(service)メソッドがRecurringTaskで定義され、デフォルトでfalseを返すように回答を編集しました。このように、別のタスククラスを追加する必要がある場合、兄弟タスクに触れる必要はありません。
イルワタール2015年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.