私は「特定の方法で使用する必要がある」クラスを書いています(すべてのクラスがそうでなければならないでしょう...)。
たとえば、fooManager
クラスを作成します。これはrequiresの呼び出しで、たとえばInitialize(string,string)
です。また、例をもう少しプッシュするには、ThisHappened
アクションをリッスンしないと、クラスは役に立たなくなります。
私のポイントは、私が書いているクラスはメソッド呼び出しを必要とするということです。ただし、これらのメソッドを呼び出さない場合は問題なくコンパイルされ、空の新しいFooManagerが作成されます。ある時点で、クラスとその機能に応じて、機能しないか、クラッシュする可能性があります。私のクラスを実装するプログラマーは明らかにその中を見て、「ああ、Initializeを呼び出さなかった!」と気づき、それで問題ありません。
しかし、私はそれが好きではありません。理想的には、メソッドが呼び出されなかった場合にコードがコンパイルされないようにすることです。それは単に不可能だと思います。または、すぐにはっきりと見えるもの。
私はここにいる現在のアプローチに悩まされています。それは次のとおりです。
クラスにプライベートブール値を追加し、クラスが初期化されている場合は、必要なすべての場所を確認します。そうでない場合は、「クラスが初期化されていません。.Initialize(string,string)
を呼び出していますか?」という例外をスローします。
私はそのアプローチで大丈夫ですが、それはコンパイルされる多くのコードにつながり、結局、エンドユーザーには必要ありません。
また、呼び出すInitiliaze
だけではなく、メソッドの数が多い場合は、さらに多くのコードになることがあります。私はクラスにパブリックメソッド/アクションをあまり残さないようにしていますが、それは問題を解決するのではなく、妥当なものに保つだけです。
私がここで探しているのは:
簡単に言うと、そのクラスが後で再利用されたとき、または他の誰かによって再利用されたときに、呼び出しを実装することを決して忘れない方法を見つけようとしています。
明確化:
ここで多くの質問を明確にするために:
私は間違いなく、クラスの初期化の部分について話しているだけでなく、生涯にわたって話しています。同僚がメソッドを2回呼び出さないようにし、Yの前にXを呼び出すようにします。最終的には必須であり、ドキュメントでは必須ですが、コード内でできるだけシンプルで小さいものを望みます。私はアサートのアイデアが本当に好きでしたが、アサートは常に可能であるとは限らないため、他のアイデアをいくつか組み合わせる必要があると確信しています。
私はC#言語を使用しています!どうしてそんなこと言わなかったの!?私はXamarin環境にあり、PCL、iOS、AndroidおよびWindowsプロジェクトを含む)ソリューションで通常約6から9のプロジェクトを使用してモバイルアプリを構築しています。 1年半(学校と仕事を合わせたもの)なので、ときどき馬鹿げた発言\質問です。これらすべてはおそらくここでは関係ありませんが、情報が多すぎると必ずしも悪いことではありません。
プラットフォームの制限と依存性注入の使用により、インターフェイスに必須ではないパラメーターがあるため、コンストラクターに必須のすべてを常に配置できるわけではありません。または、私の知識が十分でない可能性があります。ほとんどの場合、それは初期化の問題ではありませんが、
彼がそのイベントに登録したことをどのように確認できますか?
「ある時点でプロセスを停止する」ことを忘れないようにするにはどうすればよいですか
ここで、Ad fetchingクラスを覚えています。広告が表示されているビューが表示されている限り、クラスは毎分新しい広告をフェッチします。そのクラスは、Adを表示できる場所に構築されたときにビューを必要とします。ただし、ビューがなくなったら、StopFetching()を呼び出す必要があります。そうでなければ、クラスはそこにさえないビューの広告をフェッチし続けるでしょう、そしてそれは悪いです。
また、そのクラスには、「AdClicked」など、リッスンする必要のあるイベントがあります。聞いていない場合はすべて正常に機能しますが、タップが登録されていない場合、分析の追跡が失われます。ただし、広告は引き続き機能するため、ユーザーと開発者は違いを見ることはなく、アナリティクスのデータは正しくありません。それは回避する必要がありますが、開発者がtaoイベントに登録する必要があることをどのようにして知ることができるかわかりません。これは単純化された例ですが、「利用可能なパブリックアクションを使用することを確認する」という考え方があり、適切なタイミングで使用できます。
そのような場合は、適切な初期化を支援するために、言語の型システムを使用するのが最善です。初期化せずにFooManager
がsedになるのを防ぐにはどうすればよいですか? FooManager
がcreatedになるのを防ぐことにより、適切に初期化するために必要な情報がありません。特に、すべての初期化はコンストラクターの責任です。コンストラクターに不正な状態のオブジェクトを作成させないでください。
FooManager
を構築する必要があります。 FooManager
は依存関係として渡されるためです。FooManager
がない場合は作成しないでください。代わりにできることは、オブジェクトを渡すことですretrieve完全に構築されたFooManager
ですが、初期化情報のみが含まれます。 (関数型プログラミングでは、コンストラクターに部分的なアプリケーションを使用することをお勧めします。)例:
_ctorArgs = ...;
getFooManager = (initInfo) -> new FooManager(ctorArgs, initInfo);
...
getFooManager(myInitInfo).fooMethod();
_
これの問題は、FooManager
にアクセスするたびにinit情報を提供する必要があることです。
ご使用の言語で必要な場合は、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 ...;
}
}
_
その場合は、すべてのメソッドで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
に抽出されます。
クライアントがオブジェクトを「誤用」するのを防ぐ最も効果的で役立つ方法は、オブジェクトを不可能にすることです。
最も簡単な解決策は、Initialize
をコンストラクターとマージすることです。これにより、クライアントは初期化されていない状態でオブジェクトを使用できなくなり、エラーは発生しなくなります。コンストラクタ自体で初期化できない場合は、ファクトリメソッドを作成できます。たとえば、クラスで特定のイベントの登録が必要な場合、コンストラクターまたはファクトリーメソッドシグネチャのパラメーターとしてイベントリスナーを要求できます。
初期化前に初期化されていないオブジェクトにアクセスできるようにする必要がある場合は、2つの状態を別々のクラスとして実装することができるため、UnitializedFooManager
インスタンスで開始し、Initialize(...)
InitializedFooManager
を返すメソッド。初期化された状態でのみ呼び出すことができるメソッドは、InitializedFooManager
にのみ存在します。このアプローチは、必要に応じて複数の状態に拡張できます。
実行時例外と比較して、状態を個別のクラスとして表す方が多くの作業ですが、オブジェクトの状態に対して無効なメソッドを呼び出さないことをコンパイル時に保証し、状態遷移をコードでより明確に文書化します。
しかし、より一般的には、理想的なソリューションは、特定の時間に特定の順序でメソッドを呼び出す必要があるなどの制約なしにクラスとメソッドを設計することです。常に可能とは限りませんが、多くの場合、適切なパターンを使用することで回避できます。
複雑な時間結合がある場合(いくつかのメソッドを特定の順序で呼び出す必要があります)、1つの解決策は制御の反転を使用することです。そのため、適切な順序でメソッドを呼び出すクラスを作成します。 、ただしテンプレートメソッドまたはイベントを使用して、クライアントがフローの適切な段階でカスタム操作を実行できるようにします。このようにして、正しい順序で操作を実行する責任がクライアントからクラス自体にプッシュされます。
簡単な例:ファイルから読み取ることができるFile
- objectがあります。ただし、クライアントはOpen
を呼び出す前にReadLine
を呼び出す必要があり、(例外が発生した場合でも)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();
}
利点は同じです。クライアントは、特定の順序でメソッドを呼び出すことを覚えておく必要はありません。メソッドを間違った順序で呼び出すことは不可能です。
私は通常、初期化されていないことを確認し、初期化されていないときにIllegalStateException
を使用しようとすると(たとえば)スローします。
ただし、コンパイル時の安全性を確保したい場合(それは称賛に値し、好ましい方法です)、初期化をファクトリメソッドとして扱い、構築および初期化されたオブジェクトを返します。
ComponentBuilder builder = new ComponentBuilder();
Component forClients = builder.initialise(); // the 'Component' is what you give your clients
そしてあなたはオブジェクトの作成とライフサイクルを制御し、クライアントは初期化の結果としてComponent
を取得します。これは実質的に遅延インスタンス化です。
私は他の答えから少し脱線して反対します。あなたがどの言語で働いているかを知らずにこれに答えることは不可能です。これが価値のある計画であるかどうか、そして与えるための適切な「警告」ユーザーは、言語が提供するメカニズムと、その言語の他の開発者が従う傾向がある規則に完全に依存しています。
HaskellにFooManager
がある場合、criminalにして、ユーザーがFoo
sを管理できないものを作成できるようにします。これはとても簡単になり、Haskell開発者が期待する規則です。
一方、Cを作成している場合、同僚はあなたを連れ戻し、別のstruct FooManager
およびstruct UninitializedFooManager
さまざまな操作をサポートする型。これは、コードを不必要に複雑にして、メリットをほとんどもたらさないためです。
Pythonを作成している場合、これを行うための言語のメカニズムはありません。
あなたはおそらくHaskell、Python、またはCを作成していませんが、これらは型システムが期待され、実行できる機能の量/量の点で例を示しています。
あなたの言語で開発者の合理的な期待に従ってください、そして自然で慣用的な実装を持たないソリューションを過剰設計するという衝動に抵抗してください(間違いがない限り作成するのが簡単で、見つけるのが非常に難しいので、不可能にするために極端な長さに行く価値があります)。何が妥当かを判断するのに十分な言語の経験がない場合は、あなたよりもそれをよく知っている誰かのアドバイスに従ってください。
コードチェックを顧客に出荷したくないようです(ただし、プログラマーにとっては問題ないようです)。プログラミング言語で利用できる場合は、 assert 関数を使用できます。
このようにして、開発環境でチェックを行い(他の開発者が呼び出すテストは予測どおりに失敗します)、アサート(少なくとも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 ...構文の大部分を占めません。キャッチする必要はありません。
初期化に主に焦点を当てている現在の回答よりも一般的にこの問題を見てください。 a()
とb()
の2つのメソッドを持つオブジェクトを考えます。要件は、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); }
}
}
b()になるまで非表示のインターフェースに実装されているため、最初にa()を呼び出さずにa()を呼び出すことは不可能です。確かに、これはかなりの労力であるため、私は通常はこのアプローチを使用しませんが、それが有益である状況があります(特に、クラスが実装、または信頼性が重要なコードに精通していないプログラマによる多くの状況です。これは、既存の回答の多くが示唆しているように、ビルダーパターンの一般化であることに注意してください。同様に、実際の唯一の違いは、データが(返されたオブジェクトではなく元のオブジェクトに)保存される場所と、それが使用される予定であるとき(いつでも初期化中のみ)です。
答えは、はい、いいえ、そして時々です。 :-)
あなたが説明する問題のいくつかは、少なくとも多くの場合、簡単に解決されます。
コンパイル時のチェックは、実行時のチェックよりも確かに望ましいです。実行時のチェックは、まったくチェックしないよりも望ましいです。
REランタイム:
ランタイムチェックを使用して、関数を特定の順序などで強制的に呼び出すことは、少なくとも概念的には簡単です。どの関数が実行されたかを示すフラグを挿入し、各関数を「前提条件1でない場合または前提条件2でない場合は例外をスローする」のようなもので開始させます。チェックが複雑になった場合は、プライベート関数にプッシュすることができます。
いくつかのことを自動的に行うことができます。簡単な例を挙げると、プログラマーはオブジェクトの「怠惰な母集団」についてよく話します。インスタンスが作成されたら、is-populatedフラグをfalseに設定するか、一部のキーオブジェクト参照をnullに設定します。次に、関連データを必要とする関数が呼び出されると、フラグがチェックされます。 falseの場合、データを入力し、フラグをtrueに設定します。それが本当である場合、それはデータがそこにあると仮定して続くだけです。他の前提条件でも同じことができます。コンストラクターで、またはデフォルト値としてフラグを設定します。次に、いくつかの前提条件関数が呼び出されるべきポイントに達したときに、まだ呼び出されていない場合は、それを呼び出します。もちろん、これは、その時点で呼び出すデータがある場合にのみ機能しますが、多くの場合、「外部」呼び出しの関数パラメーターに必要なデータを含めることができます。
REコンパイル時:
他の人が言ったように、オブジェクトを使用する前に初期化する必要がある場合は、コンストラクターで初期化を行います。これはOOPのかなり典型的なアドバイスです。可能であれば、コンストラクタにオブジェクトの有効で使用可能なインスタンスを作成させる必要があります。
初期化する前にオブジェクトへの参照を渡す必要がある場合の対処方法についての説明が表示されます。頭の上の実際の例を考えることはできませんが、それが起こる可能性はあると思います。解決策が提案されていますが、ここで見た解決策や私が考えることのできる解決策はすべて厄介で、コードを複雑にします。ある時点で、実行時チェックではなくコンパイル時チェックができるように、見苦しくて理解しにくいコードを作成する価値はありますか?それとも、ニースであるが必須ではない目標のために、私自身や他の人のために多くの作業を作成していますか?
2つの関数を常に一緒に実行する必要がある場合、簡単な解決策はそれらを1つの関数にすることです。広告をページに追加するたびに、そのページのメトリックハンドラーも追加する必要がある場合と同様に、メトリック広告ハンドラーを追加するコードを「広告の追加」関数内に配置します。
コンパイル時に確認することがほとんど不可能になるものもあります。特定の関数を複数回呼び出せないという要件のように。 (私はそのような多くの関数を書きました。私は最近、魔法のディレクトリでファイルを探し、それを処理し、それらを削除する関数を書きました。もちろん、一度ファイルを削除すると、再実行することはできません。など)コンパイル時に同じインスタンスで関数が2回呼び出されるのを防ぐことができる機能を備えた言語は知りません。コンパイラがそれが起こっていることを明確に理解する方法を知りません。できることは、実行時のチェックを行うことだけです。特にランタイムチェックは明白ですよね。 「すでに存在する場合= trueの場合は例外をスローし、それ以外の場合は既に存在する= trueに設定する」。
REを実施することは不可能
ファイルまたは接続のクローズ、メモリの解放、DBへの最終結果の書き込みなど、完了時にクラスが何らかのクリーンアップを要求することは珍しくありません。どちらのコンパイル時にもこれを強制する簡単な方法はありません。または実行時。ほとんどのOOP言語は、インスタンスがガベージコレクションされたときに呼び出される「ファイナライザ」関数に何らかの種類の規定があると思いますが、ほとんどの場合、これが実行されることを保証できないと思います。OOP言語には、インスタンスの使用、「using」または「with」句、およびプログラムの終了時にスコープを設定するためのなんらかの準備が整った「dispose」関数が含まれていることがよくありますそのスコープは破棄を実行します。ただし、これにはプログラマーが適切なスコープ指定子を使用する必要があります。プログラマーが正しいことを強制するのではなく、正しいことを簡単にするだけです。
プログラマーにクラスを正しく使用するように強制することが不可能な場合は、誤った結果を出すのではなく、プログラムが間違った場合にプログラムが爆破するようにします。簡単な例:データを正しく入力するための関数の呼び出しに失敗した場合でも、ユーザーがnullポインター例外を取得しないように、一連のデータをダミー値に初期化する多くのプログラムを見てきました。そして私はいつもなぜだろうと思います。あなたは、バグを見つけにくくするために邪魔をしています。プログラマーが「データのロード」関数の呼び出しに失敗した後、あからさまにデータを使用しようとすると、nullポインター例外が発生し、例外によって問題の場所がすぐにわかります。ただし、そのような場合にデータを空白またはゼロにすることによって非表示にすると、プログラムは完全に実行されますが、結果が不正確になる可能性があります。場合によっては、プログラマは問題が発生したことに気付かないこともあります。彼が気づいた場合、彼はそれがどこで間違っていたかを見つけるために長い道をたどる必要があるかもしれません。勇敢に続けようとするよりも早く失敗する方がいい。
一般に、確かに、オブジェクトの誤った使用を単純に不可能にできる場合は常に良いことです。プログラマーが無効な呼び出しを行う方法がまったくないような方法で関数が構造化されている場合、または無効な呼び出しがコンパイル時エラーを生成する場合は、良いことです。
しかし、あなたが言わなければならないいくつかの点もあります、プログラマーは関数に関するドキュメントを読むべきです。
有用になる前に追加の初期化情報または他のオブジェクトへのリンクを必要とする基本クラスを実装する場合、その基本クラスを抽象化し、そのクラスでいくつかの抽象メソッドを定義して、基本クラスのフローで使用します( abstract Collection<Information> get_extra_information(args);
とabstract Collection<OtherObjects> get_other_objects(args);
は継承の規約により具象クラスによって実装する必要があり、その基本クラスのユーザーは基本クラスで必要なすべてのものを提供する必要があります。
したがって、基本クラスを実装すると、抽象メソッドを実装するだけでよいので、基本クラスを正しく動作させるために何を記述しなければならないかを即座に明確に把握できます。
編集:明確にするために、これは基本クラスのコンストラクターに引数を提供することとほとんど同じですが、抽象メソッドの実装では、抽象メソッドの呼び出しに渡された引数を処理で実装できます。または、引数が使用されていない場合でも、抽象メソッドの戻り値がメソッド本体で定義できる状態に依存している場合に役立ちます。これは、変数を引数として変数に渡す場合は不可能です。コンストラクタ。継承よりも構成を使用したい場合は、同じ原理に基づいて動的な動作を持つ引数を渡すことができます。
はい、あなたの本能はスポットオンです。何年も前に 私は雑誌で記事を書きました (このような場所ですが、デッドツリーにあり、月に1回発行します)議論デバッグ、Abugging、およびAntibugging。
コンパイラに誤用を検出させることができれば、それが最善の方法です。使用できる言語機能は言語によって異なり、指定しませんでした。とにかく、このサイトの個々の質問については、具体的な手法について詳しく説明します。
しかし、実行時ではなくコンパイル時に使用状況を検出するテストを行うことは、実際には同じ種類のテストです。あなたstillは、最初に適切な関数を呼び出して使用することを知る必要があります正しい方法で。
そもそも問題にならないようにコンポーネントを設計することははるかに微妙であり、私は Buffer is the Element の例が好きです。パート4(20年前に公開!Wow!)には、あなたの場合と非常によく似たディスカッションの例が含まれています。read()の使用を開始した後にbuffer()が呼び出されない可能性があるため、このクラスは慎重に使用する必要があります。むしろ、建設直後に1回だけ使用する必要があります。クラスが自動的にそして呼び出し側に見えないようにバッファを管理することで、概念的に問題を回避できます。
テストを実行時に実行して適切な使用を確認できるかどうかを確認しますか?
yesと言います。コードが保守され、特定の使用法が混乱したときに、チームはかなりのデバッグ作業をいつの日か節約できます。テストは、constraintsおよびinvariantsの正式なセットとして設定できます。テストできます。これは、状態を追跡し、チェックを実行するための余分なスペースのオーバーヘッドが、すべての呼び出しで常に残されることを意味しません。これはクラス内にあるため、呼び出すことができ、コードは実際の制約と仮定を文書化します。
おそらく、チェックはデバッグビルドでのみ行われます。
おそらくチェックは複雑です(たとえば、heapcheckを考えると)、それはpresentであり、たまに行うことができます。または、コードが機能しているときに、あちこちに追加された呼び出し上でいくつかの問題が表面化したり、特定のテストを行ってunit-testingまたは同じクラスにリンクする統合テストプログラム。
結局のところ、オーバーヘッドは取るに足らないものであると判断するかもしれません。クラスがファイルI/Oを実行し、追加の状態バイトとそのテストが行われる場合は何も起こりません。一般的なiostreamクラスは、ストリームの状態が悪いかどうかをチェックします。
あなたの本能は良いです。がんばり続ける!
情報の隠蔽(カプセル化)の原則は、クラスの外部のエンティティは、このクラスを適切に使用するために必要なもの以上のものを知ってはならないということです。
あなたのケースは、クラスのオブジェクトを外部ユーザーにクラスについて十分に知らせずに適切に実行しようとしているようです。 必要以上の情報を隠しました。
知恵の言葉:
*どちらの方法でも、クラスやコンストラクタ、メソッドを再設計する必要があります。
*クラスを適切に設計せず、正しい情報からクラスを奪うことにより、クラスが1つではなく複数の場所で壊れる危険性があります。
*例外とエラーメッセージの記述が不十分な場合、クラスはそれ自体についてさらに多くを提供します。
*最後に、部外者がa、b、cを初期化してからd()、e()、f(int)すると、抽象化に漏れが生じます。
C#を使用していることを指定したので、状況に対する実行可能なソリューションは Roslynコードアナライザー を活用することです。これにより、違反をすぐにキャッチでき、さらに コード修正を提案 することもできます。
これを実装する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コードアナライザーを作成したことがないので、これが最良の実装ではない可能性があります。ただし、Roslynコードアナライザーを使用してAPIが正しく使用されていることを確認するという考えは、100%健全です。