フィルタとして実現したプログラムは、解こうとする問題を、より小さな(そしてより簡単な)複数の問題に分割して扱うことを可能にする。また、フィルタをパイプでつなげたパイプラインもやはりフィルタとして扱うことができる。これは、フィルタプログラムを、より小さな(そしてより簡単な)複数のフィルタプログラムに分割して実装できることを意味する。部分と全体を同一視が可能。つまり、「パイプとフィルタ」は「Composite パターン」になっているのだ。
以上を踏まえて、「Blogger で作ったブログの記事一覧を作成する」で書いた「かけら」に戻ってみよう。今回の課題は、これをより小さなフィルタをつなげたパイプラインとして実現することだ。
処理の分割
get_titles.rb は Blogger へのフィードのリクエストから、HTML 化までを 1 つのプログラムで処理するものだった。これを処理の流れに沿って分割するとこうなる。
- フィードを取得する
- 特定の要素(タイトルと URL)を抽出する
- リスト形式(HTML の OL)に変える
- HTML として出力する
それぞれを独立したフィルタプログラムとして書きパイプでつなげれば、get_titles.rb と同等の処理が実現できる。
Ruby による実装
それぞれのプログラムを見ていこう。
フィードを取得する
(all_rss.rb)
1: #!/opt/local/bin/ruby1.9 -w
2: # -*- coding: utf-8 -*-
3: # all_rss.rb: get all feeds in the RSS format.
4:
5: require 'net/http'
6: require 'uri'
7:
8: BLOGID = "put your blog id here"
9:
10: def get_rss(max_results = 0)
11: url = URI.parse('http://www.blogger.com/')
12: res = Net::HTTP.start(url.host, url.port) { |http|
13: http.get("/feeds/#{BLOGID}/posts/default?alt=rss&max-results=#{max_results}")
14: }
15: res.body
16: end
17:
18: # get meta information only
19: /<openSearch:totalResults>(\d+)<\/openSearch:totalResults>/ =~ get_rss(0)
20: total_posts = Regexp.last_match[1].to_i
21:
22: STDOUT.puts get_rss(total_posts)
get_titles.rb との違いは、全記事数を取得するために、max-results=0
のリクエストを投げていること。さらに、そうして取得した全記事数をもとに、一度のリクエストで全ての記事を取得していること、だ。記事が何千、何万とあるなら別だが、たかだか 200 や 300 なら一度のリクエストでまとめて取得した方が効率が良い。最終結果の HTML を分割するなら、それはプログラムの中でやれば良いことだ。
特定の要素を抽出する
(titles_rss.rb)
1: #!/opt/local/bin/ruby1.9 -w
2: # -*- coding: utf-8 -*-
3: # titles_rss.rb: get post tiltes from Blogger RSS feed.
4:
5: require 'rss'
6:
7: rss = RSS::Parser.parse(STDIN.readlines.join, false)
8: rss.channel.items.each { |item|
9: STDOUT.puts "(\"#{item.title}\" . \"#{item.link}\")"
10: }
入力は、RSS 形式のフィード(XML のテキスト)。RSS として読み取り、記事のタイトルと URL だけを抽出している。
get_titles.rb との違うのは、出力の形式。Lisp の cons ペアとしても読み取れる形式にしてある。
リスト形式に変える
(mklist.rb)
1: #!/opt/local/bin/ruby1.9 -w
2: # -*- coding: utf-8 -*-
3: # mklist.rb: make a HTML fragment contains ordered list.
4:
5: # SPECIFICATION:
6: # input data must be written in the following format.
7: # (<title> . <uri>)
8:
9: STDOUT.print <<LISTSTART
10: <ol>
11: LISTSTART
12:
13: STDIN.each { |pair|
14: if /^\(\"(.*)\"\s.\s\"(.*)\"\)$/ =~ pair
15: md = Regexp.last_match
16: STDOUT.puts "<li><a href='#{md[2]}'>#{md[1]}</a></li>"
17: end
18: }
19:
20: STDOUT.print <<LISTEND
21: </ol>
22: LISTEND
入力としてわたってくるのは、2 つの文字列が Lisp の cons ペア形式で書かれたもの。取り出すのには正規表現を使っている。
ここで、ちょっと寄り道をしてみる。この段階だけを Gauche (Scheme) で書くとどうなるか?
(mklist.scm)
1: #!/opt/local/bin/gosh
2: ; -*- coding: utf-8 -*-
3:
4: ;;; mklist.scm: make a HTML fragment contains ordered list.
5:
6: ;; SPECIFICATION:
7: ;; input data must be written in the following format.
8: ;; (<title> . <url>)
9:
10: (define (apply-filter proc port)
11: (let ((s-exp (read port)))
12: (if (not (eof-object? s-exp))
13: [begin
14: (proc s-exp)
15: (apply-filter proc port)])))
16:
17: ;; BEGIN
18: (display "<ol>\n" (standard-output-port))
19:
20: ;; MAIN
21: (apply-filter
22: (lambda (s-exp)
23: (let ((title (car s-exp))
24: (url (cdr s-exp)))
25: (display #`"<li><a href=',|url|'>,|title|</a></li>\n"
26: (standard-output-port))))
27: (standard-input-port))
28:
29: ;; END
30: (display "</ol>\n" (standard-output-port))
プログラムの骨格は「Gauche でフィルタを書く」で書いたものと共通だ。
↑に示した Ruby 版との違いは、読み取るデータの形式が Lisp のデータそのものになっているため正規表現によるパターンマッチが必要ない、という点だ。データの形式によっては特定の言語の方がすっきりと書けることもある。その実例だ、と言うにはちょっと恣意的なサンプルだけど。
HTML として出力する
(mkhtml5.rb)
1: #!/opt/local/bin/ruby1.9 -w
2: # -*- coding: utf-8 -*-
3: # mkhtml.rb: put input data into a well formed HTML template.
4:
5: print <<HEADER
6: <!DOCTYPE HTML>
7: <html lang='ja'>
8: <head>
9: <meta content='text/html; charset=UTF-8' http-equiv='Content-Type'/>
10: <title>Blog Posts</title>
11: </head>
12: <body>
13: HEADER
14:
15: STDIN.each { |line|
16: STDOUT.puts line
17: }
18:
19: print <<FOOTER
20: </body></html>
21: FOOTER
入力から流れてくるのは、HTML の OL 部分。ここでは、その先頭と末尾に HTML ファイルとして必要な部分を付け加えることで、全体を HTML ファイルとして仕上げている。
プログラムを小さく分割して作ることの意義は何か?
「小さく分割する」とは、プログラムを互いに独立した要素として作るという意味で、つまりはモジュール化だ。すぐにわかる利点は以下のようなものだ。
- 理解しやすい
- テストしやすい
- デバッグしやすい
何よりテストしやすいことは重要だ。端的に言ってしまえば、今回のように分割することで単体テストレベルのテストが可能になるからだ。テストしやすいからこそ、デバッグもしやすくなる。テストで動作の検証が簡単に行えるなら、リファクタリングも機能拡張もぐっとやりやすくなる。
関連リンク
- Composite パターン (Wikipedia:ja)
0 件のコメント:
コメントを投稿