Java.use(better); Episode#23 ファインダーを作成する
《前の記事|記事一覧|次の記事》
Java.use(better, Scala);
Episode#23ファインダーを作成する:リファクタリング
悲しめる心よ、落ちついて悔ゆるのをやめよ
雲の後ろには太陽が照っている
Henry Wadsworth Longfellow - Wikipedia
《関連記事》
┃余録:Complete Constructor, idiom#68
■ 事例:class Progression
次の事例は、その理解を深めるための便宜的なものです。任意の等差数列を生成するときに、
scala> print( new Progression(5).toList ) List(0, 1, 2, 3, 4) scala> print( new Progression(3,7).toList ) List(3, 4, 5, 6) scala> print( new Progression(3,9,2).toList ) List(3, 5, 7)
コンストラクターには、3つの引数を指定できます。このとき、どの引数を省略するかで、その解釈が異なります。これは次のように実現します。
import scala.collection.mutable.ListBuffer class Progression(start: Int, end: Int, step: Int) { def this(start: Int, end: Int) = this(start, end, 1) def this(end: Int) = this(0, end) def toList() = { val buf = ListBuffer.empty[Int] var i = start while (i < end) { buf += i; i += step } buf.toList } }
引数を2つ指定すると、基本コンストラクター this(Int,Int,Int) の第3引数に 1 を指定したと見なします。引数を1つ指定すると、コンストラクター this(Int,Int) の第1引数に 0 を指定したと見なします。
コンストラクターを多重定義するときには、ひとつの「完全な」基本コンストラクターに帰着させます。Scala では、クラスを定義するときに、基本コンストラクターの引数を指定できます。すると、これを見るだけで、意味のあるインスタンスを生成するために、何が必要かが分ります。
┃未来への考察
成功の秘訣は「自分に最適な」ダイエット健康法を身に付けることです。同様に、チームにとって最適な開発環境を構築することが、プロジェクトを成功へ導く鍵を握ります。「正解が1つとは限らない」ところに、プログラミングの醍醐味があります。また「正解があるとは限らない」ところが、プログラミングの奥深さです。
アジャイル開発では、耐震から免震へ転換した、建築の歴史から多くを学べます。始めに最上階から構築するという逆転の発想は、古典的な手法の問題を解決しました。というのも、完成の後に足場を解体する「無駄な作業」を解消するからです。上流工程から下流工程へ進む古典的な手法は、足場がないと建築が進まない「始めに設計ありき」というものでした。
美術品の修復は、修復技術がさらに進化したとき、やり直すことを前提に行われます。その考え方は、ソフトウエア開発にも通じます。ソフトウエアが進化を続けるかぎり、要求仕様の変更は避けられません。なによりそれは、そのソフトウエアが支持されている証です。どんな変化にも柔軟に対処できる開発環境を構築するには、チームが「リファクタリングを開発スケジュールに盛り込めるか否か」に掛かっています。
》作業中です《
↑ TOP
Java.use(better); Episode#23 ファインダーを作成する
┃余録: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
Java.use(better); Episode#23 ファインダーを作成する
《前の記事|記事一覧|次の記事》
Java.use(better, Scala);
Episode#23ファインダーを作成する:リファクタリング
我々は耳は二つ持っているのに
口は一つしか持たないのは
より多くのことを聞いて
話す方はより少なくするためなのだ
Zeno of Citium - Wikipedia
《関連記事》
■ 情報隠蔽の原則
注目に値するのは「情報隠蔽の原則」に沿って、利用者(プログラマー)は知る必要のないコンポーネントの詳細から解放され、アプリケーションの開発に専念できることです。その一方で、作成者(プログラマー)は、リスナーの登録では、どのリスナー ActionListener をどうやって登録するか addActionListener を知る必要があります。また、イベントの処理では、どのイベント ActionEvent にどうやって呼応するか actionPerformed を知る必要があります。というのも、これらの組み合わせは多種多様だからです。
パッケージ scala.swing を利用すると、利用者(プログラマー)は、面倒な事務手続きから解放されます。というのも、共通のメソッド listenTo を利用するなら、コンポーネント/イベントの組み合わせに専念できるからです。それが、今回の TextField/EditDone であり、前回の TreeView/TreeSelectionChanged も、これに準拠するように新規作成しました。
┃ファイルシステムから得られる情報を活用する
前回は、既存のコンポーネント JTree を介して、ツリー構造を扱うときの基本を紹介しました。今回は、ファイルシステムから得られる情報を扱います。
15: import java.io._ 16: class FileSystem(file: File) { 17: def this(pathname: String) = this(new File(pathname)) 18: 19: override def toString = { 20: val buf = new StringBuffer 21: appendRoot(buf, file) 22: appendNode(buf, file, 0) 23: buf.toString 24: } 25: def appendRoot(buf: StringBuffer, file: File) = 26: buf.append("%s\n" format last(file)) 27: def appendNode(buf: StringBuffer, 28: file: File, level: Int): Unit = 29: file.listFiles foreach { e => 30: buf.append("%s%s %s\n".format( 31: tabs(level), 32: if (e.isDirectory) "+" else ".", 33: e.getName)) 34: if (e.isDirectory) appendNode(buf, e, level+1) 35: } 36: def last(file: File) = { 37: val s = file.toString.split("/") 38: s(s.length-1) 39: } 40: def tabs(level: Int) = " "*level 41: }
[17]コンストラクター this(String) の引数に文字列を指定すると、ファイルの名前(パス名)を指定したものと見なして、特定のファイル file を扱う基本コンストラクター this(File) を呼び出します。多重定義されたコンストラクターはすべて、基本コンストラクターに帰着します。
☞ 余録:Complete Constructor
[19-24]オブジェクトを表現する文字列を生成します。前回と同じく、バッファー StringBuffer を用意して、ファイルシステムから得られた情報を蓄積します。ここには、分割したメソッド呼び出しだけを列挙します。
☞ 余録:Composed Method
[25-26]特定のファイル file をツリーの頂点として、その情報をバッファー buf に追加します。
[27-35]特定のファイル file を頂点 level として、その傘下にあるファイル/フォルダーの情報をバッファー buf に追加します。[29]既存のメソッド java.io.File.listFiles を利用すると、file の直下にあるファイル/フォルダーを列挙した配列が得られます。[31]先行するタブ tabs(level) と[32]フォルダー "+" または、ファイル "." の後に[33]名前を続けます。[34]再帰呼び出し appendNode が階層構造を反映するので、次のレベルを探索するときに、実引数の値 level+1 を増やします。
》作業中です《
↑ TOP
Java.use(better); Episode#23 ファインダーを作成する
┃パス名を指定する:入力フィールドを利用する
前回に続き、アプリケーションを進化させます。今回は、入力したパス名を頂点として、ファイルシステムから得られる情報を表示します。
$ scala cherry.pie.Tips
アプリケーションを起動すると、次のようなウインドーが現れます。
(上)フィールドにパス名を入力すると(右下)テキスト領域にファイルシステムから得られる情報が表示されます。このとき「+」に続くのはフォルダーで「.」に続くのはファイルです。先行するインデントが、その階層に相当します。表示した情報はプレーンテキストなので、他のアプリケーションでも再利用できます。
■ イベントを処理する
前回に続き、イベント処理を進化させます。今回は、入力したパス名を頂点として、ファイルシステムから得られる情報を表示します。
20: class View(frame: Frame) extends BorderPanel { 21: self_ => 22: import swing.event.EditDone 23: val textField = new TextField { 24: new Publisher { 25: reactions += { 26: case EditDone(source) => update(text) 27: } 28: }.listenTo(this) 29: self_.add(this, BorderPanel.Position.North) 30: } 31: def update(s: String) { 32: s match { 33: case "" => 34: case s => updateTextArea(new FileSystem(s)) 35: } 36: } 37: def updateTextArea(fs: FileSystem) { 38: textArea.text = "" 39: textArea.append("%s\n" format fs) 40: textArea.caret.position = 0 41: }
[22-30]コンポーネント TextField を生成するときに、どのイベントに呼応して何をしたいかを記述します。[24]Publisher は、[28]生成するコンポーネント this で発生したイベントに傾聴 listenTo して、[25]reactions に登録したイベントに呼応します。[26]コンストラクターパターンから、イベント EditDone が発生したコンポーネント source を特定できますが(それを無視して)フィールドに入力されたテキスト text を利用します。
[37-41]フィールドにパス名を入力したときに発生するイベントに呼応します。テキスト領域に表示された情報を消去して、新たな情報を追記してからキャレットを戻すと、ファイルシステムから得られる情報を先頭から閲覧できます。
》作業中です《
↑ TOP
Java.use(better); Episode#23 ファインダーを作成する
《前の記事|記事一覧|次の記事》
Java.use(better, Scala);
Episode#23ファインダーを作成する:リファクタリング
未来というのは結局この一瞬一瞬の積み重ねなんだ
この今の延長に未来がある
遠い未来もこの瞬間の積み重ねなんだよな
Takeshi Kitano - Wikipedia
《関連記事》
《目的》同じアプリケーションを異なるプログラミング言語 Java/Jython/Scala で作成する過程(作譜:プログラミング)を通して、言語の違いに依らない概念を適用するための手法(算譜:プログラム)を紹介します。
《動機》ともすると、プログラミング言語の違いに目を奪われて、言語の違いに依らないプログラムの本質を見失いがちです。そこに、同じアプリケーションを異なるプログラミング言語で作成することの意義があります。その本質を理解できれば、新たなプログラミング言語を習得するときに役立ちます。
┃リファクタリング:メソッドの抽出
リファクタリングを「いつ実践すべきか」という問題に唯一の正解はありません。要求仕様の変更は、リファクタリングを実践する好機です。というのも、その要求に対して「何が影響を受け、何が影響を受けないか」が、明確になるからです。
■ 要求仕様の変更
$ scala cherry.pie.Tips
(左)ツリー項目を選択すると(右)それを頂点とするツリーが文字列で表示されます。表示した情報はプレーンテキストなので、他のアプリケーションでも再利用できます。
今回の要求仕様の変更は、ツリーを表現するとき「ファイルとフォルダーを区別したい」という些細なものです。その影響を受けるコードの断片を抽出する仕掛があると、プログラミング(作譜)の効率が上がります。リファクタリングの指針として「要求仕様の変更の影響を受けるコードの断片を、メソッドとして抽出する」が挙げられます。
■ 引数を介して情報を蓄積する
再帰呼び出しごとにバッファーを生成するのは、効率的ではありません。この問題を回避するために「リファクタリング」を実践します。さらに「再帰呼び出しに依存する」コードの断片を特定して、独立したメソッドを抽出します。
☞ 余録:Move Accumulation to Collecting Parameter
15: import javax.swing.tree.TreeNode
16: class Node(node: TreeNode) {
17: override def toString() = {
18: val buf = new StringBuffer
19: appendNode(buf, node, 0)
20: buf.toString
21: }
22: def appendNode(buf: StringBuffer,
23: node: TreeNode, level: Int): Unit = {
24: appendParent(buf, node, level)
25: appendChildren(buf, node, level)
26: }
27: def appendParent(buf: StringBuffer,
28: node: TreeNode, level: Int) =
29: buf.append("%s%s %s\n".format(
30: tabs(level),
31: if (node.isLeaf) "." else "+",
32: node))
33: def appendChildren(buf: StringBuffer,
34: node: TreeNode, level: Int) =
35: nodeIterator(node) foreach { appendNode(buf, _, level+1) }
[17-21]オブジェクトを表現する文字列を生成します。バッファー buf を用意して、再帰呼び出しごとに使い回します。メソッド appendNode を下請にして、特定のノード node を頂点 0 とするツリーを文字列で表現します。
[22-26]特定のノード node を頂点 level とするツリーを文字列で表現します。用意したバッファー buf に、各ノードの情報を追加します。このとき、メソッドの本体には、分割したメソッド呼び出しだけを列挙します。すると、書籍の目次と同様に、その内容(目的)を一覧できるので、プログラミング(作譜)の効率が上がります。ここでは、[24]具体的な処理と[25]再帰呼び出しを、独立したメソッドとして抽出しました。
☞ 余録:Composed Method
再帰呼び出しでは、リターン値の型を明記する必要があるので注意してください。さもないと、次のようにコンパイル時のエラーとなります。
$ scalac src/{_Tips,AppWindow,Node,TreeView}.scala src/Node.scala:41: error: recursive method appendNode needs result type nodeIterator(node) foreach { appendNode(buf, _, level+1) } ^ one error found
[27-32]用意したバッファー buf を使って、具体的な処理を行います。先行するタブの数 tabs(level) が、ノードのレベルに対応します。"." の後にはファイルの名前を、"+" の後にはフォルダーの名前を続けます。
注目に値するのは「要求仕様を変更した影響を受ける」コードの断片がすべて、このメソッドに「限定」されることです。これが、リファクタリングによって抽出した、メソッドの正当性を裏付ける根拠になります。新たな要求に対処する前に、このリファクタリングを実践しておくと、プログラミング(作譜)の効率が良くなります。というのも、変更しなかった箇所の単体テストを「割愛」できるからです。
[33-35]再帰呼び出し appendNode がツリーの階層構造を反映するので、次のレベルを探索するときに、実引数の値 level+1 を増やします。ここでは、特定のデータ構造(文字列バッファー)に依存しない「抽象表現」になっています。その具体的な処理は、前述したメソッド appendParent に委ねます。
バッファーを導入した影響は、すべてのメソッドに及びます。逆にそれは、すべてのメソッドが「リファクタリングに恩恵に浴する」とも言えます。
》作業中です《
↑ TOP