LoginSignup
5
5

More than 5 years have passed since last update.

デザインパターンを使ってみる(エクスポーター編)

Last updated at Posted at 2016-12-18

概要

MayaPythonでデザインパターンを使ってみようという初学者向けの内容です。
デザインパターンに関する詳細な説明は他サイトにお任せするとして、
実際に使ってみるとどういう風にコードが良くなるのか?にフォーカスします。
適応例としてデータエクスポーターのようなものをサンプルにします。

モチベーション

デザインパターンを実際に使ってみて設計力とコード品質を上げたい。
ツール実装において特に使えるパターンを見つけたい。

デザインパターン

今回使ってみるのは下記のパターンです。
パターンの詳細はページ最下部の参考サイトなどを確認して下さい。

パターン 概要(私の認識) メリット
TemplateMethod 親クラスで呼び出すメソッドを抽象化し、メソッド内部の実装はサブクラスに任せる。 処理の流れを縛りつつ各処理の振る舞いはサブクラスに任せられる。
FactoryMethod クラス内で別のオブジェクトを使う場合、生成処理を抽象化し、(抽象化された)生成オブジェクトを使う。生成処理とメソッドの実装は各サブクラスに任せる。 TemplateMethodの良さを受け継いでいる。別オブジェクトの生成とメソッド呼び出しを行う場合に、処理の流れ生成振る舞いを分離できる。
Factory 指定された条件に応じたオブジェクト生成のみを行うクラスを用意して使う。 オブジェクトを使う側から条件に応じたオブジェクトの生成を分離できる。

エクスポータースクリプトに使ってみる

Mayaシーンの情報をエクスポートするという処理は比較的多い処理かなと思ったのでチョイスしました。
イメージ的には適切なデータをFBXエクスポートを使って出力するようなものをイメージしています。
条件によって出力データを選んだり、専用処理を行ったり、後から仕様が変更されやすい認識があります。
デザインパターンを適応する前と後でどんな感じのコードになるのかを記載しています。

ベースコード

特に何も考えずに処理を並べていくと下記のような感じになるのではないでしょうか。
※出力モードなどのオプションパラメータは事前に別クラスにしてパラメータ化している想定。

scene_exporter.py
def scene_export(option):
    """シーン内のモデルやアニメーションを出力する"""
    # 事前処理てきなもの

    if option.mode == ExportMode.head:
        # Hierarchyの中から Headメッシュデータをみつける
        # なにか処理をする
        # エクスポートする
    if option.mode == ExportMode.body:
        # Hierarchyの中から Bodyメッシュデータをみつける
        # なにか処理をする
        # エクスポートする
    if option.mode == ExportMode.animation:
        # Hierarchyの中から Skeletonデータをみつける
        # なにか処理をする
        # エクスポートする

    # 事後処理てきなもの

サンプルコードはコメントしか書いてませんが実際に実装していくと行数はガンガン増えそうです。
さらにモードが増えるたびにコードが肥大化していきメンテナンス性が下がってしまいそうですね。

TemplateMethod適応

とりあえずエクスポート処理を共通化するためにTemplateMethodパターンを適応します。
※コンストラクタや内部実装などは省略しています。

exporter.py
from abc import ABCMeta, abstractmethod


class BaseExporter(object):
    """エクスポーター基本クラス"""
    __metaclass__ = ABCMeta

    def export(self):
        select_export_targets()
        export_selection()

    @abstractmethod
    def select_export_targets(self):
        """エクスポート対象を選択する"""
        pass

    @abstractmethod
    def export_selection(self):
        """選択しているオブジェクトをエクスポートする"""
        pass


class HeadExporter(BaseExporter):
    """HEAD用エクスポータークラス"""
    def select_export_targets(self):
        cmds.select("|char|mesh|head")

    def export_selection(self):
        # HEAD用の出力処理を実装

class BodyExporter(BaseExporter):
    """BODY用エクスポータークラス"""
    def select_export_targets(self):
        cmds.select("|char|mesh|body")

    def export_selection(self):
        # BODY用の出力処理を実装

class AnimExporter(BaseExporter):
    """ANIM用エクスポータークラス"""
    def select_export_targets(self):
        cmds.select("|char|skel|root")

    def export_selection(self):
        # ANIM用の出力処理を実装

scene_exporter_v2.py
def scene_export(option):
    """シーン内のモデルやアニメーションを出力する"""
    # 事前処理てきなもの

    if option.mode == ExportMode.head:
        HeadExporter().export()
    if option.mode == ExportMode.body:
        BodyExporter().export()
    if option.mode == ExportMode.animation:
        AnimExporter().export()

    # 事後処理てきなもの

作成したエクスポータークラスを使うようにメイン処理を修正しました。
内部処理が隠蔽されてスッキリしました。

さらにFactoryパターンを適応

TemplateMethodを適応しても、モード追加時のメンテナンス性がまだ改善されていません。
新たにFactoryクラスを作成して分岐と生成をお任せします。
メイン処理ではexport()が存在するBaseExporterサブクラスを使うだけになります。

exporter_factory.py
class BaseExporterFactory(object):
    """BaseExporterクラスのFactoryクラス"""
    def create(option):
        if option.mode == ExportMode.head:
            return HeadExporter()
        if option.mode == ExportMode.body:
            return BodyExporter()
        if option.mode == ExportMode.animation:
            return AnimExporter()

scene_exporter_v3.py
def scene_export(option):
    """シーン内のモデルやアニメーションを出力する"""
    # 事前処理てきなもの

    BaseExporterFactory().create(option).export()

    # 事後処理てきなもの

ここまでくると、モード追加時のメンテナンス性がだいぶ上がりました。
モードが増えたらBaseExporterサブクラスを用意してFactoryクラスで生成可能にします。

さらにFactoryMethodパターンを適応

仕様追加が発生して「BaseExporterクラス内部でログ用オブジェクトを生成して使う」ことにします。
BaseExporterクラスでオブジェクト生成と使用を行うことになるので FactoryMethodを適応します。
※ここからはHeadExporter以外のBaseExporterサブクラスは省略します。

FactoryMethodのProductにあたるExporterLogの親クラスとサブクラスを実装します。

exporter_log.py
from abc import ABCMeta, abstractmethod


class BaseExporterLog(object):
    """エクスポーターログ基本クラス"""
    __metaclass__ = ABCMeta

    @abstractmethod
    def open(self):
        """ログを開始する"""
        pass

    @abstractmethod
    def close(self):
        """ログを終了する"""
        pass


class MyExporterLog(BaseExporterLog):
    """エクスポーターログサブクラス"""
    def open(self):
        print "export logging start"

    def close(self):
        print "export logging close"

FactoryMethodのCreatorにあたるBaseExporterにログの生成と使用を定義し、実装はサブクラスに任せます。

exporter.py
from abc import ABCMeta, abstractmethod


class BaseExporter(object):
    """エクスポーター基本クラス"""
    __metaclass__ = ABCMeta

    def export(self):
        self.log = create_exporter_log()
        self.log.open()

        select_export_targets()
        export_selection()

        self.log.close()

    @abstractmethod
    def create_exporter_log(self):
        """エクスポートログを生成する"""
        pass

    @abstractmethod
    def select_export_targets(self):
        """エクスポート対象を選択する"""
        pass

    @abstractmethod
    def export_selection(self):
        """選択しているオブジェクトをエクスポートする"""
        pass


class HeadExporter(BaseExporter):
    """HEAD用エクスポータークラス"""
    def create_exporter_log(self):
        return MyExporterLog()

    def select_export_targets(self):
        cmds.select("|char|mesh|head")

    def export_selection(self):
        # HEAD用の出力処理を実装

あとはメイン処理側でログを使うような感じになったり、ならなかったりすると思います。

scene_exporter_v4.py
def scene_export(option):
    """シーン内のモデルやアニメーションを出力する"""
    # 事前処理てきなもの

    exporter = BaseExporterFactory().create(option)
    exporter.export()

    # ログを処理するとかしないとか

    # 事後処理てきなもの

感想

認識や実例が適切かどうかはさておき、パターン適応後のコードを見るとある程度納得感がありますね。
コード量だけならベースコードが最小ですが、後から変更することを考えるとパターンを使った
リファクタリングに時間をかけるのは有効ですね。

今回使ったパターンでは、FactoryMethodパターンの理解が難しく感じます。
名前からFactoryパターンの派生のように感じてしまいますが、TemplateMethodの派生という認識をしました。
そう思ったのでパターン適応の流れもTemplateMethodを最初にもってきました。

多方面からのツッコミやアドバイスも待ちつつ、追加のお勉強をしていきたいと思います。

参考サイト

5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5