via Berto Garcia

オブジェクトさん

 “オブジェクト指向プログラミング”が市民権を得て何十年も経ちました。プログラミングを勉強している人はオブジェクト指向から避けて通れないかなと思います。かく言う筆者もそうで、ただいま勉強中です。ただ、オブジェクト指向はふわふわしていて理解が難しく、“継承”“カプセル化”“多態性(ポリモーフィズム)”など、謎の単語が出てきて理解を妨げてきます。いろいろな書籍やサイトでは、こういった単語についての説明がされています。

 しかし、オブジェクト指向は目的ではなく手段であり、「コードをより良く書く」ための一つの方法です。なので、“なぜ”オブジェクト指向をするのかということを初めに知っておく必要があります。そこがわかると、謎の単語の意図などの理解も進みます。この“なぜ”という部分については、それこそ筆者よりも詳しい人に言わせるといろいろな話があるのでしょうが、今回は、筆者なりのシンプルな解釈を紹介したいと思います。

ルールと責任

 筆者の考えるオブジェクト指向とは、データの構造であるオブジェクトにルールを与え、そのルールを守る責任を負わせるということです。そして、オブジェクトに責任を与えることで、コードを書く人間に対する負担を減らすことこそが、オブジェクト指向をする理由です。

 それだけ言われてもなんのこっちゃだと思うので、以下に簡単なソースコードを示します。消費税を計算するだけのプログラムです。C#ですが、簡単なので他のオブジェクト指向なプログラミング言語を使ったことがある人なら読めると思います。

using System;

class Money
{
	const double TaxRate = 1.08;

	readonly int origin;

	public Money(int origin)
	{
		this.origin = origin;
	}

	public int WithoutTax()
	{
		return this.origin;
	}

	public int WithTax()
	{
		var wt = (double)this.origin;
		wt *= TaxRate;
		return (int)Math.Round(wt);
	}
}

class TestProgram
{
	static void Main()
	{
		var price = new Money(105);
		Console.WriteLine(price.WithoutTax());
		Console.WriteLine(price.WithTax());
	}
}

 税率を定数でもっているのは説明を簡単にするための方便ですのでご容赦ください。

 このソースコードでは、1.08倍するだけの計算にわざわざクラスをつくり、インスタンスを生成しています。こんな面倒くさい手順を踏んでいるのは、クラスにルールと責任を与えているからです。この場合、Moneyクラスには、

  • 消費税をかける前の金額(データ)を知っている。
  • 消費税をかけた値を返す。

というルールが課せられています。このルールが共有されているとき、オブジェクト指向プログラミング言語の最大の特徴である「保守しやすい」というメリットが生まれるのです。

 参考までに、ユーティリティクラスで同様の処理をしてみましょう。

using System;

static class MoneyUtil
{
	const double TaxRate = 1.08;

	public static int WithTax(int origin)
	{
		var wt = (double)origin;
		wt *= TaxRate;
		return (int)Math.Round(wt);
	}
}

class TestProgram
{
	static void Main()
	{
		var price = 105;
		Console.WriteLine(price);
		Console.WriteLine(MoneyUtil.WithTax(price));
	}
}

 なんと、ユーティリティクラスをつくったらコードがかなり短くなりました。しかし、Moneyにあった2個の約束のうち、片方はなくなってしまいました。MoneyUtilクラスも、WithTaxメソッドも、税抜きだか税込みだかは知りません。単に1.08倍する処理を行なっているだけです。ルールがなくなったので、それに伴う責任もなくなりました。

 少し余談ですが、C#には拡張メソッドという便利な機能があります。筆者もよく使います。上のユーティリティクラスのコードは以下のようにも書き換えられます。

using System;

static class MoneyUtil
{
	const double TaxRate = 1.08;

	public static int WithTax(this int origin)
	{
		var wt = (double)origin;
		wt *= TaxRate;
		return (int)Math.Round(wt);
	}
}

class TestProgram
{
	static void Main()
	{
		var price = 105;
		Console.WriteLine(price);
		Console.WriteLine(price.WithTax());
	}
}

 呼び出し側がかなり簡単に書けました。拡張メソッドはうまく使えばクラスに新しい責任を負わせることができるのですが、今回のはただ処理を足しているだけなのであまり意味がありませんね。やっていることはユーティリティクラスと同じで、intに新しい責任を負わせているわけではありません。

 責任責任と言っていますが、責任がないと一体何が困るのでしょうか? 上のユーティリティクラスの実際の使われ方を見てみましょう。

var price = 100;
.
. // price に対する処理
.
var newPrice = MoneyUtil.WithTax(price);     // (a)
.
. // newPrice に対する処理
.
var lastPrice = MoneyUtil.WithTax(newPrice); // (b)

 処理を見てもらえばわかりますが、この一連の処理は明らかに間違っていて、プログラムの実行結果は意図したものにならないでしょう。しかし、a行、b行はそれぞれ、間違ったことをしているわけではないですよね。なので、エラーの箇所を探すため、関連すると思わしき部分をすべて読んで、a行とb行の両方を見て、初めて間違いに気づくわけです。

 オブジェクトをつくるとどう違うのでしょうか?

var price = new Money(100);
.
. // price に対する処理
.
var newPrice = new Money(price.WithTax());     // (a)
.
. // newPrice に対する処理
.
var lastPrice = new Money(newPrice.WithTax()); // (b)

 今回の場合、a行、b行ともに明らかに間違った処理をしているということが、その1行を見ただけでわかります。「消費税をかける前の金額(データ)を知っている。」というルールを破っているからです。これが、オブジェクト指向の1番大事なポイントで、オブジェクト指向によって、「ただ1.08倍する処理」が「税込みの金額を求める処理」になるのです。

 オブジェクト指向において語られる“継承”、“多態性”、“カプセル化”は、この“ルールと責任”を管理するためにあります。それぞれ、

  • “継承”は「Aを継承したBならAと同じ役割を果たせるよね?」
  • “多態性”は「AでもBでもどっちでもいいからCの役割を果たせるよね?」
  • “カプセル化”は「この処理については、私でなくあなたがすべての責任を負ってください」

と言っていると自分では解釈しています。また、オブジェクト指向でよく言われる、「クラスはなるべく小さくつくる」というのも、ルールを細かく切り分け、責任の所在を明確にするためです。約束は小さければ小さいほど扱いやすいですよね。

 ソースコードは、「人間に読まれるためにある」ということを忘れてはいけません。

おわりに

 最初にも書いたとおり、オブジェクト指向は目的に対する手段の中の一つにすぎません。オブジェクト指向にハマっていくと、全体のコード量は少しずつ長くなっていきます。コードの中に約束を書く以上、仕方がありません。ですが、重要な部分のコードはどんどん簡潔になっていきます。コードは、書いている時間よりも読む時間のほうが長いのです。読みやすいコードを書くことこそが、生産性を高める近道なのです。

 このエントリが、そういったオブジェクト指向の第一歩の一助となれば幸いです。もし、読んだせいでかえってわからなくなったという人がいましたら申し訳ありません。