web-dev-qa-db-ja.com

特定の順序で関数を呼び出す必要があるインターフェース設計

タスクは、入力仕様に応じて、デバイス内のハードウェアを構成することです。これは次のようにして達成する必要があります。

1)構成情報を収集します。これは、さまざまな時間と場所で発生する可能性があります。たとえば、モジュールAとモジュールBは両方とも、(異なる時点で)私のモジュールにいくつかのリソースを要求できます。これらの「リソース」は、実際の構成です。

2)要求が実現されなくなることが明らかになった後、要求されたリソースの概要を示す起動コマンドをハードウェアに送信する必要があります。

3)その後のみ、上記のリソースの詳細な構成を行うことができます(必要があります)。

4)また、2)の後でのみ、宣言された呼び出し元への選択されたリソースのルーティングを行うことができます(必要があります)。


バグの一般的な原因は、それを書いた私でさえ、この順序を間違えたことです。初めてコードを見た人がインターフェイスを使用できるようにするために、どのような命名規則、デザイン、またはメカニズムを採用できますか?

24
Vorac

これは再設計ですが、多くのAPIの誤用を防ぐことができますが、呼び出すべきではないメソッドを利用できません。

たとえば、first you init, then you start, then you stopの代わりに

コンストラクタは、開始可能なオブジェクトをinitsし、停止できるセッションをstartが作成します。

もちろん、一度に1つのセッションに制限がある場合は、誰かがすでにアクティブになっているセッションを作成しようとするケースに対処する必要があります。

次に、そのテクニックを自分のケースに適用します。

45
CashCow

起動メソッドに、必須パラメーターであるオブジェクトを構成に返すようにさせることができます。

リソース* MyModule :: GetResource(); 
 MySession * MyModule :: Startup(); 
 void Resource :: Configure(MySession * session); 

MySessionが空の構造体である場合でも、型の安全性により、起動前にConfigure()メソッドを呼び出せないように強制されます。

19
jpa

Cashcowの答えに基づいて構築する-新しいインターフェイスだけを提示できるのに、なぜ呼び出し側に新しいオブジェクトを提示しなければならないのですか?ブランド変更パターン:

class IStartable     { public: virtual IRunnable      start()     = 0; };
class IRunnable      { public: virtual ITerminateable run()       = 0; };
class ITerminateable { public: virtual void           terminate() = 0; };

セッションを複数回実行できる場合は、ITerminateableにIRunnableを実装させることもできます。

あなたのオブジェクト:

class Service : IStartable, IRunnable, ITerminateable
{
  public:
    IRunnable      start()     { ...; return this; }
    ITerminateable run()       { ...; return this; }
    void           terminate() { ...; }
}

// And use it like this:
IStartable myService = Service();

// Now you can only call start() via the interface
IRunnable configuredService = myService.start();

// Now you can also call run(), because it is wrapped in the new interface...

この方法では、最初はIStartable-Interfaceしかなく、start()を呼び出した場合にのみrun()メソッドにアクセスできるため、適切なメソッドのみを呼び出すことができます。外側から見ると、複数のクラスとオブジェクトを持つパターンのように見えますが、基になるクラスは1つのクラスのままで、常に参照されています。

8
Falco

あなたの問題を解決するための多くの有効なアプローチがあります。 Basile Starynkevitchは、シンプルなインターフェースを残し、適切にインターフェースを使用するプログラマーに依存する「ゼロ官僚主義」アプローチを提案しました。私はこのアプローチが好きですが、私はより多くの独創性を持っていますが、コンパイラーがいくつかのエラーをキャッチできるようにする別のアプローチを紹介します。

  1. UninitialisedStartedConfiguredなど、デバイスのさまざまな状態を特定します。リストは有限でなければなりません。¹

  2. 各状態について、その状態に関連する必要な追加情報を保持するstructを定義します。 DeviceUninitialisedDeviceStartedなど。

  3. すべての処理を1つのオブジェクトDeviceStrategyにパックします。ここで、メソッドは2.で定義された構造を入力および出力として使用します。したがって、DeviceStarted DeviceStrategy::start (DeviceUninitalised dev)メソッド(またはプロジェクトの規則に従って同等のメソッドが何であれ)がある場合があります。

このアプローチでは、有効なプログラムは、メソッドプロトタイプによって適用されたシーケンスでいくつかのメソッドを呼び出す必要があります。

さまざまな状態は無関係なオブジェクトですが、これは置換の原則によるものです。これらの構造が共通の祖先を共有することが有用である場合、ビジターパターンを使用して、抽象クラスのインスタンスの具象型を回復できることを思い出してください。

固有のDeviceStrategyクラスについては3.で説明しましたが、それが提供する機能をいくつかのクラスに分割したい場合があります。

それらを要約すると、私が説明した設計の重要なポイントは次のとおりです。

  1. 置換の原則のため、デバイスの状態を表すオブジェクトは区別する必要があり、特別な継承関係を持つべきではありません。

  2. デバイス処理をデバイス自体を表すオブジェクトではなく、startegyオブジェクトにパックします。これにより、各デバイスまたはデバイスの状態はそれ自体のみを参照し、戦略はそれらすべてを参照して、それらの間の可能な遷移を表現します。

これらの行に続いてtelnetクライアントの実装の説明を見たことがありますが、それを再び見つけることはできませんでした。それは非常に有用なリファレンスでした!

¹:これについては、直感に従うか、「method₁〜method₂iff」の関係の実際の実装でメソッドの等価クラスを見つけます。同じオブジェクト上でそれらを使用することは有効です」—デバイス上のすべての処理をカプセル化する大きなオブジェクトがあると仮定します。状態をリストする両方の方法は、素晴らしい結果をもたらします。

ビルダーパターンを使用します。

上記のすべての操作のメソッドを持つオブジェクトを用意します。ただし、これらの操作はすぐには実行されません。後で使用できるように、各操作を記憶するだけです。操作はすぐには実行されないため、ビルダーに渡す順序は重要ではありません。

ビルダーですべての操作を定義したら、execute- methodを呼び出します。このメソッドが呼び出されると、上にリストしたすべてのステップが、上に保存した操作を使用して正しい順序で実行されます。この方法は、ハードウェアに書き込む前に、操作にまたがる健全性チェック(まだセットアップされていないリソースの構成を試みるなど)を実行するのにも適しています。これにより、無意味な構成でハードウェアを損傷するのを防ぐことができます(ハードウェアが影響を受けやすい場合)。

2
Philipp

インターフェースがどのように使用されているかを正しく文書化し、チュートリアルの例を示すだけです。

一部のランタイムチェックを行うデバッグライブラリバリアントがある場合もあります。

おそらく、いくつかの命名規則を正しく定義して文書化します(例:preconfigure*startup*postconfigure*run*....)

ところで、既存のインターフェースの多くは同様のパターンに従っています(X11ツールキットなど)。

クライアントプログラムが「文法的に」正しい必要がある一方で、コンパイラは構文条件のみを強制できるため、これは確かに一般的で油断のできない種類のエラーです。

残念ながら、命名規則はこの種のエラーに対してほぼ完全に無効です。文法に違反しないように本当に人々に働きかけたい場合は、前提条件の値で初期化する必要があるなんらかのコマンドオブジェクトを渡して、できない手順を実行する必要があります順序の。

1
Kilian Foth
public class Executor {

private Executor() {} // helper class

  public void execute(MyStepsRunnable r) {
    r.step1();
    r.step2();
    r.step3();
  }
}

interface MyStepsRunnable {

  void step1();
  void step2();
  void step3();
}

このパターンを使用すると、すべての実装者がこの正確な順序で実行されることが確実になります。さらに一歩進んで、カスタム実行パスでエグゼキューターを構築するエグゼキューターファクトリーを作成できます。

1
Silviu Burcea