オブジェクト指向
オブジェクト指向とは
オブジェクト指向とは、複数のオブジェクトを組み合わせてプログラムを構築する考え方である。
オブジェクトとはデータと処理の集まり。漠然とした集まりではなく、ひとつのテーマを持った集まり。
人をオブジェクトとして考えてみる。人には「名前」や「生年月日」、「年齢」、「住所」などのデータがありそうである。
オブジェクトはこれらのデータ(状態)の他に振る舞いも持つ。振る舞いは自分自身に対する操作。 オブジェクトに対して「名前を教えて」と伝えたときにオブジェクト自身が保持している名前を返す、などの動作を指す。
たとえば「鈴木さん」と「高橋さん」がいるとする。この場合「鈴木さん」と「高橋さん」は別々の異なるオブジェクトである。
鈴木さんは「名前が鈴木である」という状態を保持していて、「名前を教えて」と問えば「鈴木」と答える振る舞いを持っている。同様に高橋さんの場合は、「名前を教えて」と問えば「高橋」と答える振る舞いを持っている。
オブジェクト指向とクラス
これまでのクラスはオブジェクトの設計図である。先ほどの人をオブジェクトとして考える例をクラスで表現してみる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Person
def initialize(name, birthday, age, address)
@name = name
@birthday = birthday
@age = age
@address = address
end
def answer_name
puts @name
end
end
//実行結果
tanaka = Person.new("tanaka", "1993/5/5", "23", "東京都渋谷区道玄坂")
=> #<Person:0x007fc55c1604f8 @name="tanaka", @birthday="1993/5/5", @age="23", @address="東京都渋谷区道玄坂">
irb(main):016:0> tanaka.answer_name
tanaka
|
解説
人(Person)クラスを定義して、名前や生年月日などのデータを持つようにしている。answer_nameという自分の名前を答える振る舞いも持っている。クラスの中にそのクラスとして必要なデータや振る舞いを持たせることで、そのオブジェクトの仕事はそのオブジェクトに任せることができる。
オブジェクト指向を取り入れた場合
オブジェクト指向を知っているだけではオブジェクト指向をプログラミングができるようになるわけではない。オブジェクト指向を全く取り入れずにアプリケーションを作ることもできる。アプリケーションがオブジェクト指向で作られたかどうかはユーザーに気づかれることはなく、サービスの体験が変わるわけでもない。オブジェクト指向はユーザーのための知識ではなく、開発者同士の円滑なコミュニケーションのためにある。
手続き型とオブジェクト指向
オブジェクト指向を学び始めると、まず「手続き型とオブジェクト指向は何が違うのか」というテーマにあたる。手続き型(手続き型言語)とは、「プログラムを順序どおりに実行させるような書き方」のことを言う。
例えば、「Rubyを使ってあるファイルから品物の合計金額を表示する」というタスクを考えた時に以下のように書くことができる。
1 2 3 4 5 |
りんご,100,3
ばなな,180,2
ぶどう,240,1
みかん,300,5
いちご,500,2
|
手続き型の場合
手続き型で記述をすると以下のようなコードになる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# ファイルを読み込む
filename = 'sample.txt'
file = File.open(filename)
while text = file.gets
# 1行ずつ解析する
name, price, count = text.chomp.split(',')
price = price.to_i
count = count.to_i
puts "#{name}: 合計#{price * count}円"
end
file.close
|
オブジェクト指向の場合
オブジェクト指向で記述すると以下のようなコードになる。Itemクラス
を作成し、読み込んだファイルの値をもとにインスタンスを作成し、インスタンスメソッドで合計金額を計算するようにする。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# 品物クラス
class Item
def initialize(name, price, count)
@name = name
@price = price
@count = count
end
def total
@price * @count
end
end
# ファイルを読み込む
filename = 'sample.txt'
file = File.open(filename)
while text = file.gets
# 1行ずつ解析する
name, price, count = text.chomp.split(',')
price = price.to_i
count = count.to_i
item = Item.new(name, price, count)
puts "#{name}: 合計#{item.total}円"
end
file.close
|
手続き型とオブジェクト指向の比較
オブジェクト指向は手続き型に比べコード行数が増えていて、メリットが少ないように感じられる。しかし、このコードに機能追加が増えてくると、「ファイルを読み込む部分」と「品物に関して処理する部分」が分離されているので見通しが良くなることが期待できる。
このように、簡単に記述するなら手続き型風の記述は便利だが、ロジックが増えることが予想される部分はオブジェクト指向型の記述方法を取り込む。
オブジェクト指向の例
Ruby on Railsでオブジェクト指向活かしたアプリケーションを作成する例を見る。
前提
名刺管理アプリケーションを開発していると想定してみる。
以下のような機能がある:
- 個人の名前、メールアドレスを登録できる
- 企業の名前を登録できる
- 個人は企業と紐づく(所属する)
モデルについて
Ruby on Railsでは、以下のようなモデルになる。
1 2 3 4 5 6 7 8 9 10 11 |
class Person < ActiveRecord::Base
belongs_to :company
# DBにname, emailカラムを持つ
end
class Company < ActiveRecord::Base
has_many :people
# DBにnameカラムを持つ
end
|
人物情報の表示
例えばあるページで次のような表示をしたいとする。
1 |
山田 太郎 (株式会社◯◯ 所属)
|
このためにViewに次のような記述をした。
1 |
<%= @person.name %> (<%= @person.company.name %>)
|
このような表示に変えたい箇所がアプリケーション内に複数あることがわかった。
もし、すべての箇所でこの記述に変更すると、同じコードがたくさんの場所にコピーされることになる。
これだけなら、特に大きな問題にはならない。
サービスの仕様変更
サービスの仕様変更により、企業に紐付かない個人も登録することができるようになったらどうなるのか?
個人が企業に紐づくかどうかによって以下のように場合分けが必要になる。
1 2 |
山田 太郎 (株式会社◯◯ 所属) <- 所属がある場合
山田 太郎 (フリーランス) <- 所属が無い場合
|
Viewは以下のように変更する必要がある。
1 2 3 4 5 |
<% if @person.company %>
<%= @person.name %> (<%= @person.company.name %>)
<% else %>
<%= @person.name %>
<% end %>
|
変更箇所が1つなら良いが、既にアプリケーション内の多くの場所に変更を加えなければいけなくなった。
これでは、変更すべき箇所が抜け漏れたり、多くの箇所が変更されることによりバグが混入するかもしれない。
WEBサービスの不具合のうち一定数はこのような仕様変更によって発生する。
どうするべき??
このような仕様変更はサービスの品質向上に欠かせないので、「初めからそのようにViewを書いておけばよかった」だけが原因ではない。「このサービスには今後このような仕様変更の可能性があるかも」と予測を立て、変更に強いアプリケーションの設計をしておくべき。これは開発者の責任である。
具体的には、以下のように変更することで今回のような大規模なViewの変更という事態は防げた。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Person < ActiveRecord::Base
belongs_to :company
def display_info
if company
"#{name} (#{company.name} 所属)"
else
"#{name} (フリーランス)"
end
end
end
# view
<%= @person.display_info %>
|
このように共通部分を Person
クラスのメソッドとして実装すれば、所属情報を変更したくなってもView側は一切変更する必要がなくなる。
オブジェクト指向のメリット
このように、「人物情報を出力する」という処理をまるごと「人物 (Person
)」の中に入れてしまうことで、将来発生するだろう面倒を避けることができる。
これが、オブジェクト指向を活用して得られる多くのメリットの内の1つである。
世の中のアプリケーションやサービスは唯一無二なものを目指して開発されるが、何十年にも渡るソフトウェア開発の歴史なかで本当に新しい概念を使って開発されるものはほとんどない。
おおよそ、今までに数多く作られた仕組みを部分的に再利用して開発していることが多い。
その叡智の一つがオブジェクト指向である。
オブジェクト指向に則ったアプリケーションを開発することで、開発時に現れる課題が自動的に解決される。
どこまで予測するべきか
実際の開発では時間的な制限もあり、どこまで予測すればいいのか悩むこともある。たとえば、display_info
というメソッドを作ったにもかかわらず、そのような仕様変更が発生しなかった、ということにもなりかねない。それでは、Person
クラスの行数とそこにかけた開発工数が増えただけでメリットが無いようにも思える。
そのバランスはサービスを開発しているエンジニアの設計力や予測力に依存する。
クラスとインスタンス
上記の名刺管理アプリケーションの例を使って用語の説明をすると
- Person
- クラス。具体的なデータに紐付かない「データの抽象的な振る舞い」を設定するもの。
- @person
- Personクラスの インスタンス。クラスから生成される「具体的なデータとそれに対する処理」をまとめたもの。
- @person.name
- プロパティ。インスタンスが持つデータのこと。Ruby on Railsの場合には、データベースのカラムと同一のものを指している事が多い。
- @person.display_info
厳密には、Rubyという言語の性質上、プロパティとメソッドは実は同じ仕組みで成り立っており違いはないが、「オブジェクト指向」として考えた時には違いがあるので区別した。
役に立つ原則
オブジェクト指向を活用したプログラミング手法は長年研究されていて、いくつかの原則としてまとめられている。
ここでは、Ruby on Railsを使ったWEBアプリケーションの開発にあたって知っておくと便利な原則を幾つかピックアップする。
これ以外にも「オブジェクト指向の原則」は様々なものがある。
DRY
DRYとは、Don't Repeat Your Selfの略で繰り返しを避けるという原則。アプリケーションのコードはどのコードも将来バグになる可能性がある。重複があると、アプリケーションのコードは不必要に大きくなり、それにつれて、バグが生じる危険性も高まる。また、システムの構造は、そう意図していないにもかかわらず複雑になってしまう。重複によりコードベースが大きくなれば、開発に携わる人間がシステム全体を完全に理解することも難しくなる。特に困るのはコードに変更を加える時である。どこかに変更を加えた場合、それとロジック等が重複している箇所にも同様の変更が必要かどうか確認しなければならない。DRY原則を守ればこのような事態を防ぐことができる。
YAGNI
YAGNIとは、You Aren't Gonna Need It の略で実際に必要になった機能だけを追加すべきという原則。漠然とした予想や見込みで追加された機能が使われないまま放置されたり、プログラムの規模が必要以上に大きくなることで設計や構造が複雑になり保守や修正が難しくなり、バグや不具合が起きやすくなる。つまり、今現在具体的な必要性がないのに、将来必要になるかもしれない、あれば便利かもしれないなどといった見込みや思い込みで機能や要素を追加するべきではないということ。
単一責任の原則
単一責任の原則とは、クラスが果たす役割を一つだけにするという原則。例えば、Rectangleクラス(四角形クラス)があるとする。このクラスには四角形を描くメソッドと四角形の面積を計算するメソッドがあるとする。Rectangleクラスには描画と計算の2つの役割があることになる。計算方法を変える(単位の変更など)時と、描画方法を変える(描画する正方形に色をつけるなど)時でクラスの記述を変更する理由が2つになる。役割が増えるほど変更理由が増え、変更が起きやすくなる。このような時は役割ごとにクラスを分けるべきである。役割ごとに分けることで変更する際に該当するクラスを変更するだけで済む。
インターフェイス分離の原則
あるクラスを利用するクライアントが複数存在するような場合に、各クライアントに共通の大きなインターフェイスを使ってしまうと、各クライアント内では使用しないメソッドがあった場合、クラスが使わないメソッドにも依存してしまう。つまり、各クライアントが利用していないメソッドに対する変更の影響まで受けてしまう可能性があるということになる。そこで各クライアントが呼び出す必要のあるサービスの単位でクライアントをグループ分けし、そのグループに特化したインターフェイスを準備することで、異なるサービス間での関連性を分断することができる。