突然変異法のための独立したインターフェース


11

私はいくつかのコードのリファクタリングに取り組んできましたが、ウサギの穴を降りる最初の一歩を踏み出したのではないかと思います。私は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();返すことができているDefaultFooDefaultMutableFooが、隠されているので、createImmutableFooリターンFoo)?
  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);
    }
}


1
問題は、その「不変」インターフェースにアクセスしている場合でも、オブジェクトは可変であると思います。Player.TrainStats()で可変オブジェクトを持つ方が良い
ユアン

@gnat私はそれについてもっと学ぶことができる場所への参照を持っていますか?編集された質問に基づいて、どのように、またはどこでそれを適用できるかわかりません。
ザイムス

@gnat:リンク(およびその2次および3次リンク)は非常に便利ですが、キャッチーな知恵のフレーズはほとんど役に立ちません。誤解や軽deを招くだけです。
-rwong

回答:


6

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

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

これは少し刺激的かもしれませんが、実際には「過剰設計」または「過剰複雑化」と呼びます。同じクラスの可変および不変のバリアントを提供することにより、同じ問題に対して機能的に同等の2つのソリューションを提供します。これらのソリューションは、パフォーマンス動作、API、副作用に対するセキュリティなどの非機能面でのみ異なります。どちらを好むかを決めるのを恐れているか、C#でC ++の「定数」機能を実装しようとしているからだと思います。すべての場合の99%で、ユーザーが可変または不変のバリアントを選択しても、大きな違いは生じないと思います。彼はどちらか一方の問題を解決できます。したがって、「使用されるクラスの可能性」

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

それらをこのように分割するのは、それらが等しく使用される可能性が高い場合、または別のより受け入れられたパターンがある場合、「正しい」アプローチです

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

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

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


1
私はあなたが「 -愚か、それをシンプルに保つKISS」を意味だと思う
Epicblood

2

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

この分離が有用であり、私が見ない利点を提供する場合、これには名前があります。分離が利益をもたらさない場合、他の2つの質問は意味をなしません。

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

顧客が商品を変更することができない注文になる可能性のある可変カート(より多くの記事を入れることができる)を備えたショップを考えることができます。注文ステータスは引き続き変更可能です。

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

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

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


主なユースケースを説明するためにOPを変更しました。
ザイマス

2
「javaにはインターフェースReadOnlyListはありません」-率直に言って、あるはずです。List <T>をパラメーターとして受け入れるコードがある場合、変更できないリストで機能するかどうかを簡単に判断することはできません。リストを返すコードです。リストを安全に変更できるかどうかを知る方法はありません。唯一の選択肢は、潜在的に不完全または不正確な文書に依存するか、万が一に備えて防御的なコピーを作成することです。読み取り専用のコレクションタイプを使用すると、これがはるかに簡単になります。
ジュール

@Jules:確かに、Javaの方法は上記のすべてのようです。ドキュメントが完全かつ正確であることを確認し、万が一に備えて防御的なコピーを作成することです。確かに非常に大規模なエンタープライズプロジェクトに対応します。
rwong

1

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

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

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

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

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


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


これが私のアプローチです(不変性に基づいていません)。

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

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

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


1

ここには大きな問題があります。データ構造が不変だからといって、それを修正したバージョンが必要ないわけではありません本物の不変のデータ構造のバージョン提供しませんsetXsetYsetZ方法-彼らはちょうど返す新しい代わりにあなたがそれらを上と呼ばれるオブジェクトを変更する構造を。

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

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

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

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

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

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

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

概念的には変化しているのは実際には統計ではなく、プレイヤーの現在の統計なので、これはとにかく理にかなっています。統計オブジェクトではありません。プレーヤーオブジェクトが持っている統計オブジェクトへの参照。そのため、その参照に依存するシステムの他の部分はplayer.currentStats()、プレーヤーの基礎となる統計オブジェクトを取得し、どこかに保存し、突然変異を通じて更新することに依存する代わりに、すべて明示的に参照するか、何らかのものでなければなりません。


1

うわー、これは本当に私を取り戻します。私はこの同じアイデアを数回試しました。学ぶには何か良いことがあると思ったので、私はそれをあきらめるには頑固すぎました。他の人もコードでこれを試してみました。ほとんどの実装では、私はあなたのような読み取り専用のインターフェイスと呼ばれる見てきたFooViewerReadOnlyFooと書き込み専用インターフェイスFooEditorまたはWriteableFooまたはJava Iには、私が見たと思いますFooMutator一度。このように物事を行うための公式の語彙や一般的な語彙があるかどうかはわかりません。

これは、私が試した場所で私にとって有益なことを決してしませんでした。私はそれを完全に避けます。他の人が提案しているように私はやるだろうし、一歩後退して、あなたが本当にあなたのより大きなアイデアでこの概念を必要とするかどうかを検討してください。私がこれを試している間に生成したコードを実際に保持したことがないため、これを行う正しい方法があるかどうかはわかりません。かなりの努力と単純化の後、バックアウトするたびに。そしてそれによって、私は他の人がヤグニとキッスとドライについて言ったことに沿った何かを意味します。

考えられる欠点の1つは、繰り返しです。潜在的に多くのクラスに対してこれらのインターフェイスを作成する必要があり、1つのクラスだけに対しても、各メソッドに名前を付け、各シグニチャを少なくとも2回記述する必要があります。コーディングで私を燃やすすべてのことの中で、この方法で複数のファイルを変更しなければならないことは、私にとって最も厄介なことです。結局、どこかで変更を加えるのを忘れてしまいます。FooというクラスとFooViewerおよびFooEditorというインターフェイスを作成したら、Barを呼び出す方が良いと判断した場合、本当に驚くほどスマートなIDEがない限り、3回リファクタリングする必要があります。これは、かなりの量のコードがコードジェネレーターから送られてきた場合にも当てはまります。

個人的には、実装が1つしかないもののインターフェイスを作成することも嫌いです。つまり、コード内を移動するときに、ただ1つの実装に直接ジャンプすることはできず、インターフェイスにジャンプしてからインターフェイスの実装にジャンプするか、少なくともいくつかの追加キーを押してそこに到達する必要があります。それらの事は合計します。

そして、あなたが言及した合併症があります。この特定の方法でコードを再び使用することはできません。これは、コードの全体的な計画(私が作成したORM)に最適な場所であっても、これを引き出して、コードを作成しやすいものに置き換えました。

私はあなたが言及した構成について本当にもっと考えたいと思います。なぜそれが悪い考えだと思うのか興味があります。私は、統計を変更する可能性のあるもの(一時的な効果、アイテム、いくつかの拡張エリアの場所、疲労)を表すいくつかのセットをさらに構成するようなEffectiveStats構成および不変のStatsようなものを持つことを期待していましたが、それらがどのようなものであり、どのような効果をどのような種類の統計に与えるかを管理します。のStatModifiersStatModifierEffectiveStatsStatModifiersStatModifierさまざまなもののインターフェースとなり、「私はゾーンにいる」、「いつ薬が消える」などのことを知ることができますが、他のオブジェクトにそのようなことを知らせる必要さえありません。現時点でどの統計とどのように変更されたかを言うだけです。さらに良いのStatModifierは、適切に変更されたために異なるStats別の不変物に基づいて、新しい不変物を生成するメソッドを単に公開することですStats。その後、次のようなことを行うことができcurrentStats = statModifier2.modify(statModifier1.modify(baseStats))、すべてStatsが不変になります。私はそれを直接コーディングすらしません。おそらくすべての修飾子をループし、それぞれをで始まる前の修飾子の結果に適用しbaseStatsます。

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