2008年9月3日水曜日

Google App Engine でダイエット表の作成 (2)

追記(2008.9.6) : ダイエット表を作成するには → http://4diet.appspot.com/


Google App Engine でダイエット表の作成 (1)」の続き。

前回、Django のテンプレートタグを使って table 要素を生成するのを諦め、代わりに DietTable クラスで table 要素を生成するように変更した。しかし、実装しているうちにコードが複雑になり、実装を一時中断することに。 (+_+)

 

table 要素を生成することに専念した HtmlTable クラス

DietTable クラスが複雑になってしまった理由を考えてみると、

  1. ダイエットのための情報から「表」を作成するための「ルール」を管理している
  2. HTML の table 要素を生成する

という二つの異なることを担当しているからだと気がついた。ここで言う「ルール」というのは、例えば、作成するダイエット表において

  • 「土日の列は色を変える」
  • 「表の横線の一目盛は 0.1 kg 」
  • 「目標として設定できる体重には制限がある」

などのこと。これらは HTML の table 要素を生成することとは直接関係がない。関係がないので、HTMLを生成する責務を DietTable クラスから独立させ、 DietTable クラスはそのクラスを使うように変更してみた。 HTML の table 要素を生成するクラスなので HtmlTable と名付けた。これにより HtmlTable クラスは、保持する内容を HTMLとして書き出すことのみに専念し、 DietTable クラスはフォームに入力された Diet 情報から表として生成可能か、ルールに照らしてその妥当性を検証し、また、表のどこに何を書くかを決めることに専念できる。これにより役割分担がはっきりするようになり、誰に何を頼めばいいのか理解しやすくなった。

g5922

 

構造

HtmlTable クラスの構造は上図に示した通りである。クラスの内部は、シンプルに「表」の構造を表現。 HtmlTable クラスの直下にある Rows クラスは、表の各列を管理する Row クラスの集合を管理。そして、一つの Row オブジェクトは、表の列数だけ Cell を管理。 Cell クラスは、セルに設定された内容と、そこにおける複数のスタイルを管理するようにした。

HtmlTable クラスにおいては、

  • 表の大きさ (行・列数) を設定する
  • 特定のセルの内容とスタイルを設定する
  • 特定の行・列のスタイルを設定する
  • HTML の table 要素に変換する

の操作を持つ。

 

余談

当初は「列」とその集合を管理する Cols, Col クラスも作り、Cell へのリンクを持つようにしてた。しかし、結局必要がなかったので削除。 Cols, Col クラスの実装は、Rows, Row クラスとほぼ同じだった。もし、Cell に対して列方向からもリンクが必要であるならば、次元を表わす Dimention クラスを作成し、そのインスタンスとして「行・列」を表現するのがいいのかもしれない。

 

実装

Cell オブジェクトは、「内容やスタイル」が設定されたりする方が、何も設定されないものより圧倒的に少ない予定。だから、最初に HtmlTable オブジェクトを作成したときに、Cell オブジェクトを「行 × 列」の数だけ生成するのではなく、内容やスタイルが設定されたときに、はじめて Cell オブジェクトを生成するようにした。それは、Row オブジェクトについても同様にした。まぁ、どうせ表なんて大きくないのだから、最初から全て Cell オブジェクトを生成しておいてもいいような気もしたし、その方が実装がシンプルになったような気もするが、とりあえずよしとしておくか。 ^^;

 

ソースコード

以下にコードを示す。

HtmlTable.py

class HtmlTable:
    u""" HTML のtable 要素を作成するためのクラス

    構造: HtmlTable - Rows -* Row -* Cell
    """
    def __init__(self, row, col):
        self.rows = Rows()      # 行を管理するオブジェクト
        self.row = row          # 行数
        self.col = col          # 列数
        
    def setCell(self, row, col, content=""):
        u""" 指定された行・列番号のセルの内容を設定する

        セルを設定した場合、設定した Cell オブジェクトのみが存在する。
        """
        # 指定された行・列番号が生成したテーブルの範囲を超えてないことを確認する
        if self.row < row or self.col < col:
            raise OutOfRangeErr
        # セルを作成して、行方向からリンクする
        self._setLink(Cell(row, col, content))

    def _setLink(self, cell):
        u""" セルを行方向からリンクする """
        self.rows.setCellRow(cell)
        
    def addCssToCell(self, row, col, css):
        u""" セルに CSS を追加する"""
        # 指定された行・列番号のセルの存否を確認する
        c = self.findCell(row, col)
        if c: c.addCss(css)
        else:
            newCell = Cell(row, col, "")
            newCell.addCss(css)
            self._setLink(newCell)
    
    def addCssToCol(self, col, css):
        u""" 列に CSS を設定する """
        for i in range(0, self.row):
            self.addCssToCell(i, col, css)
            
    def addCssToRow(self, row, css):
        u""" 行に CSS を設定する """
        for i in range(0, self.col):
            self.addCssToCell(row, i, css)
        
    def findCell(self, row, col):
        u""" 行番号 row, 列番号 col のセルを取得する """
        return self.rows.findCell(row, col)
        
    def toHtml(self):
        u""" HTML に変換する """
        return "<table align='center'>\n" + \
                    self.rows.toHtml(self.row, self.col) +\
                "</table>"

class OutOfRangeErr(Exception):
    pass

class Rows:
    u""" 行の集合を管理するクラス """
    def __init__(self):
        self.rows = []      # 行のリスト
        
    def setCellRow(self, cell):
        u""" 指定されたセルを行に設定する """
        row = self.findRow(cell.row)
        if row:
            # 新しくセルが設定された場合、古いセルは上書き。
            row.setCell(cell)
        else:
            newRow = Row(cell.row)
            newRow.setCell(cell)
            self.add(newRow)

    def findRow(self, num):
        u""" 指定された番号の行が存在するか検索する """
        for row in self.rows:
            if row.num == num:
                return row
        return None

    def findCell(self, row, col):
        u""" セル(row,col) を取得する """
        r = self.findRow(row)
        if r: return r.findCell(col)
        else: return None

    def add(self, row):
        u""" 指定された行を追加する """
        if not self.rows:
            self.rows.append(row)
        else:
            for i,r in enumerate(self.rows):
                if r.num > row.num:
                    self.rows.insert(i-1, row)
                    return
            self.rows.append(row)
    
    def toHtml(self, row, col):
        u""" HTML に変換する

        HtmlTable#setCell() において設定されてないセルは、Cell オブジェクトが
        空である。その際、このオブジェクトが空の tr 要素を出力する責務がある。
        """
        result = ""
        for i in range(0,row):
            r = self.findRow(i)
            if r:
                result += r.toHtml(i, col)
            else:
                # 行の要素がない場合
                result += "\t<tr>\n\t\t" + "<td></td>"*col +"\n\t</tr>\n"
        return result

class Row:
    u""" 行を表わすクラス """
    def __init__(self, num):
        self.cells = []     # セルのリスト
        self.num = num      # 行番号

    def setCell(self, cell):
        u""" 列番号の順でセルのリストに追加する。 """
        for i,c in enumerate(self.cells):
            if c.col == cell.col:
                # 同じ列番号のセルが見つかった場合は上書きする。
                self.cells[i] = cell; return
            if c.col > cell.col:
                self.cells.insert(i-1, cell); return
        self.cells.append(cell)

    def findCell(self, col):
        u""" 指定された行番号のセルを検索する """
        for c in self.cells:
            if c.col == col: return c
        return None

    def toHtml(self, row, col):
        u""" HTML に変換する

        HtmlTable#setCell() において設定されてないセルは、Cell オブジェクトが
        空である。その際、このオブジェクトが空の td 要素を出力する責務がある。
        """
        result = "\t<tr>\n"
        for i in range(0,col):
            result += "\t\t"
            c = self.findCell(i)
            if c:
                result += c.toHtml()
            else:
                result += "<td></td>"
            result += "\n"
        result += "\t</tr>\n"
        return result

class Cell:
    u""" セルを表わすクラス """
    def __init__(self, row, col, content=""):
        self.row = row              # 行番号
        self.col = col              # 列番号
        self.content = content      # 内容
        self.csss = []              # CSS のリスト

    def addCss(self, css):
        u""" セルに CSS を追加 """
        self.csss.append(css)

    def toHtml(self):
        return "<td class='" + " ".join(str(x) for x in self.csss) + "'>" \
                    + str(self.content) + "</td>"

「セルの内容を削除する」など、今回必要のないメソッドは実装していない。

あ~、これでやっと各々のメソッドがシンプルになった。 ^^

 

ユニットテスト

ついでに ユニットテストもちょっとだけ書いておこう。 (cf. PyScripter で UnitTest を自動で生成)

import unittest
import HtmlTable

class TestHtmlTable(unittest.TestCase):

    def setUp(self): 
        #  2行3列のテーブルを作成
        self.tb = HtmlTable.HtmlTable(2,3)
        # セルの設定
        self.tb.setCell(0,0,"hoge")
        self.tb.setCell(0,1,"piyo")
        self.tb.setCell(1,0,"fuga")
        # セルにCSS を追加
        self.tb.addCssToCol(0, "sunday")
        self.tb.addCssToCol(2, "saturday")
        self.tb.addCssToCell(1,1,"sunday")
        # 行・列に CSS を追加
        self.tb.addCssToRow(1, "x_axis")
        self.tb.addCssToCol(1, "y_axis")

    def tearDown(self): 
        pass

    def testsetCell(self):
        tb = HtmlTable.HtmlTable(3,4)
        # 例外が投げられることを確認する
        self.assertRaises(HtmlTable.OutOfRangeErr, tb.setCell, 3, 5, "hoge")
        self.assertRaises(HtmlTable.OutOfRangeErr, tb.setCell, 5, 3, "hoge")
        self.assertRaises(HtmlTable.OutOfRangeErr, tb.setCell, 5, 5, "hoge")
        # 例外が投げられないことを確認する
        try:
            tb.setCell(3,3,"hoge")
        except OutOfRangeErr:
            fail("expected a OutOfRangeErr")
    
    def testtoHtml(self):
        self.assertEqual(self.tb.toHtml(), """\
<table align='center'>
 <tr>
  <td class='sunday'>hoge</td>
  <td class='y_axis'>piyo</td>
  <td class='saturday'></td>
 </tr>
 <tr>
  <td class='sunday x_axis'>fuga</td>
  <td class='sunday x_axis y_axis'></td>
  <td class='saturday x_axis'></td>
 </tr>
</table>\
""", self.tb.toHtml())

if __name__ == '__main__':
    unittest.main()

 

テストについて

久しぶりにちょっと本をひもといて、

私は、クラスがなすべきことをすべて調べてから、それらについて 1 つずつ、不具合を起こしそうな条件でテストするようにしています。プログラマによっては「すべての公開メソッドをテストする」よう勧めていますが、これとは違います。テストはリスク主導であるべきです。 (…)

テストをたくさん書こうとする余り、必要なテストを書き漏らしてしまうからです。(…)

大事なことは、一番怪しいと思う部分をテストすることです。

(リファクタリング , p97 より。太字は引用者による。)

なるほど。

あ、決してテストをあまりしてない言訳に使っているわけではありません…。 ^^;


関連記事