web-dev-qa-db-ja.com

ゲッター/セッターやプロパティを使用するのは悪い考えですか?

私はこの答えの下のコメントに困惑しています: https://softwareengineering.stackexchange.com/a/358851/212639

ユーザーはゲッター/セッターとプロパティの使用に反対してそこで議論しています。彼はほとんどの場合それらを使用することは悪いデザインのしるしであると主張します。彼のコメントは賛成票を集めています。

まあ、私は初心者のプログラマですが、正直なところ、プロパティの概念は非常に魅力的だと思います。配列でsizeまたはlengthプロパティに書き込んでサイズを変更できないのはなぜですか? sizeプロパティを読み取って、バイナリツリーのすべての要素を取得すべきではないのはなぜですか?車のモデルがその最大速度をプロパティとして公開してはいけないのはなぜですか? RPGを作成している場合、モンスターがその攻撃をゲッターとして公開すべきではないのはなぜですか?そしてなぜそれが現在のHPをゲッターとして公開すべきではないのでしょうか?または、家の設計ソフトウェアを作成している場合、colorセッターに書き込むことで壁の色を変更できないようにする必要があるのはなぜですか。

すべて自然に聞こえるので、私には自明ですが、プロパティやゲッター/セッターを使用することが警告のサインになるかもしれないという結論には至りませんでした。

プロパティやゲッターセッターを使用することは通常悪い考えですが、もしそうなら、なぜですか?

12
gaazkam

パブリックフィールド、プロパティ、またはアクセサーメソッドのいずれであるかにかかわらず、フィールドを単に公開することは、オブジェクトモデリングが不十分であることを示す場合があります。その結果、さまざまなデータのオブジェクトをaskして、そのデータを決定します。したがって、これらの決定はオブジェクトの外部で行われます。これが繰り返し発生する場合、その決定はオブジェクトの責任であり、そのオブジェクトのメソッドによって提供される必要があります。オブジェクトをtellする必要があります何をすべきか、そしてそれをそれ自体でどのように行うかを理解する必要があります。

もちろん、これは常に適切であるとは限りません。これをやり過ぎると、オブジェクトは結局、一度だけ使用される多くの小さな無関係なメソッドのワイルドなコレクションになります。多分、あなたの行動をあなたのデータから分離し、より手続き的なアプローチを選ぶ方が良いでしょう。 「貧血ドメインモデル」 と呼ばれることもあります。その後、オブジェクトはほとんど動作しませんが、何らかのメカニズムを介してオブジェクトの状態を公開します。

何らかの種類のプロパティを公開する場合は、命名と使用するテクニックの両方で一貫している必要があります。これを行う方法は、主に言語に依存します。すべてをパブリックフィールドとして公開するか、すべてプロパティ経由で公開するか、すべてアクセサメソッド経由で公開します。例えば。 _rectangle.x_フィールドをrectange.y()メソッドと組み合わせたり、user.name()メソッドをuser.getEmail()ゲッターと組み合わせたりしないでください。

プロパティ、特に書き込み可能なプロパティまたはセッターの問題の1つは、オブジェクトのカプセル化を弱めるリスクがあることです。カプセル化は重要です。オブジェクトの正当性を単独で判断できるためです。このモジュール化により、複雑なシステムをより簡単に理解できます。しかし、高レベルのコマンドを発行する代わりにオブジェクトのフィールドにアクセスすると、そのクラスにますます結合されるようになります。オブジェクトの外部から任意の値がフィールドに書き込まれる可能性がある場合、オブジェクトの一貫した状態を維持することは非常に困難になります。オブジェクトの整合性を保つのはオブジェクトの責任であり、これは受信データを検証することを意味します。例えば。配列オブジェクトの長さを負の値に設定してはいけません。この種の検証はパブリックフィールドでは不可能であり、自動生成されたセッターでは忘れがちです。 array.resize()のような高レベルの操作を使用すると、より明確になります。

詳細情報の推奨検索用語:

22
amon

これは本質的に悪い設計ではありませんが、他の強力な機能と同様に悪用される可能性があります。

プロパティのポイント(暗黙的なゲッターメソッドとセッターメソッド)は、強力な構文の抽象化を提供し、コードを使用して、それらを定義するオブジェクトの制御を保持しながらフィールドとして論理的に処理できることです。

これはカプセル化の観点からよく考えられますが、多くの場合、Aの制御状態と、呼び出し側にとっての構文上の利便性との関係がより多くなります。

いくつかのコンテキストを取得するには、まずプログラミング言語自体を考慮する必要があります。多くの言語には「プロパティ」がありますが、用語と機能は劇的に異なります。

C#プログラミング言語には、セマンティックレベルと構文レベルの両方で、非常に高度なプロパティの概念があります。ゲッターまたはセッターのいずれかをオプションにすることができます。また、異なる可視性レベルを持つことが重要です。きめ細かいAPIを公開したい場合、これは大きな違いになります。

本質的にサイクリックグラフであるデータ構造のセットを定義するアセンブリを考えてみましょう。以下に簡単な例を示します。

public sealed class Document
{
    public Document(IEnumerable<Word> words)
    {
        this.words = words.ToList();
        foreach (var Word in this.words)
        {
            Word.Document = this;
        }
    }

    private readonly IReadOnlyList<Word> words;
}

public sealed class Word
{
    public Document Document
    {
        get => this.document;
        internal set
        {
            this.document = value;
        }
    }

    private Document document;
}

理想的には、Word.Documentはまったく変更できませんが、言語でそれを表現することはできません。WordsをDocumentsにマップするIDictionary<Word, Document>を作成することもできますしかし、最終的にはどこかに変異性があるでしょう

ここでの目的は、Wordが指定された関数が、Word.Documentプロパティを介してWordに対してDocumentをクエリできるように、循環グラフを作成することです(getter) 。また、Documentが作成された後の消費者による変異を防止したいと考えています。セッターの唯一の目的は、リンクを確立することです。一度だけ書かれることを意図しています。最初にWordインスタンスを作成する必要があるため、これは困難です。 異なるsameプロパティのゲッターおよびセッターへのアクセシビリティのレベルを規定するC#の機能を使用することにより、Word.Documentプロパティの可変性をカプセル化できます包含アセンブリ。

別のアセンブリからこれらのクラスを使用するコードは、プロパティを読み取り専用として認識します(getsetではなく)。

この例では、プロパティについての私の好きなことを紹介します。彼らはゲッターのみを持つことができます。単に計算された値を返したいだけなのか、それともreadだけを公開し、書き込みはしないようにしたいのかに関わらず、プロパティの使用は実際には両方の実装で直感的でシンプルですand消費コード。

のささいな例を考えてみましょう

class Rectangle
{
   public Rectangle(double width, double height) => (Width, Height) = (width, height);

   public double Area { get => Width * Height; }

   public double Width { get; private set; }

   public double Height { get; private set; }
}

さて、前に述べたように、一部の言語にはプロパティとは異なる概念があります。 JavaScriptはその一例です。可能性は複雑です。プロパティをオブジェクトで定義する方法は多数ありますが、可視性を制御する方法は大きく異なります。

ただし、C#の場合と同様に、JavaScriptを使用すると、getterのみを公開するようにプロパティを定義できます。これは突然変異を制御するために素晴らしいです。

検討してください:

function createRectangle(width, height) {
   return {
     get width() {
       return width;
     },
     get height() {
       return height;
     }
   };
}

それでは、プロパティはいつ避けるべきですか?一般に、ロジックがあるsettersは避けます。多くの場合、単純な検証でも他の場所で行うほうがより適切です。

C#に戻ると、次のようなコードを作成したくなることがよくあります。

public sealed class Person
{
    public Address Address
    {
        get => this.address;
        set
        {
            if (value is null)
            {
                throw new ArgumentException($"{nameof(Address)} must have a value");
            }
            this.address = value;
        }
    }

    private Address address;
}

public sealed class Address { }

スローされた例外は、おそらく消費者にとって驚きです。結局のところ、Person.Addressは可変として宣言されており、nullAddress型の値として完全に有効な値です。さらに悪いことに、ArgumentExceptionが提起されています。コンシューマーは、関数を呼び出していることを認識していない可能性があるため、例外のタイプによって、驚くべき動作がさらに追加されます。おそらく、InvalidOperationExceptionなどの別の例外タイプの方が適切でしょう。しかし、これはセッターが醜いところを強調できるところを強調しています。消費者は物事を別の言葉で考えています。このようなデザインが役立つ場合があります。

ただし、代わりに、Addressを必須のコンストラクター引数にするか、書き込みアクセスを公開し、ゲッターのみでプロパティを公開する必要がある場合は、それを設定するための専用メソッドを作成することをお勧めします。

この回答にさらに追加します。

3
Aluan Haddad

ゲッターとセッターを再帰的に追加する(つまり、あなたがリンクする質問に従って反射的にそれを行うことと混同しないように)は、問題の兆候です。まず、フィールドのパススルーゲッターとセッターがある場合、フィールドを公開するだけではどうですか。例えば:

_class Foo
{
    private int _i;
    public int getData() { return _i; }
    public void setData(int i) { _i = i; };
}
_

上記で行ったことは、データを使いにくくし、コードを使用して読みにくくすることです。単に_foo.i += 1_を実行するのではなく、foo.setData(foo.getData() + 1)を実行するようユーザーに強制します。そして、何のために?カプセル化やデータ制御のためにゲッターとセッターを使用していると考えられますが、パススルーパブリックゲッターとセッターがあれば、どちらも必要ありません。後でデータコントロールを追加する必要があると主張する人もいますが、実際にはどれくらいの頻度で行われますか?あなたがパブリックゲッターとセッターを持っているなら、あなたは単に抽象化のレベルですでに働いています、あなたは単にそれをしていません。そして、あなたがそうする可能性が低い場合は、その時点でそれを修正することができます。

パブリックゲッターとセッターは、カーゴカルトプログラミングの兆候です。ここでは、コードが改善されていると信じていますが、実際にそうであるかどうかについてはまだ考えていません。コードベースがゲッターとセッターでいっぱいの場合は、なぜPODを使用していないのかを考えてください。おそらく、それがドメインのデータを操作するためのより良い方法でしょうか。

元の質問に対する批判は、ゲッターまたはセッターが悪い考えではなく、原則としてすべてのフィールドにパブリックゲッターおよびセッターを追加するだけでは、実際には何も効果がないということです。

3

ゲッターとセッターの問題を理解するには、抽象化とインターフェースの概念について話を戻すことが重要だと思います。 Javaのようなプログラミングキーワードinterfaceではなく、より一般的な意味でのインターフェイスを意味します。たとえば、電子レンジを持っている場合、それを制御できるようなユーザーインターフェイスがいくつかあります。インターフェース設計は、システム設計の最も基本的な側面の1つです。それはあなたのシステムがどのように進化するかに直接関係します。

否定的な例から始めましょう。在庫に関する情報を保存したいとします。そこで、クラスを作成します:Inventory。まず、私の要件は、この情報をプログラムの他の部分で使用できるように、在庫にあるアイテムの数や特定のタイプの数を知る必要があるということです。 OKコーディングしましょう:

public class Inventory {
    public final String sku;
    public int numberOfItems;
}

これで、プログラムで共有できるInventoryオブジェクトを作成できるようになりました。しかし、すぐに問題が発生します。アイテムが販売または在庫に追加されると、これらのオブジェクトを更新する必要があります。これらのものを同時に売買する多くの異なる人々がいる可能性があります。プログラムの2つの部分が同時に数値を更新しようとすると、エラーが発生し始めます。たとえば、50個のアイテムがあり、100個のアイテムが倉庫に持ち込まれたが、同時に25個を販売したとします。したがって、2つの部分が値を読み取り、変更してから書き戻します。販売コードが最後に値を設定すると、オブジェクトには25のアイテムが含まれますが、実際には125になります。

したがって、ラッパーを追加して同期します。

public class Inventory {
    public final String sku;
    private int numberOfItems;

    public int getNumberOfItems() {
        synchronized (this) {
            return numberOfItems;
        }
    }

    synchronized public void setNumberOfItems(int number) {
        synchronized (this) {
            this.numberOfItems = number;
        }
    }
}

すごい。これで、2つのスレッドが同時に読み書きできなくなります。しかし、私たちは本当に問題を解決していません。 2つの部分は引き続きゲッターから(一度に1つずつ)読み取り、間違った新しい値を(一度に1つずつ)書き戻すことができます。実際にはどこにも到達していません。

これはより良いアプローチです:

public class Inventory {
    private final String sku;
    private int numberOfItems;

    public int getSku() {
      return sku;
    }

    public int getNumberOfItems() {
        synchronized (this) {
            return numberOfItems;
        }
    }

    public void addItems(int number) {
        synchronized (this) {
            this.numberOfItems += number;
        }
    }
}

今、私たちは実際に問題に対処する何かを持っています。これにより、インベントリの変更が1つずつ行われるようになります。

これは非常に単純な例であり、2017年の優れたコードの記述方法を示すものではありません。ここでのポイントは、堅牢な設計を実現するために、オブジェクト/クラスが他の部分へのコントロールを定義する方法に焦点を当てる必要があるということですアプリケーションの。内部状態の定義は、これとはほとんど関係がありません。上記の例では、会社が成長し、現在は分散しているとします。インベントリは、どこかのリモートサーバーに保存されます。フィールドもありません。ゲッター/セッターインターフェイスを維持するには、Beanを管理し、データをあちこちに転送する他のヘルパークラスを作成する必要があります。これは、手続き型(COBOLなど)プログラムの動作方法です。データはやり取りされ、そのデータを操作するさまざまなプログラムがあります。これらの種類の設計は、非常に壊れやすく、変更にコストがかかります。これは、協調して変更する必要のある異種のコードが多数あるためです。各部分は、システムの他の部分を壊すことを行うことができます。

改善されたデザインにはまだゲッターがあることに注意してください。ゲッターはセッターよりも問題が少ないです。個人的にはget接頭辞が嫌いですが、期待に合わせるのが最善の場合もあります。しかし、それでもゲッターで問題が発生する可能性があります。ロジック付きのコードの一部に配信するためにデータのバッグのようなオブジェクトを使用している場合、それは本質的に同じ問題です。

お役に立てば幸いです。コメントを歓迎します。

0
JimmyJames