🍒 はじめに
チェリー本輪読会の第11週目のエントリーになります。
輪読会の概要については第1週目にまとめています。
- 第1週目のエントリーはこちら
- 第2週目のエントリーはこちら
- 第3週目のエントリーはこちら
- 第4週目のエントリーはこちら
- 第5週目のエントリーはこちら
- 第6週目のエントリーはこちら
- 第7週目のエントリーはこちら
- 第8週目のエントリーはこちら
- 第9週目のエントリーはこちら
- 第10週目のエントリーはこちら
🍒 輪読会 第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 弦楽器
楽器は弦楽器の一種である
クラスの図はサブクラスからスーパークラスへ向かって矢印↑を伸ばすことで、クラスの関係を表します。
上記の話しをベースに、楽器クラスを題材にクラスの継承関係を考えてみました。
- まず楽器は、弦楽器、管楽器、打楽器などありますが、弦楽器を選択してみました。
- 弦楽器 is a 楽器
- それから、弦楽器の中からギターを選び、エレキギターを選択していきます。
- ギター is a 弦楽器
- エレキギター is a ギター
- さらに、メーカーはギブソンを選択します(もちろん、フェンダーもグレッチも同じ関係を持つことができます)
- 最後に、ギブソンのシリーズ名を選択します(ここでも、フライングV、SGなども同じ関係を持つことができます)
スーパークラス(楽器)に向かうほど、汎化していき、サブクラス(ギブソン)に向かうほど特化していきます。スーパークラスからサブクラスへ、上から下(左から右)へ性質を受け継いでいきます。これを「継承」と呼びます。
サブクラスである「ギブソン」は、スーパクラスの「エレキギター」の特徴を全て継承しています。(鉄製の弦を装備している。ピックアップを内蔵している。弦の振動を電気信号に変換している。など)しかし、スーパークラスの関係性にない「フェンダー」の特徴は継承していません。(製作している工場が違う。ピックアップの種類が違う。音色が違う。など)
オブジェクトのクラスの確認方法
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
他、こちらの伊藤さんの記事も参考にさせていただきました。
輪読会「オブジェクト指向でなぜつくるのか第3版」へ参加
第2回目の輪読会へ参加しました。オブジェクト指向の三大要素(クラス、ポリモーフィズム、継承)を中心に学びました。
そこで、オブジェクト指向設計においての大事な原則を教えてもらいましたので紹介したいと思います。
Tell, Don't Ask(求めるな、命じよ)
クラスの役割についての原則です。
理想的なオブジェクト指向設計において、クラスを呼び出す側は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
参考書籍
- 伊藤淳一 著/『プロを目指す人のためのRuby入門 言語仕様からテスト駆動開発・デバッグ技法まで』/技術評論社/2017年https://gihyo.jp/book/2017/978-4-7741-9397-7
- 五十嵐邦明,松岡浩平 著/『ゼロからわかる Ruby 超入門』/技術評論社/2018年https://gihyo.jp/book/2018/978-4-297-10123-7
- 高橋征義、後藤裕蔵 著/『たのしいRuby第6版』/SBクリエイティブ/2019年https://tanoshiiruby.github.io/6/index.html
- プログラミング言語 Ruby リファレンスマニュアル https://docs.ruby-lang.org/ja/
- 小餅良介 著/ 『独習Ruby on Rails』https://www.shoeisha.co.jp/book/detail/9784798160689
- 平澤章 著/『オブジェクト指向でなぜつくるのか 第3版 』https://www.nikkeibp.co.jp/atclpubmkt/book/21/S00180/
🍒 まとめ
クラスの後半戦に入り、徐々に内容が難しくなってきました。
しかし、同時に参加している『オブジェクト指向でなぜつくるのか』輪読会で学んだことも理解の後押しとなり、オブジェクト指向の需要な要素である「クラス」について、理解ができるようになってきました。
クラスは「まとめて、隠して、たくさん作る」仕組みです。プログラムの無駄を省いて整理整頓するために、このクラスの仕組みが必要不可欠となります。まだまだとっちらかっている自分で書いたプログラム。今後はクラスを用いて整理整頓できるように、試行錯誤していきたいと思います。
では、また来週!(次回、第12週目)