LoginSignup
357
357

More than 5 years have passed since last update.

隊長、Androidアプリのソースがぐちゃぐちゃであります!!

Posted at

複数の責務をFragmentやActivityに押し込めてるのが原因です。
公式サイトに書いてあるようなこともありますが、今一度まとめてみました。

--

Activityの長さが10000行を超えました!!とても保守できません!!隊長!!

初期のAndroid開発は手探りでした。Activity、Intent等、大きな枠組みでは優れてましたが、その上の層に関してはノータッチでした。

皆Activityが単位として大きすぎるのは理解してましたが、多くの人はActivityにコードを詰め込む道を選びました。フレームワークを 使う ことに慣れすぎて、 作る ことには不慣れだったのです。

とはいえ、そんなコードはすぐ破綻します。それではまずいということで、GUIフレームワークの知見のある人達は、各々、オレオレフレームワークを内部で抱え込むことになりました。暗黒時代です。

しばらくして、開発が追いつき、Andoridはこの問題に対処するための(かどうかわかりませんが)フレームワークを用意しました。それが Fragment です。

これはPACやH-MVCと呼ばれるパターンに似ています。

H-MVCってなに?

H-MVCはMVCに階層構造を付与した、MVCパターンの亜種です。MVCに関してはこちらの記事
を参照してください。以下、MVCの意義を理解しているものとして進めます。

1画面1MVC組ですと、よっぽど優れたウィジェットが用意されてない限り、各々が大きくなって、複雑になってしまいます。複雑は悪です。

なので分割を試みます。どのように分割するといいでしょうか。

ある人は、画面は、その中にさらにミニ画面的なものが存在する、フラクタル的な構造にあることに着目しました。

例えば、ログイン画面を考えます。

ログインフォームがありそうですね、注意を表示する場所もありそうです。
さらに分割すると、ログインフォームにはID、パスワードを入力するテキストフィールド、そしてログインボタンがあります。
これらはウィジェットとして一般的に提供されています。よってここが最小単位です。


ログイン画面  -ログインフォーム   -IDフィールド
                            -パスワードフィールド
                            -送信ボタン
            -注意書き

以上から、上のような階層構造を成します。

そして、各々の要素に対して、Push-MVC組を割り当てます。(View層が無い場合もあります)
もちろん、このままでは協調して動けませんので、上の要素が管理します。
例えば送信ボタンが押されますと、

  • ログインフォームControllerはイベントを受け取って、IDフィールド、パスワードフィールドを取得。バリデーションの後、ログインイベントを発行する。
  • ログイン画面Controllerはログインイベントを受け取り、ログイン処理を走らせる。

のように上に向かってイベントを送信します。上が下へイベントを送信する場合は、どれが応答責務をもっているかわからないため(直近の子供でない可能性もあります)、ブロードキャストします。

Fragmentに当てはめる

ではAndroidに話を戻しましょう。AndroidのFragmentは3層の階層構造になります。


ログインActivity    - ログインFragment - 各種ウィジェット
                    - 注意書きFragment - 各種ウィジェット

FragmentはgetActivity()でアクティビティを取得できます。Fragment間の通信はActivityを経由することになります。
ですが、具体的なActivityに依存すると、Fragmentの疎結合性が損なわれます。
なので、Activityは例えばLoginListenerというインターフェイスを実装して、Fragmentではそれにキャストして使用するのがよいです。


class LoginActivity implements LoginListner{
    @Override
    public void onLogin(String id, String password){
        if(model.login(id, password)){
            //画面遷移
        }
    }
}

class LoginFragment{
    public void onLoginButton(){
        ((LoginListener)getActivity())
            .onLogin(getId(), getPassword());
    }
}

もちろん、イベントオブジェクトを使った薄いラッパーを作成してもかまいません!!

キチンと責務分割できれば、このFragmentは大きな武器です。
上手く使いたいですね!!

隊長、View操作コードが大きすぎて手を付けられません!!

iOSアプリに比べてAndroidアプリの見た目がひどいと言われた大きな理由、それがView層の機能の欠如でした。
特にアニメーション関連はひどく、個人的にはLolipopでようやくiOSの背中が見えたのでは?と思っています。

これは、気持ちのいい画面遷移やアクションを行うためには、まだまだプログラマが頑張らなければならないことを意味します。

これらのコードは分離しましょう。本来View側の処理です。ActivityやFragmentに書くべきコードではありません。

  • 操作クラスを用意する

ウィジェットを簡単に外側から操作する場合や、ウィジェット間で連携を取る必要がある場合です。Method Objectや、Utilityの形式を取ることになると思います。
ただし、時間変化を伴い、AnimatorやAnimationを使わないなら、FragmentやActivityのライフサイクルに合わせなければならない点にだけは注意してください。操作してる最中にActivityが消えた!!という状況に対処する必要があります。

  • Viewを拡張する

特に描画を伴う場合は必須です。また、操作対象が一つで完結する場合も、こちらが扱いやすいでしょう。
View(や他のウィジェット)を継承して、新たなViewを作ります。

内在するステートマシンを洗い出す

規約に同意にチェックを押さないと次に遷移しないという処理を考えてみましょう。
チェックボタンをタップしたイベントを拾って、遷移ボタンを有効化すればいい……のでしょうか?ここで疑問を持ってください。持たないことは、コールバック地獄への片道切符です!!

オブジェクト指向は、状態をオブジェクトの中に詰め込みます。すなわち、状態機械を飼いならす手段という側面を持ちます。 オブジェクトの持つ状態を常に意識しましょう。

今回の場合、規約非同意状態、規約同意状態、二つの状態があります。規約同意状態に遷移するトリガは、チェックボタンにチェックが入ったとき、ですね。そして規約同意状態に遷移時、同時にボタンが有効化されます。

おっと、規約同意のチェックが外れると、規約非同意状態に遷移しますね!!忘れるところでした!!この遷移時には、ボタンが再び無効化されますね。

今回の例は単純でしたが、状態が複雑になると管理が難しくなります。これを管理する手段の一つとして、状態一つ一つにオブジェクトを割り当てることも考えられます。
いわゆるStateパターンです。Javaでは一つ一つにクラスを割り当てる重いStateパターンもありますが、enumを使った軽量Stateパターンも存在します。意外と知らない人が多いので紹介します。こんな感じです。


class Hoge{
    enum State{
        Idle{
            @Override
            void process(Hoge parent){
                if(parent.check()){
                    parent.changeState(State.Processing);
                }
            }
        },
        Processing{
            @Override
            void process(Hoge parent){
                if(parent.processHoge()){
                    parent.changeState(State.Idle);
                }
            }
        },
        ;

        void enter(Hoge parent){}
        void leave(Hoge parent){}
        void process(Hoge parent){}
    }

    State mState = State.Idle;

    void changeState(State newState){
        mState.leave();
        mState = newState;
        newState.enter();
    }

    public void process(){
        mState.process();
    }

    boolean processHoge(){
        /*do something*/
    }
}

各状態固有変数をカプセル化できないという弱点もありますが、状態遷移のトリガが自然とenumに集まり、わかりやすいというメリットもあります。switch-caseと合わせて、状況に応じて選択しましょう!!

Fragmentに似たようなコードが多すぎます!!隊長!指示を!!

Fragmentで画面を分割し、Viewにまつわるコードは他のクラスに移しました。さて、ビジネスロジックはどうしましょう?

状態をもち、Fragmentとおなじライフサイクルの場合は、普通にModelとしてクラスに切り分けましょう。Activityの場合も同様です。

状態を持たない場合は、Utilityにしましょう。(名前は適切につけましょう。すべての名前をUtilサフィックスにする必要はありませんよ)

Activityをまたぐ場合はどうしましょう?
そのようなライフサイクルを持つクラスは、大抵、Applicationのライフサイクルと一致します。よって、Applicationが管理します。

static変数にもつことも可能です。この場合はシングルプロセスアプリにしましょう。

余談:クラス分割すべきときは、変数とメソッドが教えてくれる

分割は重要なのですが、最初からきれいに分割できる訳ではありません。不必要な分割は複雑性を生みます。複雑は悪です。

分割すべきときはいつでしょうか?これは難しいです。私もまだ道の途中もいいところです。ですが、簡単な指針があります。
それは、ある変数群に対して、複数のメソッドがアクセスしているときです。
A,B,Cという変数があり、Am, Bm, Cmというメソッドがあるとします。例えば

Am => A,B
Bm => B,C
Cm => C,A

という参照を行っている場合、クラス分割候補です。ですが、まだ候補です。
この変数、メソッド群に対して、適切な名前が付けられる場合、それはクラスです。

適切な名前、というのがポイントで、この語彙の豊富さが、オブジェクト指向を上手く使えるか、そう出ないかの分かれ道になる、と私は考えています。

また、この前提として、メソッドが適切に名前付け、分割されている必要があります。

ダイエットに成功したよー!!

根本にある考え方は、 FragmentやActivityをControllerの責務に専念させること です。
Push-MVCにおいて、Controllerの責務は二つです。

  • Viewの低レベルなイベントを組み立て、より高レベルなイベント、つまりModelのメソッドに変換する(Control)
  • Modelの情報を変換し、Viewに流し込む(Push)

結局の所、コードをきれいに保つのは、コードを書く人です。他の誰でもありません!!

日々勉強!楽しいですよね!!
以上です!!

357
357
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
357
357