web-dev-qa-db-ja.com

ミューテーションメソッド用の個別のインターフェース

私はいくつかのコードのリファクタリングに取り組んでおり、ウサギの穴を一歩下がったと思います。私はJavaで例を書いていますが、不可知論である可能性があります。

次のように定義されたインターフェイスFooがあります

_public interface Foo {

    int getX();

    int getY();

    int getZ();
}
_

そして実装は

_public final class DefaultFoo implements Foo {

    public DefaultFoo(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public int getZ() {
        return z;
    }

    private final int x;
    private final int y;
    private final int z;
}
_

一致するミューテーターを提供するインターフェースMutableFooも持っています

_/**
 * This class extends Foo, because a 'write-only' instance should not
 * be possible and a bit counter-intuitive.
 */
public interface MutableFoo extends Foo {

    void setX(int newX);

    void setY(int newY);

    void setZ(int newZ);
}
_

存在する可能性のあるMutableFooの実装がいくつかあります(まだ実装していません)。それらの1つは

_public final class DefaultMutableFoo implements MutableFoo {

    /**
     * A DefaultMutableFoo is not conceptually constructed 
     * without all values being set.
     */
    public DefaultMutableFoo(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public int getX() {
        return x;
    }

    public void setX(int newX) {
        this.x = newX;
    }

    public int getY() {
        return y;
    }

    public void setY(int newY) {
        this.y = newY;
    }

    public int getZ() {
        return z;
    }

    public void setZ(int newZ) {
        this.z = newZ;
    }

    private int x;
    private int y;
    private int z;
}
_

これらを分割したのは、それぞれが使用される可能性が同じであるためです。つまり、これらのクラスを使用する誰かが不変のインスタンスを必要とする可能性が高いのです。

私が持っている主要なユースケースは、ゲームの特定の戦闘の詳細(ヒットポイント、攻撃、防御)を表すStatSetと呼ばれるインターフェースです。ただし、「有効な」統計、または実際の統計は、変更できない基本統計と、増加できるトレーニング済み統計の結果です。これら2つは

_/**
 * The EffectiveStats can never be modified independently of either the baseStats
 * or trained stats. As such, this StatSet must never provide mutators.
 */
public StatSet calculateEffectiveStats() {
    int effectiveHitpoints =
        baseStats.getHitpoints() + (trainedStats.getHitpoints() / 4);
    int effectiveAttack = 
        baseStats.getAttack() + (trainedStats.getAttack() / 4);
    int effectiveDefense = 
        baseStats.getDefense() + (trainedStats.getDefense() / 4);

    return StatSetFactory.createImmutableStatSet(effectiveHitpoints, effectiveAttack, effectiveDefense);
}
_

trainedStatsは戦闘のたびに増加します。

_public void grantExperience() {
    int hitpointsReward = 0;
    int attackReward = 0;
    int defenseReward = 0;

    final StatSet enemyStats = enemy.getEffectiveStats();
    final StatSet currentStats = player.getEffectiveStats();
    if (enemyStats.getHitpoints() >= currentStats.getHitpoints()) {
        hitpointsReward++;
    }
    if (enemyStats.getAttack() >= currentStats.getAttack()) {
        attackReward++;
    }
    if (enemyStats.getDefense() >= currentStats.getDefense()) {
        defenseReward++;
    }

    final MutableStatSet trainedStats = player.getTrainedStats();
    trainedStats.increaseHitpoints(hitpointsReward);
    trainedStats.increaseAttack(attackReward);
    trainedStats.increaseDefense(defenseReward);
}
_

戦闘直後は増えません。特定のアイテムを使用したり、特定の戦術を使用したり、戦場を巧みに使用したりすると、さまざまな経験を与えることができます。

今私の質問のために:

  1. アクセサーとミューテーターによってインターフェースを個別のインターフェースに分割するための名前はありますか?
  2. それらが同じように使用される可能性が高い場合、「正しい」アプローチでそれらを分割していますか、または代わりに使用する必要がある別のより受け入れられたパターンがあります(例:Foo foo = FooFactory.createImmutableFoo();これはDefaultFooまたはDefaultMutableFooですが、createImmutableFooFoo)を返すため非表示になりますか?
  3. このパターンを使用することですぐに予測できる欠点はありますか?インターフェース階層を複雑にすることを除いて?

この方法で設計を始めたのは、インターフェイスのすべての実装者が可能な限り最も単純なインターフェイスに準拠し、それ以上は何も提供しないという考え方の私がいるためです。インターフェイスにセッターを追加することで、有効な統計をその部分に関係なく変更できるようになりました。

EffectiveStatSetの新しいクラスを作成することは、機能を拡張しないため、あまり意味がありません。実装を変更して、EffectiveStatSetを2つの異なるStatSetsの合成にすることもできますが、それは適切な解決策ではないと感じています。

_public class EffectiveStatSet implements StatSet {

    public EffectiveStatSet(StatSet baseStats, StatSet trainedStats) {
        // ...
    }

    public int getHitpoints() {
        return baseStats.getHitpoints() + (trainedStats.getHitpoints() / 4);
    }
}
_
11
Zymus

私には、問題を探している解決策があるようです。

アクセサーとミューテーターによってインターフェースを個別のインターフェースに分割するための名前はありますか?

これは少々挑発的なことかもしれませんが、実際には私はそれを「過剰設計」または「過剰複雑化」と呼んでいます。同じクラスの変更可能なバリアントと不変のバリアントを提供することで、同じ問題に対して機能的に同等な2つのソリューションを提供します。これらのソリューションは、パフォーマンス動作、API、副作用に対するセキュリティなどの非機能的な側面のみが異なります。それは、どちらを好むかを決めるのを恐れているためか、C#でC++の「const」機能を実装しようとしているためだと思います。すべてのケースの99%で、ユーザーが可変または不変のバリアントを選択しても、大きな違いはないと思います。彼は、どちらか一方の問題を解決できます。したがって、「使用されるクラスの可能性」は、ライブラリで提供するものの結果である可能性があります。同じ問題に対してほぼ同じ2つのソリューションを提供する場合、ユーザーがバリアントAまたはBを選択する可能性は50%です。ソリューションを1つだけ提供すると、そのソリューションを使用する可能性が高く、満足します。

例外は、数万人以上のプログラマが使用する新しいプログラミング言語または多目的フレームワークを設計する場合です。次に、汎用データ型の不変および可変のバリアントを提供すると、実際にスケーリングが向上します。しかし、これは何千もの異なる使用シナリオがある状況です-おそらくこれはあなたが直面している問題ではないと思いますか?

それらが同じように使用される可能性が高い場合、または異なる、より受け入れられたパターンがある場合、この方法でそれらを分割しています

「より受け入れられたパターン」は、KISS-シンプルで愚かなものにしてください。ライブラリ内の特定のクラス/インターフェースの変更可能性を判断するかどうかを決定します。たとえば、「StatSet」の場合十数個以上の属性があり、それらはほとんど個別に変更されるので、変更可能なバリアントを使用し、変更すべきでない基本統計を変更しないほうがよいでしょう。属性X、Yを持つFooclassのようなもの、Z(3次元ベクトル)、私はおそらく不変のバリアントを好むでしょう。

このパターンを使用することですぐに予測できる欠点はありますか?インターフェース階層を複雑にすることを除いて?

過度に複雑な設計により、ソフトウェアのテスト、保守、進化が困難になります。

6
Doc Brown

アクセサーとミューテーターによってインターフェースを個別のインターフェースに分割するための名前はありますか?

この分離が便利で、benefitを提供する場合、これには名前がある可能性がありますi not see。分離が利益をもたらさない場合、他の2つの質問は意味がありません。

2つの別々のインターフェースが私たちに利益をもたらすビジネスユースケースを教えていただけますか、またはこの質問は学術的な問題(YAGNI)ですか?

カートを入れ替えることができる(もっと多くの商品を入れることができる)ショップは、お客様が商品を変更できない注文になり得ると思います。注文ステータスはまだ変更可能です。

私が見た実装はインターフェースを分離していません

  • javaにはReadOnlyListインターフェースはありません

ReadOnlyバージョンは「WritableInterface」を使用し、書き込みメソッドが使用された場合に例外をスローします

2
k3b

ここに大きな問題があります:データ構造が不変だからといって、変更されたバージョンが必要ないという意味ではありません。データ構造の real 不変バージョンは、setXsetY、およびsetZメソッドを提供します-それらは単に new 構造は、呼び出したオブジェクトを変更する代わりに。

_// Mutates mutableFoo
mutableFoo.setX(...)
// Creates a new updated immutableFoo, existing immutableFoo is unchanged
newFoo = immutableFoo.setX(...)
_

では、システムの一部に、他の部分を制限しながら変更できるようにするにはどうすればよいでしょうか。不変構造のインスタンスへの参照を含む可変オブジェクト。基本的に、他のすべてのクラスにその統計の不変のビューを提供しながら、プレーヤークラスがその統計オブジェクトを変更できる代わりに、その統計不変であり、 player は変更可能です。の代わりに:

_// Stats are mutable, mutates self.stats
self.stats.setX(...)
_

あなたが持っているだろう:

_// Stats are immutable, mutates self, setX() returns new updated stats
self.stats = self.stats.setX(...)
_

違いを見ます?統計オブジェクトは完全に不変ですが、プレーヤーの current 統計は、不変オブジェクトへの可変参照です。 2つのインターフェースを作成する必要はまったくありません。データ構造を完全に不変にして、状態の格納にたまたま使用している場所への可変参照を管理するだけです。

これは、システム内の他のオブジェクトが統計オブジェクトへの参照に依存できないことを意味します-プレーヤーが統計を更新した後、それらが持っている統計オブジェクトは、プレーヤーが持っている同じ統計オブジェクトではありません。

これはとにかく理にかなっています。概念的には stats が変化するのではなく、プレーヤーの現在の統計だからです。統計オブジェクトではありません。 reference は、プレーヤーオブジェクトが持つ統計オブジェクトへの参照です。したがって、その参照に依存するシステムの他の部分は、プレーヤーの基になるstatsオブジェクトを取得してどこかに保存し、ミューテーションによって更新することに依存する代わりに、player.currentStats()などを明示的に参照する必要があります。

1
Jack

変更可能な収集インターフェースと読み取り専用の契約収集インターフェースの分離は、インターフェース分離の原則の例です。原則の適用ごとに特別な名前が付けられているとは思いません。

ここにいくつかの単語に注意してください:「読み取り専用契約」と「コレクション」。

読み取り専用契約は、クラスAがクラスBに読み取り専用アクセス権を与えることを意味しますが、基になるコレクションオブジェクトが実際には不変であることを意味しません。不変とは、どのエージェントによっても、今後変更されないことを意味します。読み取り専用の契約は、受信者がそれを変更することを許可されていないことを示しているだけです。他の誰か(特に、クラスA)はそれを変更できます。

オブジェクトを不変にするには、オブジェクトを真に不変にする必要があります。エージェントは、オブジェクトを要求しても、データを変更しようとする試みを拒否する必要があります。

パターンは、リスト、シーケンス、ファイル(ストリーム)などのデータのコレクションを表すオブジェクトで最もよく見られます。


「不変」という言葉は流行しますが、概念は新しいものではありません。そして、不変性を使用する多くの方法と、他のもの(すなわち、その競合他社)を使用してより良い設計を達成する多くの方法があります。


これが私のアプローチです(不変性に基づくものではありません)。

  1. 値タプルとも呼ばれるDTO(データ転送オブジェクト)を定義します。
    • このDTOには、hitpointsattackdefenseの3つのフィールドが含まれます。
    • フィールドのみ:パブリック、誰でも書き込み可能、​​保護なし。
    • ただし、DTOは使い捨てオブジェクトである必要があります。クラスAがDTOをクラスBに渡す必要がある場合、DTOはそのコピーを作成し、代わりにコピーを渡します。そのため、Bは、Aが保持しているDTOに影響を与えることなく、好きな方法(書き込み)でDTOを使用できます。
  2. grantExperience関数は2つに分解されます:
    • calculateNewStats
    • increaseStats
  3. calculateNewStatsは、2つのDTOから入力を取得します。1つはプレーヤーの統計を表し、もう1つは敵の統計を表し、計算を実行します。
    • 入力については、必要に応じて呼び出し元が基本、トレーニング済み、または効果的な統計の中から選択する必要があります。
    • 結果は、各フィールド(hitpointsattackdefense)にその能力のインクリメントされる量が格納される新しいDTOになります。
    • 新しい「増分する量」DTOは、これらの値の上限(課される最大キャップ)を考慮しません。
  4. increaseStatsは、(DTOではなく)プレーヤーのメソッドであり、「増分する量」のDTOを取得し、その増分をプレーヤーが所有するDTOに適用し、プレーヤーのトレーニング可能なDTOを表します。
    • これらの統計に適用可能な最大値がある場合、それらはここで適用されます。

calculateNewStatsが(2つの入力DTOの値を超えて)他のプレーヤーまたは敵の情報に依存していないことが判明した場合、このメソッドはプロジェクト内の任意の場所に配置できます。

calculateNewStatsがプレーヤーおよび敵オブジェクトに完全に依存していることが判明した場合(つまり、将来のプレーヤーおよび敵オブジェクトは新しいプロパティを持つ可能性があり、calculateNewStatsを更新して、可能な限り新しいプロパティ)、次にcalculateNewStatsは、DTOだけでなく、これら2つのオブジェクトを受け入れる必要があります。ただし、その計算結果は、増分DTO、または増分/アップグレードの実行に使用される情報を運ぶ単純なデータ転送オブジェクトのままです。

1
rwong