クラスの実装について他のプログラマに警告する方法


96

「特定の方法で使用しなければならない」クラスを書いています(すべてのクラスが...する必要があると思います)。

たとえば、fooManagerクラスを作成します。これに、などへの呼び出しが必要Initialize(string,string)です。そして、例をさらに進めるために、そのThisHappenedアクションをリッスンしなければクラスは役に立たないでしょう。

私のポイントは、書いているクラスにはメソッド呼び出しが必要だということです。しかし、これらのメソッドを呼び出さなければ、コンパイルは正常に終了し、空の新しいFooManagerが作成されます。ある時点で、クラスとその動作に応じて、機能しないか、クラッシュする可能性があります。私のクラスを実装するプログラマーは明らかに内部を見て、「ああ、Initializeを呼び出さなかった!」と気付くでしょう。

しかし、私はそれが好きではありません。私が理想的に望んでいるのは、メソッドが呼び出されなかった場合にコンパイルしないコードです。それは単に不可能だと思います。または、すぐに表示され、明確になるもの。

私がここで持っている現在のアプローチに悩まされているのは次のとおりです。

クラスにプライベートブール値を追加し、クラスが初期化されている場合は必要なすべての場所を確認します。そうでない場合は、「クラスが初期化されていません.Initialize(string,string)。本当に呼び出していますか?」という例外をスローします。

私はそのアプローチで少し大丈夫ですが、多くのコードがコンパイルされ、最終的にはエンドユーザーには不要になります。

また、Initiliaze呼び出すだけのメソッドよりも多くのメソッドがあると、コードがさらに増えることがあります。パブリックメソッド/アクションが多すぎないようにクラスを維持しようとしていますが、それは問題を解決するものではなく、単に合理的なものにするだけです。

私がここで探しているのは:

  • 私のアプローチは正しいですか?
  • より良いものはありますか?
  • 皆さんは何をしますか/アドバイスしますか?
  • 非問題を解決しようとしていますか?同僚から、使用する前にクラスをチェックするのはプログラマーだと言われました。私は敬意を持って同意しませんが、それは別の問題です。

簡単に言えば、そのクラスが後で、または他の人によって再利用されたときに、呼び出しを実装することを決して忘れない方法を見つけようとしています。

明確化:

ここで多くの質問を明確にするために:

  • 私は間違いなくクラスの初期化の部分について話しているだけでなく、それは一生です。同僚がメソッドを2回呼び出さないようにし、Yの前にXを呼び出すようにします。Assertsのアイデアは本当に気に入りましたが、Assertsが常に可能であるとは限らないので、他のアイデアを混ぜる必要があると確信しています。

  • 私はC#言語を使用しています!どうして私はそれを言及しなかったのですか?!私はXamarin環境にいて、PCL、iOS、Android、およびWindowsプロジェクトを含むソリューションで通常6〜9個のプロジェクトを使用してモバイルアプリを構築しています。私は約1年半(学校と仕事を合わせて)開発者でした。そのため、時々ばかげた声明や質問があります。ここではおそらくすべては無関係ですが、情報が多すぎることは必ずしも悪いことではありません。

  • プラットフォームの制限と依存関係注入の使用のため、コンストラクターに必須のすべてを常に配置できるわけではありません。Interfaces以外のパラメーターはテーブル外にあります。または、私の知識が十分でない可能性があります。ほとんどの場合、初期化の問題ではありませんが、

彼がそのイベントに登録したことを確認するにはどうすればよいですか?

彼が「ある時点でプロセスを停止する」ことを忘れなかったことをどのように確認できますか

ここで、私は広告取得クラスを覚えています。広告が表示されるビューが表示されている限り、クラスは毎分新しい広告を取得します。そのクラスは、Adを表示できる場所に構築するときにビューを必要とします。これは明らかにパラメーターに入れることができます。ただし、ビューが消えたら、StopFetching()を呼び出す必要があります。そうしないと、クラスは、そこにさえないビューの広告を取得し続けますが、それは悪いことです。

また、このクラスには、たとえば「AdClicked」など、リッスンする必要があるイベントがあります。聞いていない場合はすべて正常に動作しますが、タップが登録されていない場合、そこでの分析の追跡が失われます。ただし、広告は引き続き機能するため、ユーザーと開発者には違いが見られず、アナリティクスのデータは間違っています。それは避ける必要がありますが、開発者がtaoイベントに登録する必要があることを開発者がどのように知ることができるかわかりません。しかし、これは単純化された例ですが、アイデアは「彼が利用可能な公開アクションを使用することを確認する」ことであり、当然のことです!


13
クラスのメソッドへの呼び出しが特定の順序で発生するようにすることは、一般的には不可能です。これは、停止する問題と同等の多くの問題の1つです。非準拠をコンパイル時エラーにする場合もありますが、一般的な解決策はありません。データフロー分析が非常に高度になったとしても、少なくとも明らかなケースを診断できる構造をサポートする言語がほとんどないのは、おそらくこれが理由です。
キリアンフォス


5
他の質問への回答は無関係であり、質問は同じではないため、異なる質問です。誰かがその議論を探している場合、他の質問のタイトルを入力しません。
ギル砂

6
ctorのinitializeのコンテンツをマージするのを妨げるものは何ですか?initializeオブジェクトの作成後、呼び出しを遅くする必要がありますか?例外をスローして作成チェーンを破ることができるという意味で、俳優は「危険」すぎるのでしょうか?
発見

7
この問題は時間的結合と呼ばれます。可能な場合は、無効な入力に遭遇したときにコンストラクターで例外をスローすることにより、オブジェクトが無効な状態にnewならないようにしてください。そうすれば、オブジェクトを使用する準備ができていない場合、呼び出しをすり抜けることはありません。初期化メソッドに頼ることは、あなたを悩ませるために戻ってくることになっています。絶対に必要でない限り、避けるべきです、少なくともそれは私の経験です。
16

回答:


161

そのような場合、適切な初期化を支援するために、言語の型システムを使用するのが最善です。どのように防ぐことができますFooManagerされてから使用され、初期化されずに?防止することによりFooManagerされてから作成し、それを正しく初期化するために必要な情報なし。特に、すべての初期化はコンストラクターの責任です。コンストラクターに違法な状態のオブジェクトを作成させないでください。

ただし、呼び出し元は、依存関係として渡されるなど、FooManager初期化する前にを構築する必要がありFooManagerます。

FooManager持っていない場合は作成しないでください。代わりにできること、完全に構築さたを取得できるオブジェクトを渡すことですFooManagerが、初期化情報のみが含まれます。(関数型プログラミングでは、コンストラクタに部分的なアプリケーションを使用することをお勧めします。)例:

ctorArgs = ...;
getFooManager = (initInfo) -> new FooManager(ctorArgs, initInfo);
...
getFooManager(myInitInfo).fooMethod();

これの問題は、にアクセスするたびにinit情報を提供する必要があることですFooManager

ご使用の言語で必要な場合getFooManager()は、ファクトリーまたはビルダーのようなクラスで操作をラップできます。

initialize()型システムレベルのソリューションを使用するのではなく、メソッドが呼び出されたことをランタイムチェックで確認したいのです。

妥協点を見つけることは可能です。を返すメソッドMaybeInitializedFooManagerを持つラッパークラスを作成get()しますがFooManager、がFooManager完全に初期化されていない場合はスローします。これは、初期化がラッパーを介して行われる場合、またはFooManager#isInitialized()メソッドがある場合にのみ機能します 。

class MaybeInitializedFooManager {
  private final FooManager fooManager;

  public MaybeInitializedFooManager(CtorArgs ctorArgs) {
    fooManager = new FooManager(ctorArgs);
  }

  public FooManager initialize(InitArgs initArgs) {
    fooManager.initialize(initArgs);
    return fooManager;
  }

  public FooManager get() {
    if (fooManager.isInitialized()) return fooManager;
    throw ...;
  }
}

クラスのAPIを変更したくありません。

その場合、if (!initialized) throw;すべてのメソッドで条件を回避する必要があります。幸いなことに、これを解決する簡単なパターンがあります。

ユーザーに提供するオブジェクトは、すべての呼び出しを実装オブジェクトに委任する空のシェルです。デフォルトでは、実装オブジェクトは、初期化されていない各メソッドに対してエラーをスローします。ただし、このinitialize()メソッドは実装オブジェクトを完全に構築されたオブジェクトに置き換えます。

class FooManager {
  private CtorArgs ctorArgs;
  private Impl impl;

  public FooManager(CtorArgs ctorArgs) {
    this.ctorArgs = ctorArgs;
    this.impl = new UninitializedImpl();
  }

  public void initialize(InitArgs initArgs) {
    impl = new MainImpl(ctorArgs, initArgs);
  }

  public X foo() { return impl.foo(); }
  public Y bar() { return impl.bar(); }
}

interface Impl {
  X foo();
  Y bar();
}

class UninitializedImpl implements Impl {
  public X foo() { throw ...; }
  public Y bar() { throw ...; }
}

class MainImpl implements Impl {
  public MainImpl(CtorArgs c, InitArgs i);
  public X foo() { ... }
  public Y bar() { ... }
}

これにより、クラスの主な動作がに抽出されますMainImpl


9
私は最初の2つのソリューション(+1)が好きですが、メソッドin FooManagerとinを繰り返している最後のスニペットUninitialisedImplはを繰り返すよりも良いとは思いませんif (!initialized) throw;
ベルギ

3
@Bergi一部の人々はクラスが大好きです。if FooManagerには多くのメソッドありますが、いくつかのif (!initialized)チェックを忘れるよりも簡単かもしれません。ただし、その場合は、クラスを分割することをお勧めします。
immibis

1
MaybeInitializedFooは初期化されたFooよりも優れているようには見えませんが、いくつかのオプション/アイデアを与えるために+1されています。
マシュージェームスブリッグス

7
+1工場は正しいオプションです。変更せずにデザインを改善することはできません。そのため、半分にする方法についての答えの最後のビットは、OPを助けるものではありません。
ネイサンクーパー

6
@Bergi最後のセクションで示した手法は非常に有用であることがわかりました(「ポリモーフィズムによる条件の置換」リファクタリング手法、および状態パターンも参照してください)。if / elseを使用すると、1つのメソッドのチェックを忘れやすくなります。インターフェイスに必要なメソッドの実装を忘れることははるかに困難です。最も重要なこととして、MainImplは、initializeメソッドがコンストラクターとマージされるオブジェクトに正確に対応します。これは、MainImplの実装が、これが提供する強力な保証を享受できることを意味し、メインコードを簡素化します。…
アモン

79

クライアントがオブジェクトを「誤用」するのを防ぐ最も効果的で役立つ方法は、オブジェクトを不可能にすることです。

最も簡単な解決策はInitialize、コンストラクターとマージすることです。この方法では、オブジェクトは初期化されていない状態のクライアントで使用できることはないため、エラーは発生しません。コンストラクタ自体で初期化を実行できない場合は、ファクトリメソッドを作成できます。たとえば、クラスで特定のイベントを登録する必要がある場合、コンストラクターまたはファクトリーメソッドシグネチャのパラメーターとしてイベントリスナーを要求できます。

初期化する前に初期化されていないオブジェクトにアクセスできるようにする必要がある場合は、2つの状態を個別のクラスとして実装することができます。そのため、UnitializedFooManagerインスタンスを起動しInitialize(...)ますInitializedFooManager。初期化された状態でのみ呼び出すことができるメソッドはにのみ存在しInitializedFooManagerます。このアプローチは、必要に応じて複数の状態に拡張できます。

実行時例外と比較して、状態を個別のクラスとして表現するのは手間がかかりますが、オブジェクトの状態に対して無効なメソッドを呼び出していないことをコンパイル時に保証し、状態遷移をコードでより明確に文書化します。

しかしより一般的には、理想的なソリューションは、特定の時間に特定の順序でメソッドを呼び出す必要があるなどの制約なしにクラスとメソッドを設計することです。常に可能とは限りませんが、多くの場合、適切なパターンを使用することで回避できます。

複雑な時間的カップリングがある場合(いくつかのメソッドは特定の順序で呼び出す必要があります)、1つの解決策はcontrolの反転を使用することです。そのため、適切な順序でメソッドを呼び出すが、テンプレートメソッドまたはイベントを使用するクラスを作成しますクライアントがフローの適切な段階でカスタム操作を実行できるようにします。このように、正しい順序で操作を実行する責任は、クライアントからクラス自体にプッシュされます。

簡単な例:Fileファイルから読み取ることができる-objectがあります。ただし、クライアントOpenReadLineメソッドを呼び出す前に呼び出す必要がCloseあり、ReadLineメソッドが呼び出されてはならない後は常に(例外が発生した場合でも)呼び出すことを忘れないでください。これは一時的な結合です。これは、コールバックまたはデリゲートを引数として取る単一のメソッドを持つことで回避できます。このメソッドは、ファイルを開き、コールバックを呼び出してからファイルを閉じます。Readメソッドを持つ別個のインターフェイスをコールバックに渡すことができます。そうすれば、クライアントがメソッドを正しい順序で呼び出すことを忘れることはありません。

時間的結合とのインターフェース:

class File {
     /// Must be called on a closed file.
     /// Remember to always call Close() when you are finished
     public void Open();

     /// must be called on on open file
     public string ReadLine();

     /// must be called on an open file
     public void Close();
}

時間的結合なし:

class File {
    /// Opens the file, executes the callback, and closes the file again.
    public void Consume(Action<IOpenFile> callback);
}

interface IOpenFile {
    string ReadLine();
}

より重い解決策はFile、開いているファイルで実行される(保護された)メソッドを実装する必要がある抽象クラスとして定義することです。これは、テンプレートメソッドパターンと呼ばれます。

abstract class File {
    /// Opens the file, executes ConsumeOpenFile(), and closes the file again.
    public void Consume();

    /// override this
    abstract protected ConsumeOpenFile();

    /// call this from your ConsumeOpenFile() implementation
    protected string ReadLine();
}

利点は同じです。クライアントは、特定の順序でメソッドを呼び出すことを覚えておく必要はありません。間違った順序でメソッドを呼び出すことは不可能です。


2
また、私は単にコンストラクタから行くことができなかった場合を覚えて、私は今を示すために例を持っていない
ギル砂

2
この例では現在、ファクトリパターンについて説明していますが、最初の文は実際にRAIIパラダイムを説明しています。それでは、この答えが推奨するのはどれですか?
Ext3h

11
@ Ext3hファクトリーパターンとRAIIは相互に排他的ではないと思います。
ピーターB

@PieterBいいえ、そうではありません。工場はRAIIを保証できます。しかし、実際にはセマンティックのように、テンプレート/コンストラクターの引数(と呼ばれるUnitializedFooManager)がインスタンス(InitializedFooManager)と状態を共有してはならないという追加の意味がある場合のみです。そうでない場合、開発者が同じテンプレートを2回使用すると、この例は新しい問題につながります。また、言語がテンプレートを確実に使用するためにセマンティクスを明示的にサポートしていない場合も、静的に防止/検証することはできません。Initialize(...)Instantiate(...)move
Ext3h

2
@ Ext3h:最も簡単なため、最初のソリューションをお勧めします。ただし、Initializeを呼び出す前にクライアントオブジェクトにアクセスできるようにする必要がある場合は使用できませんが、2番目のソリューションを使用できる場合があります。
ジャックB

39

私は通常、ちょうどintiialisationをチェックし、スロー(例えば)ANでしょうIllegalStateException、あなたがしようとすると初期化されていませんしながら、それを使用する場合。

ただし、コンパイル時の安全性を確保したい(そしてそれは称賛に値し、望ましい)場合は、初期化を、構築および初期化されたオブジェクトを返すファクトリメソッドとして扱ってください。

ComponentBuilder builder = new ComponentBuilder();
Component forClients = builder.initialise(); // the 'Component' is what you give your clients

そのため、あなたは、オブジェクトの作成およびライフサイクルを制御し、あなたの顧客が得るComponentあなたの初期化の結果として。これは事実上、遅延インスタンス化です。


1
良い答え-すてきな簡潔な答えの2つの良いオプション。上記の他の回答は、(理論的には)有効なタイプシステム体操を示しています。しかし、ほとんどの場合、これら2つのシンプルなオプションは理想的な実用的な選択肢です。
トーマスW

1
注:.NETでは、InvalidOperationExceptionはMicrosoftによって昇格され、ユーザーが呼び出しIllegalStateExceptionたものになります。
ミロクスラフ

1
私はこの答えをダウン投票していませんが、それは本当に良い答えではありません。ただ投げIllegalStateExceptionたりInvalidOperationException、青くしたりするだけで、ユーザーは自分が間違ったことを理解することはありません。これらの例外は、設計上の欠陥を修正するためのバイパスになるべきではありません。
displayName

例外のスローは可能なオプションの1つであり、優先オプションについて詳しく説明します-このコンパイル時の安全性を確認します。これを強調するために回答を編集しました
ブライアンアグニュー

14

私は他の答えから少し離れて意見を異にします:あなたが働いている言語を知らずにこれに答えることは不可能です。これが価値のある計画であるかどうか、そして与えるべき「警告」の種類ユーザーは、言語が提供するメカニズムと、その言語の他の開発者が従う傾向にある規則に完全に依存しています。

FooManagerHaskell を使用している場合、型システムにより非常に簡単になり、Haskell開発者が期待する慣習であるため、ユーザーがs を管理できないものを作成できるようにすることは犯罪になりますFoo

一方、あなたがCを書いている場合は、あなたの同僚が戻ってあなたを取り出し、別の定義のためにあなたを撮影するために、完全に彼らの権利範囲内であろうstruct FooManagerstruct UninitializedFooManager、それは非常に少しの利益のために、不必要に複雑なコードにつながるので、異なる操作をサポートする型を。

Pythonを書いている場合、これを可能にするメカニズムは言語にはありません。

たぶん、Haskell、Python、またはCを書いているわけではありませんが、それらは型システムがどれだけ/どれだけ少ない作業が期待され、できるかという点での例です。

あなたの言語で、開発者の合理的な期待をフォローし、自然な、慣用的な実装を持っていないソリューションをエンジニアリング上への衝動にレジスト間違いはそれが極端に行く価値があることをキャッチするために作ることがとても簡単で、一生懸命でない限りは(不可能にするための長さ)。あなたがあなたの言語で十分な経験があるかどうかを判断するのに十分でない場合、あなたよりもそれをよく知っている誰かのアドバイスに従ってください。


「Pythonを書いている場合、これを可能にするメカニズムは言語にありません」と少し混乱しています。Pythonにはコンストラクタがあり、ファクトリメソッドを作成するのは非常に簡単です。だから多分私はあなたが「これ」の意味を理解していないのでしょうか?
ALフラナガン

3
「これ」とは、実行時エラーではなく、コンパイル時エラーを意味します。
パトリックコリンズ

現在のバージョンであるPython 3.5は、プラグイン可能な型システムを介して型を持っています。インポートされたという理由だけで、コードを実行する前にPythonをエラーにすることは間違いなく可能です。3.5までは完全に正しいものでした。
ベンジャミングリュンバウム

ああ、わかった。遅ればせながら+1。混乱していても、これは非常に洞察力に富んでいました。
ALフラナガン

私はあなたのスタイルのパトリックが好きです。
ギル砂

4

コードチェックを顧客に出荷したくないようですが(プログラマにとっては問題ないように思えますが)、プログラミング言語で使用できる場合はアサート関数を使用できます。

そうすれば、開発環境でチェックを行うことができます(そして、仲間の開発者がWILLを呼び出すテストは予想通り失敗します)が、アサート(少なくともJavaでは)が選択的にのみコンパイルされるため、顧客にコードを出荷しません。

したがって、これを使用するJavaクラスは次のようになります。

/** For some reason, you have to call init and connect before the manager works. */
public class FooManager {
   private int state = 0;

   public FooManager () {
   }

   public synchronized void init() {
      assert state==0 : "you called init twice.";
      // do something
      state = 1;
   }

   public synchronized void connect() {
      assert state==1 : "you called connect before init, or connect twice.";
      // do something
      state = 2;
   }

   public void someWork() {
      assert state==2 : "You did not properly init FooManager. You need to call init and connect first.";
      // do the actual work.
   }
}

アサーションは、必要なプログラムの実行時状態をチェックするための優れたツールですが、実際の環境で間違ったことをしている人を実際に期待しないでください。

また、それらは非常にスリムであり、if()throw ...構文の大きな部分を占めません。キャッチする必要はありません。


3

この問題は、現在の回答よりも一般的に見ており、主に初期化に焦点を当てています。二つの方法がありますオブジェクトを考えてみましょう、a()b()。要件は、a()常に前に呼び出されることb()です。元のオブジェクトではなく、新しいオブジェクトを返し、そこa()に移動b()することで、これが発生するコンパイル時チェックを作成できます。実装例:

class SomeClass
{
   private int valueRequiredByMethodB;

   public IReadyForB a (int v) { valueRequiredByMethodB = v; return new ReadyForB(this); }

   public interface IReadyForB { public void b (); }

   private class ReadyForB : IReadyForB
   {
      SomeClass owner;
      private ReadyForB (SomeClass owner) { this.owner = owner; }
      public void b () { Console.WriteLine (owner.valueRequiredByMethodB); }
   }
}

現在、a()が呼び出されるまで非表示のインターフェイスに実装されているため、最初にa()を呼び出さずにb()を呼び出すことはできません。確かに、これはかなりの努力であるため、私は通常このアプローチを使用しませんが、それが有益な状況があります(特に、そうでないプログラマーによって多くの状況でクラスが再利用される場合)その実装に精通している、または信頼性が重要なコードで)。また、これは、多くの既存の回答で示唆されているように、ビルダーパターンの一般化であることに注意してください。ほぼ同じように機能します。唯一の本当の違いは、データが格納される場所(返されるオブジェクトではなく元のオブジェクト)と、使用する予定のタイミング(いつでも初期化中のみ)です。


2

有用になる前に追加の初期化情報または他のオブジェクトへのリンクを必要とする基本クラスを実装する場合、その基本クラスを抽象化し、基本クラスのフローで使用されるいくつかの抽象メソッドをそのクラスで定義する傾向があります(abstract Collection<Information> get_extra_information(args);そしてabstract Collection<OtherObjects> get_other_objects(args);、継承のコントラクトによって具象クラスによって実装される必要があり、その基底クラスのユーザーに、基底クラスが必要とするすべてのものを提供するように強制します。

したがって、基本クラスを実装するとき、抽象メソッドを実装するだけでよいので、基本クラスを正しく動作させるために何を書く必要があるかをすぐに明示的に知っています。

編集:明確にするために、これは基本クラスのコンストラクターに引数を提供することとほぼ同じですが、抽象メソッドの実装により、実装は抽象メソッド呼び出しに渡された引数を処理できます。または、引数が使用されていない場合でも、抽象メソッドの戻り値がメソッド本体で定義できる状態に依存している場合に役立ちます。これは、変数を引数として渡すときに不可能ですコンストラクター。継承よりも合成を使用する場合は、同じ原則に基づいて動的な動作を持つ引数を渡すことができます。


2

答えは、はい、いいえ、そして時々です。:-)

あなたが説明する問題のいくつかは、少なくとも多くの場合、簡単に解決されます。

コンパイル時のチェックは、実行時のチェックよりも確実に望ましいです。実行時のチェックは、まったくチェックしないよりも望ましいです。

REランタイム:

ランタイムチェックを使用して、関数を特定の順序などで強制的に呼び出すことは、少なくとも概念的には簡単です。実行された関数を示すフラグを設定し、各関数を「前提条件-1でない場合、または前提条件-2でない場合は例外をスローする」などのように開始します。チェックが複雑になる場合は、プライベート機能にプッシュできます。

いくつかのことを自動的に行うことができます。簡単な例を挙げると、プログラマーはしばしばオブジェクトの「怠populationな集団」について話します。インスタンスの作成時に、is-populatedフラグをfalseに設定するか、キーオブジェクト参照をnullに設定します。次に、関連データを必要とする関数が呼び出されると、フラグをチェックします。falseの場合、データを取り込み、フラグをtrueに設定します。それが本当なら、データがそこにあると仮定するだけです。他の前提条件でも同じことができます。コンストラクターで、またはデフォルト値としてフラグを設定します。次に、何らかの事前条件関数が呼び出されるはずのポイントに達したときに、まだ呼び出されていない場合は呼び出します。もちろん、これはその時点で呼び出すデータがある場合にのみ機能しますが、多くの場合、必要なデータを「

REコンパイル時:

他の人が言ったように、オブジェクトを使用する前に初期化する必要がある場合、コンストラクターに初期化を設定します。これは、OOPの非常に典型的なアドバイスです。可能な限り、コンストラクターにオブジェクトの有効で使用可能なインスタンスを作成させる必要があります。

初期化する前にオブジェクトへの参照を渡す必要がある場合の対処方法について説明します。私は頭の上のその実際の例を考えることはできませんが、私はそれが起こる可能性があると思います。解決策が提案されていますが、私がここで見た解決策と私が考えることができるものは面倒であり、コードを複雑にします。ある時点で、あなたは尋ねる必要があります。実行時チェックではなくコンパイル時チェックを行えるように、見苦しくて理解しにくいコードを作成する価値がありますか?それとも、私は自分自身や他の人のために、素晴らしいが必要ではない目標のために多くの仕事を作成していますか?

2つの関数を常に一緒に実行する必要がある場合、単純な解決策は、それらを1つの関数にすることです。広告をページに追加するたびに、そのページのメトリックハンドラも追加する必要がある場合は、「広告の追加」関数内にメトリックハンドラを追加するコードを配置します。

コンパイル時にチェックすることはほとんど不可能です。特定の関数を複数回呼び出すことができないという要件のように。(そのような多くの関数を書きました。最近、魔法のディレクトリでファイルを探し、見つかったものを処理し、それらを削除する関数を書きました。もちろん、一度ファイルを削除すると、再実行できませんなど)コンパイル時に同じインスタンスで関数が2回呼び出されるのを防ぐことができる機能を備えた言語は知りません。コンパイラーがどうしてそれが起こっているのかを明確に把握する方法はわかりません。できることは、実行時チェックを行うことだけです。特に実行時のチェックは明らかですよね?「already-been-here = trueの場合は例外をスローし、else set already-been-here = trueの場合」

施行することは不可能

ファイルや接続を閉じたり、メモリを解放したり、最終結果をDBに書き込んだりなど、完了時にクラスが何らかのクリーンアップを必要とすることは珍しくありません。コンパイル時に強制する簡単な方法はありません。またはランタイム。ほとんどのOOP言語には、インスタンスがガベージコレクションされたときに呼び出される「ファイナライザー」機能に対する何らかの種類のプロビジョニングがあると思いますが、ほとんどの場合、これが実行されることを保証できないと言っています。OOP言語には、インスタンスの使用にスコープを設定するための何らかの種類の「dispose」関数、「using」または「with」句が含まれ、プログラムがそのスコープを終了すると、disposeが実行されます。ただし、これにはプログラマーが適切なスコープ指定子を使用する必要があります。プログラマーにそれを強制することはありません。

プログラマにクラスを正しく使用させることが不可能な場合は、間違った結果を出すのではなく、間違った場合にプログラムが爆発するようにします。簡単な例:大量のデータをダミー値に初期化する多くのプログラムを見てきたので、ユーザーが関数を呼び出してデータを正しく入力しなくても、nullポインター例外を取得することはありません。そして、なぜだろうといつも思っています。バグを見つけにくくするために、あなたは邪魔にならないでしょう。プログラマーが「データのロード」関数の呼び出しに失敗し、データを快く使用しようとしたときに、nullポインター例外が発生した場合、例外はすぐに問題の場所を示します。ただし、このような場合にデータを空白またはゼロにして非表示にすると、プログラムは最後まで実行されますが、不正確な結果が生成される場合があります。場合によっては、プログラマーは問題があることに気付かないことさえあります。彼が気づいた場合、彼はそれが間違っていた場所を見つけるために長い道をたどらなければならないかもしれません。勇敢に続けようとするよりも早く失敗する方が良い。

一般に、確かに、オブジェクトの誤った使用を単に不可能にすることができるのは常に良いことです。プログラマが無効な呼び出しを行う方法がまったくないような方法で関数が構造化されている場合、または無効な呼び出しがコンパイル時エラーを生成する場合、それは良いことです。

しかし、あなたが言わなければならないいくつかのポイントもあります。プログラマは関数に関するドキュメントを読むべきです。


1
強制的なクリーンアップに関して、特定のメソッドを呼び出すことによってのみリソースを割り当てることができる場合に使用できるパターンがあります(たとえば、典型的なオブジェクト指向言語では、プライベートコンストラクターを持つことができます)。このメソッドは、リソースを割り当て、リソースへの参照とともに渡される関数(またはインターフェイスのメソッド)を呼び出し、この関数が返るとリソースを破棄します。突然のスレッド終了以外に、このパターンはリソースが常に破棄されることを保証します。これは関数型言語では一般的なアプローチですが、OOシステムで効果的に使用されることも確認しました。
ジュール


2

はい、あなたの本能はスポットオンです。何年も前に、私は雑誌に記事を書きました(この場所に似ていますが、デッドツリーにあり、月に1回発行されます)デバッグアバグアンチバグについて議論しました

コンパイラに誤用を捕まえることができるなら、それが最善の方法です。使用可能な言語機能は言語によって異なり、指定しませんでした。とにかく、このサイトの個々の質問に対して特定のテクニックが詳述されます。

しかし、コンパイル時間の代わりに、実行時に使用を検出するための試験を持つことは、本当にテストのと同じようなものです:あなたはまだ最初の適切な関数を呼び出し、正しい方法でそれらを使用するために知っておく必要があります。

そもそも問題にならないようにコンポーネントを設計することは、はるかに微妙であり、私はバッファが要素の例に似ているのが好きです。パート4(20年前に公開されました!すごい!)には、あなたの場合と非常によく似た議論の例が含まれています。むしろ、構築直後に一度だけ使用する必要があります。クラスにバッファーを自動的かつ目に見えないように呼び出し元に管理させることで、概念的に問題を回避できます。

適切な使用法を保証するために実行時にテストを実行できるかどうかを尋ねます。

はいと言うでしょう。コードが維持され、特定の使用法が混乱するとき、いつかはチームのデバッグ作業を大幅に節約できます。テストは、テストできる制約不変式の正式なセットとして設定できます。これは、状態を追跡するための余分なスペースとチェックを行うための作業のオーバーヘッドが、すべての呼び出しに常に残されることを意味するものではありません。これはクラス内にあるため、呼び出すことができ、コードは実際の制約と仮定を文書化します。

おそらく、チェックはデバッグビルドでのみ行われます。

おそらくチェックは複雑です(たとえば、heapcheckを考えてみてください)が、それは存在しており、たまに行うことができます。同じクラスにリンクする単体テストまたは統合テストプログラムに、このような問題がないことを確認してください。

結局、オーバーヘッドは些細なものであると判断するかもしれません。クラスがファイルI / Oを行い、追加の状態バイトとそのようなテストが行​​われない場合は何もありません。一般的なiostreamクラスは、ストリームの状態が悪いことを確認します。

あなたの本能は良いです。がんばり続ける!


0

情報の隠蔽(カプセル化)の原則は、クラス外のエンティティが、このクラスを適切に使用するために必要な情報以上のものを知らないことです。

あなたのケースは、外部ユーザーにクラスについて十分に伝えることなく、クラスのオブジェクトを適切に実行しようとしているようです。必要以上の情報を隠しました。

  • コンストラクターで必要な情報を明示的に要求するか、
  • または、コンストラクターをそのままにして、メソッドを変更して、メソッドを呼び出すために、欠落データを提供する必要があります。

知恵の言葉:

*いずれにしても、クラスおよび/またはコンストラクターおよび/またはメソッドを再設計する必要があります。

*適切に設計せず、クラスを正しい情報に飢えさせることにより、クラスが1つではなく複数の場所で破損する危険があります。

*例外とエラーメッセージの記述が不十分な場合、クラスは自分自身についてさらに多くの情報を提供します。

*最後に、部外者がa、b、cを初期化してからd()、e()、f(int)を呼び出す必要があることを知っている場合、抽象化にリークがあります。


0

C#の使用を指定しているため、状況に対する実行可能なソリューションはRoslyn Code Analyzersを活用することです。これにより、違反をすぐにキャッチでき、コードの修正提案することもできます。

それを実装する1つの方法は、メソッドを呼び出す必要がある順序を指定する属性で時間的に結合されたメソッドを装飾することです1。アナライザーは、クラスでこれらの属性を見つけると、メソッドが順番に呼び出されることを検証します。これにより、クラスは次のようになります。

public abstract class Foo
{
   [TemporallyCoupled(1)]
   public abstract void Init();

   [TemporallyCoupled(2)]
   public abstract void DoBar();

   [TemporallyCoupled(3)]
   public abstract void Close();
}

1:Roslyn Code Analyzerを書いたことがないので、これは最良の実装ではないかもしれません。ただし、Roslyn Code Analyzerを使用してAPIが正しく使用されていることを確認するという考え方は、100%健全です。


-1

この問題を以前に解決した方法は、プライベートコンストラクターとMakeFooManager()クラス内の静的メソッドを使用することでした。例:

public class FooManager
{
     public string Foo { get; private set; }
     public string Bar { get; private set; }

     private FooManager(string foo, string bar)
     {
         Foo = foo;
         Bar = bar;
     }

     public static FooManager MakeFooManager(string foo, string bar)
     {
         // Can do other checks here too.
         if(foo == null || bar == null)
         {
             return null;
         }
         else
         {
             return new FooManager(foo, bar);
         }
     }
}

コンストラクタが実装されているため、FooManagerを経由せずにのインスタンスを作成する方法はありませんMakeFooManager()


1
これは、数時間前に投稿された以前の回答で作成され、説明されたポイントを繰り返すようです
-gnat

1
これは、単にctorのnullチェックよりも利点がありますか?別のメソッドをラップするだけのメソッドを作成したようです。どうして?
サラ

また、fooおよびbarメンバー変数はなぜですか?
パトリックM

-1

いくつかのアプローチがあります。

  1. クラスを正しく使いやすく、間違って使いにくいものにします。他の回答のいくつかは、この点のみに焦点を当てているため、RAIIビルダーパターンについて簡単に説明します。

  2. コメントの使用方法に注意してください。クラスが自明である場合は、コメントを書かないでください。*そうすれば、クラスが実際にコメントを必要とするときに人々があなたのコメントを読む可能性が高くなります。クラスを正しく使用するのが難しく、コメントが必要なこれらのまれなケースのいずれかがある場合は、例を含めてください。

  3. デバッグビルドでアサートを使用します。

  4. クラスの使用が無効な場合、例外をスローします。

*これらは、書くべきではない種類のコメントです。

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