Java.use(better); Episode#23 ファインダーを作成する

前の記事記事一覧次の記事
Java.use(better, Scala);


Episode#23

ファインダーを作成するリファクタリング



チャンスは逃すな
まず決断せよ
石橋を叩くのは、それからである
Eizaburo Nishibori - Wikipedia

《関連記事》

┃余録:Move Accumulation to Collecting Parameter

■ 事例:class Accumulate1/Accumulate2

次の事例は、その理解を深めるための便宜的なものです。文字列を列挙したリスト、を列挙したリストという二重構造について考えます。

scala> val seq = """AB C
     | DEF"""
...
scala> var buf = new ListBuffer[List[String]]
...
scala> seq.split("\n") foreach { buf += _.split(" ").toList }

scala> print( buf )
ListBuffer(List(AB, C), List(DEF))
scala> print( seq )
AB C
DEF
scala> print( new Accumulate1(buf) )
AB C
DEF
scala> print( new Accumulate1(buf)+"" == seq )
true

seq は、空白で区切られた文字列を、改行で区切って並べた文字列です。これをもとにして、空白で分割した部分文字列を列挙した List[String] を、列挙した ListBuffer[] に分解します。Accumulate1 は、これを再び連結させて、もとの文字列を復元します。同様に、

scala> print( seq )
/** Iterators are data structures that allow to iterate over a sequence
 *  of elements. They have a `hasNext` method for checking
 *  if there is a next element available, and a `next` method
 *  which returns the next element and discards it from the iterator.
 */
scala> print( new Accumulate1(buf)+"" == seq )
true

複数行からなるコメントを部分文字列に分解したものを、Accumulate1 が復元するのを確認できます。これは次のように実現します。

class Accumulate1(seq: ListBuffer[List[String]]) {
  override def toString() = accumulate(seq)
  def accumulate(seq: ListBuffer[List[String]]) = {
    var buf = new StringBuffer()
    seq foreach { s => buf.append(accumulate2(s)+"\n") }
    val r = buf.toString
    r.take(r.size-1)
  }
  def accumulate2(s: List[String]) = {
    var buf = new StringBuffer()
    s foreach { e => buf.append("%s ".format(e)) }
    val r = buf.toString
    r.take(r.size-1)
  }
}

accumulate は、指定した seq から文字列を復元するときに、accumulate2 を下請にします。このとき、メソッドを呼び出すたびにバッファー StringBuffer を用意するのは、効率が良くありません。そこで、

class Accumulate2(seq: ListBuffer[List[String]]) {
  override def toString() = accumulate(seq)
  def accumulate(seq: ListBuffer[List[String]]) = {
    var buf = new StringBuffer()
    seq foreach { s => accumulate2(buf, s) }
    val r = buf.toString
    r.take(r.size-1)
  }
  def accumulate2(buf: StringBuffer, s: List[String]) = {
    s foreach { e => buf.append("%s ".format(e)) }
    val n = buf.length
    buf.replace(n-1, n, "\n")
  }
}

バッファー buf を用意するのは accumulate だけにして、accumulate2 を下請にするときの引数に指定すると効率的です。このとき、メソッドを呼び出すたびに、バッファー buf に文字列が累積されます。

┃余録:Composed Method, idiom#54

■ メソッドの整理

メソッドが大きく複雑になると、その内容を理解するのは困難です。そこで、小さなメソッドに分割すると、その理解も容易です。メソッドの本体に、分割したメソッド呼び出しだけを列挙したものが「目次」です。すると、メソッドを実現する手段(how)には依存せずに、メソッドを利用する目的(what)が明確になるので、コードが簡潔で見通しも良くなります。

メソッドを抽出する前のコードは、次のようなものでした。

   :   def appendNode(buf: StringBuffer,
   :     node: TreeNode, level: Int): Unit = {
   :     // appendParent(buf, node, level)
   :     buf.append("%s%s %s\n".format(
   :       tabs(level),
   :       if (node.isLeaf) "." else "+",
   :       node))
   :     // appendChildren(buf, node, level)
   :     nodeIterator(node) foreach { appendNode(buf, _, level+1) }
   :   }

抽出したメソッド呼び出しをコメントにすると、それ以外のコードの断片を見ただけでは、その目的を理解するのは困難です。なにより、作成者(プログラマー)の意図を、利用者(プログラマー)が理解しているか「確証」が持てません。逆に、

   :   def appendNode(buf: StringBuffer,
   :     node: TreeNode, level: Int): Unit = {
   :     // appendParent(buf, node, level)
   :     buf.append("%s%s %s\n".format(
   :       tabs(level),
   :       if (node.isLeaf) "." else "+",
   :       node))
   :     // appendChildren(buf, node, level)
   :     nodeIterator(node) foreach { appendNode(buf, _, level+1) }
   :   }

コメント以外のコードの断片を見なくても、メソッドの目的を理解できます。ここから、ひとつの教訓が得られます。「コメントが必要だ」と感じたなら、それは「リファクタリングの好機」です。コメントを記述する労力を、リファクタリングに投資するだけで、プログラミング(作譜)の効率が良くなります。

》作業中です《

 ↑ TOP

Created: 2010/05/23|Last updated: 2013/06/16 11:01:25