CoffeeScriptのリファクタリング

http://blog.arkency.com/2014/07/6-front-end-techniques-for-rails-developers-part-i-from-big-ball-of-mud-to-separated-concerns/

1 comment | 0 points | by WazanovaNews 2年以上前 edited


Jshiike 2年以上前 edited | ▲upvoteする | link

シングルページアプリもあり、それでなくてもフロント側のコードを書く機会は増えてきてますが、コードをうまく整理して、

  • 簡単に、もっとテストしやすいコードを書く。
  • クオリティを下げることなく開発スピードをあげる。

ためのノウハウの一端を開発会社のArkencyがシェアしてくれています。

シリーズの初回は、シンプルなリファクタリングのケーススタディ。

CoffeeScriptのコードが、DOM変換、イベントハンドラー、AJAXコールでごちゃ混ぜになる光景はよく目にするもの。依存関係を整理し、Applicationクラスをつくり、リファクタリングをとおして役割を切り分けていく。

やりたいことは、AJAXコール経由で写真データを読込み、スクリーンに表示。写真をクリックした後はグレーアウト表示させる。

まず、リファクタリング前の悪い事例は下記の通り。

$(document).ready ->
  photoHTML = (photo) =>
    "<li>
       <a id='photo_#{photo.id}' href='#{photo.url}'>
         <img src='#{photo.url}' alt='#{photo.alt}' />
       </a>
    </li>"

  $.ajax
    url: '/photos'
    type: 'GET'
    contentType: 'application/json'
    onSuccess: (response) =>
      for photo in response.photos
        node = $(photoHTML(photo)).appendTo($("#photos-list"))

        node.on('click', (e) =>
          e.preventDefault()
          node.find('img').prop('src', 
            photo.url + '.grayscaled.jpg')
        )
    onFailure: =>
      $("#photo-list").append("<li>
                                 Failed to fetch photos.
                              </li>")

このコードの問題としては、

  • コールバックにコールバックがネストしていて、メンテするのが悪夢になる。
  • このコードの定義と初期化が分離していない。つまり、常に実行したくない場合は、if $(“#phots-list”).length > 0のように何らかの条件を設定する必要がある。
  • SRP (Single responsibility principle) に明らかに違反している。データを取得して、DOMを操作し、ドメインロジック(グレースケールの写真のURLを生成)、イベントバインディング、を全てこのコード内で行っている。
  • 何をしたいのか意図を明示していない。今はよくても将来新しい機能を追加していくときにどうなるか?50-100行に拡大したときに問題となる。

典型的な解決策としては、下記のクラスに分割する。

  • GUIクラス: いくつかの小さなクラスから構成される
  • Backendクラス: Railsのバックエンドからデータを取得し、前処理する
  • Usecaseクラス: ドメインオブジェクト上でオペレートするビジネスロジック

今回はビジネスロジックがほぼないので、GuiクラスとBackendクラスで構成するが、コードの意図を示すインターフェースをつくるので、Photoドメインオブジェクトは用意する。

リファクタリング後のコードはこちら

ドメインから手をつける

Sprocketsベースのスタックに取組む場合は、application.jsのモジュール定義からつくり、新しいクラスがグローバルにアクセスでき、ネームスペースを確保できるようにする。シンプルに、application.jsのボディにPhotos = {} を置く。それから新しいクラスをrequireする。web inspectorで利用できるようになり、Photosネームスペースのコードでも使えるようになる。

常にドメイン(もしくはusecase)から始めよというのがルール。今回は、グレースケール写真のURLを変換するロジックを包むコードを用意する。

Railsとやり取りする

更にコードを分離させていく。AJAXがデータを取ってくる振る舞いを担当するBackendクラスをつくる。既存の実装をメソッドにもってくることで、新しいクラスをつくってみた。

onSuccessonFailureのcallbackを取り除いて、Promiseオブジェクト置き換えた。それにより、AJAXコールのステータスを誰にでも提供できるようになった。他のオブジェクトにコントロールを渡す場合はいつもこのように実装している。また、#thenでちょっとした工夫をしている。このメソッドのcallerのデータは、JSONデータそのままではなく、新しいPhotos.photoオブジェクトに包まれるようになる。

バックエンドの役割はドメインオブジェクトのJSONを包むことではないと言うかもしれないが、バックエンドはアプリの中心部分から隔離された世界であるべきだと思っている。アプリの中心というのはあくまでドメインオブジェクトである。純粋なバックエンドの実装では、JSONからドメインオブジェクトへのマッピングを担うオブジェクトをつくるべきである。そして中間的なステップとして、このオブジェクトをつかってバックエンドから返ってくるJSONデータを変換すべき。

可視化する

最後に、イベントをDOMオブジェクトにレンダリング & バインディングする役割をもつGuiクラスを用意する。やり方はいくつかあるが、うちの場合は、テンプレートにHandlebarを使うか、Gui全体をつくるのにReact.jsを利用する。どの手法を取ろうが、役割を拡張しないように留意すること。つまり、

  • DOMに変更がかかれば、DOMを操作するのは、そのGui(もしくはGuiを構成する別のオブジェクト)の役割
  • UIからのイベントがドメインのアクションを起せば、Guiはそれをドメインオブジェクトにデリゲートするだけで、自分で実行はしない。

これで従前のコードのロジックはカバーしたので、今度は全体をコーディネートするだけ。

まとめて動くようにする

バックエンドでは、全体のコーディネーションはサービスオブジェクト内で担う。サービスオブジェクトがなければ、コントローラーを利用する。よって、今回は、Applicationクラスをつくり、新しいオブジェクトの初期化とコーディネーションをする。

定義と初期化はわけるべきなので、初期化のコードを別途用意する。

原文末尾にあるメルマガに登録すると、シリーズの二回目以降のコンテンツが無料で配信されるようです。

#coffeescript

Back