This page looks best with JavaScript enabled
⚠️

『オブジェクト志向設計実践ガイド』6章

 ·   ·  ☕ 4 分で読めます
✏️

6章

クラスによる継承

サブクラスでメッセージに応答できない→スーパークラス(上位のクラス)にメッセージを委譲する。

継承を使うべき場所

既にBicycleクラスがある。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Bicycle
  attr_reader :size, :tape_color

  def initialize(args)
    @size = args[:size]
    @tape_color = args[:tape_color]
  end

  def spares
    { chain: '10-speed',
      tire_size: '23',
      tape_color: tape_color }
  end
end

bike = Bicycle.new(size: 'M', tape_color: 'red')

bike.size
=> "M"
bike.spares
=> {:tire_size => "23",
    :chain => "10-speed",
    :tape_color => "red"}

もしロードバイクを追加したいときは?

ロードバイクは固有のfront_shockrear_shockを必要とする。
そして、spareに必要なものも違う。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Bicycle
  attr_reader :size, :tape_color, :front_shock, :rear_shock

  def initialize(args)
    @style = args[:style] # 普通かロードバイクか
    @size = args[:size]
    @tape_color = args[:tape_color]
    @front_shock = args[:front_shock]
    @rear_shock = args[:rear_shock]
  end

  def spares
    if style == :road # この条件分岐が臭いまくる
      { chain: '10-speed',
        tire_size: '23', # millimeters
        tape_color: tape_color }
    else
      { chain: '10-speed',
        tire_size: '2.1', # inches
        rear_shock: rear_shock }
    end
  end
end

新たなstyleが増えるたびにif文がひたすら増える。
さらに、front_shockrear_shockはstyleが:roadのときには値が入らないんだよね。ということは、この値を使うときには毎回if分岐する必要がある……。

自身の分類を保持する属性を確認して、メッセージを確定するというif文には注意!!

埋め込まれた型を見つける

  • type
  • category
  • style

これらを変数で管理しているときは要注意。だって、それって意味としてはclassじゃん?

とはいえ、BicycleMountainBikeを単独の別のクラスにするには、同じものがありすぎる……DRYじゃない。
そんなときは継承です!

継承は、共通の振る舞いを持つものの、いくつかの面においては異なるという、強く関連した型の問題を解決します。

継承について

オブジェクトによる受け取ったメッセージの処理方法

  • 直接応答する(自分の責任なので自分が対応する)
  • 他のオブジェクトにメッセージを渡して応答してもらう(自分から責任を外す=委譲

継承は、この後者の関係を自動的に定義する。
自分がメッセージを理解できなかったら、継承元に「このメッセージの応答よろしくお願いしまーす」を渡す。

1129

単一継承

単一継承:親を絶対に一つしか持たない

もし単一継承じゃなかったら?
……理解できないメッセージをどっちの親に渡せばいいの?
……親1と親2に同じメソッドが定義されていたらどっちが優先される?

なので、優先順位を明確にするために、多くのオブジェクト指向言語は単一継承を採用している。

ちなみに……Rubyでは、「ただのクラス」のスーパークラスはObjectクラスになる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
irb(main):001:1* class Hoge
irb(main):002:0> end
=> nil
irb(main):003:0> hoge = Hoge.new
irb(main):004:0> hoge.class
=> Hoge
irb(main):005:0> hoge.class.superclass
=> Object
irb(main):006:0> hoge.methods
=> [:dup, :itself, :yield_self, :then, :taint, :tainted?, 
:untaint, :untrust, :untrusted?, :trust, :frozen?, :methods, 
:singleton_methods, :protected_methods, :private_methods, 
:public_methods, :instance_variables, :instance_variable_get, 
:instance_variable_set, :instance_variable_defined?, 
:remove_instance_variable, :instance_of?, :kind_of?, :is_a?, 
:tap, :display, :hash, :class, :singleton_class, :clone, 
:public_send, :method, :public_method, :singleton_method, 
:define_singleton_method, :extend, :to_enum, :enum_for, :<=>, 
:===, :=~, :!~, :nil?, :eql?, :respond_to?, :freeze, :inspect, 
:object_id, :send, :to_s, :__send__, :!, :==, :!=, :__id__, 
:equal?, :instance_eval, :instance_exec]

ちなみに

nilNilClassのインスタンスであり、(他のオブジェクトがそうであるように)Objectを継承している。
nil?メソッドはNilClass内ではtrueを返すように定義され、Object内ではfalseを返すように定義されている。
nil以外のクラスのインスタンスの場合は、継承を遡っていき、trueを返す。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[1] pry(main)> show-source nil?

From: object.c (C Method):
Owner: Kernel
Visibility: public
Signature: nil?()
Number of lines: 5

MJIT_FUNC_EXPORTED VALUE
rb_false(VALUE obj)
{
    return Qfalse;
}

[2] pry(main)> cd nil
[3] pry(nil):1> show-source nil?

From: object.c (C Method):
Owner: NilClass
Visibility: public
Signature: nil?()
Number of lines: 5

static VALUE
rb_true(VALUE obj)
{
    return Qtrue;
}

そっと私は見なかったことにした。KernelはCらへんなので……。

テンプレートメソッド

superクラスで定義したメソッドがサブクラスでしか定義されていないってなんか嫌じゃね?
サブクラスを新たに作ったときに、そのクラスを絶対使わないといけないけど、それに気づけない。

NoMethodErrorじゃなくってあえて名前をつけたエラーをraiseすることで原因がわかりやすくなる!
エラーが出るのは同じじゃね?って思うけど、それが奥深くに入ってしまうとスタックを追うのが超面倒。
こういうときにエラーが出るよなあってのが実装時にわかっているなら、未来のためにもちゃんとそれ用にエラーがraiseするようにしておこうね……。

テンプレートメソッド(Template Method) | Ruby デザインパターン | 酒と涙とRubyとRailsと
テンプレートメソッド自体は継承じゃなくても「良い」。
最終的に手順を示す形のメソッド。メソッドだけを入れたメソッド。

省略

最終的な形

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class Bicycle
  attr_reader :size, :chain, :tire_size

  def initialize(args={})
    @size = args[:size]
    @chain = args[:chain]
    @tire_size = args[:tire_size]
    post_initialize(args)
  end

  def spares
    {
      tire_size: tire_size,
      chain: chain
    }.merge(local_spares)
  end

  def default_tire_size
    raise NotImplementedError
  end

  # ここより下はサブクラスがオーバーライドする前提
  def post_initialize(args)
    nil
  end

  def local_spares
    {}
  end

  def default_chain
    '10-speed'
  end
end

class RoadBike < Bicycle
  attr_reader :tape_color

  def post_initialize(args)
    @tape_color = args[:tape_color]
  end

  def local_spares
    { tape_color: tape_color }
  end

  def default_tire_size
    '23'
  end
end

class MountainBike < Bicycle
  attr_reader :front_shock, :rear_shock

  def post_initialize(args)
    @front_shock = args[:front_shock]
    @rear_shock = args[:rear_shock]
  end

  def local_spares
    { rear_shock: rear_shock }
  end

  def default_tire_size
    '2.1'
  end
end
Share on

END
END
@aiandrox

 
目次