27-死ぬはずのプログラムを無理に生かしておいてはいけない

プログラマが知るべき97のこと」の27個目のエピソードは、例外処理とアプリケーションの続行に関する話です。このエピソードでは、例外に対するアプローチについて安直な例外の握りつぶしは避けるように書かれています。

try-catchブロックをコードベースに大量に入れれば、「例外が発生しても絶対に止まらない」というアプリケーションを作る事が可能なはずです。ただ、これは、もう死んでいる人の体を釘か何かで固定し、無理矢理立った状態にしているようなものですが…。

このエピソードにあるような経験は、誰もがあるのではないでしょうか?自分も何度も経験しました。明らかに例外の握りつぶしが行われているコードはたくさんあります。メンテナンス時には何とか正常な例外処理に修正したい気持ちになりますが、そのコードが既にリリース済みの場合はまず変更することができません。例外を正しく投げるように修正した瞬間、アプリケーションが動かなくなるのです。仕方ないのでログにだけは吐こうとするでしょう。しかし、開発現場によっては「警告メッセージがログに出た、どういうことだ、バグを入れたな、修正しろ!」とプロジェクトマネージャ*1が怒り心頭になるのです。本来はバグとして適切なハンドリングが必要な部分に、修正はできないからせめて情報だけでも・・・とアプリケーションの品質をあげようとして徒労に終わります。当然のようにモチベーションはがた落ちです。
では、どうしてそんな状況になるのでしょうか?
原因は大きく2つあると思います。1つは例外=バグ=悪という単純な思考パターンで、特にプロジェクトマネージャ*2が考えているケースです。例えば、HDDのエラーについて一般的な業務アプリケーションのソフトウェアで考慮することはないでしょう。そのレベルのエラーまで考慮していたら予算内で作れるわけないからです。しかし、ファイルの入出力ではHDD障害に起因する例外が発生する可能性はあります。これはソフトウェアのバグではありません。クライアントからすればシステムの不具合には変わりませんが、システムがハードウェア障害に完全な対応が必要だったなら、別の方法で対応するべきでしょう。そのようなフローを全く引かずに、ソフトウェアのバグとして責任を押しつけられる傾向があります。そんな業界の空気から、プロジェクトマネージャは「絶対にバグを出すな、例外なんか出したらただじゃおかんぞ」となるのです。
2つ目の原因は、実装者の意識とスキルが足りないことです。プログラマであれば、例外について学び、基礎的な事は知っているはずです。そして、「例外を握りつぶしてはいけない」ということも聞いたことはあるはずです。しかし、悲しいことに現実の開発現場では「例外は握りつぶせ」と教える先輩PGもいます。以前、上司であるPGと例外について話したことがありますが、このエピソードにある話と全く同じ展開と結論でした。

したがって、プロジェクトマネージャも例外についてテストと共に理解を深める必要があります。スケジューリングや見積もりも重要ですが、品質について最終的な責任を負うのはプロジェクトマネージャでしょう。ソフトウェアがどのように作られ、障害やバグについてどんなパターンがあるかは知っていて当然です。
プログラマにとって、例外処理は大きな目安になるスキルです。正常系の処理は簡単な一方で、準正常系・異常系の処理は複雑で、様々な状況を予測する能力が必要です。コードについても、正常系の処理は数行に対して準星情景・異常系の処理が数十行というケースもありえる話です。テストについて言えば、正常系1つに対して準正常系・異常系は複数あるのが自然です。さらに厄介なのが準正常系・異常系については重要度があり、どれが重要かを判断する必要があります。そして、それらをプログラマはコードに効率良く書く必要がありますが、正解はありません。
プログラマとして成長するために例外処理とテストは欠かせないスキルです。その為には、たくさんのコードを読み、書いて学ぶ必要があるのです。

プログラマが知るべき97のこと

プログラマが知るべき97のこと

*1:自称だと思います

*2:繰り返しますが、自称です

Slim3 pluginでScenic3の使い方

Slim3には簡単にプロジェクトを作成する為に使えるEclipse pluginがあります。@tomotaro1065 さんが中心になって作られていますが、ご厚意でScenic3の対応もしていただいています。ですが、自分で使ってみて使い方が解らないのではないか?と気付きました。そこで、Scenic3の仕組みを含めてチュートリアルを書く事にします。内容は後でドキュメントに反映させるつもりです。

Scenic3ってなにさ?

scenic3は t2 frameworkのようなPageクラスをslim3で実現するslim3の拡張ライブラリです。

Scenic3はSlim3を薄くラップしたライブラリで、Slim3の設計思想である「"Simple" and "Less Is More"」を踏襲しつつ、1つのPageクラスに複数のアクションメソッドを記述できるようになります。spin-upへの影響は最小限になるようにデザインされています。

何が嬉しいの?

Slim3ではリクエストのパスからControllerを動的に生成するアプローチを採用しています。これはGAEというプラットフォーム上では最善の方法の1つです。しかし、リクエストパスの数だけコントローラクラスを生成しなければならないという事になります。クラスの責務としては1コントローラ=1クラスとする事は1つの選択です。しかし、あるモデルのCRUDに対応するアクションを作るならば、同じクラスに記述してまとめるのも1つの選択肢です。Scenic3では後者の設計を採用する場合に、効果的な方法を提供します。

プロジェクトの作成

New - Project - Slim3 Projectと進んだ後、プロジェクトの種別から「Use MVC of Slim3 with Scenic3」を選択します。通常のSlim3プロジェクトと同様にProject NameとRoot Packageを入力してください。

Slim3プロジェクトとの違い

Slim3プロジェクトとの違いは次の3点です。

  • scenic3-x.x.x.jar
  • Java CompilerのAnnotation Processingでのscenic3の登録
  • web.xml

自動生成されたプロジェクトの雛形には作成するページクラスの雛形としてFrontPageが生成されています。

scenic3-x.x.x.jar

scenic3-x.x.x.jarはScenic3が必要とする唯一のライブラリです。実行時に必要となりますので、WEB-INF/libに配置されています。

Java CompilerのAnnotation Processingでのscenic3の登録

APTを行う為にscenic3-x.x.x.jarが、Java CompilerのAnnotation ProcessingのFactory Pathとして登録されています。APTはコンパイル時に幾つかのクラスを生成するために使用されます。また、Slim3のルートパッケージをScenic3に伝えるために、slim3.rootPackageというパラメータがOptionで指定されています。

web.xml

FrontControllerを差し替える必要があるため、FrontControllerがorg.slim3.controller.ScenicFrontControllerに変更されています。プロジェクトで固有の拡張を行う場合は、ScenicFrontControllerのサブクラスを指定してください。

    <filter>
        <filter-name>FrontController</filter-name>
        <filter-class>org.slim3.controller.ScenicFrontController</filter-class>
    </filter>

Pageクラス

Pageクラスはアノテーションを付与した ScenicPageクラスのサブクラスです。
Scenic3を使う場合はControllerのサブクラスではなくScenic3のサブクラスとなりますが、runメソッドの中身についてはほとんど同じように記述できます。したがって、Slim3を使ったコントローラを作る事とScenic3を使ったPageクラスを作る事で覚えることに違いはほとんどありません。
現在のプラグインでは、Pageクラスはルートパッケージ直下に配置されていますが、自分の場合はpageパッケージを作成して配置しています。Pageクラスはどのパッケージにあっても構いません。

自動生成されたPageクラス

package example.scenic3;

import org.slim3.controller.Navigation;

import scenic3.ScenicPage;
import scenic3.annotation.ActionPath;
import scenic3.annotation.Default;
import scenic3.annotation.Page;
import scenic3.annotation.Var;

@Page("/")
public class FrontPage extends ScenicPage {
    // /view/100  /view/200
    @ActionPath("view/{id}")
    public Navigation view(@Var("id") String id) {
        super.request.setAttribute("id", id);
        return forward("/view.jsp");
    }

    // /
    @Default
    public Navigation index() {
        return forward("/index.jsp");
    }
}
Page, ActionPathアノテーション

実行時に選択されるPageクラスとActionメソッドは、Pageクラスに記述されたPageアノテーションとActionPathに記述されたパスで決まります。サンプルではルートパスの下にあり、viewで始まるアクションはviewメソッドが実行され、それ以外の全てのパスはindexが実行されます。また、{id}という書式を使う事で、パスに含まれるパラメータをキャプチャする事が可能です。
Slim3ではRooting機能を使って実現しますが、Scenic3では直感的な記述で、かつメソッドのパラメータとして受け取ることが出来るので便利になっています。同様にリクエストパラメータやHttpServletRequestが欲しい場合は、メソッドの引数に記述するだけでOKです。
詳細はドキュメントを確認してください。

仕組み

Scenic3ではPageクラスからAPTでControllerクラスを生成します。
生成されたControllerクラスは直ぐに確認できますが、単純にPageクラスのメソッドに処理を委譲しているに過ぎません。
次のようなControllerクラスは、メソッド毎に生成されます。

// Controller for example.scenic3.FrontPage#view
// @javax.annotation.Generated
public final class _view_id extends scenic3.ScenicController {

    private final example.scenic3.FrontPage page;

    public _view_id() {
        this.page = new example.scenic3.FrontPage();
    }

    @Override
    public final org.slim3.controller.Navigation run() throws Exception {
        setupPage(page);
        return page.view(super.var("id"));
    }
    // 以下略
}

しかし、全てのパターンをspin-up時に解析してしまうとパフォーマンス上の問題が発生します。
そこで、パターンマッチングに関しては別のクラスを用意するアプローチをとっています。
そのマッチングクラスを登録するのが次で説明するMatcherとAppUrlsです。

Matcher

Matcherは、リクエストのパスから自動生成されたControllerを選択するためのクラスで、Pageクラス毎に生成されます。

// @javax.annotation.Generated
public class FrontPageMatcher extends scenic3.UrlMatcherImpl {
    // 中略
    // Constractor.
    private FrontPageMatcher() {
        super("/");
        super.add(new scenic3.UrlPattern("/", "view/{id}"), "example.scenic3.controller._view_id");
        super.add(new scenic3.UrlPattern("/", ""), "example.scenic3.controller.$Index");
    }
}

このようにリクエストパスのマッチングパターンを生成し、対応するクラスの名前を文字列で保持します。
クラス名で保持しないのはクラスローディングを極力遅らせるためです。

AppUrls

AppUrlsは、Matcherを複数持つコンテナです。
このクラスは自動生成しない為、新しいPageを作成したら手動でMatcherを登録する必要があります。

import scenic3.UrlsImpl;
import example.scenic3.controller.matcher.FrontPageMatcher;

public class AppUrls extends UrlsImpl {

    public AppUrls() {
        excludes("/css/*");
        add(FrontPageMatcher.get());
        // TODO Add your own new PageMatcher
    }
}

理由としては除外パスの設定はユーザ毎に行う為と、マッチングを賭ける順序はプログラマで制御する方が良いからです。自分でもたまに登録を忘れますのでご注意ください。

Test

Scenic3ではテストもページクラス単位で可能です。

import scenic3.tester.PageTestCase;

public class FrontPageTest extends PageTestCase {

    @Test
    public void index() throws Exception {
        tester.start("/");
        assertThat(tester.getActionMethodName(), is("index"));
        assertThat(tester.getDestinationPath(), is("index.jsp"));
    }
}

PageTestCaseを使用するだけで、書き方はほとんど変わりません。

影響を受けたフレームワークとか

PageとActionPathのアイディアはT2フレームワークそのものです。開発の経緯としてはT2フレームワークをGAE上で使っている内にパフォーマンスの問題に直面し、「ならば最適に作り直そう」という流れです。APTで自動生成し、動的に生成しないポリシーは、Slim3から受け継いでいます。URLのマッチングの仕組みはDjangoからヒントを得ています。マッチングの仕組みについてはScalaからヒントを得ました。

最後に

Slim3にScenic3を組み合わせることで、コードの見通しがぐっと良くなります。
Controllerは自動生成されるため、メソッド名を変えても直ぐに反映されるでしょう。
パスとコントローラクラスが1:1の場合に発生するリファクタリングのやりにくさとコントローラクラスの爆発を避けることが可能です。勿論,設計ポリシー的にはControllerを作る方が良いのでケースバイケースで利用を検討してみてください。少なくともScenic3を使う為に必要な知識は今回のエントリーで紹介した事くらいです。

尚、さらにJSPも使いたくないよという人にはpirkaengineも組み合わせることがオススメです。こちらについてはまた別の機会に書こうと思います。また、pirka-mobileを組み合わせるとガラケー向けのサイトがGAEでサクサクかけるようになる、というのを1つの目標としています。