Status Code 303 - See Other

サーバサイド、iOS・アンドロイドアプリ、インフラレベルの話まで幅広くやってます。情報の誤りや指摘・意見などは自由にどうぞ。

テンプレートによるロジックとデザイン切り分け

概要

デザインとロジックを切り分ける内容。
本来これらは全く別個に管理すべきなものであるため、分離させたい場合が多くある。
今回は、Java でこの方法を実現する手順を記述。

デザインとロジックの混在する悪い例

public class Main {

	public static void main(String[] args){
		String user = "hoshi-kouki";
		boolean isMember = true;
		Cart cart = new Cart();
		cart.add(new Item("タマネギ", 100));
		cart.add(new Item("じゃがいも", 150));
		cart.add(new Item("人参", 100));
		cart.add(new Item("肉", 500));

		System.out.println(user + " 様");
		System.out.println();
		System.out.println("以下商品を購入されたことを通知します。");
		System.out.println("===========================");
		for(Item item : cart.getItems()) {
			System.out.println(item.getName() + "		"  + item.getPrice() + " 円");
		}
		System.out.println("--------------------------");
		System.out.println("合計		" + cart.sum() + " 円");
		System.out.println("--------------------------");		
		if (isMember) {
			System.out.println("いつもお買い上げ頂きありがとうございます!");		
		}else {
			System.out.println("会員登録するとポイントがもらえますよ!");		
		}
	}	
}

まずい理由

  • ロジックとデザインが混在するため、どちらも内容が分かり辛い。
  • デザイン変更→ロジック破壊、ロジック変更→デザイン破壊が起こる可能性
  • デザインを管理すべき人が自由に変更できない
    • デザイン担当する人がプログラマと同じことができるとは限らない

本来あるべき姿

  • デザインを担当する人は、その人の好きなツールで作成
  • そのデザインを内部に取り込む仕組みはプログラマが作成

テンプレートの仕組み作成

Velocity - Velocity User Guide を用いたサンプルを記述する。
インストール手順は、ここ参照(http://hack.aipo.com/archives/8360/)。

事前設定
  1. プロジェクト作成
  2. velocity.zip ダウンロード
  3. 展開 → velocity-x.x-dep.jar (x.xはバージョン) プロジェクト内にコピー
  4. ビルドパスに velocity-x.x-dep.jar 設定
詳細説明

今回実装したテンプレート利用クラス(OutputBuilderクラス).

import java.io.StringWriter;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;

/**
 * 既に定義されたテンプレートファイルからプログラム上で様々な値を
 * セットして処理に応じた結果を作成する。
 */
public class OutputBuilder {

	/**
	 * ファイル名.
	 */
	private String fileName;
	
	/**
	 * Velocity コンテキスト.
	 */
	private VelocityContext context;

	/**
	 * コンストラクタ.
	 * @param fileName ファイル名
	 */
	public OutputBuilder(String fileName) {
		this.fileName = fileName;
		context = new VelocityContext();
	}

	/**
	 * Velocityコンテキストにパラメータを設定する.
	 * @param key キー名
	 * @param value
	 * @return 呼び出したテンプレートビルダー自身
	 */
	public OutputBuilder set(String key, Object value) {
		context.put(key, value);
		return this;
	}

	/**
	 * 格納した結果から文字列を作成する.
	 * @return 文字列
	 */
	public String build() {
		Velocity.init();
		Template template = Velocity.getTemplate(fileName, "UTF-8");
		StringWriter writer = new StringWriter();
		template.merge(context, writer);
		return writer.toString();
	}
}

今回は最小構成で作るため最低限のみ。機能追加は本家ドキュメントを参考に。

使い方

請求書を作成するとして記述する。

  1. 必要データ洗い出し
    請求書に必要なデータは、今回は以下とする。

    • 購入者
    • 購入商品・その値段
    • 合計金額
    • 購入者が会員/非会員かどうか


  2. Value Object(VO)用意
    基本データ(文字・数値)のみで構成しても動作するが、モデル化クラスを利用した方が分かりやすくなる。

    • 商品の属性情報を管理するクラス。(Itemクラス)
    • 商品自体を管理するクラス (Cartクラス)


  3. 各クラス実装
    Itemクラス

    /**
     * 商品情報を管理する.
     */
    public class Item {
    	/**
    	 * 商品名.
    	 */
    	private String name;
    	/**
    	 * 単価.
    	 */
    	private int price;
    	/**
    	 * コンストラクタ.
    	 * @param name 商品名
    	 * @param price 単価
    	 */
    	public Item(String name, int price) {
    		this.name = name;
    		this.price = price;
    	}
    	public String getName() {
    		return name;
    	}
    	public int getPrice() {
    		return price;
    	}	
    }
    

    Cartクラス

    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * 購入商品を管理する.
     */
    public class Cart {
    	/**
    	 * 購入商品.
    	 */
    	private  List<Item> items;
    	/**
    	 * コンストラクタ.
    	 */
    	public Cart() {
    		items = new ArrayList<>();
    	}
    	/**
    	 * 商品リストを取得する.
    	 * @return 商品リスト
    	 */
    	public List<Item> getItems() {
    		return items;
    	}
    	/**
    	 * 商品を items に追加する.
    	 * @param item 追加する商品
    	 */
    	public void add(Item item) {
    		items.add(item);
    	}
    	/**
    	 * 商品の合計金額を取得する.
    	 * @return 金額
    	 */
    	public int sum() {
    		int sum = 0;
    		for(Item item : items) {
    			sum += item.getPrice();
    		}
    		return sum;
    	}
    }
    


  4. テンプレート(bill.vm)作成

    $name 様
    
    以下商品を購入されたことを通知します。
    ===========================
    #foreach ($item in $cart.getItems())
    $item.getName()		$item.getPrice() 円
    #end
    --------------------------
    合計		$cart.sum() 円
    ===========================
    #if($isMember)
    いつもお買い上げ頂きありがとうございます!
    #else
    会員登録するとポイントがもらえますよ!
    #end

    今回は紹介のため様々なものを使っているが、
    デザインの全体像が分かり辛くなるため分岐(#if ~ #else ~ #end)はあまり使わない方が良い。

    なお、上記にある特殊な意味を持つ文字について説明すると。

    $name

    setメソッドで渡したキー名'name'に対応した値を表示。

    $cart.getItems()

    setメソッドで渡したキー名'cart'に対応するデータに対して getItem メソッドを呼び出した結果を表示。

    #foreach ($item in $cart.getItems()) (処理) #end

    (処理)を集合個数($cartにはいったアイテム分)実行する。
    各繰り返し処理において、カートの中の各要素がループごとに $item にセットされ、処理に利用できる。

    #if($isMember) (処理1) #else (処理2) #end

    条件によって、(処理1)か(処理2)の結果が表示。


  5. ロジック記述
    ロジックにテンプレートに対する処理を記述。

    1. 使用するテンプレート指定 (OutputBuilder コンストラクタ)
    2. $name, $isMember, $cart に使用するデータ設定(setメソッド)
    3. テンプレートの指定箇所にロジックで得たデータを組み合わせた結果を得る(build メソッド)
    public class Main {
    
    	public static void main(String[] args) {
    		Cart cart = new Cart();
    		cart.add(new Item("タマネギ", 100));
    		cart.add(new Item("じゃがいも", 150));
    		cart.add(new Item("人参", 100));
    		cart.add(new Item("肉", 500));
    		
    		OutputBuilder template = new OutputBuilder ("bill.vm").set("name", "hoshi-kouki").set("isMember", true).set("cart", cart);
    		System.out.print(template.build());
    	}
    }
    

  6. 出力結果は以下。

    hoshi-kouki 様
    
    以下商品を購入されたことを通知します。
    ===========================
    タマネギ		100 円
    じゃがいも		150 円
    人参		100 円
    肉		500 円
    --------------------------
    合計		850 円
    ===========================
    いつもお買い上げ頂きありがとうございます!

    bill.vm の所定の箇所にデータが埋め込まれた形で出力できていることが分かる。

参考文献

Start up the Velocity Template Engine | JavaWorld
http://hack.aipo.com/archives/8360/

更新履歴

2016/01/20 変な表現多すぎたので、記事修正。
      TemplateBuilderクラス → OutpuBuilderクラス(テンプレート作るわけじゃないし)
2016/06/13 説明が冗長すぎて分からなかったので修正。