SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

よろずプログラマーのためのPython導入ガイド

Java meets Python - 第7回 ハリウッドの原則

よろずプログラマーのためのPython導入ガイド (9)

  • X ポスト
  • このエントリーをはてなブックマークに追加

部品を抽象化する

 新しいクラスを定義した後の、既存クラスの整理統合は重要です。

部品の抽象化
部品の抽象化

 既存のクラスが提供する特性を再利用すると共に、重複するものを整理整頓して、既存のクラスを統合する作業は、部品を「抽象化」する過程と見なせます。固有なものから共通なものを抽出することで、クラス間の継承関係を再考します。新たに抽出したクラスは、既存のクラス階層に挿入されます。

事例:抽象クラス GameItem

 具象クラスTile/Life/Stoneに共通する特性(インスタンス属性/メソッド関数/プロパティー/イベント等)を、抽象クラスGameItemとして抽出します。すると、抽出したGameItemobjectを頂点とするクラス階層に挿入されたのが分かります。

#------ before --------------------------------
class Tile(Shape):
    def __init__(self, x, y, value):
        self.x = x
        self.y = y
        self.value = value
class Life(Shape):
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.present = False
        self.future  = False
        self.neighbor = []
class GameItem(Shape):
    def __init__(self, x, y):
        self.x = x
        self.y = y
class Stone(GameItem):
    def __init__(self, x, y, state):
        GameItem.__init__(self, x, y)
        self.state = state
共通する特性を抽出
共通する特性を抽出
#------ after --------------------------------
class Tile(GameItem):
    def __init__(self, x, y, value):
        GameItem.__init__(self, x, y)
        self.value = value
class Life(GameItem):
    def __init__(self, x, y):
        GameItem.__init__(self, x, y)
        self.present = False
        self.future  = False
        self.neighbor = []

 モデルに着目します。盤面に配置するどの実体も座標x/yを持ちます。この共通する特性を抽出して、抽象クラスGameItemへと移動します。共通する処理を GameItem.__init__に委ねて、具象クラスに固有の処理だけを実行します。「差分プログラミング」は、コードの再利用を促進して、コードの作成過程を最適化します。

事例:テンプレート DefaultFrame

 ビューに着目します。どのゲームも、ウィンドウ内に任意の視覚部品を配置します。ウィンドウは特定の大きさを持ち、上部にタイトルを表示します。共通する特性を抽出して、クラスDefaultFrameへと移動します。

テンプレート DefaultFrame
テンプレート DefaultFrame
#------ before/after --------------------------------
class DefaultFrame(JFrame):
    defaultTitle = "Default Title"
    defaultSize  = Dimension(200, 100)
    def __init__(self, title=None, size=None):
        self.initialize()
        self.initializeFrame(title, size)
        self.initializeComponent()
        self.visible = True
    def initialize(self): pass
    def initializeFrame(self,
            title=defaultTitle, size=defaultSize):
        JFrame.__init__(self,
            defaultCloseOperation=JFrame.EXIT_ON_CLOSE,
            size=size, title=title)
    def initializeComponent(self): pass

 クラスDefaultFrameは、抽象クラス(理想)として、具象クラスに共通する特性を規定します。__init__は、共通するフレームワーク(テンプレート)だけを規定します。initialize/initializeComponentは、具象クラスで『必要なら』再定義されるものとして、本体を空passとします。これが「具体的なことは将来に委ねる」という祖先から子孫へと受け継がれる遺言(暗黙の了解)となります。これらのメソッドは、自分で「呼び出す」ことより、他から「呼び出される」ことに意義があります。

《Tips》パターンへの誘(いざな)い
 GoF のカタログには、Templateパターンという項目があります。抽象クラスでは、その目的(what)だけを明記して、具体的な実現方法(how)を規定しません。そして、具象クラスでは、メソッドの本体に詳細を記述します。抽象クラスには、具象クラスに関する情報を含まないので、仕様を変更しても、柔軟な対処が可能です。実際に、what/how を分割統治しておくだけで、後にコードを改良したり、仕様変更に対処する作業のコストを低減できます。

 ここで着目して欲しいのは、フレームワークがメソッドのシグニチャーだけでなく、プロトコルも規定することです。ここでは「ウィンドウのタイトル/大きさを設定してから、視覚部品を配置する」という前提で役割を分担します。しかし、フレームワークを利用する側では、その手順を知る術はありませんし、知る必要もありません。

 メソッドinitializeFrameは、タイトルtitle/大きさsizeを設定します。また、ウィンドウを閉じたときの操作defaultCloseOperation=を規定するだけで、その他は親クラスである既存の部品JFrame.__init__に委ねます。

事例:具象クラス ..Frame

 子孫クラスPuzzle15Frame/LifeGameFrame/OthelloFrameでは、具象クラス(現実)として親クラスDefaultFrameで規定した特性を再定義したり、作業の一部を他に委ねます。こうして、フレームワークで規定した暗黙の了解を履行します。

#------ before/after --------------------------------
class Puzzle15Frame(DefaultFrame):
    def initialize(self):
        self.panel = PuzzlePanel()
class LifeGameFrame(DefaultFrame):
    def initialize(self):
        self.panel = LifeGamePanel()
        self.button = JButton("Next Generation",
            actionPerformed=self.actionPerformed)
class OthelloFrame(DefaultFrame):
    def initialize(self):
        self.panel = OthelloPanel()

 メソッドinitializeは、そのクラスに固有の特性を設定します。

#------ before/after --------------------------------
class Puzzle15Frame(DefaultFrame):
    def initializeComponent(self):
        self.layout = BorderLayout()
        self.add(self.panel)
class LifeGameFrame(DefaultFrame):
    def initializeComponent(self):
        self.layout = BorderLayout()
        self.add(self.panel, BorderLayout.CENTER)
        self.add(self.button, BorderLayout.SOUTH)
class OthelloFrame(DefaultFrame):
    def initializeComponent(self):
        self.layout = BorderLayout()
        self.add(self.panel, BorderLayout.CENTER)

 メソッドinitializeComponentは、そのゲームに固有の視覚部品を配置します。

 これらのメソッドは、自分で「呼び出す」ためでなく、他から「呼び出される」ために必要です。シグニチャーさえ一致しているなら、プロトコルの詳細を知る必要はありません。記述したコードの断片を見るだけでは、そのメソッドがいつ呼び出されるか、知る由もありません。しかし、そこに意義があるです(ハリウッドの原則)。

《読者への課題》明日のために、その壱
 インスタンス属性layoutにレイアウトBorderLayoutを設定するのは、どの子孫クラスにも共通です。しかし、この共通する特性を抽出して、親クラスDefaultFrame に移動していません。それが適切/不適切な理由について考察してください。

事例:テンプレート GameBoardPanel

 すべてのゲームに共通する、盤面(ビュー)を再構成します。

ビューの再構成
ビューの再構成

リファクタリング前

#------ before --------------------------------
class PuzzlePanel(JPanel):
    def paintComponent(self, g):
        self.paintItems(g)
    def paintItems(self, g):
        for e in self.items:
            e.paint(g)
class LifeGamePanel(JPanel):
    def paintComponent(self, g):
        self.decideEdge();
        self.paintItems(g)
    def paintItems(self, g):
        for e in self.items:
            e.paint(g)
class GameBoardPanel(JPanel):
    def paintItems(self, g):
        for e in self.items:
            e.paint(g)
class OthelloPanel(GameBoardPanel):
    def paintComponent(self, g):
        self.paintItems(g)

 メソッドpaintComponentは、任意のコンポーネント(部品)を再描画します。このメソッドは、Swingが提供するフレームワークに沿って、メソッドrepaintに呼応して起動されます(ハリウッドの原則)。

 メソッドpaintItemsは、盤面に配置したすべての実体を描画します。重複するメソッドpaintItemsは、クラスPuzzlePanel/LifeGamePanelをクラス GameBoardPanelの傘下に置くことで、省略できます。

リファクタリング後

#------ after --------------------------------
class GameBoardPanel(JPanel):
    def paintComponent(self, g):
        self.prepare()
        self.paintItems(g)
    def prepare(self):
        raise NotImplementedError, "def prepare(self)"
    def paintItems(self, g):
        for e in self.items:
            e.paint(g)
class PuzzlePanel(GameBoardPanel):
    def prepare(self):
        self.__class__.itemExtent = (
            self.width/self.dim, self.height/self.dim)
class LifeGamePanel(GameBoardPanel):
    def prepare(self):
        side = min(self.width, self.height) / self.dim
        self.__class__.itemExtent = side, side
class OthelloPanel(GameBoardPanel):
    def prepare(self):
        self.__class__.itemExtent = (
            self.width/self.dim, self.height/self.dim)

 抽象クラスGameBoardPanelでは、メソッドpaintComponentをテンプレートとして規定します。本体にあるメソッド群prepare/paintItemsは、子孫クラス PuzzlePanel/LifeGamePanel/OthelloPanelで実現します。こうすることで再利用性に優れた、仕様の変更にも柔軟に対処できるものになります。

 メソッドprepareは、ウィンドウの大きさが変化したときに、盤面に配置される実体の大きさを決定して、これをクラス属性itemExtentに保持します。

次のページ
部品に分解する

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
よろずプログラマーのためのPython導入ガイド連載記事一覧

もっと読む

この記事の著者

小泉ひよ子とタマゴ倶楽部(コイズミヒヨコトタマゴクラブ)

http://tamago-club.cocolog-nifty.com/「楽しくなければ仕事じゃない」が私たちのモットー。99%の苦悩の連続も、1%の成功に報われます。だからこそ、この仕事が楽しくて仕方がないのです。楽をするための努力なら惜しみません。何もせず楽をしているのと、努力をしたから楽ができるのと...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

伊藤うさぎ(イトウ ウサギ)

ペンネームの「由来は」と言うと。苗字の方は、セミナー研修で同じチームになった、3人の合体ユニット名 [I:石塚, T:田川, O:尾沢] から来ています。名前の方は、同じ干支(卯:1987 年生)に因んだものです。既に2人は卒業して、残る1人がその名跡を継承しています。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/2253 2008/06/17 14:00

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング