読者です 読者をやめる 読者になる 読者になる

Convention over Configuration パターン (1)

python デザインパターン

Rubyによるデザインパターン』(ラス・オルセン著 ピアソン・エデュケーション刊)の例をPythonに変換して書いています。目次

"Convention over Configuration(CoC)"は「規約は設定に勝る」とでも訳すのでしょうか?

CoCはアプリケーションというよりも、APIのためのパターンです。普通、アプリを作るときは誰でも


  • よく使う機能はワンクリックで使える

  • 設定無しですぐに始めれられる

  • ユーザーに何度も同じ質問をしない


等ということを意識していると思いますが、APIでは必ずしも実践されてはいません。

たとえば、Webサーブレットの場合、普通「クラス名=対応するURL」にします。わざわざクラス名と違うURLを割り当てることは稀です。にも関わらず、設定ファイルにURLを設定する事を、毎回プログラマに要求するのは、意味がありません。

CoCはクラス名やディレクトリの規約を利用して、設定ファイルの面倒を省きます。

例として、メッセージゲートウェイを作ります。


メッセージを受け取りどこかへ送るクラスです。”どこかへ”はE-mailで送信することもあれば、HTTP POST要求をすること、ファイル書き込みすることもあります。こういった場合の常套手段として、アダプタを作りました。FileAdapter、HttpAdapterSmtpAdapterです。

そして、アダプタクラスの選択にはファクトリ関数を使います。

from file_adapter import FileAdapter
from http_adapter import HttpAdapter
from smtp_adapter import SmtpAdapter
def adapter_for(protocol):
    adapters = {
        "file":FileAdapter,
        "http":HttpAdapter,
        "smtp":SmtpAdapter,
    }
    return adapters[protocol]()

しかし、このadapter_forは、プロトコルとアダプタの対応をハードコードしてしまっています。

プログラマが後から、アダプタを追加することが出来ません。

設定ファイルを作ることも出来ますが、

#adapter.cfg
file:FileAdapter
http:HttpAdapter
smtp:SmtpAdapter

これは、ハードコードする場所が変わるだけで意味がありません。



よく考えると、File用アダプタは"FileAdapter"と名づけるに決まっています。File用を"HttpAdapter"と名づけるのは混乱するだけですから。


それならば、


アダプタクラスは、{protocol}Adapterと命名すること

という規約を作ってしまいましょう

import file_adapter
import http_adapter
import smtp_adapter

def adapter_for(protocol):
    adapter_name = "%sAdapter"%(protocol.capitalize(),)
    module_name  = "%s_adapter"%(protocol.lower(),)
    
    module = eval(module_name)
    adapter_class = getattr(module, adapter_name)
    
    return adapter_class()


adapter_forはprotocolをアダプタクラス名に変換し、
evalでクラスがあるモジュールを取得、
getattrでモジュールの中にあるクラスを手に入れます。



これに新たなアダプタクラスを追加するには、
アダプタクラスが書かれたモジュールからimportするコードを書けばよろしい。

しかし、アダプタクラスが、どのモジュールに書き込まれているかが分かりません。
ここでモジュール名を設定ファイルに書き込むように要求しては意味がありません。


が、同じような機能を持つクラスは、同じディレクトリに置くのが自然です。それならば、


アダプタクラスは、{protocol}Adapterと命名し、adapterディレクトリに置くこと

規約を追加しましょう

import imp
def adapter_for(protocol):
    adapter_name = "%sAdapter"%(protocol.capitalize(),)
    module_name  = "%s_adapter"%(protocol.lower(),)
    
    file, filename, description = imp.find_module(module_name, ["./adapter"]) 
    module = imp.load_module(module_name, file, filename, description) 
    
    adapter_class = getattr(module, adapter_name)
    return adapter_class()

そして、adapterディレクトリから、モジュールを検索して、インポートします。


find_moduleはモジュールを検索して、ファイルオブジェクト・ファイル名・モジュールの情報を返します。
load_moduleで、モジュールを手に入れます。



なお、この方法だと多分、毎回毎回モジュールの読み込み処理が必要です。
普通のimport文なら、既にimportされているファイルは、何度も読み込まれたりしません。