オブジェクト指向とはなにか
複数のオブジェクトを組み合わせてプログラムを構築する考え方です。
人をオブジェクトとして考えてみると、人には「名前」、「生年月日」、「年齢」、「住所」などのデータがあります。
オブジェクトはこれらのデータ(状態)の他に振る舞いも持ち、オブジェクトに対して「名前を教えて」と伝えたときにオブジェクト自身が保持している名前を返す動作を指します。
たとえば「鈴木さん」と「佐藤さん」がいるとします。この場合、二人は別々の異なるオブジェクトです。
鈴木さんは「名前が鈴木である」という状態を保持していて、「名前を教えて」と問えば「鈴木」と答える振る舞いを持っています。
オブジェクト指向とクラス
例
class Person
def initialize(name, birthday, age, address)
@name = name
@birthday = birthday
@age = age
@address = address
end
def answer_name
puts @name
end
end
//実行結果
suzuki = Person.new("suzuki", "1988/5/5", "31", "東京都〇〇区〇〇")
=> #<Person:0x007fc55c1604f8 @name="suzuki", @birthday="1988/5/5", @age="31", @address="東京都〇〇区〇〇">
irb(main):016:0> suzuki.answer_name
suzuki
人(Person)クラスを定義して、名前や生年月日などのデータを持つようにしています。answer_nameで自分の名前を答えます。クラスの中にそのクラスとして必要なデータや振る舞いを持たせることで、そのオブジェクトの仕事はそのオブジェクトに任せることができます。
ちなみに、オブジェクト指向を全く取り入れずにアプリケーションを作ることもできます。ですが、オブジェクト指向はユーザーのためのではなく、開発者同士の円滑なコミュニケーションのためにあると考えています。
手続き型とオブジェクト指向
「手続き型とオブジェクト指向は何が違うのか」というと、手続き型(手続き型言語)とは、「プログラムを順序どおりに実行させるような書き方」のことを言います。
例えば、「Rubyを使ってあるファイルから品物の合計金額を表示する」というタスクを考えた時に以下のように書きます。
出力結果
りんご,100,3
ばなな,180,2
ぶどう,240,1
みかん,300,5
いちご,500,2
手続き型の場合
手続き型で記述をすると以下のようなコードになります。
手続き型
# ファイルを読み込む
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クラスを作成し、読み込んだファイルの値をもとにインスタンスを作成し、インスタンスメソッドで合計金額を計算するようにします。
オブジェクト指向
# 品物クラス
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
手続き型とオブジェクト指向の比較
オブジェクト指向は手続き型に比べコード行数が増えていて、メリットが少ないように感じます。しかし、このコードに機能追加が増えてくると、「ファイルを読み込む部分」と「品物に関して処理する部分」が分離されているので見通しが良くなることが期待できます。
簡単に記述するなら手続き型風の記述は便利ですが、ロジックが増えることが予想される部分はオブジェクト指向型の記述方法を取り込みます。
オブジェクト指向の例
Railsでオブジェクト指向活かしたアプリケーションを作成する例です。
前提として
名刺管理アプリケーションを開発していると想定します。
以下のような機能があるとします。
個人の名前とメールアドレスを登録できる
企業の名前を登録できる
個人は企業と紐づく(所属する)
Railsでは、以下のようなモデルになります。
例
class Person < ActiveRecord::Base
belongs_to :company
# DBにname, emailカラムを持つ
end
class Company < ActiveRecord::Base
has_many :people
# DBにnameカラムを持つ
end
人物情報の表示
例えばあるページで次のような表示とします。
例
山田 太郎 (株式会社◯◯ 所属)
このためにViewに次のような記述にしてます。
例
<%= @person.name %> (<%= @person.company.name %>)
このような表示に変えたい箇所がアプリケーション内に複数あります。
もし、すべての箇所でこの記述に変更すると、同じコードがたくさんの場所にコピーされることになります。
今回は、個人が企業に紐付いてる場合です。
しかし、例えばサービスの仕様変更により、企業に紐付かない個人も登録することができる場合になったとします。
個人が企業に紐づくかどうかによって以下のように場合分けが必要です。
例
山田 太郎 (株式会社◯◯ 所属) <- 所属がある場合
山田 太郎 (フリーランス) <- 所属が無い場合
Viewは以下のように変更します。
例
<% if @person.company %>
<%= @person.name %> (<%= @person.company.name %>)
<% else %>
<%= @person.name %>
<% end %>
変更箇所が1つなら良いですが、既にアプリケーション内の多くの場所にコピーしてたら変更を加えなければなりません。
これでは、変更すべき箇所が抜け漏れたり、多くの箇所が変更されることによりバグが混入可能性があります。
「このサービスには今後このような仕様変更の可能性がある」と予測を立て、変更に強いアプリケーションの設計を考えましょう。
以下のように変更すると、今回のような大規模なViewの変更という事態は防げます。
例
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つです。
例えば、開発を行うとしてオブジェクト指向に則ったアプリケーションを開発することで、開発時に現れる課題が自動的に解決されます。
クラスとインスタンス
上記の名刺管理アプリケーションの例を使って用語の説明。
Person
クラス。具体的なデータに紐付かない「データの抽象的な振る舞い」を設定するもの。
@person
Personクラスの インスタンス。クラスから生成される「具体的なデータとそれに対する処理」をまとめたもの。
@person.name
プロパティ。インスタンスが持つデータのこと。Ruby on Railsの場合には、データベースのカラムと同一のものを指している事が多い。
@person.display_info
インスタンスメソッド。インスタンスがもつプロパティなどを使ってデータを加工したり処理するもの。
※厳密には、Rubyという言語の性質上、プロパティとメソッドは実は同じ仕組みで成り立っており違いはないのですが、「オブジェクト指向」として考えた時には違いがあるために区別しました。
役に立つ原則
DRY
繰り返しを避けるという原則です。アプリケーションのコードはどのコードも将来バグになる可能性があります。重複があると、アプリケーションのコードは不必要に大きくなり、それにつれて、バグが生じる危険性も高まります。また、システムの構造は、そう意図していないにもかかわらず複雑になってしまいます。重複によりコードベースが大きくなれば、開発に携わる人間がシステム全体を完全に理解することも難しくなります。特に困るのはコードに変更を加える時です。どこかに変更を加えた場合、それとロジック等が重複している箇所にも同様の変更が必要かどうか確認しなければなりません。DRY原則を守ればこのような事態を防ぐことができます。
YAGNI
実際に必要になった機能だけを追加すべきという原則です。漠然とした予想や見込みで追加された機能が使われないまま放置されたり、プログラムの規模が必要以上に大きくなることで設計や構造が複雑になり保守や修正が難しくなり、バグや不具合が起きやすくなります。つまり、今現在具体的な必要性がないのに、将来必要になるかもしれない、あれば便利かもしれないなどといった見込みや思い込みで機能や要素を追加するべきではないとうことになります。
単一責任の原則
クラスが果たす役割を一つだけにするという原則です。例えば、Rectangleクラス(四角形クラス)があるとします。このクラスには四角形を描くメソッドと四角形の面積を計算するメソッドがあるとします。Rectangleクラスには描画と計算の2つの役割があることになります。計算方法を変える(単位の変更など)時と、描画方法を変える(描画する正方形に色をつけるなど)時でクラスの記述を変更する理由が2つになります。役割が増えるほど変更理由が増え、変更が起きやすくなります。このような時は役割ごとにクラスを分けるべきで、役割ごとに分けることで変更する際に該当するクラスを変更するだけで済みます。
インターフェイス分離の原則
あるクラスを利用するクライアントが複数存在するような場合に、各クライアントに共通の大きなインターフェイスを使ってしまうと、各クライアント内では使用しないメソッドがあった場合、クラスが使わないメソッドにも依存してしまいます。つまり、各クライアントが利用していないメソッドに対する変更の影響まで受けてしまう可能性があるということになります。そこで各クライアントが呼び出す必要のあるサービスの単位でクライアントをグループ分けし、そのグループに特化したインターフェイスを準備することで、異なるサービス間での関連性を分断することができます。