AndroidアプリにStrutsのようなコントローラを導入し,画面制御させるサンプルコード (の試作品。バリデーションやビジネスロジックの骨組み)
重要なお知らせ:
この記事で公開した情報は,AndroidのMVCフレームワーク「Android-MVC」の機能の一部として取り込まれました。
より正確な設計情報や,動作可能な全ソースコードを閲覧したい場合,「Android-MVC」の公式ページより技術情報を参照してください。
AndroidのMVCフレームワーク - 「Android-MVC」
http://code.google.com/p/android-mvc-...
Androidアプリの設計に,Strutsのようなアーキテクチャを取り入れよう。という記事。
ただし,Javaにつきものの「XML地獄」は,徹底的に避けるものとする。
いかにシンプルにAndroidアプリの画面制御を管理するか?
Androidアプリは,複数の「Activity」(=画面)を持つ。
それらActivityどうしの間を行き来するためには,Intentを発行する。
通常,Intentの発行処理は,遷移元のActivityの中に書いてしまう。
Activityの中に,遷移元と遷移先の情報が埋め込まれるのだ。
バリデーションやビジネスロジックも,丸ごと1クラス内に詰め込むかもしれない。
そうすると,下記のような「密な結合」が生じてしまう。
なんとも網目状で,カオスな結合状態だ。
これは,オブジェクト指向的にはダメである。
「疎結合」なクラス設計になっていない。
むしろ,Activityにはそういった情報を埋め込まずに,
画面遷移に関る処理を集約して,1つの「コントローラ」クラスに任せる。
すると,下記のような「スター状」の結合になる。
ビューと制御の分離である。
コントローラクラスは,制御について責任を負う。バリデーションなども引き受ける。
Activityは,個別の画面におけるUI描画・イベントのハンドリングなどについて責任を負う。
どちらがよい設計か?
小規模アプリなら,前者で事足りる。
しかし規模が複雑になってくると,画面制御の洗い出しに苦労するものだ。
そして,思わぬところに画面遷移の実装漏れ・不整合が見つかったりして,テスト工程の負荷が予想外に増える。
ちょうど,Strutsフレームワークで,struts-config.xmlが
画面遷移やフォームのバリデーションに関する設定を引き受けていたのを思い出そう。
同じように,Androidアプリの「コントローラ層」の情報の集約を行なうことで
アプリ開発の保守性・生産性・拡張性・テスタビリティ等を向上させたい。
そのようなサンプルコードが完成し,Android実機上で実際に動作した。
試作品のレベルだが,下記で紹介する。
(1)コントローラ層を簡潔に,シンプルにコーディング
DSLを提供するライブラリを無視すると,ユーザが手を触れるのは,わずかな量のコードだ。
以下の4つだけ。
- アクティビティ本体
- コントローラ
- バリデータ
- ビジネスロジック
まず,Activity内に画面制御の情報をハードコードするのをやめよう。
ふだんならIntentを発行したくなる部分で,それをぐっとこらえる。
かわりに,「new Controller().submit( this );」とする。
MainActivity.java
package controller_test.dayo.activities.func_main; import controller_test.dayo.R; import controller_test.dayo.activities.base.BaseActivity; import controller_test.dayo.controller.Controller; import controller_test.dayo.controller.lib.ActivityParams; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; // メイン処理を行う画面。 public class MainActivity extends BaseActivity implements OnClickListener{ Button btn; EditText et; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // ボタン定義 btn = (Button)findViewById(R.id.btn1); btn.setOnClickListener(this); // 入力ボックス定義 et = (EditText)findViewById(R.id.editText1); } @Override public void onClick(View v) { // ボタン押下時 if( v == btn ) { // コントローラを呼ぶ。★ここが重要。 // どこのActivityに遷移するのか,このActivity自体は知らない。 new Controller().submit( this ); } } @Override public ActivityParams toParams() { // バリデーションに渡すために,この画面の状態を余さず回収。 return new ActivityParams() .add("input_value", et.getText().toString() ) ; } }
この画面はとてもシンプル。
入力ボックスと,実行ボタンが1つずつある。
入力ボックスに入れた文字列は,のちのちバリデーションされる。
そのために,アクティビティが抱えている情報を丸ごと取得するための「toParams」というメソッドが準備されており,これは基底クラスで抽象メソッドなので,子クラスで実装が強制される。
返却値は,恐るるに足らず。単なるHashMapのラッパーである。
さて,上記のアクティビティは「Controller」クラスに制御をゆだねている。
本題の「Controller」クラスを見てみよう。
package controller_test.dayo.controller; import java.util.HashMap; import android.app.Activity; import controller_test.dayo.activities.func_main.FugaAction; import controller_test.dayo.activities.func_main.FugaActivity; import controller_test.dayo.activities.func_main.HogeActivity; import controller_test.dayo.activities.func_main.MainAction; import controller_test.dayo.activities.func_main.MainActivity; import controller_test.dayo.controller.lib.BaseController; // MVCのコントローラ層に当たるクラス。 // ビューから渡された値の検証や,BLの呼び出し,画面遷移の制御などを行う。 // もし肥大化したら,ここをプロキシにして別クラスに細分化する。 public class Controller extends BaseController { // 遷移元となるActivityごとに,submit()をオーバーロードする。 // メイン画面からの遷移時 @SuppressWarnings("serial") public void submit( final MainActivity mainActivity ) { // コントロールのフローの詳細を記述する。 new ControlFlowDetail<MainActivity>( mainActivity ) .describeValidation( new ValidationExecutor(){ @Override protected void validate() { // バリデーション処理 validation_result = GateChecker.validate( mainActivity ); } }) .describeBL( new BLExecutor(){ @Override protected void doAction() { // BL action_result = new MainAction( mainActivity ).exec(); } }) .onValidationFailed( FugaActivity.class ) // バリデ失敗時の遷移先 .onBLExecuted( // BL実行完了時のルーティング・テーブル。 // BL完了時のルーティング識別子と,遷移先のActivityクラスをひもづけている。 new HashMap<String, Class<? extends Activity>>(){{ put( "success", HogeActivity.class ); // put( "piyo", MugaActivity.class ); // ... }} // NOTE: もし↑のラッパークラスを作れば@SuppressWarningsアノテーションが不要になるのだが // HashMapのラッパークラスの作りすぎのために,いい加減力尽きたので・・・。 // http://www.kinopyo.com/blog/java-init-map-with-data ) .startControl(); ; } // 別の画面でのコントロールフロー(バリデ失敗画面からの遷移時) @SuppressWarnings("serial") public void submit(final FugaActivity fugaActivity) { new ControlFlowDetail<FugaActivity>( fugaActivity ) .describeValidation( new ValidationExecutor(){ @Override protected void validate() { validation_result = GateChecker.validate( fugaActivity ); } }) .describeBL( new BLExecutor(){ @Override protected void doAction() { action_result = new FugaAction( fugaActivity ).exec(); } }) .onBLExecuted( new HashMap<String, Class<? extends Activity>>(){{ put( "success", MainActivity.class ); }} ) .startControl(); ; } }
このクラスが,Strutsでいう(ある意味,悪名高い)「struts-config.xml」に相当する。
ただしここでは設定ファイルではなく,Javaコードとして実装してある。
MainActivity用の制御フロー中では,7つのことをしているのがわかる。
- ControlFlowDetailというオブジェクトを宣言して,制御フローの詳細をその中に放り込み始める。
- まずは,MainActivityのインスタンスを登録する。
- バリデーション処理の呼び出しを定義。
- バリデーションエラー発生時の遷移先クラスを定義。
- ビジネスロジックの呼び出しを定義。
- ビジネスロジック実行完了後に,実行結果しだいで分岐先を返るためのルーティングテーブルを定義。
- これらの制御フローを開始する。
DSL(fluent interface)で記述しているので,極力読みやすく,理解しやすくなっている。
何が起こった時に,どこのActivityに遷移する事になるのか,一目瞭然に集約されている。
もしコントローラクラスが肥大化したら,機能ごとにコントローラクラスを分割するのが良い手かもしれない。
さて,バリデーションを実行しているわけだが,実行クラスはGateCheckerという名前だ。
もともと,上記のControllerは「Gate」という名前のクラスとして開発を進めていたのでそういう名称になった。
ちょうど,各Activityから見てGate(門)の役目をするからだ。
バリデーション担当のGateCheckerは下記のような内容。
package controller_test.dayo.controller; import java.util.regex.Pattern; import controller_test.dayo.activities.func_main.FugaActivity; import controller_test.dayo.activities.func_main.MainActivity; import controller_test.dayo.controller.lib.ActivityParams; import controller_test.dayo.controller.lib.GateValidationResult; // Activityごとのバリデーション操作を詰め込んだクラス。 // もし肥大化したら,ここをプロキシにして別クラスに細分化する。 public class GateChecker { // Activityごとに引数の型を変えてオーバーロードする。 // メイン画面の入力値を検証 public static GateValidationResult validate(MainActivity mainActivity) { // Activityが抱えている値情報を全て取り出す ActivityParams params = mainActivity.toParams(); GateValidationResult vres = new GateValidationResult(); // 1つの入力値に関してバリデ開始 String s = (String)params.get("input_value"); // 1文字以上であること。 // TODO: お決まりのバリデーションロジックもDSLで定型メソッド化したい。 if( ( s == null ) || ( s.length() < 1 ) ) { return vres.err("数値が入力されていません。"); } // 半角数字のみであること。マイナスやピリオドも許可しない。 if( ! Pattern.compile("^[0-9]+$").matcher(s).matches() ) { return vres.err("半角数字のみを入力してください。"); } // パース long long_value = Long.parseLong(s); // パース前後で余計な変化がないこと。先頭の0とか。 if( ! String.valueOf(long_value ).equals(s) ) { return vres.err("整数の入力形式が不正です。"); } // 0より大きいこと if( long_value < 1 ) { return vres.err("0より大きい数を入力してください。"); } // OK return vres.success(); } // バリデ失敗画面の入力値を検証 public static GateValidationResult validate(FugaActivity fugaActivity) { GateValidationResult vres = new GateValidationResult(); // 何も検査せず // OK return vres.success(); } }
MainActivityのためのバリデーションメソッド内では,GateValidationResultというオブジェクトを中心に話が展開している。
このオブジェクトに,バリデーションの最終的な結果や,発生したエラーメッセージを詰め込んでゆくのである。
次に,ビジネスロジックを実行するオブジェクト。
※Strutsと異なり,ここではActionとBLという語をとりあえず同義で用いることにした。
このサンプルでは,単にログを出力するだけのロジックなのだが。。
package controller_test.dayo.activities.func_main; import android.app.Activity; import android.util.Log; import android.widget.Toast; import controller_test.dayo.controller.lib.ActionResult; import controller_test.dayo.controller.lib.BaseAction; // メインのBL。 public class MainAction extends BaseAction { public MainAction(MainActivity fromActivity) { this.activity = fromActivity; } // BL本体を記述 public ActionResult exec() { // 〜〜ここでいろんな処理をする。〜〜 // NOTE: もし必要なら,呼び出し元のActivityをtoParams()することによって // バリデーション通過済みの入力値をここで参照し,DB登録などに利用できる。 // NOTE: BLのこの部分は非同期タスク内でラップされている事に注意。 // AsyncTaskを意識しないで実行できる。 // DB操作なんかは同期的に行える。 // NW処理も同期化するために,処理の完了をwaitする必要がある。 Log.d("myapp", "メインのBLを実行しました。"); // 実行結果を返す return new ActionResult(){ @Override public void onNextActivityStarted(final Activity activity) { activity.runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText( activity, "メインBLが終わったよ。\nboo = " + get("boo"), Toast.LENGTH_LONG ).show(); // NOTE:匿名クラスを入れ子にしているが,アウタークラス名.this.getなんて書くと // 「エンクロージング・インスタンスがアクセス不可能です」のエラーになる。 } }); } } .setRouteId("success") // この識別子が遷移先のActivityクラスとひもづく .add("boo", "BLの実行結果としてとりあえず格納した値") ; } }
ここで,奥深い層のデータ処理などを行なう。
Webアプリでいえば,サーバサイドでコントローラがドメイン領域のクラスを呼び出し始める。
そして処理が終わったら,ロジックの実行結果を「ルーティング識別子」として返す。
実行結果に関する情報は,ActionResultというオブジェクトに詰め込んで返却する。
この返却用のオブジェクトにも,BL内で自由に値をaddすることができる。
ユーザが手を触れるのは,以上の4クラス。
これだけで,画面遷移およびそれに伴う各種の処理に関る記述は終了。
実行すると,ちゃんとバリデーション・BL呼び出し・画面遷移の制御などやってくれる。
Androidアプリは,規模が小さい物もある。
なので,ビジネスロジックやコントローラといった概念を導入するのに抵抗を感じる場合もあるかもしれない。
ただでさえ,「ドメインとは何をカバーするのか」「モデルとコントローラの境界線はどこなのか」といった話題は正解がなく,議論が炎上しやすい。
単に叩き台としてコードを公開したのに,「この人はMVCについて何にも分かっていないね」というような酷評を受けることすらある。
「SmallTalk MVCとJ2EE MVCを混同してるだろ!」とか,「ドメインとモデルとDAOの違いをこれっぽっちもわかってないね」とか…。
それも覚悟のうちだ。
Android上で用意されている各種基本クラスが,どのようにMVCの各層をカバーするのか,あるいはかけ持ちをしなければならないのか,この先模索してゆかねばならないのだから…。
このサンプルは,各種処理の「スタンダードな記述ポジション」の例を示した,という点で価値があると言える。
また,Androidアプリにコントローラ層の概念を導入した,という点だけでも。
(2)これらのDSLをコーディング可能にするためのライブラリ
さて,前項で取り上げたようなコードを実現可能にするためには,どのようなライブラリを準備したらよいのか。
ほとんどのクラスはHashMapのラッパーに過ぎないので,ここでは抜粋して
重要な2クラスだけを掲載する。
まず,ユーザが記述するコントローラの基底クラス。
ユーザがコントローラ上で制御処理を記述しやすくするために存在する。
BaseController.java
package controller_test.dayo.controller.lib; import java.util.HashMap; import android.app.Activity; import controller_test.dayo.controller.task.AsyncTasksRunner; import controller_test.dayo.controller.task.SequentialAsyncTask; // コントローラの基底クラス。 public class BaseController { // 一つのコントロールフロー内でバリデーション操作を実行するクラス protected abstract class ValidationExecutor { protected abstract void validate(); public GateValidationResult validation_result; } // 一つのコントロールフロー内でBLを実行するクラス protected abstract class BLExecutor { protected abstract void doAction(); public ActionResult action_result; } // NOTE: これらのインナークラスは,親クラスのジェネリクスで解決できなかった問題を // なんとかするために,クロージャの代わりとして仕方なく提供されている。 // 一つのコントロールフローの詳細記述 protected class ControlFlowDetail<ActivityClass> { protected ActivityClass from_activity; private ValidationExecutor validation_executor; private BLExecutor bl_executor; private Class<? extends Activity> validation_failed_activity; private HashMap<String, Class<? extends Activity>> routingTable; // 初期化 public ControlFlowDetail(ActivityClass fromActivity) { this.from_activity = fromActivity; } // バリデーション処理の詳細をセット public ControlFlowDetail<ActivityClass> describeValidation( ValidationExecutor validationExecutor ) { this.validation_executor = validationExecutor; return this; } // バリデーション失敗時の遷移先をセット public ControlFlowDetail<ActivityClass> onValidationFailed( Class<? extends Activity> validation_failed_activity ) { this.validation_failed_activity = validation_failed_activity; return this; } // BL実行の詳細をセット public ControlFlowDetail<ActivityClass> describeBL( BLExecutor blExecutor ) { this.bl_executor = blExecutor; return this; } // BL実行完了時のルーティングテーブルをセット public ControlFlowDetail<ActivityClass> onBLExecuted( HashMap<String, Class<? extends Activity>> routingTable ) { this.routingTable = routingTable; return this; } // 制御フローを実行 public void startControl() { new AsyncTasksRunner( new SequentialAsyncTask[]{ // (UIで済まなかった)バリデーションを行なう非同期タスク。 // Webでいうサーバサイド・バリデーションに相当 new SequentialAsyncTask(){ public boolean main(){ // Activityのparamsをバリデート validation_executor.validate(); storeData( "validation_result", validation_executor.validation_result ); // バリデ結果の是非に関らずルーティングへ進むので必ずtrue return true; } } , // もし可能ならBLを行なう非同期タスク。 // DB操作やNW通信などを想定。 new SequentialAsyncTask(){ public boolean main(){ // 前のバリデーション処理の結果を取りだす GateValidationResult vres = (GateValidationResult)getDataFromRunner("validation_result"); // もしバリデ結果からしてBLを実行してよいのであれば if( vres.permitsExecitionOfBL() ) { // BLを実行 bl_executor.doAction(); storeData( "action_result", bl_executor.action_result ); } // BLの実行結果の是非に関らずルーティングはするので必ずtrue return true; } } , // ルーティングを行なう非同期タスク new SequentialAsyncTask(){ public boolean main(){ // バリデーション処理の結果を取りだす GateValidationResult vres = (GateValidationResult)getDataFromRunner("validation_result"); if( vres.didNotExecuteBL() ) { // バリデーションではじかれた場合のルートへ Router.go( (Activity)from_activity, validation_failed_activity, vres ); } else { // BLが実行された場合 ActionResult ares = (ActionResult)getDataFromRunner("action_result"); // BL実行結果に応じて遷移先を分岐 Router.switchByActionResult( (Activity)from_activity, ares, routingTable ); } return true; } } }) .withSimpleDialog("処理中・・・", (Activity)from_activity) // 全非同期タスクが終了するまでダイアログを出す .begin(); } } }
この中で,制御処理を逐次実行している。
非同期タスクの逐次化のために,AsyncTasksRunnerとかSequentialAsyncTaskといったクラスを利用しているが,それらについては下記のエントリで述べた。
とはいえ,下記記事に載っているのは非Android向けのクラスなので,上記のコードはAndroid用に作り替えたものを使っているのであるが。
たとえば,全ての非同期タスクの逐次実行が完了するまでの間,画面上にダイアログを表示し続ける,といった機能を追加してある。( .withSimpleDialog() の部分)
Javaの非同期処理を,シングルスレッドのようにシンプルにコーディングするための設計パターン (並列処理を逐次処理にする)
http://language-and-engineering.hatenablog.jp/entry/20120205/p1
ちなみに上記のエントリは,Androidでの応用を目的として書かれた「伏線」だったのだが,世間の人々にはなかなかわかって(勘付いて)もらえなかった。
Androidでは,AsyncTaskユーティリティが「java.util.concurrent」パッケージをひたすら隠ぺいしているのだ。
その事情を知らないと,「マルチスレッドの定番クラスなんだからjava.util.concurrent使えよ!」というアドバイスを,容赦なく多くの方々から頂く事になる。
platform_frameworks_base / core / java / android / os / AsyncTask.java
https://github.com/android/platform_f...
- ひたすらimport java.util.concurrentしていて,そのラッパークラスがAsyncTaskなのである。
まあ,仕方ない。エントリの公開戦略は,たいてい意図的に隠しているのだし。
話を戻そう。
ジェネリクス・匿名クラス・内部クラスを駆使する事により,コントローラ層の子クラスでユーザが記述するコード量は可能な限り抑えた。
(※本当はすべてジェネリクスで片付けて,Builderパターンなんぞ使わずに,もっとユーザ側の記述量の負担を減らしたかった。しかしJavaではそれは無理だった。下位レイヤであれこれオーバーロードされているメソッドを,ユーザのレイヤから透過的に扱う事が出来なかったのだ。だからユーザが記述するコードには,謎のコードの断片を1行だけ記述させるためのメソッドラッパーの無名クラスが登場する事になる。その無名クラスは,ユーザが扱う上位レイヤから下位レイヤに対し,コードを「注入」する働きを持つ。)
最後に,上記のコードでも呼び出されているが,
Intentの扱いをラップしているRouterというクラスを掲載する。
命名のヒントは,Ruby on Railsのrouting(config/routes.rb)から得た。
package controller_test.dayo.controller.lib; import java.util.HashMap; import android.app.Activity; import android.content.Intent; // 画面遷移を実行するクラス。 public class Router { // バリデ失敗時の画面遷移 public static void go( Activity from_activity, Class<? extends Activity> to_activity, GateValidationResult vres ) { Intent intent = new Intent( from_activity.getApplicationContext(), to_activity ); from_activity.startActivity(intent); // バリデ失敗を新画面上で通知 vres.onFailedActivityStarted(from_activity); } // BL実行時の画面遷移。 // アクション実行結果ごとのルーティング識別子によって分岐。 public static void switchByActionResult( Activity from_activity, ActionResult ares, HashMap<String, Class<? extends Activity>> routingTable ) { // ルーティング識別子を取得 String route_id = ares.getRouteId(); // 遷移先のActivityのクラスを取得 Class<? extends Activity> to_activity = routingTable.get(route_id); // 遷移を実行 Intent intent = new Intent( from_activity.getApplicationContext(), to_activity ); from_activity.startActivity(intent); // TODO: ActionResultをputExtraして,遷移先の画面で参照できるようにする。 // BL実行完了を新画面上で通知 ares.onNextActivityStarted(from_activity); } }
もし本稿で紹介した各種クラスの導入が面倒な場合,このような「Intentのマネージャ」だけでも作っておくと,コード量が減って助かるだろう。
できるだけ下位レイヤのAPIを隠ぺいし,ユーザにはアプリの仕様に関る記述のコーディングに専念させる。
そうやって生産性を向上させることが狙いだ。
補足
ここまでで,試作品の設計方針と,サンプルが実際に動作する旨の報告を述べた。
ここから先,まだやる事が残っている。
現在,Androidアプリ開発におけるコントローラ層を洗練するためのTODOとして:
- 他アプリとの連携機能を充実させること。そこがAndroidの強みだし。
- Controllerクラスは実質的に「Intentマネージャ」みたいなものなので,レシーバ,インテントフィルタなんかも考慮した文脈で,コントローラ層の設計を拡張する必要がある。
- バリデータをもっと楽に記述できるように,バリデーションロジックを豊富に取りそろえること。
- まるでJUnitやRSpecやQUnitでテストコードを書くかのように,バリデーションロジックを記述できたら,すごくナイスで面白いと思わないか?assertEqual() とか,いろいろ。
- Controller上にユーザが記述するコードを,ジェネリクス等のピュアJavaの記法を駆使して,さらにさらにシンプルにできないものか。本稿で掲載したコードは,私が当初期待していたコードよりも冗長な姿なのだ。
- できれば,「yamlみたいだけど型安全なJavaコード」が書いてあって,それが動く。という姿がベストなんだけど,それは土台,無理だ。GroovyでAndroidアプリが作れるようになるならまだしも。かといって,設定ファイル地獄はもうやだし。
- 先日の「jQueryっぽくレイアウトを記述する」の記事では外しておいたのだが,SilverlightとかRIA世界での「MVVMパターン」をもっと考慮する必要がある。バインディング機構とか導入すれば,バリデーションの概念が変わるはずだ。
テスト可能なUI設計パターン – 第1回 Androidテスト祭り 発表資料
http://ugaya40.net/architecture/andro...
- UI側の設計も疎結合にして,単体テストしやすくせよ
MVVMパターンとは? – わんくま同盟東京勉強会 #60 セッション資料
http://ugaya40.net/mvvm/what_is_mvvm....
- クラスの相互依存で単一責任の原則に反するとNG。開発時の作業分担がしづらい。いずれかの依存性を排除する必要が。
- 責務に関連あればインタフェースはさむ。関連薄ければObserverをはさむ。
- WPFならデータバインド。
Androidアプリの画面レイアウトを,まるでjQueryのようなコードで動的構築できるライブラリ (の試作品。UIコーディングのためのDSL)
http://language-and-engineering.hatenablog.jp/entry/20120210/p1
「AndroidとMVCアプリケーションアーキテクチャ」
http://www.android-group.jp/index.php...
- 2008年の情報
なお,バリデーションの分類としては,
今回はController上でのバリデーションだったので,「コンバリ」の土台を実装した。という事になる。
あまり知られていない,Webアプリ開発時の10の略語 (例文つき)
http://language-and-engineering.hatenablog.jp/entry/20101102/p1
- クラバリ (クライアントサイド・バリデーション)
- モデバリ (モデルクラスによるバリデーション)
- コンバリ (コントローラクラスによるバリデーション)
おまけで,Strutsを触ったことが無い人のために。
StrutsのMVCアーキテクチャの基礎については,下記ページの図表などを参照。
Strutsの知識を基に、Ruby on Railsを学ぶ
http://jibun.atmarkit.co.jp/lskill01/...
関連エントリ:
「バリデーション」APIと「単体テスト」APIの類似性,およびそのスタイルが時代と共に洗練される過程の概観
http://language-and-engineering.hatenablog.jp/entry/20120320/p1
Androidアプリ開発用のMVCフレームワーク 「Android-MVC」 ver0.2をリリース
http://language-and-engineering.hatenablog.jp/entry/20120323/p1