🍒 はじめに
チェリー本輪読会の第10週目のエントリーになります。
輪読会の概要については第1週目にまとめています。
🍒 輪読会 第10週目まとめ
第7章7.1.1〜第7章7.5.3まで
期間:2021年07月26日〜2021年07月30日
クラスを使う場合と使わない場合の比較
オブジェクト指向でなぜつくるのかに書かれていたことですが、クラスを用いる理由にも該当するため紹介したいと思います。
オブジェクト指向はソフトウエアの保守や再利用をしやすくすることを重視する技術です。個々の部品により強く着目し、部品の独立性を高め、それらを組み上げてシステム全体の機能を実現することを基本にします。部品の独立性を高めることで、修正が起きた場合の影響範囲を最小限にし、他のシステムで容易に再利用できるようにします。 出典:オブジェクト指向でなぜつくるのか 第3版 22頁
上記のオブジェクト指向を元に、まずはクラスを用いることで堅牢なプログラムの作成の基本を学びます。プログラムが大規模になるほど、データとメソッドを一緒に持ち運べるクラスのメリットは大きくなります。
クラスを使わないプログラムの例
- 配列を準備し、欲しいコンピュータのハッシュデータを入れます。
- その後、配列にしたcomputers変数のデータをeachで取り出します。
computers = []
computers << {type: 'mac', cpu: 'M1', memory: 32, storage: 'SSD256' }
computers << {type: 'win', cpu: 'intel', memory: 16, storage: 'HDD50' }
computers.each do |c|
puts "タイプ: #{c[:type]}、CPU: #{c[:cpu]}、メモリ: #{c[:memory]}、ストレージ: #{c[:storage]}"
end
=> タイプ: mac、CPU: M1、メモリ: 32、ストレージ: SSD256
=> タイプ: win、CPU: intel、メモリ: 16、ストレージ: HDD50
クラスを使ったプログラムの例
- Computerクラスを準備します。
- コンピュータのスペック表示用でメソッドを定義します。
※ attr_reader
initialize
は後ほど取り上げます。
class Computer
attr_reader :type, :cpu, :memory, :storage
def initialize(type, cpu, memory, storage)
@type = type
@cpu = cpu
@memory = memory
@storage = storage
end
def display_pc_spec
puts "タイプ: #{@type}、CPU: #{@cpu}、メモリ: #{@memory}、ストレージ: #{@storage}"
end
end
Computer.new
で、インスタンスを生成します。その際、引数に欲しいコンピュータのデータを渡しておきます。
- 定義したメソッドを呼び出します。
mac = Computer.new(:mac, :M1, 32, :SSD256)
mac.display_pc_spec
win = Computer.new(:win, :intel, 16, :HDD500)
win.display_pc_spec
=> タイプ: win、 CPU: intel、メモリ: 16、ストレージ: HDD500
クラスを使わない場合、使った場合ともに、結果は同じになります。
オブジェクト指向を完全に表現できているわけではないと思いますが、2つの例から、後者がよりオブジェクト指向寄りのプログラムになります。
クラスを定義することで、コンピュータのスペック内容を(属性)を保持することのできる、設計図が出来上がります。class Computer
それから、動作や振る舞いを表すメソッドを定義します。def display_pc_spec
今回は、コンピュータのスペック情報を表示できる機能を持たせました。
ここまで出来たら、後はクラスに命を吹き込みます。インスタンスを作ります。Computer.new
引数に欲しいコンピュータの情報を渡してあげることで、希望するインスタンスを生成することができます。
最後に、メソッドを呼び出しますmac.display_pc_spec
。レシーバ(先ほど生成したインスタンス)に対して、メソッドを実行することができます。mac変数(レシーバ)に対して、「スペックを表示して」という指示を出しています。
上記で取り上げたクラスのことですが、クラスは「オブジェクトの設計図」と表現されることが多いです。
クラスはインスタンス(オブジェクト)と対になる要素です。上記の例では、コンピュータ(クラス)、Mac(インスタンス)という例で書いてみましたが、コンピュータクラスを使って、いろいろなインスタンスを生成することができます。Winのコンピュータもあれば、Macの超ハイスペックモデルもあるかもしれません。予め容易した属性に、好きなデータを入れ込んで作ることができます。
Compute.new(:好きなOS, :好きなCPU, 希望するメモリ, :希望するストレージ)
クラスは、英語でclass「分類、種類」「同種のものの集まり」を意味します。インスタンスは、英語でinstance「具体的なモノ、実例」を意味します。クラスという種類を元に、そこから具体的なモノを生成するしくみは、プログラムをより堅牢で再利用しやすくすることに繋がります。
3つとも同じ意味です。
文脈によって使い分けられているようです。
インスタンスという言葉はオブジェクトとほとんど同じ意味で使われています。一方、あるオブジェクトが、あるクラスに属していることを強調する場合には、「インスタンス」のほうがよく使われます。 出典:たのしいRuby 79頁
レシーバは英語で書くと"receiver"で、「受け取る人」や「受信者」という意味です。なので、「レシーバ」は「メソッドを呼び出された側」というニュアンスを出したいときによく使われます。 出典:チェリー本 210頁
メソッド、メッセージ
オブジェクトが持つ「動作、振る舞い」をメソッドと表現します。
ここでは、メソッド名をdisplay_pc_spec
と定義し、Computerクラスから、インスタンスを生成するときに、コンピュータのスペックを表示する振る舞いを持つメソッドを書きました。
def display_pc_spec
puts "タイプ: #{@type}、CPU: #{@cpu}、メモリ: #{@memory}、ストレージ: #{@storage}"
end
メッセージは、レシーバと組み合わせて使われます。
変数のmac
をレシーバとし、display_pc_spec
をメッセージとして呼んでいます。「コンピュータのスペックを表示しろ」と、レシーバに対してメッセージを送っているイメージです。
mac = Computer.new(:mac, :M1, 32, :SSD256)
mac.display_pc_spec
状態(ステート)
オブジェクトごとに保持される状態のことを指します。ここでは、引数で渡されたデータによってインスタンスが生成されます。生成されたインスタンスは、「macタイプで、M1のCPUを搭載した、メモリ32でストレージSSD256のコンピュータ」になります。このコンピュータの持つデータが、オブジェクト指向の考え方で言う状態(ステート)です。
mac = Computer.new(:mac, :M1, 32, :SSD256)
mac.display_pc_spec
=> タイプ: mac、 CPU: M1、メモリ: 32、ストレージ: SSD256
attr_reader :type, :cpu, :memory, :storage
として、オブジェクトに設定することができる値を属性と呼びます。
mac = Computer.new(:mac, :M1, 32, :SSD256)
クラス.newをする際に引数を渡していますが、これにより属性:type, :cpu, :memory, :storage
へ希望する値を指定してオブジェクトを生成することができます。
属性とは、オブジェクトに属していて、取得、設定が可能な値のことです。
class Computer
attr_reader :type, :cpu, :memory, :storage
mac = Computer.new(:mac, :M1, 32, :SSD256)
mac.display_pc_spec
=> タイプ: mac、 CPU: M1、メモリ: 32、ストレージ: SSD256
initializeメソッド
インスタンスを初期化するために使われます。クラス名.new
を実行することにより、オブジェクトが生成されますが、その際、真っ先に実行されるメソッドです。
class Computer
def initialize(type, cpu, memory, storage)
puts "タイプ: #{type}、CPU: #{cpu}、メモリ: #{memory}、ストレージ: #{storage}"
end
end
Computer.new(:mac, :M1, 32, :SSD256)
=> タイプ: mac、CPU: M1、メモリ: 32、ストレージ: SSD256
引数の数が合わないとき、エラーになります。
実引数へ4つ与えられるべきですが、3つですよ。足りないですよ!と、怒られています。
class Computer
def initialize(type, cpu, memory, storage)
puts "タイプ: #{type}、CPU: #{cpu}、メモリ: #{memory}、ストレージ: #{storage}"
end
end
Computer.new(:mac, :M1, 32)
(irb):30:in `initialize': wrong number of arguments (given 3, expected 4) (ArgumentError)
アクセサメソッド
インスタンス変数の値を読み書きするメソッドのことです。
- attr_reader 読み取り専用(ゲッターメソッド)
- attr_writer 書き込み専用(セッターメソッド)
- attr_accessor 読み書き両方に対応
Module#attr_accessor (Ruby 3.0.0 リファレンスマニュアル)
ゲッターメソッド(読み取り専用部分)
attr_readerを使わない場合のプログラム
def computer
@type
end
セッターメソッド(書き込み専用部分)
attr_writerを使わない場合のプログラム
computer=
メソッドは、computerとしていますが、fooでもbarでも何でも使えます。このメソッドは、@typeを外部から変更するためのメソッドです。computer=(value)
の仮引数としてvalue
としていますが、これもcomputerと同じく何でも大丈夫です。慣例的にvalueにすることが多いそうです。
Rubyは、=
で終わるメソッドを定義すると、変数に代入するような形式でそのメソッドを呼び出すことができます。個人的にはこの項目はかなり躓きました。こちらは、『ゼロからわかるRuby超入門』の198頁の説明が分かりやすくてオススメなのでぜひご一読ください。
def computer=(value)
@type = value
end
実際にプログラムに書き込んでみます。
読み取り、書き込み共に成功します。
class Computer
def initialize(type)
@type = type
end
def computer
@type
end
def computer=(value)
@type = value
end
end
mac = Computer.new(:mac)
mac.computer
=> :mac
mac.computer = :マックブック
=> :マックブック
ゲッターメソッドだけ削除してみます。
読み取りはエラーが出ます。書き込みは成功します。
class Computer
def initialize(type)
@type = type
end
def computer=(value)
@type = value
end
end
win = Computer.new(:win)
=>
win.computer
(irb):18:in `<main>': undefined method `computer' for #<Computer:0x00007fd9520ade48 @type=:ウィンドウズ> (NoMethodError)
# 書き込みは成功。
win.computer = :ウィンドウズ
=> :ウィンドウズ
attr_accessor
メソッド(読み取り、書き込み)
attr_accessorメソッドを使ってプログラムを呼び出してみます。
このメソッドは、読み取り、書き込みの両方に対応しています。
class Computer
attr_accessor :type
def initialize(type)
@type = type
end
end
linux = Computer.new(:linux)
=>
linux.type
=> :linux
linux.type = :リナックス
=> :リナックス
attr_reader
メソッド 読み取り専用(別名:ゲッターメソッド)
読み取り専用にしたい時は、こちらのメソッドでも対応できます。
class Computer
attr_reader :type
def initialize(type)
@type = type
end
end
linux = Computer.new(:linux)
=>
linux.type
=> :linux
linux.type = :リナックス
(irb):10:in `<main>': undefined method `type=' for #<Computer:0x00007f8a9610b008 @type=:linux> (NoMethodError)
attr_writer
メソッド 書き込み専用(別名:セッターメソッド)
書き込み専用にしたい時は、こちらのメソッドでも対応できます。
class Computer
attr_writer :type
def initialize(type)
@type = type
end
end
linux = Computer.new(:linux)
=>
linux.type
(irb):9:in `<main>': undefined method `type' for #<Computer:0x00007fe0640e1108 @type=:linux> (NoMethodError)
# 書き込み
linux.type = :リナックス
=> :リナックス
クラスメソッドの定義
クラスメソッドとは、オブジェクトを作らずに呼び出せるメソッドのことです。レシーバがクラスになるので、クラスに対して呼び出せます。ひとつひとつの「インスタンスに含まれるデータは使わない」メソッドを定義したい場合もあるので、そのような場合はクラスメソッドを定義した方が良いです。
クラスメソッドを定義する方法は2つありますが、ここでは、よく使われるメソッドの前にself
を付ける手法を例に取り上げます。
class クラス名
def self.クラスメソッド
end
end
プログラムの例が無理やり感ありますが、インスタンスを生成しなくてもメソッドを呼び出すことができています。
class Computer
def initialize(type)
@type = type
end
def self.upgrade(type)
type.upcase
end
end
Computer.upgrade(:big_sur)
=> :BIG_SUR
メソッド名の表記法について
メソッドの表記についてです。るりまや、他ドキュメント等では下記のように記載されています。
#
はインスタンスメソッドであることを表しています。
- クラス名.メソッド名 または、クラス名::メソッド名
.
::
はクラスメソッドであることを表しています。
このマニュアルのヘルプ (Ruby 3.0.0 リファレンスマニュアル)
定数
一般に定数という言葉は、書き換えが不可能なことを表します。
定数 (プログラミング) - Wikipedia)
しかし、Rubyにおける定数は、書き換えが可能です。
Rubyの定数は「みんなわざわざ変更するなよ」と念押しした変数のようなものです。定数という言葉に惑わされないようにしてください。ミュータブル(変更可能)なオブジェクトの場合、定数の中身を変更できます。(String, Array, Hashなど)
定数を使用する理由は、 マジックナンバー をなくすためというのもあります。
ハードコーディングされた値のことをマジックナンバーと呼びます(本来、別の場所に保存しておくべき値をソースコードの中に直接記述してしまうこと)
また、書き方についてですが、定数は大文字で始める必要があります。最初の一文字が大文字であればOKですが、慣習的には全部大文字で書くことが多いです。
COMPUTER = 'MacBookPro'
=> "MacBookPro"
COMPUTER = 'WindowsMachine'
warning: already initialized constant COMPUTER
warning: previous definition of COMPUTER was here
=> "WindowsMachine"
selfキーワード
Rubyでは、インスタンスメソッドの中で、メソッドのレシーバ自身を参照するために、selfという特別な変数を使います。selfを付けても付けなくても挙動は変わりませんが、ここでのポイントは、selfがレシーバであることを理解することです。ちなみに、selfは省略して書かれることが多いとの事です。
以下、メソッドを呼び出す3パターンを用意しました。
- selfなしで、
greet_kiyoshiro
メソッドを呼び出す
- self付きで、
greet_hiroto
メソッドを呼び出す
- インスタンス変数
@greeting
を直接参照して、greet_yusuke
メソッドを呼び出す
class RocknrollHero
attr_accessor :greeting
def initialize(greeting)
@greeting = greeting
end
def greet_kiyoshiro
"#{greeting}!!、愛しあってるかい!?"
end
def greet_hiroto
"#{self.greeting}!!、我々はクロマニヨンズだ!!"
end
def greet_yusuke
"#{@greeting}!!、俺たちが日本のザ・ミッシェルガンエレファントだ!!"
end
end
hero = RocknrollHero.new('ロックンロール')
=>
hero.greet_kiyoshiro
=> "ロックンロール!!、愛しあってるかい!?"
hero.greet_hiroto
=> "ロックンロール!!、我々はクロマニヨンズだ!!"
hero.greet_yusuke
=> "ロックンロール!!、俺たちが日本のザ・ミッシェルガンエレファントだ!!
selfが省略できない場合
name=
メソッドのように、=
で終わるメソッドを呼び出す場合は、selfの省略ができません。メソッド内で、セッターメソッド(書き込み専用)を呼び出す際には注意が必要です。
class RocknrollHero
attr_accessor :greeting
def initialize(greeting)
@greeting = greeting
end
def greet_other_kiyoshiro
greeting = 'ハローベイベー'
end
def greet_other_yusuke
self.greeting = 'ハローベイベー'
end
end
hero = RocknrollHero.new('ロックンロール')
=>
hero.greet_other_kiyoshiro
hero.greeting
=> "ロックンロール"
hero.greet_other_yusuke
hero.greeting
=> "ハローベイベー"
クラスの章に入るタイミングで、下記の輪読会が開催されると聞き「これは良きタイミングだ!」と思い参加してみました。(7/26に開催)
www.nikkeibp.co.jp
ここ最近よく耳にする「オブジェクト指向」
わかるようでわからない。そんな、奥の深いオブジェクト指向を学べる良書ということで、ワクワクした気持ちで本を読みました。週一ペースの開催になるため、まだ第2章までしか読み進めていませんが、こちらで学んだことも一部備忘録として残しておきたいと思います。
「オブジェクト指向でなぜソフトウエアを作るのですか?」誰かにこんな質問をされたなら、著者はこう答えます。「その理由はソフトウエアを楽に作りたいからです。」 出典:オブジェクト指向でなぜつくるのか 第3版 21頁
冒頭の一文です。
当初、私はオブジェクト指向とは、よく見かけるたい焼きや動物などで表現されている、単にクラスやインスタンスの話しだけだと思っていました。しかし、そんな単純なことではなく、今やオブジェクト指向をカバーする範囲は広く、ソフトウエア開発の総合技術として、プログラマが「ソフトウエアを楽につくるため」に取られる開発手法全般を指していることがわかりました。
オブジェクト指向が難しい理由3つ
- プログラミング言語の仕組みが複雑
- 比喩を使った説明による混乱
- オブジェクト指向というコンセプトが抽象的
また、難しいと思われている理由が3つ述べられており、特に3つ目の理由にも当てはまるであろう「全てがオブジェクト」という概念。このあたりの、難しいと思っている理由を少しでも紐解き、今後の章を読み進めるなかで理解を進めていきたと思います。
参考書籍
🍒 まとめ
今週からついにクラスの章に入りました!
輪読会で一番読みたかった章です。一度、通読してはいますが、クラスの章は「手強い...」という印象で、前回は完敗とまではいきませんが、負けを認めざるえない散々な結果でした...。輪読会メンバーの多くもこの章で苦労している方が多かったです。今回はそんな強敵クラスへリベンジすべく!疑問点は輪読会内で積極的に質問したり、復習にも時間をかけたりと気合を入れて取り組みました。
また、『オブジェクト指向でなぜつくるのか第3版』の輪読会も始まり、クラスの章を学ぶ上で、オブジェクト指向の考えが助けになっています。
学習を進めれば進めるほど、色々な壁が立ちふさがりますが、ひとつひとつ積み重ねて乗り越えていきたいと思います。
では、また来週!(次回、第11週目)