部品を抽象化する
新しいクラスを定義した後の、既存クラスの整理統合は重要です。
既存のクラスが提供する特性を再利用すると共に、重複するものを整理整頓して、既存のクラスを統合する作業は、部品を「抽象化」する過程と見なせます。固有なものから共通なものを抽出することで、クラス間の継承関係を再考します。新たに抽出したクラスは、既存のクラス階層に挿入されます。
事例:抽象クラス 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
に保持します。