web-dev-qa-db-ja.com

Javaでのデータと動作の分離

私は、単一責任原則(SRP)についての現在の理解をより順守するために、コードを改良する過程にあります。もともとは、メソッドのセットといくつかのインスタンス変数を持つ、Animalというクラスがありました。 SRPによると、主な考え方は、クラスは1つの「理由」についてのみ変更されることになっているということです。私が得た傾向は、動物のbehaviorを変更することと、動物のpropertiesを変更することは、変更する2つの別々の「理由」になるため、使用する必要があると考えましたそれらを分離するカプセル化。

その結果、ラッパーとして機能し、Animalのすべての変数を格納するAnimalInfoクラスを作成しました。私が目にする問題は、変数自体を呼び出すだけでなく、AnimalInfoクラスを呼び出して情報を取得する必要があるということです。まるでそれが自分の情報の所有者ではないかのように。他のクラスがAnimalInfoクラスの情報にアクセスしたいが、Animalにしかアクセスできない場合もあります。そのため、AnimalInfoで同等のメソッドを呼び出すAnimalでゲッターとセッターを作成するのが最も理にかなっていると考えました。例:

public class Animal{

private AnimalInfo animalInfo;

public void eatFood(int amount)
{
   getFoodSupply().consume(amount); //Consume method behavior isn't important. Just an example.
}

public void sleep()
{
    setSleepMode(true);
}

public void hunt()
{
    setHuntMode(true);
}

public FoodSupply getFoodSupply()
{
    return animalInfo.getFoodSupply();
}
public void setFoodSupply(FoodSupply supply)
{
    return animalInfo.setFoodSupply(supply);
}
public boolean setSleeping(boolean sleep)
{
    return animalInfo.setSleeping(sleep);
}
public boolean setHunting(boolean hunt)
{
    return animalInfo.setHunting(hunt);
}
public boolean isHunting()
{
    return animalInfo.isHunting();
}
public boolean isSleeping()
{
    return animalInfo.isSleeping();
}

}

public class AnimalInfo()
{
  private FoodSupply foodSupply;
  private boolean sleeping;
  private boolean hunting;

public FoodSupply getFoodSupply()
{
    return foodSupply; 
}
public void setFoodSupply(FoodSupply supply)
{
    foodSupply = supply;
}
public boolean setSleeping(boolean sleep)
{
    sleeping = sleep;
}
public boolean setHunting(boolean hunt)
{
    hunting = hunt;
}
public boolean isHunting()
{
    return hunting;
}
public boolean isSleeping()
{
    return sleeping;
}

public AnimalInfo getInfo()
{
    return animalInfo;
}
public void setInfo(AnimalInfo info)
{
    animalInfo = info;
}
}

私が得る印象は、これらの「スルーライン」メソッドは不要であるということです。この戦略の結果として、1行に大量の複数のメソッドが呼び出されたため(getThis()。getThat()。doThis())、私は読みやすさのためにそれらを追加しましたが、それが最善かどうかはわかりません練習。

より広いスケールで、私はSRPのAnimalInfoクラスを実装するという私の考えに基づいていませんか?ゲッター、セッター、変数はアニマルの一部にすべきですか?インスタンス変数とゲッター/セッターを格納するためだけにクラスを区切ったように見えますが、余分なメソッド呼び出しを考慮してゲッターとセッターを増やしているため、実際にはメソッドの総数を減らしていません。動作/データ以外の異なるメトリックに基づいてカプセル化する必要がありますか? 2つは常に一緒に存在するので、AnimalInfoはAnimal内のネストされたクラスである必要がありますか? AnimalInfoはどうしてもAnimalのメソッドにアクセスする必要がなく、その逆だけなので、必要だとは思いませんでした。

私の現在の戦略について何かが間違っているようです。何が欠けていますか?

1
MagerBlutooth

はい、あなたのアプローチに何か問題があります。

メソッドからプロパティを分離することは正しい方法ではありません:

  • メソッドがプロパティにアクセスする必要があるためです。ゲッターとセッターを使用すると、制御された方法でこのアクセスが可能になります。しかし同時に、カプセル化する代わりに内部情報を公開します。
  • 結果は、クラス間の強い結合になります。これは 最小の知識の原則 に違反しており、 インターフェース分離の原則 を危険にさらす可能性さえあります。
  • 強い結合は Open/Closeの原則 を危険にさらし、独自の設計と組み合わせて複雑さを爆発させます。動物を拡張してDog and Birdクラスを作成する場合、どのようにしますか? DogとDogInfo、BirdとBirdInfoクラスを追加しますか?

SRPはあなたが考えるものではありません:

  • 正解です。SRPはクラスが行うことではなく、変更する理由です。おめでとう !
  • しかし、あなたは間違って変更する理由を得ました。ボクおじさんがこの原理の「発明者」が説明しているように、実際にはそれは人と意思決定に関するものですが、多くの異なる技術的議論が多くの記事で取り上げられているため、それはあなたのせいではありません この記事
  • Animalクラスが変更される2つの理由があるため、設計はSRPを破ります。最初に、動物の概念が進化する可能性があります(1つの意思決定、動物の責任者/チームによる、たとえばメソッドの追加hideInFOrest())、2番目のAnimalInfoは進化する可能性があります(別の意思決定、たとえば、クラスcanFlyを追加するためのそのクラスの責任者/チーム、および対応するゲッターとセッターによって、これを考慮に入れるには、そのインターフェースのAnimalで)。
  • 直感に従うようにしましょう。物事が一緒に属していると思う場合は、メソッドとプロパティを使用してクラスAnimalを設計するのが最適です。

オブジェクトは単なるゲッターとセッターの束ではありません

  • プロパティは、オブジェクトの動作を実装する手段でなければなりません。
  • ただし、Animalが行うことのほとんどは、AnimalInfoのプロパティを(間接的に)使用するだけです。動物特有の行動はありません。しかし、わかりました、多分それは非常に単純化された例のデモ効果にすぎません。
3
Christophe

私が得た傾向は、動物の動作を変更することと動物のプロパティを変更することは、変更する2つの別個の「理由」になるため、カプセル化を使用してそれらを分離する必要があると考えました。

変化する可能性があるものは、変化する理由を意味するものではありません。不条理な縮小の場合、命名規則の変更は、事実上すべてのコードを変更する理由になります。コードを名前から分離しますか?番号。

代わりに、変更する理由は常にシステムの外部にあります。それらがわからない場合は、コード内で見つけることができません。代わりに、コードが何をしようとしているのかがわかります。

たとえば、現実をモデル化していて、anuimalが実際の生活でできることを実装している場合...変更する理由は、動物が知らないことをできることを発見することです。

私の回答も参照してください 1つ以上のメソッドが単一責任の原則を破らないか?


私が目にする問題は、変数自体を呼び出すだけでなく、AnimalInfoクラスを呼び出して情報を取得する必要があるということです。まるでそれが自分の情報の所有者ではないかのように。

必ずしも問題ではありません。状態の一部または全体を別のタイプに変換することは、有効なリファクタリングです。しかし、あなたはこれを行うことで何も稼いでいるようには見えません。特にSRPの観点から、AnimalInfoの変更はAnimalの変更を意味することを考慮してください。あなたの場合、AnimalInfoを分離する必要はなく、逆効果です。


代わりに、ゲッターとセッターが問題だと思います。あなたは本質的に構造体を作っています(それがJavaでないことを除いて)。フィールドのすべての可能な組み合わせは有効な状態ですか?あなたはそれをチェックしているはずです!オブジェクトを無効な状態のままにしないでください!それがカプセル化の目的です。たとえば、動物は寝ている間に狩猟をすることができますか?

おそらくこれを状態マシンとして実装したいと思うでしょう。動物の可能な状態(狩猟、睡眠など)を持つ列挙型AnimalStateを使用できます。次に、Animalには、状態のゲッターと、状態を変更するメソッド(少なくとも状態のセッター※)があります。

正しく行われると、Animalクラスを変更せずに、Animalの状態のリストを変更できるはずです。それは、カプセル化を壊すことなく、データと動作を分離することです。

実際、正しく実行すると、AnimalStateをクラスに変更できます。可能な状態はそれぞれ、名前を持つインスタンスです。これにより、構成ファイル、データベース、またはユーザー入力などから状態のリストをロードできます。

AnimalStateをクラスにするもう1つの利点は、派生型を作成できることです。たとえば、FoodSupplyを含むタイプを使用して、それを摂食状態に使用できます。しかし、それがあなたがこれを望んでいる方法であるかどうかはわかりません。

※:ある州から別の州への移行に関するルールがあるかもしれません。また、したがって、bool TrySetState(AnimalState newState)が役立つ可能性があります。また、要件によっては、bool TransitionState(AnimalState expectedState, AnimalState newState)または同様のものが役立つ場合があります。


最後に、それはシステムの要件に依存します。それらがどのように変更できるかを考え、将来コードを変更しやすくすることには価値があります。たとえば、この場合、動物が実行できることのリストがあることはわかっています。リストが変更される可能性がある(つまり、要件が変更される)と想像できるので、その変更を容易にするコードを作成することは理にかなっています(たとえば、列挙型を使用します)。同様に、状態はデータベースからのものであると言うように要件を変更できます。

クライアントがそれを要求する可能性があるという理由だけでデータベースアダプタを作成するべきではないことに注意してください。 AnimalStateを変更せずにAnimalStateを変更できるため、Animalを分離するだけで十分です。

私は完全にオフマークである可能性があります。おそらく、動物はこのシステムで睡眠中に狩りをすることができます。疑問がある場合は、クライアントに尋ねます。要件は最も重要です。要件がどこから来るのかを理解する必要があるので、要件が変更される理由を理解できます。これにより、コードを分離する方法が決まります。


別の可能なデザインについて述べたいと思います。状態を分離する代わりに(またはさらに)、状態のmutationを分離できます。つまり、クラスを不変にしてから、状態の変更を伴うAnimalの新しいインスタンスを返すメソッドを作成できます(または、変更のない同じインスタンスが必要でした)。

2
Theraot

私はSRPのAnimalInfoクラスを実装するという考えに根拠がないのですか?

はい。

他のクラスがAnimalInfoクラスの情報にアクセスしたいが、Animalにしかアクセスできない場合もあります。

これは feature envy と呼ばれます。これは、ロジックを間違ったクラスに配置した兆候です。 Animalからのデータを必要とするメソッドは、通常Animalに属します。

メソッドが境界の反対側に存在し、Animalに移動できない場合があります。そのような場合は、データ転送オブジェクト(DTO)を使用してデータを移動します。それがAnimalInfoです。

DTOは実際には不幸な名前であり、DTOは真のオブジェクトではありません。それらはデータ構造です。彼らは行動しません。ゲッターとセッターは、ブレークポイントを設定する場所を提供するだけです。

Animalは真のオブジェクトである必要があります。動作するはずです。ゲッターとセッターがあるべきではありません。これらの境界の1つに対処する必要がある場合、何をすべきかはAnimalInfoを食べて吐き出すことです。

意味をお見せします。いくつかの意味のあるビジネスロジックが存在するように、いくつかの自由を取り入れました。

_public class Animal
{    
    private FoodSupply foodSupply;
    private boolean sleeping;
    private boolean hunting;

    public Animal(AnimalInfo animalInfo) {
        foodSupply = animalInfo.getFoodSupply();
        sleeping = animalInfo.isSleeping();
        hunting = animalInfo.isHunting();
    }    

    public void eatFood(int amount)
    {
        if (!sleeping && !hunting) {
            foodSupply.consume(amount); 
        }
    }

    public void sleep()
    {
        sleeping = true;
    }

    public void wakeup()
    {
        sleeping = false;
    }

    public void hunt()
    {
        hunting = true;
    }

    public void relax()
    {
        hunting = false;
    }

    public AnimalInfo getInfo()
    {        
        return new AnimalInfo(foodSupply, sleeping, hunting);
    }

}
_

今では、Animalsインターフェースはすべて動作に関するものです。動物に何をすべきかを伝えます。何が起こっているのかは問いません。

これで、getInfo()を除き、Animalが完全にカプセル化されました。それが中を覗く唯一の方法です。そして、それさえもコーダーが内部をいじるのを防ぐ防御的なコピーです。

それでも、AnimalInfoからAnimalを構築し、データベース、ファイル、ネットワーク、プリンター、GUI、そうですね、境界の向こう側にあるものすべてにAnimalInfoを送ることができます。

ポイントは理解できたと思いますが、AnimalInfoは次のようになります。

_public class AnimalInfo
{
    private FoodSupply foodSupply;
    private boolean sleeping;
    private boolean hunting;

    public AnimalInfo(FoodSupply foodSupply, boolean sleeping, boolean hunting) {
        this.foodSupply = foodSupply;
        this.sleeping = sleeping;
        this.hunting = hunting;
    }
    public FoodSupply getFoodSupply()
    {
        return foodSupply; 
    }
    public void setFoodSupply(FoodSupply supply)
    {
        foodSupply = supply;
    }
    public void setSleeping(boolean sleep)
    {
        sleeping = sleep;
    }
    public void setHunting(boolean hunt)
    {
        hunting = hunt;
    }
    public boolean isHunting()
    {
        return hunting;
    }
    public boolean isSleeping()
    {
        return sleeping;
    }
}
_

繰り返しますが、これはオブジェクトではありません。ここには実際のカプセル化はありません。これは、データがいつ移動しているかがわかるように、デバッグコードが詰め込まれたデータ構造です。

まだ理解していない場合、このレッスンはSRPに関するものではありません。これはカプセル化についてです。実際のカプセル化。プライベートなものすべてにパブリックゲッターとセッターを投げるだけではありません。プライベートをプライベートに保ち、それらを変更するときに何が壊れているかを心配する必要はありません。

あなたがその考えを真剣に受け止めるなら、あなたがしないことの一つは、ローカルクラスとAnimalInfoを共有することです。いいえ。ローカルクラスがこのデータを必要とする場合、Animalはクラスメソッドを移動する必要があります。

必要がない限り、データを移動しないでください。移動方法を優先します。

2
candied_orange

TL; DR:これを2つのクラスに分割することは必ずしも間違っているわけではありませんが、おそらく過剰設計です。

まず第一に、この場合、コード自体を見ても正しいことも悪いこともありません。 SRPは変更の理由に関するものであり、クラスの実装が変更される理由です。考えられる変更についてはまったく触れなかったので、これはすべて憶測に任せました。

一般に、私のスタンスは、単純なソリューションが機能する、単純なソリューションに行くことです。あなたの場合、あなたのクラスはそれほど大きくなく、AnimalInfoを分割することで解決できる問題については何も言及していませんが、意味のあるシナリオを見ることができました。私の要点を理解するために、AnimalInfoの分割が意味をなさないシナリオと、AnimalInfoを分割するシナリオの2つを紹介します。

最初のシナリオ:ゲーム理論と動物の行動に関する論文を書いていて、モデルを大幅に変更し、主に新しい機能を追加することを想像してください。新しい機能を追加するたびに、2つのクラスを変更する必要があります(たとえば、動物が泳げるようにするには、メソッドsetSwimingをAnimalInfoに追加し、メソッドswim()をAnimalに追加する必要があります。2つのクラスがありますが、同じ理由で変更されます、それは余分な作業の多くです。AnimalInfoを分割することは意味がありません。

2番目のシナリオ:ペットのようにたまごっちで遊ぶことができるWebゲームを作成していて、「泳ぐ」、「眠る」、「狩る」などのコマンドを与えるWebインターフェイスがあり、動物がそうする。シンプルな方法から始め、1台のサーバーでゲームを実行します。人々は2〜3時間プレイした後、動物のことを忘れるので、動物のゲームの状態をメモリにのみ保存しておけば大丈夫です。その後、ゲームにいくつかの改善を加えます。最初の人々は、動物に彼らがしていることをすべて停止するように指示する簡単な方法がないと不平を言っているので、ボタンと、動物に睡眠を停止して狩猟を停止するように指示するメソッドalert()を追加しますが、これはゲームを変更しませんメカニック。その後、動物の状態をローカルのSQLiteデータベースに保存することを決定しました。これにより、夜間のメンテナンス中にサーバーがダウンしても、プレーヤーは数日間にわたって動物と遊んで楽しむことができます。さらにゲームが人気を博し、複数のサーバーにスケーリングする必要があるため、ローカルのSQLiteデータベースに接続する代わりに、別の共有MySQLデータベースに接続します。これで、さまざまな理由で発生する可能性のあるさまざまな変更(UIの変更、データベースの変更)を計画しており、多くの調整を必要とせずにさまざまな人が簡単に作業できることがわかります。

ご覧のとおり、意味があるかどうかは、実際に予想されるコードの将来の変更によって異なります。確信がない場合、または変更が予想されない場合は、いつでも簡単なものをより柔軟なものにリファクタリングできる最も単純なソリューションを使用してください(逆の方法は多くの場合困難です)。

私が得る印象は、これらの「スルーライン」メソッドは不要であるということです。この戦略の結果として、1行に大量の複数のメソッドが呼び出されたため(getThis()。getThat()。doThis())、私は読みやすさのためにそれらを追加しましたが、それが最善かどうかはわかりません練習。

私自身、この慣習について何を考えればよいかわかりませんが、名前は「デメテルの法則を検索」です。

1
Helena