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

心魅 - cocoromi -

半角スペース時々全角

JSでイベントハンドラ周りにコマンドパターンを使って、メンテナンスを簡単にする

JavascriptAjaxでかつ画面遷移しまくるアプリを作るときに、イベントハンドラの付け替えが割と面倒だったりします。
そんなときには中身を入れ替えない部分の要素にだけイベントハンドラを設定して、あとはクラスをパースして処理を分けるという手段があります。

今回のサンプル:http://umezo.tsuyabu.in/samples/js/cmdPattern/

前提

ただし、そもそもイベントハンドラを付け替えなきゃ行けない時なんかあるのか?というと、以下の様なケースが該当します。

  • Ajaxによる内容の入れ替えをinnerHTMLの書き換えで行っている

と言うのも、innerHTMLで中身を書き換えてしまうと、JS上のオブジェクトに対応する、HTML要素がいなくなってしまうため、イベントが飛んでこなくなるのです。
仮に書き換える前と全く同じHTMLを読み込んだとしてもイベントハンドラの再設定が必要になってしまうのです。

なので、いったん読み込んだ内容をスタイルにより非表示にするようなタイプの実装では、今回のテクニックに出番はありません。


実装

イベントハンドラに以下のようなコードを書いておきます。

    $("#bd").click( function ( e ){
        e.preventDefault();
        //classから"cmd_"で始まるクラスをパースして取得する
        var cmd = getCommand( e.target , "cmd_" ) ;
        //イベントハンドラをまとめて持ってるオブジェクトが
        //対応する関数を持っていれば実行
        ( CommandMgr[ cmd ] || function(){ } )( e ); 
    });

イベントを飛ばしてくる部分のHTMLはたとえば以下の様になります。
中身1

<button class="cmd_load2">load 2</button>
<button class="cmd_load3">load 3</button>
<button class="cmd_alert1">alert 1</button>

中身2

<button class="cmd_load1">load 1</button>
<button class="cmd_load3">load 3</button>
<button class="cmd_alert2">alert 2</button>


この部分はif文でハードコーディングしても良いのですが、せっかくなので上記の用に抽象化しておきましょう。
コレにより、イベント処理したい要素が増えても、以下の手順で拡張することが出来ます。

  • 該当要素のclassに"cmd_"で始まるクラス"cmd_SOME_NAME"を追加する
  • "SONE_NAME"に対応する関数をCommandMgrに追加する

イベントハンドラの中身は全く書き換えなくても良い訳ですね。

あ。あと、CommandMgrの関数の中でthisを使う場合は呼び出し箇所を以下のようにいじってください。

( CommandMgr[ cmd ] || function(){ } ).call( CommandMgr , e );

ちょっと大げさになってきてしまうので、あまりthisを使わないコーディングにしましょう。

JS独特のデザインパターン適用

コマンドパターンというと、良く解説されるのは、メソッド名と引数を統一し、別々のクラスに実装することにより、システムを拡張する実装方法です。
そのため、複数人で開発するようなスタイルの時には独断で採用するには大げさなデザインパターンであると思います。
しかし、JSではそもそも、メソッド名を気軽に動的に切り替えて行けるので、わざわざクラスで実装する必要はありません。
必要なのは引数の"決め"ぐらいですが、イベントハンドラならイベントオブジェクトを渡しておけばまず問題になりません。

デザインとロジックの分離

あ・・・・・・・・・



この方法使うとデザインとロジックは分離出来ません!

onclick属性使ってるのと大して変わらないっすね。

気になる人はやっちゃだめだZE!


サンプル

そんなわけで実装済みサンプルを置いておきました。
http://umezo.tsuyabu.in/samples/js/cmdPattern/

まとめ

そんなわけで、コマンドパターンを使うとイベント周りの拡張性が高くなり、メンテナンスも楽ちんです。
ただし、いわゆるデザインとロジックの分離とは逆行していると思うので、気になる人もいるかと思います。



以下サンプルソース全文

ソース全文

  • 本体
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html lang="ja" dir="ltr" xml:lang="ja-JP">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <meta http-equiv="Content-Style-Type" content="text/css" />
        <title>JSでコマンドパターン</title>
        <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.6.0/build/reset-fonts-grids/reset-fonts-grids.css">
        <style>
            body {
                padding : 10px ;
                text-align : left ;
            }

            #bd {
                border : 2px solid #000 ;
                width : 300px ;
                height : 300px ;
                padding : 1em ;
            }

            #bd .cntrl {
                padding-bottom : 0.5em ;
                border-bottom : 2px solid #000 ;
            }

            #bd .bd {
                padding : 1em 0 ;
            }
        </style>
    </head>
    <body>
        <h1 id="hd">JSでコマンドパターン</h1>
        <div id="bd"></div>
<script type="text/javascript" src="js/jquery.js"></script>
<script type="text/javascript">
$(function(){
    var CONTENT_1 = "content1.php" ,
        CONTENT_2 = "content2.php" ,
        CONTENT_3 = "content3.php" ,
        PREFIX = "cmd_" ,
        CommandMgr = {
            alert1 : alert1 ,
            alert2 : alert2 ,
            alert3 : alert3 ,
            load1 : load1 ,
            load2 : load2 ,
            load3 : load3
        };



    $("#bd").load( "content1.php" ).click( function ( e ){
        e.preventDefault();
        var cmd = getCommand( e.target , PREFIX ) ;
        ( CommandMgr[ cmd ] || function(){ } )( e );
    });

    function load1 ( e ){
        loadToBody( CONTENT_1 );
    }
    function load2 ( e ){
        loadToBody( CONTENT_2 );
    }
    function load3 ( e ){
        loadToBody( CONTENT_3 );
    }

    function alert1 ( e ){
        alert( "中身1です" );
    }
    function alert2 ( e ){
        alert( "中身2ですにゃ" );
    }
    function alert3 ( e ){
        alert( "くまー" );
    }

    function getCommand ( elem , prefix ){
        var listClass = elem.className.split( " " ) ,
            aClass ,
            i = 0 , n = listClass.length ;
        for( ; i < n ; i++ ){
            aClass = listClass[ i ] ;
            if( aClass.indexOf( prefix ) === 0 ){
                return aClass.replace( prefix , "" );
            }
        }

        return null;
    }

    function loadToBody( url ){
        $("#bd").load( url );
    }
});
</script>
    </body>
</html>
  • 中身1
<div class="cntrl">
<button class="cmd_load2">load 2</button>
<button class="cmd_load3">load 3</button>
<button class="cmd_alert1">alert 1</button>
</div>
<div class="bd">
    中身1<br />
    わいわーい
</div>
  • 中身2
<div class="cntrl">
<button class="cmd_load1">load 1</button>
<button class="cmd_load3">load 3</button>
<button class="cmd_alert2">alert 2</button>
</div>
<div class="bd">
    中身 二<br />
    漢字
</div>
  • 中身3
<div class="cntrl">
<button class="cmd_load1">load 1</button>
<button class="cmd_load3">load 3</button>
<button class="cmd_alert3">alert 3</button>
</div>
<div class="bd">
    中身 III<br />
    ローマ数字
</div>