コンストラクタの正しい使い方

コンストラクタとは

 newでインスタンス化するときに呼ばれる便利な初期化記述用のメソッドです。DTOなどでは、これを記述しない例も少なくありません。

よくない使い方

メソッドを記述する手間を省くためだけのコンストラクタ

 よくあるのが、以下のような使い方でしょう。

class User{
  private Integer id;
  private String name;

  public User(Integer id, String name){
    this.id = id;
    this.name = name;
  }

  @Override
  public String toString(){
    return String.join(",", id.toString(), name);
  }
}


class Main{
  public static void main(String[] args){
    User user = new User(1, "Alice");
    System.out.println(user.toString());  // -> 1, Alice
  }
}

 これはコンストラクタを説明するための初学者向けの資料でも紹介されている使い方です。

何が問題か

 最大の問題は、可読性とメンテナンス性の低下です。コンストラクタの処理内容と受け取る引数の順番は、実装(もしくはJavaDocなどの資料)をいちいち確認しなければいけません。

 個別で値を入力する手間を省く目的でコンストラクタを記述しているクラスが散見されますが、可読性の観点からは到底推奨できるものではありません。

改善するには

 このコンストラクタの目的には、変数をセットするためのメソッドを一行一行書いているようでは冗長である、という理由が少なからず含まれます。それを改善するために、メソッドチェーン化してみましょう。

class User{
  Integer id;
  String name;

  public static User create(){
    return new User();
  }

  public User setId(Integer id){
    this.id = id;
    return this;
  }

  public User setName(String name){
    this.name = name;
    return this;
  }

  @Override
  public String toString(){
    return String.join(",", id.toString(), name);
  }
}


public class Main {
  public static void main(String[] args) throws Exception {
    User user = User.create().setId(1).setName("Alice");
    System.out.println(user.toString());  // -> 1, Alice
  }
}

 メソッド名で引数に意味を与えてやることによって、APIドキュメントやJavaDocを確認しなくても、何をしているかが分かるようになりましたね。

 最近のO/Rマッパやフレームワークでは、こういった設計でクラスが組まれていることがあります。

複雑な機能をもったコンストラクタ

 「IDによって自動的にDBから値を取得してbindしてくれる便利なメソッドがあるといいなー、どうせインスタンス生成時にしか呼ばれないし、コンストラクタ化しちゃえ」

class User{

  Integer id;
  String name;

  /**
  * 引数で渡されたIDをもとにデータベースから情報を取得し、
  * 各値をバインドするコンストラクタ
  */
  public User(Integer id){
    Result res = db.findById(id);
    id = res.get("id");
    name = res.get("name");
  }
}

何が問題か

 コンストラクタといえば一般的には変数の初期化に用いられるものですので、考え方としては間違いではないかもしれません、しかし、過度に複雑なロジックは別の箇所に持たせるべきです。

 また、例外発生時のハンドリングも非常に難解になります。dbへの接続に失敗したら空のモデルが返ってくるようではよくわからないことになってしまいますし、throwsすると、new演算子でインスタンスを生成しようとする度に、try~catchが必要になります。

 コンストラクタは、可能な限りシンプルに設計するのが理想です。

改善するには

 Userクラスはシンプルに、ユーザー情報を格納するだけのDTOにしてしまい、データベースアクセス専用のサービスクラスなどを作成するようにします。

class User{

  Integer id;
  String name;

  public void setId(Integer id){
    this.id = id;
  }

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

class UserRepository{
  public User findById(Integer id){
    Result res = db.findById(id);
    User user = new User();
    user.setId(res.get("id"));
    user.setName(res.get("name"));
    return user;
  }
}

 こうすることで、至る所で使いまわされるであろうUserクラスの変更を最小限に抑えることができ、クラスの安定性を上げることができます。

よい使い方

 これまで、アンチパターンとして2つの事例を紹介してきました。反面、コンストラクタとして正しい使い方をしている例を挙げてみることにします。

外部インスタンスをセットする

 コンストラクタは、「その引数が無いとインスタンスを生成できない」ことを示すのに有効です。例えば、「コネクションを生成するにはコンフィグが必要」という場合などです。

class Config{
  Integer maxIdle = 8;
  Integer minIdle = 0;

  public void setMaxIdle(Integer maxIdle){
    this.maxIdle = maxIdle;
  }

  public void setMinIdle(Integer minIdle){
    this.minIdle = minIdle;
  }
}


class Connection{
  Config config = null;

  public Connection(Config config){
    this.config = config;
  }
}


public class Main {
  public static void main(String[] args) throws Exception {
    Config conf = new Config();
    conf.setMaxIdle(10);
    conf.setMinIdle(4);

    Connection conn = new Connection(conf);
  }
}

 この場合、Connectionインスタンスを生成する際に必ずConfigインスタンスが注入されることが保証されるわけです。

「変換する」コンストラクタ

 たとえば、プリミティブ型であるintをラッパーオブジェクトのIntegerに変換するときは、以下のようにします。

int src = 10;
Integer dest = new Integer(src);

 これで、互換性のある型を変換することができます。ArrayListにも似たようなコンストラクタが用意されていますね。

 余談ですが、Javaにはオートボクシング機能がありますから、以下のようにも書けます。

int src = 10;
Integer dest = src;

初期化処理のオプションとしてのコンストラクタ

 JavaのListは、配列のサイズを意識しなくても使える、非常に便利なものですが、初期容量を予め指定することによって、配列のサイズを作り替えるコストを削減することができます。

List<String> list = new ArrayList<>(15); // デフォルトは10

 このように、変数をただセットするだけでなく、初期化処理のオプションとして渡す、といった使い方は、コンストラクタの使い方のお手本のような例です。

コンストラクタの代用としてのstaticメソッド

 少し本筋とは逸れますが、「新しいインスタンスを返す」という意味でコンストラクタの代用としてstaticメソッドを定義しているクラスがあります。

 例えば、Java8のOptionalは、インスタンスの生成(取得)にnew演算子を使わないクラスです。

Optional<String> opt = Optional.of("test");
System.out.println(opt.get());  // -> test

 これは、インスタンス取得のメソッドに「of」という名前を付けることにより、与えた引数がどのような役割を示しているのかを表すことができます。

 以下のコードでは、「空のOptionalを返す」ということがひとめで分かります。

Optional<String> opt = Optional.empty();
System.out.println(opt.isPresent());  // -> false
ちなみに、Optionalのコンストラクタはprivateとなっており、外部からnew演算子を付与してのアクセスができません。

 参考:GC:Optional

まとめ

 コンストラクタは便利なものですが、「名前を持たない」のがデメリットです。これは昨今の「可読性」を重視したプログラミングでは扱いづらいものです。

 幸いにして、コンストラクタはインスタンスの初期化処理に使われるという共通認識がありますから、その認識を逸脱しない限りは可読性が下がるということもありません。

 しかし、今回紹介したOptionalクラスのように、「必ずしも初期化子として使わなければならない」わけではありません。その場に合わせて柔軟に対応するのがよいプログラミングであるように思います。

コメント

  1. 通りすがり より:

    >最大の問題は、可読性とメンテナンス性の低下です。コンストラクタの処理内容と受け取る引数の順番は、実装(もしくはJavaDocなどの資料)をいちいち確認しなければいけません。

    確かに属性が20も30もあるならメソッドチェーン化すべき話でもあるだろうけど、たかだか属性2、3個で「いちいち確認しなければいけない」なんて考えをもつほうがおかしい。
    最大の問題でも何でもない。
    そもそもその程度でメソッドチェーン化したらかえって冗長だし可読性を損ねる。

    • tam-tam より:

      コメントありがとうございます!

      正しい情報を得られる導線があれば全く問題ないと思います。
      たとえばpythonであれば、コンストラクタであっても名前をつけることで問題を回避することができますね。

      user = User(id="xxx", password="yyy")
      

      しかしながら、コンストラクタは受け取る引数の意味がかなり曖昧な場合が多く、ぱっと見ても分からない場合があります。

      User user = new User(args[0], args[1]);

      ただし、変数に適切な名前がついていればこの限りではありません。

      String userId = args[0];
      String password = args[1];
      User user = new User(userId, password);

      さて、以下の場合はどうでしょうか。

      User user = new User(userId, password, email, nickname);

      諸説あると思いますが、userId、passwordとemail、nicknameを同列に扱うよりは、

      User user = User.withCredentials(userId, password);
      Attributes attributes = Attributes.forUser(email, nickname);
      user.addAttributes(attributes);

      など、名前がついていたほうがすんなり読めていいですよね。

      コンストラクタの意味が自明であればいいのですが、オブジェクトはプロジェクトによっても作る人によっても大きく変わります。

      ある意味、最も差異が出る場所ですから、読みやすくするという工夫がされていればそれでいいと思います。重要なのは、相手に伝わることであると考えています。