Roll With IT

tamakiのIT日記

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

f:id:shirotamaki:20210619091936p:plain

🍒 はじめに

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

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

🍒 輪読会 第11週目まとめ

第7章7.6.1〜第7章7.9.3まで

期間:2021年08月02日〜2021年08月06日

クラスの継承

オブジェクト指向プログラミングの三大要素のひとつでもある「継承」を学びました。

以下、継承を図で表してみましたが、ひと言で説明すると「継承はクラスの特徴を共有すること」です。動物やゲームのキャラクターなど、いろいろな例があるとは思いますが、生き物ではなくモノでも当てはまるか考えてみました。

is-aの関係

クラスの基本として、スーパークラス、サブクラスという関係があります。

親と子の関係でも表せます。親クラス、子クラス。

以下では、「弦楽器」というクラスの中に「ギター」というクラスが存在します。これを「is-aの関係」と呼び、「サブクラスはスーパークラスの一種である」(サブクラス is a スーパークラス)と当てはめた場合、違和感がないか?継承関係が正しいか確かめることができます。

# 文脈は正しい

ギター is a 弦楽器
ギターは弦楽器の一種である。

サブクラスのギターは、ベースでも、ウクレレでも継承関係は正しいので問題ないですが、弦楽器のスーパークラスに値する「楽器」となると、関係がおかしくなります。

# 文脈的におかしい

楽器 is a 弦楽器
楽器は弦楽器の一種である

クラスの図はサブクラスからスーパークラスへ向かって矢印↑を伸ばすことで、クラスの関係を表します。

f:id:shirotamaki:20210825182458p:plain

上記の話しをベースに、楽器クラスを題材にクラスの継承関係を考えてみました。

  • まず楽器は、弦楽器、管楽器、打楽器などありますが、弦楽器を選択してみました。
    • 弦楽器 is a 楽器
  • それから、弦楽器の中からギターを選び、エレキギターを選択していきます。
  • さらに、メーカーはギブソンを選択します(もちろん、フェンダーもグレッチも同じ関係を持つことができます)
  • 最後に、ギブソンのシリーズ名を選択します(ここでも、フライングV、SGなども同じ関係を持つことができます)

f:id:shirotamaki:20210825182341p:plain

スーパークラス(楽器)に向かうほど、汎化していき、サブクラス(ギブソン)に向かうほど特化していきます。スーパークラスからサブクラスへ、上から下(左から右)へ性質を受け継いでいきます。これを「継承」と呼びます。

サブクラスである「ギブソン」は、スーパクラスの「エレキギター」の特徴を全て継承しています。(鉄製の弦を装備している。ピックアップを内蔵している。弦の振動を電気信号に変換している。など)しかし、スーパークラスの関係性にない「フェンダー」の特徴は継承していません。(製作している工場が違う。ピックアップの種類が違う。音色が違う。など)

オブジェクトのクラスの確認方法

classメソッドを使います。

class Gibson
    puts 'I play guitar'
end

gibson = Gibson.new
=> #<Gibson:0x00007fdcdd045f08>
gibson.class
=> Gibson

instance_of?メソッドを使うと、引数にクラスを渡すことで、true、falseで確認できます。

gibson = Gibson.new
=> #<Gibson:0x00007fdcdd0d7688>
gibson.instance_of?(Gibson)
=> true
gibson.instance_of?(String)
=> false

is_a?メソッドを使うと、継承関係を確認できます。GibsonクラスのスーパークラスであるObjectクラスは、trueになります。

gibson = Gibson.new
=> #<Gibson:0x00007fdcd90d2df8>
gibson.is_a?(Gibson)
=> true
gibson.is_a?(Object)
=> true
gibson.is_a?(String)
=> false

ほかのクラスを継承したクラスを作る

class サブクラス < スーパークラス
end

Guitarクラス(スーパークラス)を作り、その後にElectric_guitarクラス(サブクラス)を作り継承させてみます。

class Guitar 
    attr_reader :brand, :price

    def initialize(brand, price)
        @brand = brand
        @price = price
    end
end

class Electric_guitar < Guitar
    attr_reader :model

    def initialize(brand, price, model)

# @brand、@priceの箇所をsuperを使って呼び出すことができる。
# superを使うと、スーパークラスの同名メソッドを呼び出すことができる。

        super(brand, price)       
        @model = model
    end
end

electric_guitar = Electric_guitar.new('Gibson', 1000000, 'LesPau
l')
=>
#<Electric_guitar:0x00007fc73d1613c0
...
electric_guitar.brand
=> "Gibson"
electric_guitar.price
=> 1000000
electric_guitar.model
=> "LesPaul"

しかし、そもそもスーパークラスとサブクラスで実行する処理が変わらなければ、サブクラスで同名メソッドを定義したり、superで呼んだりする必要はありません。

class Guitar 
    attr_reader :brand, :price

    def initialize(brand, price)
        @brand = brand
        @price = price
    end
end

class Electric_guitar < Guitar
    # スーパークラスに処理を任せる。
end

electric_guitar = Electric_guitar.new('Gibson', 1000000)
=> #<Electric_guitar:0x00007f914618c318 @brand="Gibson", @price=1000000>
electric_guitar.brand
=> "Gibson"
electric_guitar.price
=> 1000000

メソッドの公開レベル

publicメソッド

クラスの外部からでも自由に呼び出せるメソッドのことです。

インスタンスメソッドとして返るように公開することを指します。initializeメソッド以外のインスタンスメソッドは、デフォルトでpublicメソッドが指定されています。

class Guitar 
    def buy
        '私はギターを買いました' 
    end
end

guitar = Guitar.new
=> #<Guitar:0x00007fc82f1873c0>

# buyメソッドを外から呼び出している。
guitar.buy
=> "私はギターを買いました"

privateメソッド

クラスの外からは呼び出せず、クラスの内部でのみ使えるメソッドです。

クラス内でprivateキーワードを書くと、そこから下で定義されたメソッドはprivateになります。

class Guitar 
    private

    def buy
        '私はギターを買いました'
    end
end

guitar = Guitar.new
=> #<Guitar:0x00007fe8a81accf8>
guitar.buy
#=> : private method `buy' called for #<Guitar:0x00007fe8a81accf8> (NoMethodError)

レシーバを指定して呼び出すことができないメソッドになるので、以下のようにself.brandとした場合は、NoMethodErrorとなり呼び出せません。brandメソッドはprivateメソッドになるためです。

class Guitar
  def buy
    "私は、#{self.brand}ギターを買いました"
  end

  private

  def brand
    'Gibson'
  end
end

guitar = Guitar.new
guitar.buy
#=> : private method `brand' called for #<Guitar:0x00007fb3221126f0> (NoMethodError)

protectedメソッド

メソッドを定義したクラス自身とそのサブクラスからは、インスタンスメソッドをレシーバ付きで呼び出すことができます。それ以外の場所からは呼び出せません。

class Guitar
# 一旦publicメソッドとして定義する。
    attr_reader :brand, :price

# priceのみ、protectedメソッドへ変更する。
    protected :price 

    def initialize(brand, price)
        @brand = brand
        @price = price
    end

    def which_is_expensive?(other_brand)
        other_brand.price < @price
    end
end

gibson = Guitar.new('Gibson', '100万円')
=> #<Guitar:0x00007f8fedb68b20 @brand="Gibson", @price="100万円">
fender = Guitar.new('Fender', '80万円')
=> #<Guitar:0x00007f8fedaa8758 @brand="Fender", @price="80万円">

gibson.which_is_expensive?(fender)
=> false
fender.which_is_expensive?(gibson)
=> true

gibson.brand
=> "Gibson"

# クラス外からは、priceは呼び出せない。protectedメソッドのため。
gibson.price
`<main>': protected method `price' called for #<Guitar:0x00007f8fedb68b20 @brand="Gibson", @price="100万円"> (NoMethodError)

定数の再代入を防ぐ

Rubyの定数は再代入が可能です。warningと警告は出ますが、再代入ができてしまいます。

FOO = 'bar'
=> "bar"
FOO = 'hoge'
warning: already initialized constant FOO
warning: previous definition of FOO was here
=> "hoge"

誤って変更されないためには、freezeが必要になります。

しかし、単に定数へfreezeしただけでは、値の変更は防ぐことはできますが、再代入は可能なままです。

# 値の変更は防ぐことはできる。
FOO = 'bar'
=> "bar"
FOO.freeze
=> "bar"
FOO.upcase!
(irb):10:in `upcase!': can't modify frozen String: "bar" (FrozenError)

# しかし、再代入は防ぐことができない。
FOO = 'hoge'
(irb):11: warning: already initialized constant FOO
(irb):1: warning: previous definition of FOO was here
=> "hoge"

再代入を防ぐには、クラスを準備する必要があります。

クラス外部から定数を参照するには、クラス名::定数名 とします。

# クラスを凍結して再代入を阻止する。
class Hoge
    FOO = 'bar'
end

Hoge.freeze
Hoge::FOO = 'piyo'
`<main>': can't modify frozen #<Class:Hoge>: Hoge (FrozenError)

# クラス内でfreezeを呼び再代入を阻止する。
class Hoge
   FOO = 'bar'
  
  freeze
   FOO = 'piyo'
end

`<class:Hoge>': can't modify frozen #<Class:Hoge>: Hoge (FrozenError)

freezeを適用すれば、上記の例のように再代入を防止できますが、通常はこのようにfreezeを呼ぶことはないようです。

マジックコメント # frozen_string_literal: true

# frozen_string_literal: trueというマジックコメントをファイルの先頭に書いておくと、文字列が最初からfreezeされます。freezeされるのは文字列リテラル( ' , " で作成する String オブジェクト) のみであることに注意が必要です。

以下、定数での例ですが、ここでは定数・変数で挙動に違いはありません。

# frozen_string_literal: true

FOO = 'hoge'
FOO.upcase!

`upcase!': can't modify frozen String: "hoge" (FrozenError)

また、文字列リテラルをデフォルトで イミュータブル にする変更は、Ruby 3.0 で導入予定だったようですが、今のところ取りやめになっているようです。

I officially abandon making frozen-string-literals default (for Ruby3).

和訳:Ruby3.0から、frozen-string-literalsをデフォルトにすることは公式に取りやめます。

Matz.

Feature #11473: Immutable String literal in Ruby 3 - Ruby master - Ruby Issue Tracking System

他、こちらの伊藤さんの記事も参考にさせていただきました。

文字列をfreezeさせるいくつかの方法 - Qiita

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

www.nikkeibp.co.jp

第2回目の輪読会へ参加しました。オブジェクト指向の三大要素(クラス、ポリモーフィズム、継承)を中心に学びました。

そこで、オブジェクト指向設計においての大事な原則を教えてもらいましたので紹介したいと思います。

Tell, Don't Ask(求めるな、命じよ)

llcc.hatenablog.com

クラスの役割についての原則です。

理想的なオブジェクト指向設計において、クラスを呼び出す側はAsk(求めること)はせずに、Tell(命令)だけするべきという原則です。オブジェクトに対してあれこれと聞いて手元でロジックを組み立てること(Ask)は良い設計とされていません。オブジェクトに対してロジックの結果を聞け(Tell)ということです。

悪い例 Ask 版

ここでは呼び出す側は求めています。

もし、macのosが:mojaveだったら、osを:catalineにして変更してね。と求めています。

class Computer
  attr_accessor :type, :cpu, :memory, :hard_disk, :os

  def initialize(type, cpu, memory, hard_disk, os)
    @type = type
    @cpu = cpu
    @memory = memory
    @hard_disk = hard_disk
    @os = os
  end
end

mac = Computer.new(:mac, :intel, 32, 124, :mojave)
mac.os
=> :mojave

# もし、macのosが:mojave だったら、osを:catalineにしてね。と求めている。
mac.os = :catalina if mac.os == :mojave
mac.os
=> :catalina

良い例 Tell版

ここでは呼び出す側は命令しています。

mojave?メソッドを呼びます。呼ばれたmojave?メソッドは、@os == :mojaveを調べます。その後、mojaveだったら、trueなので、次は、uppgrade!メソッドが呼ばれます。:catalineが代入されます。

class Computer
  attr_accessor :type, :cpu, :memory, :hard_disk, :os

  def initialize(type, cpu, memory, hard_disk, os)
    @type = type
    @cpu = cpu
    @memory = memory
    @hard_disk = hard_disk
    @os = os
  end

  def upgrade!
    @os = :catalina
  end

  def mojave?
    @os == :mojave
  end
end

mac = Computer.new(:mac, :intel, 32, 124, :mojave)
mac.upgrade! if mac.mojave?
=> :catalina

参考書籍

🍒 まとめ

クラスの後半戦に入り、徐々に内容が難しくなってきました。

しかし、同時に参加している『オブジェクト指向でなぜつくるのか』輪読会で学んだことも理解の後押しとなり、オブジェクト指向の需要な要素である「クラス」について、理解ができるようになってきました。

クラスは「まとめて、隠して、たくさん作る」仕組みです。プログラムの無駄を省いて整理整頓するために、このクラスの仕組みが必要不可欠となります。まだまだとっちらかっている自分で書いたプログラム。今後はクラスを用いて整理整頓できるように、試行錯誤していきたいと思います。

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