はじめに
プログラミングを学び始めるとすぐに、オブジェクト指向という言葉に出会います。
オブジェクト指向に関する書籍もたくさんあり、読めば何となく理解した気になれます。
しかし、あるプログラムを見てそれがオブジェクト指向になっているかを判断することができるでしょうか ?
例えば、Java や Ruby などのオブジェクト指向プログラミング言語を使用していればオブジェクト指向なのでしょうか ?
それは違います。これらの言語はオブジェクト指向プログラミングをサポートしているだけであり、オブジェクト指向を強要しているわけではありません。
処理をメソッドに切り出して共通化していればオブジェクト指向なのでしょうか ?
それも違います。処理を切り出したからといってオブジェクト指向ではありません。
それはむしろ手続き型というプログラミングパラダイムの考え方です。
class と new を使っていればオブジェクト指向なのでしょうか ?
実はそれも違います。
では結局なにがオブジェクト指向なのでしょうか ?
この記事では、どこまでが非オブジェクト指向でどこからがオブジェクト指向なのか、具体的な境界をサンプルコードで説明します。
題材
「1/2 + 2/3 + 1/5 を計算し、計算結果を表示する」というプログラムを Ruby で書き、非オブジェクト指向のコードを徐々にオブジェクト指向にリファクタリングしていきます。1
プログラムを実行したときの挙動は以下の通りです。
$ ruby fraction_1_without_method.rb
1/2 + 2/3 + 1/5 = 41/30
コードはこちらからも参照できます。
Step 1. 最初の一歩
まずはプログラミング入門者の気持ちになって、何も考えずに書いてみます。
※ サンプルコードの単純化のため、約分はしない方針で実装していきます。
numerator1 = 1
denominator1 = 2
numerator2 = 2
denominator2 = 3
numerator3 = 1
denominator3 = 5
tmp_numerator = numerator1 * denominator2 + numerator2 * denominator1
tmp_denominator = denominator1 * denominator2
result_numerator = tmp_numerator * denominator3 + numerator3 * tmp_denominator
result_denominator = tmp_denominator * denominator3
puts "#{numerator1}/#{denominator1} + " +
"#{numerator2}/#{denominator2} + " +
"#{numerator3}/#{denominator3} = " +
"#{result_numerator}/#{result_denominator}"
プログラミング入門者がこう書いてくれたら個人的には結構うれしいです。
何と言っても変数宣言と計算がちゃんと分離しているので、数字を変えてプログラムを動かしやすいです。
しかしこのコードには問題がたくさんあります。
例えば、4 つ目の分数を足したくなったらどうすればいいのでしょうか ?
そのときは 分子1 * 分母2 + ... のようなコードをまた書くのでしょうか ?
もしその計算式に間違い (バグ) があったら、全箇所直さなければいけないのでしょうか ... ?
Step 2. 処理の共通化
上記の課題を解決するため、同じ計算はメソッドとして 1 箇所にまとめました。
def calculate_numerator_for_fraction_addition(numerator1, denominator1, numerator2, denominator2)
numerator1 * denominator2 + numerator2 * denominator1
end
def calculate_denominator_for_fraction_addition(denominator1, denominator2)
denominator1 * denominator2
end
def to_fraction_string(numerator, denominator)
"#{numerator}/#{denominator}"
end
numerator1 = 1
denominator1 = 2
numerator2 = 2
denominator2 = 3
numerator3 = 1
denominator3 = 5
tmp_numerator = calculate_numerator_for_fraction_addition(numerator1, denominator1, numerator2, denominator2)
tmp_denominator = calculate_denominator_for_fraction_addition(denominator1, denominator2)
result_numerator = calculate_numerator_for_fraction_addition(tmp_numerator, tmp_denominator, numerator3, denominator3)
result_denominator = calculate_denominator_for_fraction_addition(tmp_denominator, denominator3)
puts "#{to_fraction_string(numerator1, denominator1)} + " +
"#{to_fraction_string(numerator2, denominator2)} + " +
"#{to_fraction_string(numerator3, denominator3)} = " +
"#{to_fraction_string(result_numerator, result_denominator)}"
だいぶいい感じになりました。
しかし、このメソッドを使うのは結構気を遣います。
いくつもある引数の順番を間違えてはいけないのです。
さて、どうしたものでしょう。
numerator1 と denominator1 などは、かなり関係性の深い変数です。
これらをまとめて扱うことはできないでしょうか ... ?
Step 3. DTO の作成
numerator と denominator をまとめて扱うため、Fraction クラスを作成し、各メソッドの引数で使うようにしました。
add_numerator と add_denominator というメソッドは、Fraction のインスタンスを返す add というメソッドに統合しました。
class Fraction
attr_reader :numerator, :denominator
def initialize(numerator, denominator)
@numerator = numerator
@denominator = denominator
end
end
def add(fraction1, fraction2)
numerator = fraction1.numerator * fraction2.denominator + fraction2.numerator * fraction1.denominator
denominator = fraction1.denominator * fraction2.denominator
Fraction.new(numerator, denominator)
end
def to_fraction_string(fraction)
"#{fraction.numerator}/#{fraction.denominator}"
end
fraction1 = Fraction.new(1, 2)
fraction2 = Fraction.new(2, 3)
fraction3 = Fraction.new(1, 5)
tmp_fraction = add(fraction1, fraction2)
result_fraction = add(tmp_fraction, fraction3)
puts "#{to_fraction_string(fraction1)} + " +
"#{to_fraction_string(fraction2)} + " +
"#{to_fraction_string(fraction3)} = " +
"#{to_fraction_string(result_fraction)}"
この Fraction クラスのように、ただデータを入れる箱として使うクラスを DTO (Data Transfer Object) と言います。2
DTO を使うことで、メソッドの引数・戻り値が整理されました。
プログラムの雰囲気はかなりいい感じです。
class や new を使い始めたので、オブジェクト指向になった気もします。
。。。
実は、このプログラムはオブジェクト指向ではありません。
DTO を new しているからといって、それはオブジェクト指向ではありません。
Step 4. オブジェクト指向
最後のステップです。
足し算と文字列化のメソッドを Fraction クラス内に移動しました。
ついでに to_fraction_string というメソッドの名前を to_s に変えました。
class Fraction
protected attr_reader :numerator, :denominator
def initialize(numerator, denominator)
@numerator = numerator
@denominator = denominator
end
def add(other)
numerator = @numerator * other.denominator + other.numerator * @denominator
denominator = @denominator * other.denominator
Fraction.new(numerator, denominator)
end
def to_s
"#{@numerator}/#{@denominator}"
end
end
fraction1 = Fraction.new(1, 2)
fraction2 = Fraction.new(2, 3)
fraction3 = Fraction.new(1, 5)
result_fraction = fraction1.add(fraction2).add(fraction3)
puts "#{fraction1} + #{fraction2} + #{fraction3} = #{result_fraction}"
Step 3 のプログラムと Step 4 のプログラムの違いこそが、非オブジェクト指向プログラムとオブジェクト指向プログラムの違いです。
「分子」や「分母」といったある構造の内部データを使う処理を、その構造の内側に書き、外からは指示を出す3だけにすることがオブジェクト指向です。
Step 3 のコードのように、クラスの外部から getter などを通して内部データを取り出して計算するのは非オブジェクト指向です。
参考: OOコード養成ギブス
結論
あるデータを使う処理をそのデータを持つ塊 (オブジェクト) に持たせ、外部からは指示を出すだけにするのがオブジェクト指向プログラミングです。
一言で言うと、「データ」と「処理」を一緒の場所に持たせるのがオブジェクト指向です。
おまけ - 実際の開発でどう使うのか
オブジェクト指向について学ぶと、実際に Web アプリケーションなどを開発するときにどう使うんだという疑問にぶち当たります。
まず、そもそもオブジェクト指向を使うことは必須ではありません。4
多くのシステムは非オブジェクト指向で開発されているのではないかと思います。
オブジェクト指向を使うか主に判断すべき部分は、アプリケーション内のプレゼンテーション層・ビジネスロジック層・データアクセス層のうち、ビジネスロジック層です。
ビジネスロジック層には「トランザクションスクリプト」と「ドメインモデル」という大きく 2 つの実装パターンがあります。
「サービス」クラスにビジネスロジックを書き、DTO にデータを持たせて受け渡すようなコードは、「トランザクションスクリプト」パターンであり、まさに非オブジェクト指向です。5
「ドメインモデル」パターンを採用することが、オブジェクト指向を採用することとかなり近い意味になります。
-
Ruby に関してはほとんど経験がないため、Ruby らしくない記述などはご指摘ください。 ↩
-
Ruby の場合、Struct などを使うのが自然かもしれませんが、次のステップへのリファクタリングのために class を使っています。 ↩
-
外から指示を出すこと、つまりメソッド呼び出しを「メッセージパッシング」と言ったりします。 ↩
-
フレームワークなどが一部オブジェクト指向的なコードを強要してくる場合もあります。 ↩
-
ただし、これらのパターンにはそれぞれメリット・デメリットがあり、一概に「ドメインモデル」パターンが優秀ということはできません。オブジェクト指向は難しく、使わない方がよかったということもあり得ます。 ↩