LL脳がscalaの勉強を始めたよ その29


Scalaコップ本の第10章をやりますよー、この章では2Dレイアウトライブラリーの実装を例にしてScalaオブジェクト指向をちょっぴり掘り下げて進めていきますねー

キーワード的には合成、継承、拡張、抽象クラス、オーバーライドetcと盛り沢山ですよー

2Dレイアウトライブラリー

この章では2Dレイアウト要素の構築とレンダリングを行うライブラリーの実装を例にするので、まずはそいつの概要を羅列しておきますね

ライブラリーの概要

このライブラリーは渡されたデータを基に新しい要素の構築をするelemという名前のファクトリーメソッドを提供するらしいデス.

ちなみにelemについてはイこんなメージですね。実際の要素はテキストが書き込まれた矩形になるらしいですが…

// 構築される要素はElement型になるそうデス
elem(s:String):Element

実際の操作イメージはこんな塩梅らしいっす

// 2つの要素を横に結合(合成)しますよ
val column1 = elem("hello") above elem("***")
val column2 = elem("***") above elem("world")
// 2つの要素を縦に結合(合成)しますよ
column1 beside column2

// 処理結果デス
hello ***
*** world

まあ、なんとなくのイメージだけつかんで進めていきましょうかねー

ついでに結構うろ覚えな単語やらが多かったので、以下にちょこっと補足をいれていきますよー

ファクトリーメソッド

名前は聞いたことあるなぁ…な、ファクトリーメソッドってこんな感じ?デザパタでみたことある気がするなぁ…

親クラスでインスタンスの生成のインタフェースを定義し、実際のインスタンスの生成をサブクラスで行うパターン

とりあえずここの説明がわかりやすいなあ、と思ったのでc⌒っ゚д゚)っφ メモメモ...

ファクトリーメソッドだとabstractで定義するとかってイメージで合ってるかしら?

コンビネータ

同じドメインの要素を結合して新しい要素をつくるようなモノをコンビネーターというらしいデス。ちなみに上のサンプル上のaboveやbesideのような合成演算子コンビネーターになるっぽいですねぇ。

ここでいうドメインは"特定のタスクの固まり"とかって解釈でいいのかしら?普段ドメインていったらdomain nameしか思い浮かばないからなぁ(´・ω・`)

ちなみにコンビネーターについて考えることはライブラリーを設計することに対して良いアプローチらしいですね

コップ本ではこんなことを自問自答するのがよろしい(`・ω・´)とおっしゃっております

  • 単純なオブジェクトとは何か?
  • 単純なオブジェクトからより面白いオブジェクトを構築するにはどうすればよいか?
  • コンビネーターはどのように組み合わせられるか?
  • 最も一般的なコンビネーションとは何か?
  • コンビネーターが満たす面白い法則はあるか?

上記の問いにキチンと答えられるならライブラリー設計はうまく言っていると言えるそうです。

…しょ、精進します(´・ω・`)、でもドメイン駆動型の開発ってこんな感じなのかな?いや、全く知らないでの憶測なんですけども…

抽象クラス

まずはレイアウト要素を表現するElement型を抽象クラスとして定義しますよー

今回作成するライブラリーのレイアウト要素は文字列表現の2次元矩形なので、Elementでは文字列配列のcontentsというメンバー(抽象メンバー)を持っております

では、実際に書いてみましょうか

// 抽象クラスなのでabstract宣言です
abstract class Element {
  //  抽象メンバーなので定義のみです 
  def contents:Array[String]
}

ちなみにabstract宣言による抽象クラスは、”クラスメンバーに抽象メンバーをもつかもしれない”という意味らしいですね…どちらかというとメンバーありきってことですかねー

それとElementは抽象クラスなのでインスタンスが作れないっすね(´・ω・`)まあ、実装がないメンバーがあるからなんでしょうかねー

// abstractですから!って怒られました(´・ω・`)
scala> val e = new Element    
<console>:5: error: class Element is abstract; cannot be instantiated
       val e = new Element
               ^

ライブラリーとして使う場合はこいつのサブクラスを作ってやっていくみたいですねー

ちなみにcontentsのように宣言だけで定義がない抽象メソッドには、上のようにabstract修飾子はつけなくてもいいらしいです。(付けてもいいし、Javaだと必須みたいですね)

ついでに実装があるようなのを抽象メソッドに対して具象メソッドと呼ぶらしいので、軽くめもっておきましょうかね...正確には”具象メソッドを定義”するみたいな語の使い方をするみたいですな

パラメータなしのメソッド定義

Elementクラスに要素を追加していきますよー、まずは要素の高さ(行数)を取得するheightメソッドと先頭行の幅を取得するwidthメソッドです

abstract class Element {
  def contents:Array[String]
  // 要素の高さ(行配列の要素数)を取得します
  def height():Int = contents.length
  // 要素の高さが0出ない場合に幅(先頭行の文字数)を取得しますよ
  def width():Int = if(height == 0) 0 else contents(0).length
}

さて、Scalaではメソッドの空括弧()を省略できるので、次のように書き換えることができます。なお"()"付きのメソッドは空括弧メソッドと呼ぶみたいです。

abstract class Element {
  def contents:Array[String]
  // あまり見た目は変わらないけど、ちょっとすっきり
  def height:Int = contents.length
  def width:Int = if(height == 0) 0 else contents(0).length
}

こいつがパラメーターなしのメソッド定義っちゅーやつっみたいですね

ちなみに空括弧メソッドに対するクライアントからのアクセスも()は省略できるみたいですねー

scala> def hoge():Int = 1 + 2 + 3
hoge: ()Int
//()があってもー
scala> hoge()
res2: Int = 6
//()がなくても-
scala> hoge()
res3: Int = 6

ただし、空括弧省略メソッド(造語)だとクライアントからのアクセスは()省略必須になるみたいです

scala> def hoge:Int = 1 + 2 + 3
hoge: Int
// から括弧なしだとOK
scala> hoge                      
res6: Int = 6
// パラメーター(゚&#65533;゚)イラネって怒られました
scala> hoge()
<console>:6: error: hoge of type Int does not take parameters
       hoge()
       ^

…まるでフィールドのようです(´・ω・`)

defじゃなくても(・∀・)イイヨネー

上でやったように空括弧省略メソッドは、フィールドで定義しても特に困らなそうです

試しにElemtntを書き換えてみましたよ

abstract class Element {
  def contents:Array[String]
  // フィールドにしてみまった
  val height:Int = contents.length
  val width:Int = if(height == 0) 0 else contents(0).length
}

クライアントからのアクセスはどちらも同じで◯.heightとか△.widthとかでアクセスできるわけですねー

このように空括弧省略でフィールドやメソッドを区別しないで使えるようにすることを”統一形式アクセスの原則”と呼ぶらしいです。

統一形式アクセスの原則では「属性をフィールドとメソッドのどちらで実装するかによってクライアントコードが影響をうけてはならない」というものらしいですな

フィールドのメリット、メンバーのメソッドのメリット

一般的にフィールドで実装すると初回に計算結果を保持するので値の呼び出しが早いぶんメモリを確保してしまうのデス。メソッドであれば毎回計算するので呼び出しに若干時間がかかる分メモリを節約できる…と。

なので、状況にあわせて使い分ければ( ・∀・)イインダヨってことらしいです

まあ、利用状況によって内部実装を変更することがあるだろうからこその”統一形式アクセスの原則”みたいですねー

空括弧の省略基準は?

空括弧メソッドでの括弧の省略基準としては副作用のあり、なしが基準だとイイかもね(`・ω・´)だそうです

たとえばこんな感じですなー

// 副作用がないので()を省略
"hoge".length
// 副作用ありなので省略しません
println()

ここを統一しておけばパッと見の判別が付きやすいとのことです。たしかにねー(´・ω・`)

なお、副作用ありを省略しない理由は以下のような人を発生させないためです

         ,. -‐'''''""¨¨¨ヽ 
         (.___,,,... -ァァフ|          あ…ありのまま 今 起こった事を話すぜ! 
          |i i|    }! }} //| 
         |l、{   j} /,,ィ//|       『フィールドだと思ってアクセスしたら
        i|:!ヾ、_ノ/ u {:}//ヘ        副作用が起こってしまった』 
        |リ u' }  ,ノ _,!V,ハ | 
       /´fト、_{ル{,ィ'eラ , タ人        な… 何を言ってるのか わからねーと思うが 
     /'   ヾ|宀| {´,)⌒`/ |<ヽトiゝ        おれも何をされたのかわからなかった 
    ,゙  / )ヽ iLレ  u' | | ヾlトハ〉 
     |/_/  ハ !ニ⊇ '/:}  V:::::ヽ        頭がどうにかなりそうだった… 
    // 二二二7'T'' /u' __ /:::::::/`ヽ 
   /'´r -―一ァ‐゙T´ '"´ /::::/-‐  \    バグだとかスパゲッティだとか 
   / //   广¨´  /'   /:::::/´ ̄`ヽ ⌒ヽ    そんなチャチなもんじゃあ 断じてねえ 
  ノ ' /  ノ:::::`ー-、___/::::://       ヽ  } 
_/`丶 /:::::::::::::::::::::::::: ̄`ー-{:::...       イ  もっと恐ろしいものの片鱗を味わったぜ… 

いじょー

とりあえずベースになるElementクラスまでやってみましたよ

abstract class Element {
  def contents:Array[String]
  def height:Int = contents.length
  def width:Int = if(height == 0) 0 else contents(0).length
}

次回はこいつを拡張する話ですかねー