コンストラクターでセッターを使用することが一般的なパターンにならなかったのはなぜですか?


23

アクセサーと修飾子(セッターとゲッター)は、次の3つの主な理由で便利です。

  1. これらは変数へのアクセスを制限します。
    • たとえば、変数にアクセスすることはできますが、変更することはできません。
  2. パラメータを検証します。
  3. 彼らはいくつかの副作用を引き起こす可能性があります。

大学、オンラインコース、チュートリアル、ブログ記事、およびWeb上のコード例はすべて、アクセサと修飾子の重要性について強調しています。最近のコードには「必須」のように感じられます。したがって、以下のコードのように追加の値を提供しなくても、それらを見つけることができます。

public class Cat {
    private int age;

    public int getAge() {
        return this.age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

そうは言っても、実際にパラメーターを検証し、無効な入力が提供された場合に例外をスローしたりブール値を返したりする、より有用な修飾子を見つけることは非常に一般的です:

/**
 * Sets the age for the current cat
 * @param age an integer with the valid values between 0 and 25
 * @return true if value has been assigned and false if the parameter is invalid
 */
public boolean setAge(int age) {
    //Validate your parameters, valid age for a cat is between 0 and 25 years
    if(age > 0 && age < 25) {
        this.age = age;
        return true;
    }
    return false;
}

しかし、それでも、修飾子がコンストラクターから呼び出されることはほとんどないため、直面している単純なクラスの最も一般的な例は次のとおりです。

public class Cat {
    private int age;

    public Cat(int age) {
        this.age = age;
    }

    public int getAge() {
        return this.age;
    }

    /**
     * Sets the age for the current cat
     * @param age an integer with the valid values between 0 and 25
     * @return true if value has been assigned and false if the parameter is invalid
     */
    public boolean setAge(int age) {
        //Validate your parameters, valid age for a cat is between 0 and 25 years
        if(age > 0 && age < 25) {
            this.age = age;
            return true;
        }
        return false;
    }

}

しかし、この2番目のアプローチの方がはるかに安全だと思うでしょう。

public class Cat {
    private int age;

    public Cat(int age) {
        //Use the modifier instead of assigning the value directly.
        setAge(age);
    }

    public int getAge() {
        return this.age;
    }

    /**
     * Sets the age for the current cat
     * @param age an integer with the valid values between 0 and 25
     * @return true if value has been assigned and false if the parameter is invalid
     */
    public boolean setAge(int age) {
        //Validate your parameters, valid age for a cat is between 0 and 25 years
        if(age > 0 && age < 25) {
            this.age = age;
            return true;
        }
        return false;
    }

}

あなたの経験で似たようなパターンを見ていますか、それとも私だけが不運なのですか?そして、もしそうなら、それは何を引き起こしていると思いますか?コンストラクターから修飾子を使用することには明らかな欠点がありますか?それは何か他のものですか?


1
@stg、ありがとう、興味深い点!それを展開して、おそらく起こりうる悪いことの例を挙げて答えに入れてもらえますか?
Vlad Spreys


8
@stg実際、コンストラクターでオーバーライド可能なメソッドを使用することはアンチパターンであり、多くのコード品質ツールがエラーとして指摘します。オーバーライドされたバージョンが何をするかわからないので、コンストラクタが終了する前に「this」をエスケープさせるなどの厄介なことがあり、あらゆる奇妙な問題が発生する可能性があります。
ミチャウコスムルスキ

1
@gnat:これはクラスのメソッドが独自のゲッターとセッターを呼び出すべきであるとは思わないのですか?、完全に形成されていないオブジェクトで呼び出されるコンストラクター呼び出しの性質のため。
グレッグブルクハート

3
また、「検証」では、年齢が初期化されていない(または、言語に応じてゼロ、または他の何か)ままになることに注意してください。コンストラクターを失敗させる場合は、戻り値を確認し、失敗時に例外をスローする必要があります。現状では、検証によって異なるバグが導入されています。
役に立たない

回答:


37

非常に一般的な哲学的推論

通常、コンストラクターに(事後条件として)構築されたオブジェクトの状態に関するいくつかの保証を提供するように依頼します。

通常、インスタンスメソッドは、これらの保証が呼び出されたときに既に保持されていることを(前提条件として)想定し、それらを壊さないことを確認するだけでよいことも期待しています。

コンストラクター内からインスタンスメソッドを呼び出すと、これらの保証の一部またはすべてがまだ確立されていない可能性があるため、インスタンスメソッドの前提条件が満たされているかどうかを判断するのが難しくなります。あなたがそれを正しく取得したとしても、例えばに直面して非常に壊れやすいことがあります。インスタンスメソッド呼び出しまたはその他の操作の順序を変更します。

また、言語は、コンストラクターの実行中に、基本クラスから継承される/サブクラスによってオーバーライドされるインスタンスメソッドの呼び出しを解決する方法も異なります。これにより、さらに複雑な層が追加されます。

具体例

  1. これがどのように見えるべきであるかについてのあなた自身の例はそれ自体間違っています:

    public Cat(int age) {
        //Use the modifier instead of assigning the value directly.
        setAge(age);
    }

    これはからの戻り値をチェックしませんsetAge。どうやらセッターを呼び出しても、正確さを保証するものではありません。

  2. 次のような初期化順序に依存するような非常に簡単な間違い:

    class Cat {
      private Logger m_log;
      private int m_age;
    
      public void setAge(int age) {
        // FIXME temporary debug logging
        m_log.write("=== DEBUG: setting age ===");
        m_age = age;
      }
    
      public Cat(int age, Logger log) {
        setAge(age);
        m_log = log;
      }
    };

    私の一時的なロギングがすべてを壊した場所。おっと!

また、コンストラクターからセッターを呼び出すと、デフォルトの初期化が無駄になることを意味するC ++などの言語もあります(一部のメンバー変数については、少なくとも回避する価値があります)。

簡単な提案

ほとんどのコードこのように書かれていないのは事実ですが、コンストラクターをクリーンで予測可能な状態に保ち、事前条件と事後条件のロジックを再利用したい場合、より良いソリューションは次のとおりです。

class Cat {
  private int m_age;
  private static void checkAge(int age) {
    if (age > 25) throw CatTooOldError(age);
  }

  public void setAge(int age) {
    checkAge(age);
    m_age = age;
  }

  public Cat(int age) {
    checkAge(age);
    m_age = age;
  }
};

またはさらに良い場合は、可能であれば:プロパティタイプで制約をエンコードし、割り当て時に独自の値を検証します。

class Cat {
  private Constrained<int, 25> m_age;

  public void setAge(int age) {
    m_age = age;
  }

  public Cat(int age) {
    m_age = age;
  }
};

そして最後に、完全を期すために、C ++の自己検証ラッパーです。面倒な検証を行っていますが、このクラスは何もないので、チェックするのは比較的簡単です

template <typename T, T MAX, T MIN=T{}>
class Constrained {
    T val_;
    static T validate(T v) {
        if (MIN <= v && v <= MAX) return v;
        throw std::runtime_error("oops");
    }
public:
    Constrained() : val_(MIN) {}
    explicit Constrained(T v) : val_(validate(v)) {}

    Constrained& operator= (T v) { val_ = validate(v); return *this; }
    operator T() { return val_; }
};

OK、それは実際には完全ではありません。さまざまなコピーと移動のコンストラクタと割り当てを省略しました。


4
重く答えられた答え!OPが教科書で説明されている状況を見て、それが良い解決策にならないという理由だけで付け加えておきます。クラスに属性を設定する2つの方法(コンストラクターとセッターによる)があり、その属性に制約を適用する場合、両方の方法で制約をチェックする必要があります。そうでない場合は設計バグです。
Doc Brown

1)セッターにロジックがない場合、または2)セッターのロジックが状態に依存しない場合、コンストラクターでセッターメソッドを使用することをお勧めしませんか?常にメンバ変数に適用する必要がありますセッターでの条件のいくつかのタイプがある場合(私は頻繁にそれを行ういけないが、私は個人的に、正確に、後者の理由で、コンストラクタで使用setterメソッドを持っている
クリスMaggiulli

常に適用する必要がある条件のタイプがある場合、それセッターのロジックです。私は確かにそれが優れていると思わ可能であればそれは自己完結を強制されるので、メンバーにこのロジックをエンコードするために、クラスの残りの取得依存することはできません。
無駄な

13

オブジェクトが構築されるとき、それは定義により完全には形成されません。セッターが提供した方法を検討してください。

public boolean setAge(int age) {
    //Validate your parameters, valid age for a cat is between 0 and 25 years
    if(age > 0 && age < 25) {
        this.age = age;
        return true;
    }
    return false;
}

検証の一部に、オブジェクトの年齢のみが増加することを保証するためsetAge()ageプロパティに対するチェックが含まれている場合はどうなりますか?その状態は次のようになります。

if (age > this.age && age < 25) {
    //...

猫は一方向にしか時間を移動できないため、これはかなり無害な変化のように見えます。唯一の問題は、有効な値this.agethis.age既にあると想定しているため、初期値の設定に使用できないことです。

コンストラクターでセッターを回避すると、コンストラクターが実行しているのはインスタンス変数を設定し、セッターで発生する他の動作をスキップすることだけであることが明確になります。


OK ...しかし、オブジェクトの構築を実際にテストせずに潜在的なバグを公開しなければ、いずれの方法でも潜在的なバグが存在します。そして今、2 つの問題があります。その潜在的な潜在的なバグがあり、2か所で年齢範囲のチェックを実施する必要があります。
svidgen

Java / C#では、コンストラクター呼び出しの前にすべてのフィールドがゼロになるため、問題はありません。
デデュプリケーター

2
はい、コンストラクターからインスタンスメソッドを呼び出すことは、推論するのが難しい場合があり、一般的には推奨されません。検証ロジックをプライベートな最終クラス/静的メソッドに移動し、コンストラクターとセッターの両方から呼び出す場合は、これで問題ありません。
役に立たない

1
いいえ、非インスタンスメソッドは、部分的に構築されたインスタンスに触れないため、インスタンスメソッドの巧妙さを回避します。もちろん、検証の結果を確認して、構築が成功したかどうかを判断する必要があります。
役に立たない

1
この答えは、私見のポイントが欠けていることです。「オブジェクトが構築されるとき、それは定義によって完全には形成されません」 -構築プロセスの途中で、もちろん、コンストラクターが行われるとき、属性に対する意図された制約は保持されなければなりません。私はそれを重大な設計上の欠陥と呼びます。
Doc Brown

6

ここでいくつかの良い答えが既にありますが、ここで2つのことを1つに尋ねていることに気づいていないようです。あなたの投稿は、2つの別々の質問として最もよく見られます:

  1. セッターで制約の検証がある場合、それがバイパスされないことを確認するために、クラスのコンストラクターにもあるべきではありませんか?

  2. コンストラクターからこの目的のためにセッターを直接呼び出さないのはなぜですか?

最初の質問に対する答えは明らかに「はい」です。コンストラクターは、例外をスローしない場合、オブジェクトを有効な状態のままにしておく必要があり、特定の属性に対して特定の値が禁止されている場合、これを回避することはまったく意味がありません。

ただし、派生クラスのセッターのオーバーライドを回避する手段を講じない限り、2番目の質問に対する答えは通常「いいえ」です。したがって、コンストラクターだけでなくセッターからも再利用できるプライベートメソッドで制約の検証を実装することをお勧めします。私はここで@Uselessの例を繰り返すつもりはありません。これはまさにこの設計を示しています。


コンストラクターが使用できるようにセッターからロジックを抽出する方が良い理由はまだわかりません。...これは、合理的な順序でセッターを単に呼び出すこととはどう違うのですか?? 特に、セッターが「
すべき

2
@svidgen:JimmyJamesの例を参照してください。要するに、問題を引き起こす、オーバーライド可能なメソッドをctorで使用することは得策ではありません。ファイターであり、他のオーバーライド可能なメソッドを呼び出さない場合は、ctorでセッターを使用できます。さらに、検証が失敗したときに検証を処理する方法から分離することで、ctorとsetterで検証を異なる方法で処理する機会が得られます。
ドク・ブラウン

さて、私は今でもあまり確信していません!しかし、他の答えを教えてくれてありがとう
...-svidgen

2
することにより、合理的な順序で、あなたは意味脆く、簡単に無実に見える変化によって破壊不可視の順序に依存して、右?
役に立たない

@役に立たない、いや...私が意味するのは、コードの実行順序が重要ではないことを提案するのは面白いことです。しかし、それは本当にポイントではありませんでした。事のリードのその種-を越えて、私はセッターにクロスプロパティの検証を持っているのは良いアイデアだと思いましたしましたところ、私はちょうど私の経験でインスタンスを思い出すことができない、例を考案し、必ずしも「見えない」の呼び出し次の要件に。これらの呼び出しがコンストラクターで発生するかどうかに関係なく
..-svidgen

2

以下は、コンストラクターで最終以外のメソッドを使用した場合に発生する可能性のある問題の種類を示す馬鹿げたJavaコードです。

import java.util.regex.Pattern;

public class Problem
{
  public static final void main(String... args)
  {
    Problem problem = new Problem("John Smith");
    Problem next = new NextProblem("John Smith", " ");

    System.out.println(problem.getName());
    System.out.println(next.getName());
  }

  private String name;

  Problem(String name)
  {
    setName(name);
  }

  void setName(String name)
  {
    this.name = name;
  }

  String getName()
  {
    return name;
  }
}

class NextProblem extends Problem
{
  private String firstName;
  private String lastName;
  private final String delimiter;

  NextProblem(String name, String delimiter)
  {
    super(name);

    this.delimiter = delimiter;
  }

  void setName(String name)
  {
    String[] parts = name.split(Pattern.quote(delimiter));

    this.firstName = parts[0];
    this.lastName = parts[1];
  }

  String getName()
  {
    return firstName + " " + lastName;
  }
}

これを実行すると、NextProblemコンストラクターでNPEが取得されます。これはもちろん些細な例ですが、複数レベルの継承がある場合は、すぐに複雑になります。

これが一般的にならなかった大きな理由は、コードを理解するのがかなり難しくなるからだと思います。個人的に、私はセッターメソッドをほとんど持っておらず、メンバー変数はほとんど常に(しゃれを意図した)最終的なものです。そのため、コンストラクターで値を設定する必要があります。不変オブジェクトを使用する場合(および使用する多くの正当な理由がある場合)、問題は議論の余地があります。

いずれにせよ、検証やその他のロジックを再利用することは良い目標です。これを静的メソッドに入れて、コンストラクターとセッターの両方から呼び出すことができます。


これは、継承の実装が不十分であるという問題の代わりに、コンストラクターでセッターを使用することの問題ですか?つまり、ベースコンストラクターを事実上無効にしている場合、なぜそれを呼び出すのでしょうか?!
svidgen

1
ポイントは、セッターをオーバーライドしても、コンストラクターが無効にならないことだと思います。
クリスウォーラート16

@ChrisWohlert OK ...しかし、セッターロジックをコンストラクターロジックと競合するように変更すると、コンストラクターがセッターを使用するかどうかに関係なく問題になります。構築時に失敗するか、後で失敗するか、目に見えないように失敗します(b / cビジネスルールをバイパスしました)。
svidgen

ベースクラスのコンストラクターがどのように実装されているかよく分からないことがよくあります(サードパーティのライブラリーのどこかにあるかもしれません)。ただし、オーバーライド可能なメソッドをオーバーライドしても、ベース実装の規約が破られることはありません(おそらく、この例ではnameフィールドを設定しないことで破られます)。
ハルク

@svidgenの問題は、コンストラクターからオーバーライド可能なメソッドを呼び出すと、オーバーライドの有用性が低下することです。オーバーライドされたバージョンでは、まだ初期化されていない可能性があるため、子クラスのフィールドを使用できません。さらに悪いことに、thisまだ完全に初期化されていることを確認できないため、他のメソッドに-referenceを渡してはいけません。
ハルク

1

場合によります!

プロパティを設定するたびにヒットする必要がある簡単な検証を実行するか、セッターでいたずらな副作用を発行する場合:セッターを使用します。通常、代替手段は、セッターからセッターロジックを抽出し、セッターコンストラクターの両方から実際のセットが続く新しいメソッドを呼び出すことです。これは、セッターを使用するのと実質的に同じです。(ただし、明らかに、2か所に小さな2行のセッターを保持しています。)

ただし、特定のセッター(または2つ!)が正常に実行される前にオブジェクトを配置するために複雑な操作を実行する必要がある場合は、セッターを使用しないください。

どちらの場合でも、テストケースがあります。どのルートを選択しても、ユニットテストがある場合は、いずれかのオプションに関連する落とし穴を明らかにする可能性が高くなります。


また、その遠い昔の記憶がはるかに正確である場合、これも大学で教えられた推論の一種です。そして、私が取り組んでいる成功したプロジェクトとはまったく関係ありません。

推測しなければならなかった場合、コンストラクターでセッターを使用することから生じる典型的なバグを特定する「クリーンコード」のメモを見逃しました。しかし、私の側では、私はそのようなバグの犠牲者ではありません...

そのため、この特定の決定は抜本的で独断的である必要はないと主張します。

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