読者です 読者をやめる 読者になる 読者になる

ネットの海の片隅で

技術ネタの放流、あるいは不法投棄。

「継承より委譲」≠「継承使うな」

Ruby オブジェクト指向

TL;DR

適材適所。

「継承より委譲」

オブジェクト指向に関する有名な警句に「継承より委譲」があります。

僕はオブジェクト指向について語れるほど立派な人間じゃありませんが、

  • 「むやみに継承使わないほうが良いよ」
  • 「移譲のほうが良いケースが多いよ」

ってことですね。

ただ、今回は継承の価値を見なおしてみようという話です。

仮定

次のようなものを作りたいと仮定します。

  • 文字列に対して署名を行い、署名された文字列が欲しい。
    • HMAC/SHA256で変換した結果をBase64に変換。
  • 引数として「署名対象の文字列」と「署名に使うkey」を渡す。

設計1

まず、何も考えずに書いてみました。

class Signatory
  def initialize(key)
    @key = key
  end

  def sign(text)
    digest = OpenSSL::HMAC.digest("SHA256", @key, text)
    Base64.strict_encode64(digest)
  end
end
signatory = Signatory.new("KeyString")
signatory.sign("ReadableString") # => "SignedString"

感想:だるい。

なぜこんな処理に2行も使わないといけないのかというのが正直な思いでした。*1

設計2

というわけで、設計を変えてみました。

class Signature < ::String
  def initialize(text, key)
    digest = OpenSSL::HMAC.digest("SHA256", key, text)
    signature = Base64.strict_encode64(digest)

    super(signature)
  end
end
Signature.new("RawString", "KeyString") # => "SignedString"

1行でシンプルだし、署名を作っているというのがわかりやすいと思います。

継承を使うべきとき/使うべきでないとき

いわゆる「is-a」関係がキレイに成り立つときは、継承を使っても良いと思います。

一方、処理やデータ構造など何かしらの「共通化」のためにはやはり継承を使うべきではないと考えています。

今回のケースでは署名された文字列が欲しかったので、継承がキレイに使えたと思っています。 返ってくるインスタンスがStringだろうがSignatureだろうが大きな違いはありません。 なぜなら、「Signature is a String」だから。

また、そういう意味でRubyでは特に理由がない限り、原則としてinstance_of?ではなくis_a?を使うべきです。

蛇足

Javaとかで使われているextendsというキーワードはわりと「スーパークラスに共通機能を作りこんで、サブクラスで拡張する!」みたいな印象を与えがちな気がするので、isAとかに変えてみると良いかもしれない。

*1:署名を行うオブジェクトを引き回す場合はこれで良いと思います。