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

既存のクラスが提供する特性を再利用すると共に、重複するものを整理整頓して、既存のクラスを統合する作業は、部品を「抽象化」する過程と見なせます。固有なものから共通なものを抽出することで、クラス間の継承関係を再考します。新たに抽出したクラスは、既存のクラス階層に挿入されます。
事例:抽象クラス GameItem
具象クラスTile/Life/Stoneに共通する特性(インスタンス属性/メソッド関数/プロパティー/イベント等)を、抽象クラスGameItemとして抽出します。すると、抽出したGameItemがobjectを頂点とするクラス階層に挿入されたのが分かります。
#------ 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へと移動します。
#------ 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とします。これが「具体的なことは将来に委ねる」という祖先から子孫へと受け継がれる遺言(暗黙の了解)となります。これらのメソッドは、自分で「呼び出す」ことより、他から「呼び出される」ことに意義があります。
ここで着目して欲しいのは、フレームワークがメソッドのシグニチャーだけでなく、プロトコルも規定することです。ここでは「ウィンドウのタイトル/大きさを設定してから、視覚部品を配置する」という前提で役割を分担します。しかし、フレームワークを利用する側では、その手順を知る術はありませんし、知る必要もありません。
メソッド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に保持します。



