web-dev-qa-db-ja.com

オブジェクトのすべての作業をコンストラクターで行う理由はありますか?

これは私のコードでも同僚のコードでもない、と言って前置きさせてください。数年前、私たちの会社が小規模だったとき、私たちには必要ないくつかのプロジェクトがありました。現在、私はアウトソーシングや請負業者に対しては一般的に何も反対していませんが、彼らが作成したコードベースは大量のWTFです。そうは言っても、それは(ほとんど)機能するので、私が見たアウトソーシングプロジェクトの上位10%にあると思います。

私たちの会社が成長するにつれ、社内での開発をより多く取り入れることを試みました。この特定のプロジェクトは私の膝に着地したので、私はそれを調べ、片付け、テストの追加などを行ってきました。

何度も繰り返されているパターンが1つあります。その理由は何かあるのではないかと思って、見ないだけなのかと思うほど、ひどく恐ろしいようです。パターンは、パブリックメソッドまたはメンバーのないオブジェクトであり、オブジェクトのすべての作業を実行するパブリックコンストラクターにすぎません。

たとえば、(それが問題になる場合、コードはJavaですが、これがより一般的な質問になることを願っています):

public class Foo {
  private int bar;
  private String baz;

  public Foo(File f) {
    execute(f);
  }

  private void execute(File f) {
     // FTP the file to some hardcoded location, 
     // or parse the file and commit to the database, or whatever
  }
}

ご参考までに、このタイプのコードは多くの場合、次の方法で呼び出されます。

for(File f : someListOfFiles) {
   new Foo(f);
}

さて、ループでインスタンス化されたオブジェクトは一般に悪い考えであり、コンストラクターは最小限の作業を行うべきであるとずっと以前に教えられました。このコードを見ると、コンストラクターを削除してexecuteをpublic staticメソッドにする方が良いように見えます。

私は請負業者になぜこのように行われたのか尋ねました、そして私が得た応答は「あなたが望むならそれを変えることができます」でした。これは本当に役に立ちませんでした。

とにかく、プログラミング言語でこのようなことをする理由はありますか、それともこれはデイリーWTFへの別の提出ですか?

49
Kane

さて、リストを下に行く:

ループでインスタンス化されたオブジェクトは一般に悪い考えであると私はずっと前に教えられました

私が使用した言語ではありません。

Cでは、変数を事前に宣言することをお勧めしますが、それはあなたが言ったこととは異なります。ループの上でオブジェクトを宣言して再利用すると、少し速くなるかもしれませんが、この速度の増加が意味をなさない言語がたくさんあります(そしておそらくあなたのために最適化を行ういくつかのコンパイラがあります:))。

一般に、ループ内にオブジェクトが必要な場合は作成します。

コンストラクタは最小限の作業を行う必要があります

コンストラクターは、オブジェクトのフィールドをインスタンス化し、オブジェクトを使用できるようにするために必要なその他の初期化を行う必要があります。これは通常、コンストラクターが小さいことを意味しますが、これがかなりの作業量になるシナリオがあります。

プログラミング言語でこのようなことをする理由はありますか、それともこれはデイリーWTFへの別の提出ですか?

それはデイリーWTFへの提出物です。確かに、コードでできることはもっと悪いものもあります。問題は、作成者がクラスとは何か、およびそれらの使用方法について非常に誤解していることです。具体的には、このコードで間違っているのは次のとおりです。

  • クラスの誤用:クラスは基本的に関数のように振る舞います。あなたが述べたように、それは静的関数で置き換えられるべきであるか、またはその関数はそれを呼び出しているクラスで実装されるべきです。それが何をするか、どこで使用されるかに依存します。
  • パフォーマンスのオーバーヘッド:言語によっては、オブジェクトの作成が関数の呼び出しよりも遅くなる場合があります。
  • 一般的な混乱:このコードの使用方法は、プログラマーにとって一般的に混乱します。それが使われているのを見なければ、誰も作者が問題のコードをどのように使用するつもりであったのかわからないでしょう。
60
riwalk

私にとって、コンストラクターで多くの作業を行うオブジェクトを取得することは少し驚きです。そのため、私はそれをお勧めしません(最小の驚きの原則)。

良い例での試み

この使用法がアンチパターンであるかどうかはわかりませんが、C#では、次のようなコードが見られます(例の一部を構成しています)。

using (ImpersonationContext administrativeContext = new ImpersonationContext(ADMIN_USER))
{
    // Perform actions here under the administrator's security context
}
// We're back to "normal" privileges here

この場合、ImpersonationContextクラスは、コンストラクターとDisposeメソッドですべての作業を行います。これは、C#の開発者がよく理解しているC#のusingステートメントを利用しているため、usingの外に出ると、コンストラクターで発生するセットアップが確実にロールバックされます。ステートメント、例外が発生した場合でも。これはおそらく私がこれまでに見た中で最高の使用法です(つまり、コンストラクターでの "work")。ただし、それを "良い"と考えるかどうかはまだわかりません。この場合、それはかなり自明で機能的で簡潔です。

良い、そして同様のパターンの例は command pattern です。あなたは間違いなくそこでコンストラクターでロジックを実行しませんが、クラス全体がメソッド呼び出し(メソッドの呼び出しに必要なパラメーターを含む)と同等であるという意味で似ています。このようにして、メソッド呼び出し情報をシリアル化するか、後で使用するために渡すことができます。これは非常に便利です。おそらくあなたの請負業者は同様のことを試みていましたが、私はそれを非常に疑っています。

あなたの例へのコメント:

それは非常に明確なアンチパターンだと思います。それは何も追加せず、期待されたものに反し、コンストラクターで過度に長い処理を実行します(コンストラクターでFTP?WTFは確かに、この方法でWebサービスを呼び出すことが知られていると思いますが)。

引用を見つけるのに苦労していますが、Grady Boochの本「Object Solutions」には、C++に移行するC開発者のチームに関する逸話があります。どうやら彼らは訓練を受けており、経営陣からは「オブジェクト指向」のものを絶対にやらなければならないと言われました。何が悪いのかを理解するために持ち込まれ、作者はコードメトリックツールを使用して、クラスごとのメソッドの平均数は正確に1であり、「Do It」というフレーズのバリエーションであることがわかりました。私はこれまでこの特定の問題に遭遇したことがないことを言わなければなりませんが、どうすればその方法と理由を本当に理解せずに、人々がオブジェクト指向コードの作成を余儀なくされている兆候のようです。

27
Daniel B

番号。

すべての作業がコンストラクターで行われる場合、それはオブジェクトがそもそも必要とされなかったことを意味します。おそらく、請負業者は単にオブジェクトをモジュール化するために使用していました。その場合、静的メソッドを持つインスタンス化されていないクラスでも、余分なオブジェクトインスタンスを作成しなくても同じ利点が得られます。

オブジェクトコンストラクタですべての作業を行うことが許容されるケースは1つだけです。これは、オブジェクトの目的が、作業を実行するのではなく、長期的に一意のアイデンティティを表すことである場合です。このような場合、オブジェクトは意味のある作業を実行する以外の理由で保持されます。たとえば、シングルトンインスタンスです。

14
Kevin A. Naudé

私は確かに、コンストラクタで何かを計算するために多くの作業を行う多くのクラスを作成しましたが、オブジェクトは不変なので、その計算の結果をプロパティを介して利用できるようにし、それ以外は何もしません。これは、通常の不変性です。

ここで奇妙なのは、副作用のあるコードをコンストラクターに入れることです。プロパティやメソッドにアクセスせずにオブジェクトを作成して破棄するのは奇妙に見えます(ただし、少なくともこの場合、他のことが起こっていることは明らかです)。

これは、関数がパブリックメソッドに移動され、クラスのインスタンスが挿入され、forループがメソッドを繰り返し呼び出す場合に適しています。

7
Scott Whitlock

コンストラクターですべてのオブジェクトの作業を行う理由はありますか?

番号。

  • コンストラクターには副作用があってはなりません。
    • プライベートフィールドの初期化以上のものは、副作用と見なす必要があります。
    • 副作用のあるコンストラクターは、単一責任原則(SRP)を破り、オブジェクト指向プログラミング(OOP)の精神に反して実行されます。
  • コンストラクターは軽量でなければならず、失敗してはなりません。
    • たとえば、コンストラクター内のtry-catchブロックを見ると、いつも身震いしています。コンストラクターは例外をスローしたり、エラーをログに記録したりしてはなりません。

これらのガイドラインに合理的に疑問を投げかけ、「しかし、私はこれらのルールに従わないので、私のコードは正常に動作します!」それに対して、私は「そうでない限り、それは本当かもしれない」と答えるでしょう。

  • コンストラクター内の例外とエラーは非常に予想外です。そうするように指示されない限り、将来のプログラマーは、これらのコンストラクター呼び出しを防御的なコードで囲む傾向はありません。
  • 本番環境で何かが失敗した場合、生成されたスタックトレースは解析が難しい場合があります。スタックトレースの先頭はコンストラクター呼び出しを指している可能性がありますが、コンストラクターで多くのことが発生し、失敗した実際のLOCを指しているとは限りません。
    • これが当てはまる多くの.NETスタックトレースを解析しました。
4
Jim G.

これをしている人は、Javaの制限をハッキングしようとしていたため、ファーストクラスの市民として関数を渡すことができません。関数を渡す必要があるときはいつでも、 applyなどのメソッドでクラス内にラップする。

ですから、彼はショートカットを使用し、関数の置き換えとしてクラスを直接使用したと思います。関数を実行したいときはいつでも、クラスをインスタンス化するだけです。

これは、少なくともここで理解しようとしているため、明らかに良いパターンではありませんが、Javaの関数型プログラミングに似た方法を行うには、最も冗長な方法ではないかと思います。

2
Andrea

私はここで流れに逆らうつもりです-すべてがいくつかのクラスにある必要があり、静的クラスを持つことができない言語では、必要な機能が孤立している状況に遭遇する可能性がありますput somewhereであり、他のものには適合しません。選択できるのは、関連のないさまざまな機能をカバーするユーティリティクラスか、基本的にこれだけを行うクラスです。

このようなビットが1つしかない場合、静的クラスは特定のクラスです。したがって、すべての作業を行うコンストラクター、またはすべての作業を行う単一の関数のいずれかを使用します。コンストラクターですべてを行うことの利点は、一度だけ発生することです。これにより、再利用やスレッド化を心配することなく、プライベートフィールド、プロパティ、メソッドを使用できます。コンストラクター/メソッドがある場合、パラメーターを単一のメソッドに渡したくなるかもしれませんが、スレッド化の潜在的な問題を引き起こすプライベートフィールドまたはプロパティを使用する場合。

ユーティリティクラスを使用しても、ほとんどの人は静的クラスがネストされているとは考えません(言語によっては不可能かもしれません)。

基本的に、これは言語で許可されている範囲内でシステムの残りの部分から動作を分離する比較的理解しやすい方法ですが、できるだけ多くの言語を利用できます。

率直に言って、私はあなたの請負業者がしたのと同じように答えたでしょう、それを2つの呼び出しに変えることはマイナーな調整です、一方を他方に推薦することはそれほど多くありません。そこにはどのような不利な点がありますか、おそらく実際よりも仮説的なものです。なぜそれが重要ではなく、おそらくデフォルトのアクションだけではそれほど決定されなかったのに、決定を正当化しようとするのでしょうか。

1
jmoreno

Pythonのように、関数とクラスがほとんど同じである言語に共通するパターンがあります。Pythonでは、基本的にオブジェクトを使用して、副作用が多すぎたり、名前付きオブジェクトが複数返されたりする関数になっています。

この場合、オブジェクト自体は単一の作業バンドルのラッパーにすぎず、別の関数に詰め込む十分な理由がないため、すべての作業がコンストラクターで実行されるわけではありません。何かのようなもの

var parser = XMLParser()
var x = parser.parse(string)
string a = x.LookupTag(s)

本当により良いです

 var x = ParsedXML(string)
 string a = x.LookupTag(s)

副作用を完全に直接持つ代わりに、オブジェクトを呼び出してキューに副作用を強制することができます。これにより、視認性が向上します。オブジェクトに悪影響を与える場合は、すべての作業を行ってからオブジェクトを渡すことができます。オブジェクトに悪影響を与え、ダメージを与えます。この方法でオブジェクトを識別し、呼び出しを分離することは、複数のそのようなオブジェクトを関数に一度に渡すよりも明確です。

x.UpdateView(v)

オブジェクトのアクセサーを通じて複数の戻り値を作成できます。すべての作業は完了しましたが、すべてを状況に応じて必要とし、複数の参照を関数に渡したくありません。

var x = new ComputeStatistics(inputFile)
int max = x.MaxOptions;
int mean = x.AverageOption;
int sd = x.StandardDeviation;
int k = x.Kurtosis;
...

したがって、Python/C#の共同プログラマーとして、これは私にとって奇妙なことではないようです。

1

コンストラクタ/デストラクタがすべての作業を行う1つのケースが思い浮かびます:

ロック。通常、ロックの有効期間中にロックを操作することはありません。 C#のアーキテクチャを使用すると、デストラクタを明示的に参照せずにこのようなケースを処理するのに適しています。ただし、すべてのロックがこの方法で実装されているわけではありません。

また、ランタイムライブラリのバグにパッチを適用するための、コンストラクタのみのコードに相当するコードも記述しました。 (実際にコードを修正すると、他の問題が発生します。)パッチを元に戻す必要がないため、デストラクタはありませんでした。

1
Loren Pechtel

私にとっての経験則は、メンバー変数の初期化を除いて、コンストラクターで何もしないことです。その最初の理由は、SRPの原則に従うのに役立つことです。通常、長い初期化プロセスは、クラスが実行する以上のことを示しており、初期化はクラスの外のどこかで行われる必要があるためです。 2番目の理由は、この方法では必要なパラメーターのみが渡されるため、結合の少ないコードを作成するためです。複雑な初期化を持つコンストラクターは、通常、別のオブジェクトを構築するためだけに使用され、元のクラスでは使用されないパラメーターを必要とします。

1
rmaruszewski