java LoginCommand username password データベース起動フラグ[0|1] |
それぞれのパッケージの指定は省きますが、実行イメージは以下のようになっています。
ユーザ/パスワードの正しい組み合わせがkent/beckの場合
> java LoginCommand kent beck 1 > ログインが成功しました |
> java LoginCommand kent back 1 > ユーザまたはパスワードが間違っています |
> java LoginCommand kunt beck 1 > ユーザまたはパスワードが間違っています |
> java LoginCommand kent beck 0 > システムの異常です。管理者にご相談ください |
ユースケース名 :ユーザログイン アクタ :ユーザ 事前条件 :なし 成功時保障 :ユーザがシステムにログインしている 主成功シナリオ : 1.システムがアクタにログインを要求する 2.アクタがユーザ名/パスワードを入力する 3.システムが入力されたユーザ名/パスワードを検証する 4.ログインの成功した旨を表示する 拡張: 3a.ユーザ名/パスワードが間違っている場合 3b.ユーザが存在しない場合 3c.データベースに接続できない場合 |
図1.サンプルの適当なクラス図
Database.startup(); …@データベースを起動します。 Database db = Database.connect(); …Aデータベースに接続します。 Record record = db.findByPrimaryKey(tablename, key); …Btablenameのテーブルをkeyで検索します |
@で偽データベースを起動します。
Aで、偽データベースに接続します。もし、偽データベースが起動していなかったらDatabaseNotFoundExceptionがスローされます。
Btablenameの表の主キー(key)検索を実行します。とはいっても偽なので、user表の偽データが詰まっています。user表にはkent/beck(user/password)というデータとmartin/fowler(同右)というデータが入っています。key=kentで検索したら、表の1レコードを表すRecordクラスのオブジェクトとして、kent/beckのデータが帰ってきます。user表のkentを検索して、ユーザ名とパスワードを取るイメージは例えば
Record record = db.findByPrimaryKey("user", "kent"); System.out.println("ユーザ名:" + record.getName()); System.out.println("パスワード:" + record.getPassword()); |
という感じになります。
まず、ここまでの情報で仕様を満たすようなコーディング(LoginCommandクラスとUserクラスの作成)を思いのままに実施してみましょう。
さて、できましたか?では、私の回答例を解説しましょう。左側が例外脳で右側が非例外脳です。
テスト付きソースコードもダウンロードできるようにしています。ちなみにWebの表示の都合上全角スペースなどを使用しているので、カットアンドペースト不可です。
例外脳 | 非例外脳 |
package oo; import db.*; /** * Userを表すドメインオブジェクト */ public class User { private User(){} /** * キーからUserオブジェクトを作成する * @param key 検索キー * @param db データベースオブジェクト * @return Userオブジェクト */ public static User getUser(String key, Database db) throws ObjectNotFoundException { return getUser(getRecord(key, db)); } /** * ログインするメソッド * @param name ユーザ名 * @param password パスワード * @param db Databaseクラスのオブジェクト * @return Userクラスのオブジェクト */ public static User login(String name, String password, Database db) throws LoginException { try { User user = getUser(name, db); if(user.getPassword().equals(password)) { return user; } else { throw new LoginException(); } } catch (ObjectNotFoundException e) { throw new LoginException(e); } } /** * 名前を返却する * @return 名前 */ public String getName() { return name; } /** * パスワードを返却する * @return パスワード */ public String getPassword() { return password; } private static Record getRecord(String key, Database db) throws ObjectNotFoundException { return db.findByPrimaryKey("user", key); } private static User getUser(Record record) { User user = new User(); user.name = record.getName(); user.password = record.getPassword(); return user; } private String name; private String password; } |
package nonoo; import db.*; /** * 例外脳なしなUserクラス **/ public class User { /** * ログイン成功 **/ public static int LOGIN_SUCCESS = 1; /** * ログイン失敗 **/ public static int LOGIN_FAIL = 2; // テスト用 User() {} /** * ユーザクラスを取得するファクトリ * @param name キーの名前 * @param db * @return nameをもったUserオブジェクト * ただし、該当するnameをもったUserがいない場合空のUserオブジェクトを返却 * し、データベースが接続できない場合はnullを返却する */ public static User getUser(String name, Database db) { if(db != null) { Record record = null; try { record = db.findByPrimaryKey("user", name); } catch (ObjectNotFoundException oe) { return new User(); } User user = new User(); user.name = record.getName(); user.password = record.getPassword(); return user; } else { return null; } } /** * ログインするメソッド * @param password パスワード * @return 戻り値 */ public int login(String password) { if(password != null) { if(password.equals(this.password)) { return LOGIN_SUCCESS; } } return LOGIN_FAIL; } /** * このオブジェクトが空か調べる * @return 空かどうか */ public boolean isEmpty() { if(name == null && password == null) { return true; } else { return false; } } private String name; private String password; } |
Userクラス | Userクラス |
package oo; import db.*; /** * 例外脳型のコーディング */ public class LoginCommand { public static void main(String[] args) { LoginCommand command = new LoginCommand(); command.execute(args[0], args[1], args[2]); } public void execute(String name, String password, String startupFlag) { // データベースが起動していない場合をサンプル用にシュミレート if("1".equals(startupFlag)) { Database.startup(); } try { Database db = Database.connect(); User user = User.login(name, password, db); System.out.println("ログイン成功しました"); } catch (DatabaseNotFoundException de) { System.out.println("システム管理者にご連絡ください"); } catch (LoginException le) { System.out.println("ユーザもしくはパスワードが間違っています"); } } } |
package nonoo; import db.*; /** * 非例外脳のコーディング */ public class LoginCommand { public static void main(String[] args) { LoginCommand command = new LoginCommand(); command.execute(args[0], args[1], args[2]); } public void execute(String name, String password, String startupFlag) { // データベースが起動していない場合をサンプル用にシュミレート if("1".equals(startupFlag)) { Database.startup(); } Database db = null; try { db = Database.connect(); } catch (DatabaseNotFoundException e) { // データベースが起動していないケース System.out.println("システム管理者にご連絡ください"); System.exit(0); } User user = User.getUser(name, db); if(user != null) { if(user.isEmpty()){ // ユーザが見つからないケース System.out.println("ユーザもしくはパスワードが間違っています"); } else { // 正常コース int result = user.login(password); if(result == User.LOGIN_SUCCESS) { // 正常コース System.out.println("ログイン成功しました"); } else { // パスワードが違う System.out.println("ユーザもしくはパスワードが間違っています"); } } } else { // データベースが起動していないケース System.out.println("システム管理者にご連絡ください"); } } } |
LoginCommandクラス | LoginCommandクラス |
ちょっと長いので見にくいですね。とりあえず、ポイントを絞って解説しましょう。例外脳発生のコツは以下のポイントを押させればOKです。
さて、一つづつ解説することにしましょう。例外を考えるには先ず奥のクラス(Userクラス)から考えていきます。
まず、このクラス構成で、各クラスの責任を考えて見ましょう。既存のDatabaseクラスはもちろんデータベースを表しています。データベースを起動したり、データベースを検索したりする責任があります。次にUserを考えると、このクラスはその名のとおり「ユーザ」をあらわすクラスです。カプセル化の教えどおり、このクラスの利用者はこのユーザのデータがデータベースから来ているかどうかなど知らなくてよいことになっています。実際にはデータベースの「ユーザ表」のデータを管理しています。最後にLoginCommandクラスですが、これはユーザインターフェイスもかねていますが、名前のとおり、ログインするというアクションを表しているクラスです。
各クラスの責任は上記のようになっているので、LoginCommandクラスはUserクラスがデータベースの「ユーザ表」のデータをカプセル化して管理しているということは知らないはずです。LoginCommandがUserクラスを使うのは、ユーザのログインをしたいからというだけであって、それが成功したのか失敗したのかということさえ分かればいいはずです。「データベースの表(ユーザ表)にその主キー(例えばkent)のデータがない」、とかは知らなくてもいいはずです。
ですのでDatabaseクラスのfindByPrimaryKey()メソッドはObjectNotFoundExceptionを返却しますが、これはあくまでデータベース的な話であって、LoginCommandクラスは「ログインに成功したか、成功しなかったのか?」にしか興味がないはずです。ですので、新たにLoginExceptionというExceptionを作ることにしました。
public static User login(String name, String password, Database db) throws LoginException { try { User user = getUser(name, db); if(user.getPassword().equals(password)) { return user; } else { throw new LoginException(); } } catch (ObjectNotFoundException e) { throw new LoginException(e); …@ } } |
例外脳のUserクラスのloginメソッド
try { User user = User.login(name, password, db); System.out.println("ログイン成功しました"); } catch (LoginException le) { System.out.println("ユーザもしくはパスワードが間違っています"); } |
例外脳のLoginCommandクラスのログイン部分(抜粋)
このように、LoginCommandクラスからみたら、loginメソッドからはLoginExceptionしかかえって来ませんので、内部でデータベースがつかわれているかどうか知りません。もちろんDatabaseクラスのfindByPrimaryKey()メソッドをUserクラスの中で使うので、内部でObjectNotFoundExceptionがスローされるのですが、それを@のように編み変えています。
このように、LogingCommandクラスが知らなくてもいい例外はLoginCommandが興味のある例外に編みかえてあげるテクニックを覚えましょう。
事前条件ってなんだか難しい言葉ですね。「メソッドを使うときの事前条件」という説明をすると、「このメソッドを使うときは、この条件は満たしといてね。」というお約束のことを言います。なんで、ここでこんなことを考えないといけないでしょうか?非例外脳の例で説明してみましょう。事前条件を考えないコーディングをすると、例えばUserクラスのlogin()メソッドやそれを使っている部分はこんなになってしまいます。
/** * ユーザクラスを取得するファクトリ * @param name キーの名前 * @param db * @return nameをもったUserオブジェクト * ただし、該当するnameをもったUserがいない場合空のUserオブジェクトを返却 * し、データベースが接続できない場合はnullを返却する */ public static User getUser(String name, Database db) { if(db != null) { Record record = null; try { record = db.findByPrimaryKey("user", name); } catch (ObjectNotFoundException oe) { return new User(); } User user = new User(); user.name = record.getName(); user.password = record.getPassword(); return user; } else { return null; } } public int login(String password) { if(password != null) { if(password.equals(this.password)) { return LOGIN_SUCCESS; } } return LOGIN_FAIL; } |
非例外脳のUserクラスのloginメソッド
Database db = null; try { db = Database.connect(); } catch (DatabaseNotFoundException e) { // データベースが起動していないケース System.out.println("システム管理者にご連絡ください"); System.exit(0); } User user = User.getUser(name, db); if(user != null) { if(user.isEmpty()){ // ユーザが見つからないケース System.out.println("ユーザもしくはパスワードが間違っています"); } else { // 正常コース int result = user.login(password); if(result == User.LOGIN_SUCCESS) { // 正常コース System.out.println("ログイン成功しました"); } else { // パスワードが違う System.out.println("ユーザもしくはパスワードが間違っています"); } } } else { // データベースが起動していないケース System.out.println("システム管理者にご連絡ください"); } |
非例外脳のLoginCommandクラスのログイン部分(抜粋)
例えばUserクラスがLoginCommandクラスから呼ばれるとして、UserクラスのgetUser()メソッド、login()メソッドではDatabseクラスのオブジェクトとユーザ名が引数になっています。それらの例えばnullチェックや、Databaseがすでに起動しているか?のチェックなんかはどこがやればいいのでしょうか?どこがやればいいかわからなかったらとりあえずUserクラスにも、LoginCommandクラスでも心配なので両方でチェックしてしまいそうです。これがこの非例外脳のコーディングになっています。ですので同じようなチェックをUserクラスでもLoginCommandクラスでもやっていてなんだか凄くごちゃごちゃしていますね。
ここで、役に立つのが事前条件の考え方です。まず、私は次のように考えました。
つまりlogin()やgetUser()メソッドは「データベースが起動している」という前提にしたのだ。どうやってそれを決めたかって?それは私が独断と偏見できめました。「そんなんでええか?」って?ええのです。他の例でいくと以下のようなメソッドがあるとします。
public boolean isEqual(String name1, String name2) { if(name1.equals(name2)) { return true; } else { return false; } } |
この例では、例えばname1がnullだったら、NullPointerExceptionになってしまいます。ですので、このメソッド内でnullチェックをしないといけないはずです。しかし、事前条件であなたが、「このメソッドを使う時にはnullを引数にしないこと」と決めればそのコーディングはしなくていい決まりになっています。ですので、逆にこのメソッドを使う側でその処置をしないといけないということになります。一般的にはこういった事前条件がある場合はメソッドのコメントに書いておくとよいです。
なんかいい加減そうですが、オブジェクト指向の場合はオブジェクトがオブジェクトをよんでまたそのオブジェクトが・・・という状況があたりまえなので、そのすべてのクラスで例えばnullチェックなんかをしたら、それはもう大変面倒なことになるのです。(非例外脳のようにたった2階層ですが)しかもExceptionの機構があるので、内部でExceptionがおこってもExceptionをスローして呼び出しもとのほうで処理できるので、特に問題でもないというわけです。ですので、事前条件をしっかり決めてコーディングすることによって、クラスの設計がすっきりするわけです。
ちなみにどこでもかしこでもnullチェックするようなコーディングのことを防御的プログラミングって言うらしいです。
というわけなので、事前条件を決めることによって、発生する例外をどこで処理すべきかというのがはっきりしてきます。例外脳のコーディングではこんな感じです。
しっかりと、出てくるExceptionをどこで処理するか?というのが決まっているので余計な重複したコーディングがでてきません。
最後の1つのテクニックをご紹介しましょう。例外脳のLoginCommandの実際の処理の抜粋を見ると面白いことに気づきます。LoginCommandはLoginというアクションを表しています。
例外脳のLoginCommand(抜粋) | シナリオ |
try { Database db = Database.connect(); User user = User.login(name, password, db); System.out.println("ログイン成功しました"); } catch (LoginException le) { System.out.println("ユーザもしくはパスワードが間違っています"); } } catch (DatabaseNotFoundException de) { System.out.println("システム管理者にご連絡ください"); } |
主成功シナリオ : 1.システムがアクタにログインを要求する 2.アクタがユーザ名/パスワードを入力する 3.システムが入力されたユーザ名/パスワード を検証する 4.ログインの成功した旨を表示する 拡張: 3a.ユーザ名/パスワードが間違っている場合 3b.ユーザが存在しない場合 3c.データベースに接続できない場合 |
よく見ると、tryの中のロジック部分が主成功シナリオのと一緒で、逆に拡張の部分はcatchのコーディングと一緒です。このようにアクションのコーディングが(ユースケースの)シナリオの流れと一致します。Javaの例ですとSessionBeanやCommandの部分にこのコーディングが見られることが多いです。このように、try〜catchの仕組みはシナリオとも凄く親和性がよろしくて、要求→分析→設計→コーディングにシームレスにマッピングされているのが、現実世界っぽい世界をオブジェクトで表現できるオブジェクト指向っぽくていいですね。
おまけですが、例外を使った場合例のだらだらでてくるスタックトレースのことについて少々。オブジェクト指向の場合、オブジェクトが協調して1つの仕事をするため、どこのクラスのオブジェクトから、どのクラスのオブジェクトが呼ばれたか?ということはデバッグをするにあたって必須となります。ですので、1行だけでなくてだらだらと出てくるわけです。
最後に
さらに例外をより深く学びたい人のために以下のような参考文献をお勧めします。
Desgin By Contract By Example.
JavaWorld誌 2002/12月号 攻略! Javaの例外処理
JavaWorld誌 2002/9月号 Design by Contract実践のススメ
他にいいのがあったら、MLで教えてくださりまし。
牛尾 剛 (Tsuyoshi Ushio) Ver .01 [2003/8/22]