web-dev-qa-db-ja.com

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

アクセサーと修飾子(別名セッターとゲッター)は、主に次の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;
    }

}

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

23
Vlad Spreys

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

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

通常、インスタンスメソッドは、(前提条件として)呼び出されたときにこれらの保証が既に成立していることを前提とできることも期待しています。

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

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

具体例

  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++のような言語もあり、コンストラクターからセッターを呼び出すと、デフォルトの初期化が無駄になります(少なくとも一部のメンバー変数では、回避する価値があります)。

簡単な提案

ほとんどのコードis n'tは次のように記述されていますが、コンストラクターをクリーンで予測可能な状態に保ち、事前および事後条件ロジックを再利用する場合、より良い解決策は次のとおりです。

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_; }
};

わかりました。完全ではありません。コピーと移動のさまざまなコンストラクタと割り当ては省略しました。

37
Useless

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

_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.age_の初期値を設定できないことです。これは、_this.age_がすでに有効な値を持っていると想定しているためです。

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

13
Caleb

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

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

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

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

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

6
Doc Brown

Javaコードのばかげたコードを次に示します。これは、コンストラクターでfinal以外のメソッドを使用して発生する可能性のある問題の種類を示しています。

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が取得されます。もちろん、これは簡単な例ですが、複数のレベルの継承がある場合、状況はすぐに複雑になります。

これが一般的ではなくなった大きな理由は、コードが理解しにくくなるためです。個人的に、私はセッターメソッドをほとんど持っておらず、メンバー変数はほとんど常に(しゃれた)finalです。そのため、値はコンストラクターで設定する必要があります。不変オブジェクトを使用する場合(そしてそうするのに十分な理由がある場合)、問題は疑わしいものです。

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

2
JimmyJames

場合によります!

プロパティが設定されるたびにヒットする必要がある、単純な検証を実行したり、セッターで悪質な副作用を発行したりする場合:セッターを使用します。代わりの方法は、一般にセッターからセッターロジックを抽出し、新しいメソッドを呼び出してから、セッターコンストラクターの両方から実際のセットを呼び出すことです。セッターを使用するのと実質的に同じです! (今は例外として、確かに小さい2行セッターを2か所に維持しています。)

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

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


また、遠くからの記憶がはるかに正確な場合でも、これは私が大学で教えたような推論です。そして、それは私が取り組んでいる特権を持っていた成功したプロジェクトと全く一致していません。

推測しなければならない場合は、ある時点で、コンストラクターでセッターを使用することから発生するいくつかの典型的なバグを特定できる「クリーンなコード」のメモを見落としました。しかし、私としては、そのようなバグの被害を受けたことはありません...

したがって、この特定の決定は徹底的かつ独断的である必要はないと主張します。

1
svidgen