オブジェクトを「再利用可能な部品」として設計・実装をすることがオブジェクト指向における重要な方針でした。その中で重要な役割を果たしてくれるのが「クラス」という機能です。クラスを使うときに必ず使用する「コンストラクタ」を掘り下げます。コンストラクタを活用することでオブジェクトのあり方を使いこなせるようになるのです。最終的に「完全コンストラクタパターン」と呼ばれるクラスの使用法までを解説します。
コンストラクタ引数は「要求」
「コンストラクタ」はクラスをインスタンス化する時に必ず最初に通るメソッドのことです。コンストラクタは戻り値のないメソッドとして定義されている機能になります。
この時にコンストラクタにはクラスの外側から引数を渡すことができます。先ほどコンストラクタは「メソッドの一種」と記述しましたが、メソッドと同じように引数を渡してコンストラクタの処理を実行することが可能です。記述方法はメソッドと全く同じになります。
で、「コンストラクタを使うことで得られるメリットは何か」という点ですが、これは「オブジェクトに必要な情報を要求できる」ということになります。C#ではメソッドの引数に必要は値を渡さない限りコンパイルエラーになって先に進めません。それと同じようなことがコンストラクタにも当てはまります。
これをもう少し具体的にいうと「クラスをインスタンス化する(オブジェクトにする)時に必要な情報を要求できる」ということになります。クラスに必要なデータを外側から渡さない限り使用できない、つまりクラスに必要なデータを強制することができるということになります。
public class Name
{
public string Name { get; }
public Name(string name)
{
//nameを外から強制できる
Name = name;
}
}
上記のようにコンストラクタを通して「クラスに必要なデータ」を要求できます。「Nameクラスをインスタンス化するときには、必ず文字列型を渡さないと動かない」というメッセージを外部に送っているのと同等になるということです。ここから見出しの通り「コンストラクタは要求」ということができます。
完全コンストラクタの活用法
上記のクラスの使い方から派生して「完全コンストラクタパターン」と呼ばれるものが出てきました。完全コンストラクタパターンとは、クラスに必要なデータはすべてコンストラクタから渡してもらうパターンで、クラスのインスタンス化以降は値の変更ができないオブジェクトの設計方法になります。
要するに「インスタンス化されるタイミングで完全な状態になること」を完全コンストラクタパターンと言っています。この「完全コンストラクタ」はオブジェクト指向において、オブジェクトを持ちまわるための一つの戦略ともいえるので覚えておいて損のないクラスの作り方になります。
using System;
namespace App2
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("1つ目の数字を入力してください。");
int input1 = Convert.ToInt32(Console.ReadLine());
Console.WriteLine("2つ目の数字を入力してください。");
int input2 = Convert.ToInt32(Console.ReadLine());
var calc = new Calculation(input1, input2);
var output = calc.Add();
Console.WriteLine($"計算結果は { output.ToString() } です。");
Console.ReadLine();
}
}
public class Calculation
{
public int Input1 { get; }
public int Input2 { get; }
public Calculation(int input1, int input2)
{
this.Input1 = input1;
this.Input2 = input2;
}
public int Add()
{
return Input1 + Input2;
}
}
}
例えば上記のアプリケーションではCalculationオブジェクトを生成するためにint型の引数が2つ必要であることが分かります。そしてプロパティは「get」しかないため、値を外側から渡すことはできません。コンストラクタ以外で外から値を設定できないようなオブジェクトの生成方法を完全コンストラクタパターンと呼んでいます。
イミュータブルなオブジェクト
完全コンストラクタパターンでオブジェクトを作ると何がいいのか?という点ですが、これは「変更不可なオブジェクトが作れる」という点につきます。一般的にオブジェクト指向を採用した開発手法では、クラスを作りながら一時的なデータ、かつ処理部品としてクラスのオブジェクトを持ちまわることになります。
この時に問題となるのが、渡したデータが参照で渡しているのか値で渡しているのか分からなくなることです。参照渡しをしている場合に重要な箇所を、意図せずに上書きされてしまう可能性があるため、バグの温床になってしまいます。そのために一つのオブジェクトの作り方として「不変なオブジェクトを作って使う」という考え方があります。これを「イミュータブル」なオブジェクトと呼んでいます。
イミュータブル(不変)なオブジェクトは一度オブジェクトの値を確定させると後の処理で値を再設定することができません。もし加工処理をして値を変更するような場合は、新規のイミュータブルなオブジェクトを生成して返すことになります。
こうした「不変なオブジェクト」を作って処理に使用し、必要な場合には再生成して後続の処理に使わせるという考え方は、オブジェクト指向において「オブジェクトの状態が想定外のところで変更されることに起因するバグに対する強力な対抗手段」として認識されています。こういう設計手法を採用するのも選択肢の一つになります。
using System;
namespace App3
{
class Program
{
static void Main(string[] args)
{
//完全コンストラクタパターンでオブジェクトを生成
var yamada = new Person("Yamada", 25);
Console.WriteLine($"{ yamada.Name }さんの残りHPは{ yamada.HP }です。");
//ファクトリメソッドでオブジェクトを生成
var hoshino = Person.Create("Hoshino", 20);
Console.WriteLine($"{ hoshino.Name }さんの残りHPは{ hoshino.HP }です。");
Console.ReadLine();
}
}
public class Person
{
public string Name { get; }
public int HP { get; }
public Person(string name, int hp)
{
Name = name;
HP = hp;
}
public static Person Create(string name, int hp)
{
return new Person(name, hp);
}
}
}
イミュータブルなオブジェクトを作成するには、完全コンストラクタパターンと基本的に同一な考え方をすることになります。文字列型や数値型などのプリミティブな型を内部に持ち回って、外側から変更可能なメソッド・プロパティは作成しません。場合によってはクラス内に新規オブジェクトを作成するファクトリメソッドを作るのも良いです。
コンストラクタを使いこなす
「完全コンストラクタパターン」でも「イミュータブルなオブジェクト」でも、基本的に重要な役割を果たすのが「コンストラクタ」になります。コンストラクタを使いこなすことで、柔軟にオブジェクトを使いこなすことが可能になります。
変更可能なオブジェクトを作る際でも「必要最低限」のデータをコンストラクタで要求することが可能になりますし、完全コンストラクタパターンやイミュータブルな設計で解説した通り、コンストラクタで設定を促す値でオブジェクトの状態を確定させてしまう手法もあります。
オブジェクトにおいてクラスを使用した「オブジェクトの生成」は超重要な技術になりますが、それをより外部的なメッセージとして伝える役割を担っているのが「コンストラクタ」と言えるでしょう。コンストラクタをどのように使うかによって、オブジェクト指向な設計手法が決まってくるといっても過言ではありません。