2014年4月17日木曜日

Programming in Scala, First Edition: Chapter 10

10. Composition and Inheritance
  Composition(集約) 他のクラスへの参照を保持して、その機能を利用する。
  Inheritance(継承) super class, sub classの関係
  abstract classes, parameterless methods, extending classes,
  overriding methods and fields, parametric fields,
  invoking superclass constructors, polymorphism and dynamic binding,
  final members and classes, factory objects and methods

10.1 A two-dimensional layout library
  この章で例題として作成するlibraryの説明。
  長方形の文字列を格納するlayout elementを作成する。
    elem(s: String): Element // factory methods
    val column1 = elem("hello") above elem("***") // combinator
    val column2 = elem("***") above elem("world")
    column1 beside column2  // operatoer
    ->
    hello ***  // result
    *** world
  layout elementは単純パーツから構成演算子でオブジェクトを生成する好例。
    above, besideはcomposing operator, combinato
  combinatorの視点から設計を検討するのは良い方法。
    application domainのobjectの性質を反映しているから。
    基本的なオブジェクトとは?
    基本的なオブジェクトからより複雑なものを構成する方法は?
    意味のある規則に従っている?

10.2 Abstract classes
  最初に行うのはtype Elementの定義から。
  保持している内容はArray[String]とする。
    各Stringが行で、列を配列で表現する。
  内容を返すcontentsメソッドを定義する。
    abstract class Element {
      def contents: Array[String]
    }
  実装のないメソッドを含むクラスは抽象クラス。abstract修飾子を付ける。
    インスタンス化出来ない。
    サブクラスでメソッドを実装したらインスタンス化出来る。
  実装のないメソッドには特に修飾子は付けない。
    javaとは違う。
  実装があるのは具象(concrete)クラス。
  宣言(declarations)と定義(definitions)
    抽象クラスElementは抽象メソッドcontentsを宣言しているが具象メソッドは定義していない。

10.3 Defining parameterless methods
  Elementに幅と高さを返すメソッドを加える。
    幅: 最初の行の文字列長 or 一行も無いときは0
    高さ: 配列の要素数
    abstract class Element {
      def contents: Array[String]
      def height: Int = contents.length
      def width: Int = if (height == 0) 0 else contents(0).length
    }
  メソッドに引数を示す()が無い。parameterless methods
    scalaでは一般的
  空の()があるものはempty-paren methods.
    def height(): Int
  どちらを使うかはscalaの慣習に従うと良い。
    parameterless methods
      副作用なしで、そのオブジェクト内のmutable stateのみを参照する場合。
    empty-paren methods
      上記以外の場合。
  uniform access principle
    クライアントコードが、属性へのアクセスがメソッドで実装されているか、フィールドで実装されているかの影響を受けないようにすべき。
    メソッドだと実行時の処理時間が多少長くなる。
    フィールドだとメモリ消費が多少多くなる。
    どちらにせよクライアントコードから違いが見えないようにする。
      副作用が無く他のオブジェクトの可変状態を使わないようにする。
  javaはこの原則に従っていない。メソッドは()が必要。
    parameterless methodとempty-paren methodを互いにoverrideさせている。
  引数無しのメソッドは全て()無しでも呼び出せる。
    ()を付けた方がいい場合もある。
      レシーバに含まれていない属性にアクセスする場合。
      I/O, 可変値の書き込み, レシーバのフィールド以外の可変値の読み出し。
      直接、間接的にmutable objectsを使う。
    何か値を返す以上の事が行われていることを示す手がかりになる。

10.4 Extending classes
  サブクラスの作成。Javaと同様にextends clauseを使う。
    class ArrayElement(conts: Array[String]) extends Element {
      def contents: Array[String] = conts
    }
  クラス図
    scala.AnyRef<<java.lang.Object>>
          ^
    Element<<abstract>>
          ^
    ArrayElement <>-- Array[String]
  extends clauseを使わない場合は暗黙的にscala.AnyRefを継承する。
    scala.AnyRefの実体はjava.lang.Object
  二つの例外を除いて全てのメンバを継承する。
    privateメンバは継承しない。
    上書き(override)しているメンバは継承しない。
      スーパークラスと同じ名前、パラメータのメンバ。
      抽象メソッドを上書きする際は、実装する(implements)とも言う。
        この場合はoverride修飾子がいらない。
        具象メソッドの場合はoverride修飾子必要。
  サブクラスはサブタイプにもなる。
    スーパークラスが使える所ならサブクラスも使える。互換性があるため。
  ArrayElementはArray[String]を集約(compositon)している。
    集約と継承の設計上の考察は Section 10.11 で行う。

10.5 Overriding methods and fields
  scalaではフィールドとメソッドで上書きが出来る。
    class ArrayElement(conts: Array[String]) extends Element {
      val contents: Array[String] = conts
    }
    uniform access principleに従っている。
    名前空間(namespace)が同じだから。
    javaでは出来ない。
  scalaの名前空間
    values (fields, methods, packages, and singleton objects)
    types (class and trait names)
  javaの名前空間
    fields, methods, types, and packages
  scalaではスーパークラスのメソッドをサブクラスではフィールドで定義したりする。

10.6 Defining parametric fields
  クラスのパラメータをそのままフィールドとして使える。parametric fields
    class ArrayElement(
      val contents: Array[String]
    ) extends Element
  valやvarをパラメータに付けるだけ。
  privateやoverrideやprotected等の修飾子も使える。
  名前の衝突を減らせる。
    単に衝突避けるためだけに付けられた名前があった時、それは冗長の"code smell"
  ** "code smell"を嗅ぎ取って簡素化出来たら良い。

10.7 Invoking superclass constructors
  class LineElement(s: String) extends ArrayElement(Array(s)) {
    override def width = s.length
    override def height = 1
  }
  具象クラスのサブクラスを作成する。
    一行のみのElementを作成する場合を考える。
    ?? 補助コンストラクタ(Auxiliary constructor)でも同様の事が出来る??
  スーパークラスの主コンストラクタ(primary constructor)にパラメータを渡す。
    ... extends ArrayElement(Array(s)) ...

10.8 Using override modifiers
  override修飾子は上書きするときは必要。
    実装(implement)の時はあってもなくても良い。
  上書きも実装もしない時は付けられない。
    typoで名前を間違った時にコンパイラがエラーを出せる。
    スーパークラスに新しいメンバを追加した時に役に立つ。
      既存のサブクラスと名前が衝突する可能性がある。fragile base class problem
      既存のサブクラスに同名のメンバが定義済みの場合エラーに出来る。
        overrideを過去には付ける事が出来なかったから。
      完全な解決では無いが、不可解な動作をするならエラーになった方がマシ。

10.9 Polymorphism and dynamic binding
  Polymorphism
    スーパークラスの型の変数にサブクラスの型の値を入れられる。
      下位のクラスは上位のクラスと互換性があるから。
    スーパークラスの型の変数の振る舞い代入するサブクラスで色々変えられる。
  dynamic binding
    型ではなくて、実行時の値によって実際に実行されるメソッドが決定される。

10.10 Declaring final members
  final修飾子を使うと、メンバのやクラスの上書き、サブクラス化を禁止出来る。
    メンバ定義にfinalを付けるとサブクラスでのoverrideを禁止できる。
      final def method ..., final val field ...
    クラス定義にfinalを付けるとサブクラスの作成を禁止できる。
      final class ClassName(...) {...

10.11 Using composition and inheritance
  集約と継承では、再利用を考えると一般的には集約の方が好ましい。
    継承だとfragile base class problemでうっかりサブクラスの実装を壊してしまう
  継承の方が良い場合もある。
    is-aの関係がある時。
    サブクラスの型をスーパークラスの型として使いたい場合。
  LineElementのについて考えると、Elementのサブクラスにするのが相応しい。
    class LineElement(s: String) extends Element {
      val contents = Array(s)
      override def width = s.length
      override def height = 1
    }
    Section 10.7では具象クラスのサブクラスの例が必要だったので無理やり定義。

10.12 Implementing above, beside, and toString
  ** 元々仕様としてElement内の各行は同じ幅(文字列数)。
     10.2 の説明時点では良く分からなかった。
     現時点では幅が同じかどうかのチェックする処理は入れていない。
     ?? 後で追加される??
  aboveメソッドの実装
    def above(that: Element): Element =
      new ArrayElement(this.contents ++ that.contents)
    現状では簡単のために同じ幅前提。後で拡張する。
    scalaの配列はjavaに基づいているが多くの拡張もしている。
      scala.Seqを継承していてシーケンスを扱うメソッドが多数ある。
  besideメソッドの実装
    def bside(that: Element): Element =
      new ArrayElement(
        for (
          (line1, line2) <- this.contents zip that.contents
        ) yield line1 + line2
      )
    ループを使ってもかけるが命令型的なので別の実装にする。
      indexを使った配列のループが出てきたら命令型の"telltale sign"
    zipメソッドは二つの配列を各要素をTuple2sに結合した一つの配列を返す。
      要素数が異なる場合は、少ない方の要素数になる。
    (line1, line2) <- はパターマッチング。Chapter 15参照。
    ?? besideの実装はサブクラスに依存している。循環??
  toStringメソッドの上書き
    override def toString = contents mkString "\n"
    toStringは()を使わないparameterless method
      uniform access principleに従うとpure methodには()付けない方が良い。

10.13 Defining a factory object
  factory object
    他のクラスを生成するメソッドを持ったオブジェクト。
    clietからクラス階層等の実装を隠蔽するため。
      後で修正したときにclient codeへの影響が出難いようにするため。
        詳細をなるべく見せない方がそれに依存したコードを書かれなくて良い。
  factory objectの実装方法
    object Element {
      def elem(contents: Array[String]): Element = new ArrayElement(contents)
      def elem(chr: Char, width: Int, height: Int): Element =
        new UniformElement(chr, width, height)
      def elem(line: String): Element = new LineElement(line)
    }
    抽象クラスElementのcompanion objectとして実装する。
      overloadして統一的に呼べるようにしている。
      ** 戻り値の型をElementにしているのがポイント
      ?? applyメソッド使わないのは何故??
    Elementだけを公開すれば良くなる。
      ArrayElement, LineElement, UniformElementの実装は隠蔽。
      Element内にprivate classとして定義する。
    import Element.elem とすればクラス内からクラスのメソッド同様に呼べる。

10.14 Heighten and widen
  大きさの違うElementを結合出来るように拡張する。
  小さいほうの両端に空白、空行を入れて大きい方に合わせる。
  空白を入れるためのhelper methodsを定義する。
    def widen(w: Int): Element =
      if (w <= width) this
      else {
        val left = elem(' ', (w - width) / 2, height)
        var right = elem(' ', w - width - left.width, height)
        left beside this beside right
      }
    ** left beside this beside right が見たままで分かりやすくて凄い!
    ?? どうしてprivateで定義しないのか??
    ?? beside内でwidenを使うので呼び出しが循環参照になっている??
  改良後のbeside
    def beside(that: Element): Element = {
      val this1 = this heighten that.height
      val that1 = that heighten this.height
      elem(
        for ((line1, line2) <- this1.contents zip that1.contents)
        yield line1 + line2)
    }
  ** heighten, aboveも同様に改良。

10.15 Putting it all together
  与えられた数の角を持つ渦巻きを書くアプリケーションを例題として作成する。
    殆ど全ての機能を使う練習として面白い。
    ** 素晴らしい!!

  import Element.elem

  object Spiral {

    val space = elem(" ")
    val corner = elem("+")

    def spiral(nEdges: Int, direction: Int): Element = {
      if (nEdges == 1)
        elem("+")
      else {
        val sp = spiral(nEdges - 1, (direction + 3) % 4)
        def verticalBar = elem('|', 1, sp.height)
        def horizontalBar = elem('-', sp.width, 1)
        if (direction == 0)
          (corner beside horizontalBar) above (sp beside space)
        else if (direction == 1)
          (sp above space) beside (corner above verticalBar)
        else if (direction == 2)
          (space beside sp) above (horizontalBar beside corner)
        else
          (verticalBar above corner) beside (space above sp)
      }
    }

    def main(args: Array[String]) {
      val nSides = args(0).toInt
      println(spiral(nSides, 0))
    }
  }

10.16 Conclusion
  abstract classes, inheritance and subtyping, class hierarchies,
  parametric fields, method overriding, class hierarchy
  layout libraryはChapter 14で再び使う。

0 件のコメント:

コメントを投稿