web-dev-qa-db-ja.com

コンストラクターは副作用のためだけに使用されるべきですか?

概要:副作用のためだけにコンストラクターを設計し、その戻り値を変数に割り当てずにコンストラクターを使用するのはなぜ間違っているのですか?

カードゲームのモデリングを含むプロジェクトに取り組んでいます。ゲームのパブリックAPIは、奇妙なパターンを使用して、RankオブジェクトとSuitオブジェクトをインスタンス化します。どちらも静的リストを使用してインスタンスを追跡します。標準のデッキでは、4つのスーツオブジェクトと13のランクオブジェクトしかありません。カードを構築するとき、ランクとスートは静的getRank()getSuit()で取得され、静的リストから識別されたアイテムを返します。ランクとスートには、removeRank()clear()などの他の静的メソッドがあります。

奇妙なのは、静的なリストに新しいランクまたはスートを追加するための静的なadd()メソッドがないことです。代わりに、この動作は、RankまたはSuitコンストラクターがnewを介して呼び出された場合にのみ発生します。 RankおよびSuitのインスタンスは静的getRank()で取得されるため、新しいコンストラクターからの戻り値はまったく関係ありません。

一部のサンプルコードは次のようになります。

_//set up the standard suits
Suit.clear(); //remove all Suit instances from the static list
new Suit("clubs", 0);  //add a new instance, throw away the returned value
new Suit("diamonds", 1);
new Suit("hearts", 2);
new Suit("spades", 3);

//create a card
Deck theDeck = new Deck()
for (int i = 0; i < 4; i++) {
    for (int j = 0; j < 13; j++) {
        Suit theSuit = Suit.getSuit(i);
        Rank theRank = Rank.getRank(j);
        theDeck.add(new Card(theSuit, theRank));
    }
}
_

コンストラクターによって返されるオブジェクトを割り当てないコードを見たことがありません。 Javaでテストするまで、これが可能だとは思いもしませんでした。しかし、静的addSuit()メソッドが見つからないという問題が発生した場合、応答は

「タイプSuitおよびRankのオブジェクトはコンストラクターを介して作成されます。意図は、オブジェクトがコンストラクターを介して作成され、クラスがそのようなオブジェクトのコレクションを維持するJava.lang.Enumクラスの動作を模倣することです。現在の動作は意図したとおりに設計されています」

この方法でコンストラクターを使用すると、本当に悪臭がするので、問題を再開したいのですが、サポート(コーディングガイドライン、アンチパターン、その他のドキュメント)で多くの資料を見つけることができません。これは実際、私が今まで遭遇したことのない有効なコーディングパターンですか?そうでない場合、APIを変更するために私ができる良い議論は何ですか?

これは 記事 であり、コンストラクターが何を行うことが期待されるかについて説明しています。

コンストラクターは常に新しいインスタンスを初期化する必要があることに同意でき、コンストラクターが常にインスタンスを完全に初期化する必要があることに今でも同意するかもしれません。

編集:問題に対するデザイナーの応答に関する追加情報を追加しました。コンストラクターがすべきこと、すべきでないことについて述べる記事をいくつか追加しました。

5
BrianHVB

確かに、これは技術的には機能しますが、いわゆる 最小驚きの原則 に違反します。これは、コンストラクターの使用方法に関する一般的な規則と、コンストラクターの利点とが異なるためです。さらに、これは [〜#〜] srp [〜#〜] に違反しています。これは、コンストラクターが通常行うべきものではなく、2つのことを行うためです-オブジェクトの構築および静的リストに追加します。

要するに、これは派手なコードの例ですが、良いコードではありません。

13
Doc Brown

この「パターン」の合理的な理由はわかりません。おっしゃるように、必要な機能は静的関数で実装する方が適切です。これを行うためにコンストラクターを使用しても実際には機能的な問題が発生しないことはおそらく/おそらく真実ですが、それは最小サプライズの原則を壊します:あなたのように、私はそのコードを見て、「ハァッ! 」私はadvantageを見て、静的関数を持つよりもこの方法でそれを行うことはできません。

スーツのグローバルな性質を少し後退させることもコードの匂いです。同じプログラムで異なるスーツを使用して、ブリッジと(たとえば)タロットの両方をモデル化したい場合はどうなりますか?より優れた設計には、SuitCollectionまたは同様のものが含まれます。

あなたはそれについて何ができますか?それは状況に大きく依存します。これが商業プロジェクトである場合は、同僚と話し合って、この決定に正当な理由があるかどうかを確認し、そうでない場合は、その応答がある人に直接話しかけて、話し合ってください。オープンソースプロジェクトなどの場合、私の個人的なアドバイスは「逃げる」ことです。質の低いコードと不十分な通信を組み合わせても、楽しいプロジェクトにはなりません。

6
Philip Kendall

まず、ソフトウェア設計の主な目標の1つが、コードが理解しやすく、変更しやすいことを確認することによる、保守性です。

次に、コンストラクタは、新しく作成されたオブジェクトを有効な状態に初期化する特別なタイプのメソッドであるとしましょう。

これらの2つの仮定に照らして、適切な設計に副作用コンストラクターを含めることは無効であると言っても安全です。コンストラクターは、その性質上、新しく作成されたオブジェクトとそれが囲むオブジェクトに影響を与えるべきではありません。

このコードを考えると:

_public void MyMethod()
{
    DoSomething();
    new MyClass();
    DoSomething();
}
_

簡単に維持できると思いますか? MyClass()DoSomething()を並べ替えることができますか? MyClassコンストラクターにアクセスして、それが何をするかを見るまではわかりません。

このようにして、プログラムの小さな部分を変更するために、プログラム全体を理解する必要があることに気づくでしょう。これは、優れたデザインとは正反対です。

その上、通常、副作用コンストラクターがあると、バックグラウンドで何か静的なことが行われていることを意味します。これも、テストと適切な設計に関しては、危険信号です。静的なコンテキストは一見安くて扱いやすいように見えますが、非常に速く成長し、将来のプログラムの変更を妨げます。

明らかに、プログラミングは妥協の仕事であり、カードタイプの静的なコレクションの存在が市場投入までの時間の大幅な改善を実現する場合(私はそれについて非常に強い疑問があります)、静的を介して表現された意図を確認する方がはるかに良いでしょうメソッドと1回限りの初期化:

_public void ApplicationSetup()
{
    Suit.Initialize("clubs", "diamonds", "hearts", "spades"); // throws if called more than once
}
_

ただし、この方法でも、初期化が実際に行われる前にSuitを初期化する必要があるプログラムの部分を呼び出すことになる可能性があります。 「このクラス/メソッドではSuitの初期化が必要」を表現する最良の方法は、Suitをパラメーターとして受け取ることです。これは、静的なSuitアイデア全体とは対照的です。

3

番号!それは悪いコードです!これは混乱を招くパターンであり、単純な方法で実行する場合に比べて利点がありません。

コンストラクターは、空想の静的メソッドとしてのみ使用されます。 Suit.addSuit("spades", 3);として呼び出すと、混乱することなく同じ効果が得られ、役に立たないインスタンスが返されます。

Enumの比較は、奇妙なコンストラクタの使用を正当化しません-enumはこのパターンを使用しません。列挙型では、可能なオプションを定義するのではなく、コンストラクターを使用して、タイプのを作成します(たとえば、Suit.getSuit()の代わりとして) 、専用の構文で発生します。

さらに、Suit.Clear()の使用は、これがEnumとまったく同等ではないことを示します。列挙型のポイントは、使用可能な値が定義されて修正されていることですコンパイル時したがって、静的にチェックできます。 Clear()メソッドは、オプションが実行時に変化することを示します。実行時にスーツのセットが異なる場合は、それを静的コレクションとして定義しないでください。

1
JacquesB

このパターンは貧弱で、さまざまなソフトウェアエンジニアリングの原則に違反しているという他の回答にも同意します。どうやらあなたの同僚はそれらをあまり気にしません。必要なのは、このパターンが失敗する、または使いにくいサンプルユースケースです。ここにいくつかあります:

  1. ゲームはブリッジです。入札後、4つの既存のスーツの1つの名前を「切り札」に変更する必要がある場合があります。そのコードを書いてみてください。私はそれが気まぐれでバグが多いと予測し、既存のコードを壊すことさえあるでしょう。
  2. ゲームはハーツです。ハートの女王の名前を「The Lady」に変更します。 (PGバージョン)。
  3. あなたのゲームは中国とアルメニアで大ヒットしています。ゲーム中にいつでも選択できる言語のメニューを追加します。競合他社より先にこのバージョンを市場に出す必要があります!そうそう、それはマルチプレイヤーゲームで、アルメニア対上海の大きな試合が近づいており、ケーブルテレビの視聴者が米国にいます。
1
user949300