2008年5月16日金曜日

Springフレームワークの続き(画面追加~挫折)

Springフレームワークのチュートリアルをベースに少し機能を追加してみます。
(注意:最後の一歩で頓挫しています。)
(5/23追記:Springフレームワークの続き(リベンジ)で一応成功しました)

チュートリアルでは価格を割増しするだけのビジネスロジックを作成しましたが、初期の価格を設定するロジックを追加します。
すでに存在するプロダクトのリストを取得してフォームとして画面に表示し、編集/入力された新規の価格をDBに反映するロジックです。

二つの画面が増えることになります。
1.最初の画面から変更対象選択画面への遷移
 編集画面に現在登録されているプロダクトを表示。
 変更対象選択画面では、チェックボックスで変更対象を選択することができます。
2.価格設定画面へ遷移
 '1'で選択された項目が表示されます。価格を設定することができます。

【手順概要】
Productsインターフェースに以下のメソッドを追加します。
public void setPrice(int id, int price);

SimpleProductManagerでsetPrice(int id, int price)を実装します。

SimpleProductManagerTestでsetPriceのテストを行います。

変更対象選択用Bean(SelectProduct)を追加します。

SelectInventoryコントローラを追加します。

変更対象選択画面を追加します。

springapp-servlet.xmlにエントリーを追加します。

メッセージを追加します。

リンクを追加します。

価格設定用Bean(SetPrice)を追加します。

バリデータ(SetPriceValidator)を追加します。

SetPriceFormControllerを追加します。

価格設定用画面を追加します。

springapp-servlet.xmlにエントリーを追加します。

【手順詳細】
Productsインターフェースに以下を追加します。
public void setPrice(int id, int price);

SimpleProductManagerを実装します。
 public void setPrice(int id, double price) {
List<Product> products = productDao.getProductList();
if (products != null) {
for (Product product : products) {
if (product.getId() == id) {
product.setPrice(price);
productDao.saveProduct(product);
break;
}
}
}
}

次にSimpleProductManagerTestです。
    private static int CHAIR_ID = 1;
private static int TABLE_ID = 2;
private static int POSITIVE_PRICE_INCREASE = 10;
private static int NEGATIVE_PRICE_INCREASE = 10;
(中略)
public void testSetPrice() {
productManager.setPrice(TABLE_ID, TABLE_PRICE_UPDATED);
productManager.setPrice(CHAIR_ID, CHAIR_PRICE_UPDATED);

List<Product> products = productManager.getProducts();

for (Product product: products) {
if (product.getId() == TABLE_ID) {
assertEquals(TABLE_PRICE_UPDATED, product.getPrice());
} else if (product.getId() == CHAIR_ID) {
assertEquals(CHAIR_PRICE_UPDATED, product.getPrice());
}
}
}

次に変更対象選択用Bean(SelectProduct)を追加します。
以下のサイトを参考にしました。LazyListは使っていません。(LazyListでは動きませんでした。なぜかは未調査)
<a href="http://mattfleming.com/node/134">Dynamic list binding in Spring MVC</a>
package springapp.service;

import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.domain.Product;

public class SelectProduct {
/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());
//logger.info("Descriptions set to "+strs);
private List<Product> products = null;
/*
LazyList.decorate(new ArrayList(),
FactoryUtils.instantiateFactory(Product.class));
*/

public List<Product> getProducts() {
return products;
}
public void setProducts(List<Product> products) {
this.products = products;
}
}

SelectInventoryFormControllerを追加します。
package springapp.web;

import org.springframework.web.servlet.mvc.SimpleFormController;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.service.ProductManager;
import springapp.service.SelectProduct;


public class SelectInventoryFormController extends SimpleFormController {
/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());

private ProductManager productManager;

protected Object formBackingObject(HttpServletRequest request) throws ServletException {
SelectProduct selectProduct = new SelectProduct();
logger.info("productManager:"+productManager.getProducts().size());
selectProduct.setProducts(productManager.getProducts());
logger.info("selectProd:"+selectProduct.getProducts().size());
return selectProduct;
}
public ModelAndView onSubmit(Object command)
throws ServletException {

logger.info("Executed.");

return new ModelAndView(new RedirectView(getSuccessView()));
}

public void setProductManager(ProductManager productManager) {
this.productManager = productManager;
}

public ProductManager getProductManager() {
return productManager;
}
}

変更対象選択画面(selectproduct.htm)を追加します。
<%@ include file="/WEB-INF/jsp/include.jsp" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<html>
<head><title><fmt:message key="title"/></title></head>
<body>
<h1><fmt:message key="selectproduct.heading"/></h1>
<h3>Products</h3>
<form:form method="post" commandName="setPrice">
<c:forEach items="${selectProduct.products}" var="prod" varStatus="row">
<input type="radio" id="id" name="id" value="<c:out value="${prod.id}"/>">
<c:out value="${prod.description}"/>
<i>$<c:out value="${prod.price}"/></i><br><br>
</c:forEach>
<input type="submit" align="center" value="Execute">
</form:form>
<br>
<a href="<c:url value="hello.htm"/>">back</a><br>
<br>
</body>
</html>

springapp-servlet.xmlにエントリーを追加します。
    <bean name="/selectproduct.htm" class="springapp.web.SelectInventoryFormController">
<property name="sessionForm" value="true"/>
<property name="commandName" value="selectProduct"/>
<property name="commandClass" value="springapp.service.SelectProduct"/>
<property name="formView" value="selectproduct"/>
<property name="successView" value="setprice.htm"/>
<property name="productManager" ref="productManager"/>
</bean>

メッセージを追加します。
selectproduct.heading=Select Product :: SpringApp

hello.jspにリンクを追加します。
<a href="<c:url value="selectproduct.htm"/>">Select product</a>

価格設定用Bean(SetPrice)を追加します。
package springapp.service;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class SetPrice {
/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());
private int id;
private String description;
private double price;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
logger.info("id:"+id+" set.");
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
logger.info("description:"+description+" set.");
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
logger.info("description:"+description+" set.");
}

}

バリデータ(SetPriceValidator)を追加します。
package springapp.service;

import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class SetPriceValidator implements Validator {

private int DEFAULT_MIN_PRICE = 1;
private int minPrice = DEFAULT_MIN_PRICE;

/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());

public boolean supports(Class clazz) {
return SetPrice.class.equals(clazz);
}

public void validate(Object obj, Errors errors) {
SetPrice ps = (SetPrice) obj;
if (ps == null) {
errors.rejectValue("price", "error.not-specified", null, "Value required.");
}
else {
logger.info("Validating with " + ps + ": " + ps.getPrice());
double price = ps.getPrice();
if (price <= minPrice) {
errors.rejectValue("price", "error.too-cheape",
new Object[] {new Integer(minPrice)}, "Value too low.");
}
}
}

public void setMinPrice(int i) {
minPrice = i;
}

public int getMinPercentage() {
return minPrice;
}

public void setMaxPercentage(int i) {
minPrice = i;
}

public int getMaxPercentage() {
return minPrice;
}

}

SetPriceFormControllerを追加します。
package springapp.web;

import java.util.Enumeration;

import org.springframework.beans.factory.parsing.ParseState.Entry;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.servlet.mvc.SimpleFormController;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.domain.Product;
import springapp.service.ProductManager;
import springapp.service.PriceIncrease;
import springapp.service.SetPrice;


public class SetPriceFormController extends SimpleFormController {
/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());

private ProductManager productManager;

public ModelAndView onSubmit(Object command)
throws ServletException {
int id = ((SetPrice) command).getId();
double price = ((SetPrice) command).getPrice();

productManager.setPrice(id, price);
logger.info(
"Update prices by " + price+ ". Id is "+id+"." );
logger.info(
"returning from PriceIncreaseForm view to " + getSuccessView());

return new ModelAndView(new RedirectView(getSuccessView()));
}

protected Object formBackingObject(HttpServletRequest request) throws ServletException {
// 結局ここで挫折しました。以下は挫折の痕跡です。
/*
SetPrice ret = new SetPrice();
Enumeration<String> enume = request.getAttributeNames();
while(enume.hasMoreElements()) {
String key = enume.nextElement();
logger.info(key+":"+request.getAttribute(key));
}
*/

logger.info(getFormSessionAttributeName(request));

String str = ServletRequestUtils
.getStringParameter(request, "id");

SetPrice ret = new SetPrice();
// 最後は決め打ちのハードコード(泣)
int i = 1;
for (Product product : productManager.getProducts()) {
if (product.getId() == i) {
ret.setId(product.getId());
ret.setDescription(product.getDescription());
ret.setPrice(product.getPrice());
break;
}
}
return ret;
}

public void setProductManager(ProductManager productManager) {
this.productManager = productManager;
}

public ProductManager getProductManager() {
return productManager;
}

}

価格設定用画面(setprice.jsp)を追加します。
<%@ include file="/WEB-INF/jsp/include.jsp" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<html>
<head>
<title><fmt:message key="title"/></title>
<style>
.error { color: red; }
</style>
</head>
<body>
<h1><fmt:message key="setprice.heading"/></h1>
<form:form method="post" commandName="setPrice">
<c:out value="${setPrice.description}"/>
<form:input path="price"/>
<form:errors path="price" cssClass="error"/>
<br>
<input type="submit" align="center" value="Execute">
</form:form>
<a href="<c:url value="hello.htm"/>">Home</a>
</body>
</html>


springapp-servlet.xmlにエントリーを追加します。
    <bean name="/setprice.htm" class="springapp.web.SetPriceFormController">
<property name="sessionForm" value="true"/>
<property name="commandName" value="setPrice"/>
<property name="commandClass" value="springapp.service.SetPrice"/>
<property name="validator">
<bean class="springapp.service.SetPriceValidator"/>
</property>
<property name="formView" value="setprice"/>
<property name="successView" value="hello.htm"/>
<property name="productManager" ref="productManager"/>
</bean>


以上ですが、残念ながら途中で頓挫してしまいました。

DAOと連携したDBの更新は成功するのですが(IDハードコードで確認済み)
一覧から更新対象を選択し、次の画面にIDを渡すことがどうしても出来ませんでした。
formBackingObject(HttpServletRequest request)で、requestからradioの値を引っ張ることができません。悔しいのですが二日間チャレンジして出来なかったので、これで諦めます。

#なんで "String str = ServletRequestUtils.getStringParameter(request, "id");"でPOSTされたidが取れないんだ!!!

(5/23追記)SimpleFormControllerはrequestをクリアしてしまうようですね。
"id"は前の画面のFormにちゃんと入っていました。これを生のHttpSessionに登録し、次画面で受け取ることができます。Spring的にキレイではないとは思いますが。

以上。

2008年5月14日水曜日

「同じ」と「違う」(3)~「同じ」から「似ている」へ

対象を事物に絞れば「同じ」モノは存在しない、と言いました。

違和感ありますよね。「同じ」という言葉は非常にリアリティを持っている。実は同じものが存在しないことを認めると、人間にとって世界の根本的な成り立ちが怪しくなるのです。

確かに、「同じ」ものが存在するような気がする。では何が「同じ」なのでしょうか。

養老猛司氏は、テープに録音された音声としてのテキストと文書に書かれたテキストが同じであるという例を上げ、「同じ」なのは情報である。と述べました。情報それ自体は媒体に関係なく「同じ」である、と。

氏の意見に同意してもよいのですが、私としてはもう少し踏みたい。
「情報」という言葉にすると「同じ」かどうかは怪しいと思うからです。理由は簡単。例えば(何でもいいのですが)夏目漱石の小説を読んだとします。私が読むのと他の人が読むのとでは当然感想が違います。確かに文字の並びは同じといってよいでしょう。しかし、同じ本を読んだとしても同じものを見ていることにはならないのです。あるいは聖書。キリスト教者が読むのとそうではない人が読むのとでは、全く受け止め方が違ってくるでしょう。

私は、同じなのは「記号の配列」であると思います。厳密に言えば。

でも情報だって同じだろう。翻訳されたら記号の配列は変るが、相対性理論は日本でもアメリカでも同じ相対性理論だ。その通りです。しかし、この議論の前に私は「同じ」という言葉をもう少し緩めて「似ている」という範囲に広げたいと思っています。

アメリカの相対性理論も日本の相対性理論も、究極的には同じだと思います。

しかし、やはり人によって理解のレベルも着眼点も解釈も異なるはずです。ですから現実的には「相対性理論は相対性理論である」というよりも、人の解釈が「似ている」から「限りなく同じ」のグラデーションのどこかに分布する、というのが妥当だと思います。私と現役の科学者が相対性理論について語り合ったって、どこにもたどり着かないでしょうから。

次に「似ている」について、もう少し踏み込んでみましょう。

(続く)

2008年5月13日火曜日

「同じ」と「違う」(2)~「同じ」は存在しない

自分は自分である
ゴキブリはゴキブリだ

以上は「AはAである」という構造を持っています。言い換えればAとAは同じだ、ということです。

では

自分は自分ではない
ゴキブリはゴキブリではない

はどうでしょう。AとAが違う、ということでこれはほとんど論理的には矛盾していますね。

以上より、A=Aは真だし、A≠Aは偽だということが一般に言えます。

(何をつまらんことを言っているんだ、という声が聞こえてきそうです。もう少々お付き合い下さい)

しかし、実はここに大きな問題があるのです。

具体的に考えて見ましょう。具体的に、というのは実際に存在する事物についてです。ここでは事物を空間的に位置を占め、重さを持つものとします。

すると、どうなるか。
目の前に缶コーヒーが二本あるとします。同じメーカーの、同じ種類の缶コーヒーです。同じ自動販売機で、午前と午後に一本ずつ購入したものです。
この二本の缶コーヒーは「同じ」なのでしょうか。

ブランドが同じ。種類が同じ。価格が同じ。同じに決まっている。

果たしてそうでしょうか。
「同じ」としている主語に注目して下さい。「同じ」としているのは缶コーヒーではありませんね。すべて缶コーヒーの言わば属性とも言うべきものです。缶コーヒーそれ自体ではない。もう少し言えばその属性も先ほど定義した「事物」ではない。

缶コーヒーそれぞれは、同じではないのです。なぜなら空間的に別の位置を占めているからです。座標が違うものは同一ではありえません。つまり、世の中に存在する事物に、同じ物は一つとして存在しないのです。

「同じ」は実体=事物的な存在ではありません。「同じ」とは価値評価に過ぎません。

言い換えれば「同じ」は人間の頭に存在する概念でしかありません。

ところがあたりを見れば「同じ」事物があふれているように見える。それは頭が周りの事物を「同じだ」「同じだ」と強制的にラベリングしているからなのです。

(続く)

東京三菱のシステム障害に思うこと

原因はカタカナで転送すべきだったデータを漢字で送っていたためだったそうです。

早速「テストが甘いかったんじゃないか」などという結果だけを見て偉そうなことを言うひとがちらほら出てきています。この手の原因と結果を転倒させる人は本当に始末に悪いですね。

確かにテストが甘かったのは事実でしょう。外部システムとの正常系テスト(記帳の回数オーバーのハンドリングはほとんど正常系です)をしていれば、事前に発見できたはずですから。しかし、部外者がそのレベルのことをしたり顔で言うのは、プロ野球の監督の采配に文句をつける酔っ払いオヤジとかわりません。

安易に時系列(原因⇒結果)を逆転させてはなりません。なぜなら、それは安直な分析につながるからです。今にしてみればあまりに自明だったことが、必ずしも現在自明であるとは限りません。むしろ原因と結果という図式を単純に信奉して「ああすればこうなる」式に(これも養老節ですな)手を打つ方が危険です。原因と結果という図式は、本来は不可能であるはずの未来の予測を、いとも容易に見せてしまうまやかしの図式でもあるからです。

送信データミスが原因で、結果として障害が発生した、という解釈は正しいです。それを逆転して「送信データミスがなければ障害は起こらなかったのに」と考えるのも、まあ妥当な感想でしょう。でも、そこから「テストが甘かった。私の成功体験では・・・」と断ずるのは早計です。

システム構築中は、取り返しのつかない意思決定の連続です。そして意思決定には常にリスクが伴う。人間ですからどこかで誤ることは十分ありえるからです。そのリスクをいかに取るかが大事なのです。

今回について言えば(結果を知っている部外者からは)明らかに必須と思えるテストの実施がなぜ漏れたのかが問題です。
外部システムとのインターフェースがある以上、結合テストはほとんど必然です。漏れようがない。それから今回の問題は正常系のテストでカバーできるはずです。異常系・障害系のテストなら時間との兼ね合いで割り切る判断もあるかもしれませんが、正常系のテストが行われなかった理由はほとんど謎です。

私には以下の要因が思い当たります(どれも当たり前のことですが・・・)。どちらも「ピーク6000人を投入した」「失敗が絶対に許されない」と言われるからこそ、気になる点です。

1.作業に優先順位が定められていたか
2.現場レベルのエンジニアに無用なプレッシャーが掛かっていなかったか

人が多くても、適切にリソースが配分されていなければ意味がありません。その人員は、何の仕事をしていたのでしょうか。システムに無理解な上層部(残念ながら昨今はリーダークラスにも多いですね)の思いつきに対応するために使われたのではないでしょうか?本当に現場に必要とされるタスクに、回されたのでしょうか。そもそも、何が重要か、適切に判断されていたのでしょうか?

それから「失敗は許されない」というスローガンは、前にも書きましたが実に百害あって一利なしなのです。このスローガンによって引き起こされる副作用には、恐らくは瑣末事大主義と優先順位付けの不能でしょう。失敗をできるだけ起こさないためには、リスクをきちんと評価して、重要な作業を優先しなければなりません。しかし「失敗は許されない」無思考に陥ると、作業の割り切りやリスクテイクが出来なくなるのです。

今回発生した障害の裏に何があったのかは知りませんが、同じエンジニアとしてはこれ以上何事も起こらないことを祈っています。

2008年5月12日月曜日

「同じ」と「違う」(1)

埴谷雄高氏の「死霊」に出てくる有名な言葉に「自同律の不快」というのがあります。私はこの言葉を、カントの「同一律」と物自体、ニーチェの力への意志、それから養老猛司の「同じ」ってやつだな、うんうん、と勝手に解釈し、それなりに納得したつもりでいました。

ある日、もう半年以上前でしょうか、日経新聞の夕刊で「死霊」に関する記事を読みました。そこでは「自同律の不快」のことを『「私は私である」と言い切るのは不快だという感覚』であると解説していました。
さすが日経、浅はかな解釈であることよ、と鼻で笑っていたのですが、Webで見る限りどうも一定のお墨付きを得た解釈のようです。むしろ埴谷雄高氏がこの言葉を使って自ら語ったことがあるような様子すら見受けられました。(気楽なエッセーなので言質を取ったりしち面倒くさいことはしませんが)

改めて読めば、確かに大きく的を外した解釈ではなさそうです。「私は私だ」という経験上自明である言明に不快感を示す、一体どういうことだ?と興味を引きますし、少なくともA=Aが不快だ、と言うよりは分かり易い。

しかし私は文字通り「A=A」が不快である、という意味で「自同律の不快」という言葉を受け止めたいと思います。

「私が私である」はもちろん(養老猛司風に言えば)「虫は虫だ」というのも自同律です。A=Aであるという形式を元に世界を構成する理性の乱暴さ。それに対抗する「自同律の不快」は普遍的な共感を得ることができると思うのです。

(続く)

Springフレームワークを使ってみた

Springフレームワークに含まれる、以下のチュートリアル文書に従ってSpringフレームワークを試してみました。
spring-framework-2.5.3/docs/MVC-step-by-step/html_single/index.html

まだ第一印象レベルですが、その感想です。

■XML地獄は健在
Strutsと同様、SpringでもXML設定ファイル地獄が存在します。
単純に比較するのも酷ですが、多機能なだけSpringの方がとっつきにくいという感想です。

XDocletを使えば少しはマシになるのでしょうか。

■DAOはイケてる?
DAOは結構イケてる気がします。
Torque(だったかな?)よりも簡単そうですね。(チュートリアルが優れているせいかも?)

■Spring's JDBC framework
神経を使う connect / close をしないで済むのが素晴らしい。
WebSphere(v5以降?)ではUserTransactionを使うのですが、JNDIを使わないだけシンプルで良いと思います。(WASで同じことをやる場合、JNDIの登録作業が発生して面倒くさいです。WASの実装に依存するところもブラックボックス的で気に入りませんし)

■総評
目的は違うのでしょうが、Strutsよりマシかどうかは分かりません。
Springで提供している機能がバッチリ要件にハマれば採用の価値はあると思います。
しかし、それにしても相応の学習負荷と開発ルールの策定負荷があります。

例によって喧伝されている技術ではありますが、難解さ/取っ付きの悪さが目立ちます。

Strutsと同様Springそれ自体は優れたアプリケーションなので、デザインを勉強するにはよいと思います。

以上。