Roll With IT

tamakiのIT日記

チェリー本輪読会 第10週目まとめ

f:id:shirotamaki:20210619091936p:plain

🍒 はじめに

チェリー本輪読会の第10週目のエントリーになります。

輪読会の概要については第1週目にまとめています。

🍒 輪読会 第10週目まとめ

第7章7.1.1〜第7章7.5.3まで

期間:2021年07月26日〜2021年07月30日

クラスを使う場合と使わない場合の比較

オブジェクト指向でなぜつくるのかに書かれていたことですが、クラスを用いる理由にも該当するため紹介したいと思います。

オブジェクト指向はソフトウエアの保守や再利用をしやすくすることを重視する技術です。個々の部品により強く着目し、部品の独立性を高め、それらを組み上げてシステム全体の機能を実現することを基本にします。部品の独立性を高めることで、修正が起きた場合の影響範囲を最小限にし、他のシステムで容易に再利用できるようにします。 出典:オブジェクト指向でなぜつくるのか 第3版 22頁

上記のオブジェクト指向を元に、まずはクラスを用いることで堅牢なプログラムの作成の基本を学びます。プログラムが大規模になるほど、データとメソッドを一緒に持ち運べるクラスのメリットは大きくなります。

クラスを使わないプログラムの例

  1. 配列を準備し、欲しいコンピュータのハッシュデータを入れます。
  2. その後、配列にした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

クラスを使ったプログラムの例

  1. Computerクラスを準備します。
  2. コンピュータのスペック表示用でメソッドを定義します。

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
  1. Computer.newで、インスタンスを生成します。その際、引数に欲しいコンピュータのデータを渡しておきます。
  2. 定義したメソッドを呼び出します。
mac = Computer.new(:mac, :M1, 32, :SSD256)
mac.display_pc_spec
#=> タイプ: mac、 CPU: M1、メモリ: 32、ストレージ: SSD256

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)
#=> #<Computer:0x00007ffb288ff400 @type=:mac>

# 読み取り
mac.computer
=> :mac

# 書き込み
# 変数に代入しているように見えるが、メソッドのcomputer=(value)を呼び出している。
mac.computer = :マックブック
=> :マックブック

ゲッターメソッドだけ削除してみます。

読み取りはエラーが出ます。書き込みは成功します。

class Computer
    def initialize(type)
        @type = type
    end

    def computer=(value)
        @type = value
    end
end

win = Computer.new(:win)
=> #<Computer:0x00007fd9520ade48 @type=: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)
=> #<Computer:0x00007fd955a23ce0 @type=:linux>

# 読み取り
linux.type
=> :linux

# 書き込み
linux.type = :リナックス
=> :リナックス

attr_reader メソッド 読み取り専用(別名:ゲッターメソッド)

読み取り専用にしたい時は、こちらのメソッドでも対応できます。

class Computer
    attr_reader :type

    def initialize(type)
        @type = type
    end
end

# インスタンスを生成する。
linux = Computer.new(:linux)
=> #<Computer:0x00007fd955a23ce0 @type=: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)
=> #<Computer:0x00007f8a9b2133b0 @type=: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パターンを用意しました。

  1. selfなしで、greet_kiyoshiroメソッドを呼び出す
  2. self付きで、greet_hirotoメソッドを呼び出す
  3. インスタンス変数@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('ロックンロール')
=> #<RocknrollHero:0x00007f9cdd97df70 @greeting="ロックンロール">

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('ロックンロール')
=> #<RocknrollHero:0x00007f9cda3e71e0 @greeting="ロックンロール">

hero.greet_other_kiyoshiro
hero.greeting
=> "ロックンロール"    # 書き込み(変更)できない

hero.greet_other_yusuke
hero.greeting
=> "ハローベイベー"

輪読会「オブジェクト指向でなぜつくるのか」へ参加

クラスの章に入るタイミングで、下記の輪読会が開催されると聞き「これは良きタイミングだ!」と思い参加してみました。(7/26に開催)

www.nikkeibp.co.jp

ここ最近よく耳にする「オブジェクト指向

わかるようでわからない。そんな、奥の深いオブジェクト指向を学べる良書ということで、ワクワクした気持ちで本を読みました。週一ペースの開催になるため、まだ第2章までしか読み進めていませんが、こちらで学んだことも一部備忘録として残しておきたいと思います。

オブジェクト指向はソフトウエア開発の総合技術

オブジェクト指向でなぜソフトウエアを作るのですか?」誰かにこんな質問をされたなら、著者はこう答えます。「その理由はソフトウエアを楽に作りたいからです。」 出典:オブジェクト指向でなぜつくるのか 第3版 21頁

冒頭の一文です。

当初、私はオブジェクト指向とは、よく見かけるたい焼きや動物などで表現されている、単にクラスやインスタンスの話しだけだと思っていました。しかし、そんな単純なことではなく、今やオブジェクト指向をカバーする範囲は広く、ソフトウエア開発の総合技術として、プログラマが「ソフトウエアを楽につくるため」に取られる開発手法全般を指していることがわかりました。

オブジェクト指向が難しい理由3つ

  1. プログラミング言語の仕組みが複雑
  2. 比喩を使った説明による混乱
  3. オブジェクト指向というコンセプトが抽象的

また、難しいと思われている理由が3つ述べられており、特に3つ目の理由にも当てはまるであろう「全てがオブジェクト」という概念。このあたりの、難しいと思っている理由を少しでも紐解き、今後の章を読み進めるなかで理解を進めていきたと思います。

参考書籍

🍒 まとめ

今週からついにクラスの章に入りました!

輪読会で一番読みたかった章です。一度、通読してはいますが、クラスの章は「手強い...」という印象で、前回は完敗とまではいきませんが、負けを認めざるえない散々な結果でした...。輪読会メンバーの多くもこの章で苦労している方が多かったです。今回はそんな強敵クラスへリベンジすべく!疑問点は輪読会内で積極的に質問したり、復習にも時間をかけたりと気合を入れて取り組みました。

また、『オブジェクト指向でなぜつくるのか第3版』の輪読会も始まり、クラスの章を学ぶ上で、オブジェクト指向の考えが助けになっています。

学習を進めれば進めるほど、色々な壁が立ちふさがりますが、ひとつひとつ積み重ねて乗り越えていきたいと思います。

では、また来週!(次回、第11週目)