2015年6月11日木曜日

1-9. Decorator パターン

オブジェクトに動的に機能を追加します。継承より柔軟な機能拡張を提供します。



一般的に既存のクラスに機能を拡張するには、継承したクラスを作成します。
しかしこの方法だと継承元クラスが固定されるため、既存クラスが複数あった場合に
既存クラスごとに継承したクラスを作成しなければなりません。
interface Component {
	public void operation();
}
class ConcreteComponent1 implements Component {
	public void operation() {
		System.out.println("ConcreteComponent1");
	}
}
class ConcreteComponent2 implements Component {
	public void operation() {
		System.out.println("ConcreteComponent2");
	}
}
class AdditionalConcreteComponent1 extends ConcreteComponent1 {
	public void operation() {
		super.operation();
		System.out.println("AdditionalConcreteComponent");
	}
}
class AdditionalConcreteComponent2 extends ConcreteComponent2 {
	public void operation() {
		super.operation();
		System.out.println("AdditionalConcreteComponent");
	}
}
Decorator パターンでは継承を使用せず、委譲を使って動的に委譲元クラスを差し替える
ことで機能を拡張します。
abstract class Decorator implements Component {
	protected Component component;
}
class ConcreteDecorator extends Decorator {
	public ConcreteDecorator(Component original) {
		this.component = original;
	}
	public void operation() {
		this.component.operation();
		System.out.println("ConcreteDecorator");
	}
}
void client() {
	System.out.println("// 継承による機能拡張");
	new AdditionalConcreteComponent1().operation();
	new AdditionalConcreteComponent2().operation();
	System.out.println("// 委譲による機能拡張");
	new ConcreteDecorator(new ConcreteComponent1()).operation();
	new ConcreteDecorator(new ConcreteComponent2()).operation();
}
実行結果:
// 継承による機能拡張
ConcreteComponent1
AdditionalConcreteComponent
ConcreteComponent2
AdditionalConcreteComponent
// 委譲による機能拡張
ConcreteComponent1
ConcreteDecorator
ConcreteComponent2
ConcreteDecorator
サンプルプログラムは、CSV, XML から DOM を構築するパーサに、
Decorator でソートする機能を追加しています。
出力結果:
// CSV -> URL でソート
root: 
  entry: name: bing URL: http://www.bing.com/ 
  entry: name: Google URL: http://www.google.co.jp/ 
  entry: name: Yahoo URL: http://www.yahoo.co.jp/ 
// XML -> name でソート
root: 
  entry: name: apple price: 100 
  entry: name: banana price: 200 
  entry: name: orange price: 50 
package design_pattern;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintStream;
import java.io.Reader;
import java.io.StringReader;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Decorator
 * GoF, Code Complete
 * オブジェクトに動的に機能を追加します。継承より柔軟な機能拡張を提供します。
 * 
 * 出力結果:
 * <pre>
 * // CSV -> URL でソート
 * root: 
 *   entry: name: bing URL: http://www.bing.com/ 
 *   entry: name: Google URL: http://www.google.co.jp/ 
 *   entry: name: Yahoo URL: http://www.yahoo.co.jp/ 
 * // XML -> name でソート
 * root: 
 *   entry: name: apple price: 100 
 *   entry: name: banana price: 200 
 *   entry: name: orange price: 50 
 * </pre>
 * 
 * @author 2015/05/26 matsushima
 */
public class DecoratorSample {
	/**
	 * main
	 * 
	 * @param args
	 */
	public static void main(String[] args) {
		try {
			new DecoratorSample().client();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	/**
	 * Client
	 */
	void client() throws IOException {
		String csvText =
				"name,URL\n"
				+ "Google,http://www.google.co.jp/\n"
				+ "Yahoo,http://www.yahoo.co.jp/\n"
				+ "bing,http://www.bing.com/\n"
				;
		String xmlText =
				"<entry><name>apple</name><price>100</price></entry>"
				+ "<entry><name>orange</name><price>50</price></entry>"
				+ "<entry><name>banana</name><price>200</price></entry>"
				;
		System.out.println("// CSV -> URL でソート");
		Parser_Component c1 = new NameSortedParser_ConcreteDecorator(
				new CsvParser_ConcreteComponent(), "URL");
		Node n1 = c1.parse(new StringReader(csvText));
		printNode(System.out, n1, "");
		System.out.println("// XML -> name でソート");
		Parser_Component c2 = new NameSortedParser_ConcreteDecorator(
				new XmlParser_ConcreteComponent(), "name");
		Node n2 = c2.parse(new StringReader(xmlText));
		printNode(System.out, n2, "");
	}
	/**
	 * ノードを出力。
	 */
	void printNode(PrintStream writer, Node node, String indent) {
		if (node.getChildNodes().isEmpty()) {
			writer.print(node.getText() + " ");
		} else {
			String r2 = (2 == indent.length() ? "\n" + indent : indent.trim());
			String r0 = (0 == indent.length() ? "\n" : "");
			writer.print(r2 + node.getText() + ": ");
			for (Node child: node.getChildNodes()) {
				printNode(writer, child, indent + "  ");
			}
			writer.print(r0);
		}
	}
	
	/** ノード。 */
	public class Node {
		private String text;
		private LinkedList<Node> children = new LinkedList<>();
		public Node(String text) { this.text = text; }
		public String getText() { return this.text; }
		public Node appendChild(Node node) { this.children.add(node); return node; }
		public List<Node> getChildNodes() { return this.children; }
	}
	
	// Component ///////////////////////////////////////
	/** パーサのインターフェース。 */
	public interface Parser_Component {
		public Node parse(Reader in) throws IOException;
	}
	/** CSV パーサ。 */
	public class CsvParser_ConcreteComponent implements Parser_Component {
		public Node parse(Reader in) throws IOException {
			Node root = new Node("root");
			String[] names = null;
			BufferedReader reader = new BufferedReader(in);
			for (String line; null != (line = reader.readLine()); ) {
				String[] cols = line.split(",");
				if (null == names) {
					names = cols;
				} else {
					Node entry = root.appendChild(new Node("entry"));
					for (int i = 0; i < Math.min(names.length, cols.length); ++ i) {
						entry.appendChild(new Node(names[i]))
								.appendChild(new Node(cols[i]));
					}
				}
			}
			return root;
		}
	}
	/** XML パーサ。 */
	public class XmlParser_ConcreteComponent implements Parser_Component {
		private final Pattern p1 = Pattern.compile("\\s*<\\s*([^>/]*)\\s*>\\s*");
		private final Pattern p2 = Pattern.compile("\\s*</\\s*([^>]*)\\s*>\\s*");
		public Node parse(Reader in) throws IOException {
			// 入力データ読み込み
			StringBuilder sb = new StringBuilder();
			char[] buf = new char[1024];
			for (int read; -1 != (read = in.read(buf)); ) {
				sb.append(buf, 0, read);
			}
			// ノードごとのパース処理
			Node root = new Node("root");
			this.parseImpl(root, sb, 0, p1.matcher(sb), p2.matcher(sb));
			return root;
		}
		private int parseImpl(Node node, CharSequence text, int index, Matcher m1, Matcher m2) {
			while (index < text.length()) {
				int i1 = m1.find(index) ? m1.start() : text.length();
				int i2 = m2.find(index) ? m2.start() : text.length();
				if (i1 < i2) { // element
					Node childElement = new Node(m1.group(1));
					node.appendChild(childElement);
					index = this.parseImpl(childElement, text, m1.end(), m1, m2);
				} else { // text
					String value = text.subSequence(index, i2).toString().trim();
					if (!value.isEmpty()) {
						node.appendChild(new Node(value));
					}
					index = i2 < text.length() ? m2.end() : text.length();
					break;
				}
			}
			return index;
		}
	}

	// Decorator ///////////////////////////////////////
	/** ソートするパーサのデコレータの抽象クラス。 */
	public abstract class SortedParser_Decorator implements Parser_Component {
		protected Parser_Component parser;
		protected SortedParser_Decorator(Parser_Component original) {
			this.parser = original;
		}
	}
	/** 項目でソートするパーサのデコレータ。 */
	public class NameSortedParser_ConcreteDecorator extends SortedParser_Decorator {
		private String sortKey;
		protected NameSortedParser_ConcreteDecorator(Parser_Component original, String sortKey) {
			super(original);
			this.sortKey = sortKey;
		}
		public Node parse(Reader in) throws IOException {
			Node node = this.parser.parse(in);
			// ソートする
			Collections.sort(node.getChildNodes(), new Comparator<Node>() {
				public int compare(Node o1, Node o2) {
					return findText(o1, sortKey).compareTo(findText(o2, sortKey));
				}
			});
			return node;
		}
		private String findText(Node node, String name) {
			if (null != node.getChildNodes()) {
				if (name.equals(node.getText())) {
					return node.getChildNodes().get(0).getText();
				}
				for (Node child: node.getChildNodes()) {
					String text = findText(child, name);
					if (null != text) {
						return text;
					}
				}
			}
			return null;
		}
	}
}

0 件のコメント:

コメントを投稿