MVP + Clean Architecture における DI 設計の一案

MVP + Clean Architecture っぽいプロジェクトで各レイヤーの依存関係を、 DI コンテナである dagger2 で表現する一案の備忘録。

結論

  • クラス数が多くなる。あまり現実的じゃない。
  • 結局 ViewLayer から見れば、注入オブジェクトたちを ApplicationComponent で管理してるのと変わらないのでは。

サンプルコード

sho5nn/android-tasting-mvp-dagger2

Activity で依存オブジェクトを確認できる。

UserView FriendListView FriendProfileView
f:id:sho5nn:20170910163030p:plain f:id:sho5nn:20170910163043p:plain f:id:sho5nn:20170910163054p:plain

依存図

各レイヤー間の依存方向を青い矢印で、依存性の注入方向を赤い矢印で表した図。

f:id:sho5nn:20170910162920p:plain

言わないこと

  • Clean Architecture + MVP の在り方
    • コードで表現したときの在り方やネーミング等は、適宜補完してもらえると。前述したサンプルコードにおいては、例えば Repository が抽象的ではない( Interface を定義していない)など。
    • 本来は DomainLayer に Repository Interface を定義し、 DataLayer で実装するのが望ましい気がする。

スタートアップ企業に移って 5 ヶ月目の備忘録

  • 以前の現場
    • 大企業のモバイル開発グループの技術基盤 Android チーム
    • 在籍期間は約 2 年半 
  • 現在の現場
    • スタートアップ企業の Android アプリ開発チーム
    • アプリは正式リリースからまだ 1 年も経過していない
    • 在籍期間 5 ヶ月目に入った

スタートアップ企業に移って 5 ヶ月目の気持ちの備忘録。

開発スピードの向上と維持

f:id:sho5nn:20170902195542j:plain

実は、スタートアップ企業での開発経験は、今の現場が 2 回目ではあるけど、 1 回目のときは、この業界に入って 2 年目ぐらいの頃で、とにかくがむしゃらに開発、具体的に仮説立てて施策立てて検証して次に回して、とかそういう感じではなかった。

今の現場は、エンジニアやマーケチーム、事業推進の方々など見渡せど頭のキレる方ばかり。最小の入力で最大ダメージコンボをいかに叩き込めるか、を常に念頭に置いてプロダクトを成長させようとしている感じ。とにかく刺激があるし、そんな 0F コンボを如何程に安定して出せるんですか、という。言わずもがな、以前の現場とは全く違う。

当然ながら競合他社は存在していて、最近、その競合他社がリリースしているアプリをチーム内で使ってみようということになり、結果として「我々のよりも優れているな…」という評価が出ているなど、やっていきのこころが高まっている。

いかに自社プロダクトをユーザーたちに浸透させ、いかに競合他社とのアドバンテージを取るか、を考えると、障害となる問題とその課題を見つけ検証して改善する、当たり前だけど当たり前に維持するのが難しい PDCA をどれだけ早く回せるか、につきる。

どれだけ早く回せるかを、開発者視点のアプローチとしても取り組み続けるべきで、日々の作業にかかる時間の短縮化、本来やりたいことから道を逸れてしまうような問題に遭遇しないよう防御策を仕込んでおくなど、小さいことでもいいのでチリを積んでおけば早いサイクルの山を築けるのでは、と感じている。本当にやりたい PDCA は叶えたいのだ。

例えば直近では、テンプレート機能でクラスファイルを自動生成する仕組みを導入(きりみん氏ありがとう)、ビルド速度の短縮化、 Primary Issue の合間にリファクタリングを行うなど、大小関係なく取り組み続けている。全く書かれていなかったテストコードも今に至っては主要機能はカバーできているようになり、デグレで無駄足を踏む確率は確実に低くなっている。

目に見えて劇的な効果は感じられなくとも、長期的スパンで見れば効果は出るので、このマインドを持ってして 0F コンボを叩き込みたい。そんな 5 ヶ月目。

Dagger2 メモ

n 個の @Component から 1 個のインスタンスに inject

不可

public class Main {

  @Inject
  Red red;

  @Inject
  Blue blue;

  public Main() {
    DaggerBlueComponent.builder().blueModule(new BlueComponent.BlueModule()).build().inject(this);
    DaggerRedComponent.builder().redModule(new RedComponent.RedModule()).build().inject(this);
  }
}
public class Blue {
}

@BlueScope
@Component(modules = BlueComponent.BlueModule.class)
public interface BlueComponent {

  void inject(Main main);

  @Module
  class BlueModule {

    @BlueScope
    @Provides
    Blue provideBlue() {
      return new Blue();
    }
  }
}

@Scope
@Retention(RUNTIME)
public @interface BlueScope {
}
public class Red {
}

@RedScope
@Component(modules = RedComponent.RedModule.class)
public interface RedComponent {

  void inject(Main main);

  @Module
  class RedModule {

    @RedScope
    @Provides
    Red provideRed() {
      return new Red();
    }
  }
}

@Scope
@Retention(RUNTIME)
public @interface RedScope {
}
:sample:compileDebugJavaWithJavac
/Users/shogo/dev/code/android/myrepos/android-tasting-dagger2/sample/src/main/java/com/sho5nn/sample/di/BlueComponent.java:13: エラー: com.sho5nn.sample.di.Red cannot be provided without an @Inject consttor or from an @Provides- or @Produces-annotated method.
  void inject(Main main);
       ^
      com.sho5nn.sample.di.Red is injected at
          com.sho5nn.sample.Main.red
      com.sho5nn.sample.Main is injected at
          com.sho5nn.sample.di.BlueComponent.inject(main)
/Users/shogo/dev/code/android/myrepos/android-tasting-dagger2/sample/src/main/java/com/sho5nn/sample/di/RedComponent.java:13: エラー: com.sho5nn.sample.di.Blue cannot be provided without an @Inject consttor or from an @Provides- or @Produces-annotated method.
  void inject(Main main);
       ^
      com.sho5nn.sample.di.Blue is injected at
          com.sho5nn.sample.Main.blue
      com.sho5nn.sample.Main is injected at
          com.sho5nn.sample.di.RedComponent.inject(main)
エラー2個

@Scope ( @RedScope, @BlueScope ) を持っている/持っていないは関係ない

n 個の @Component に依存した 1 個の @Component から 1 個のインスタンスに inject

可能

public class Main {

  @Inject
  Red red;

  @Inject
  Blue blue;

  public Main() {
    BlueComponent blueComponent = DaggerBlueComponent.builder()
      .blueModule(new BlueComponent.BlueModule()).build();
    RedComponent redComponent = DaggerRedComponent.builder()
      .redModule(new RedComponent.RedModule()).build();

    DaggerColorComponent.builder()
      .blueComponent(blueComponent)
      .redComponent(redComponent)
      .build()
      .inject(this);
  }
}
@ColorScope
@Component(dependencies = {BlueComponent.class, RedComponent.class})
public interface ColorComponent {

  void inject(Main main);
}

@Scope
@Retention(RUNTIME)
public @interface ColorScope {
}
//@BlueScope
@Component(modules = BlueComponent.BlueModule.class)
public interface BlueComponent {

  Blue blue();

  @Module
  class BlueModule {

//    @BlueScope
    @Provides
    Blue provideBlue() {
      return new Blue();
    }
  }
}

//@RedScope
@Component(modules = RedComponent.RedModule.class)
public interface RedComponent {

  Red red();

  @Module
  class RedModule {

//    @RedScope
    @Provides
    Red provideRed() {
      return new Red();
    }
  }
}

依存先 @Component 全てが @Scope ( @RedScope, @BlueScope ) を持っていると不可

:sample:compileDebugJavaWithJavac
/Users/shogo/dev/code/android/myrepos/android-tasting-dagger2/sample/src/main/java/com/sho5nn/sample/Main.java:5: エラー: シンボルを見つけられません
import com.sho5nn.sample.data.DaggerBlueComponent;
                             ^
  シンボル:   クラス DaggerBlueComponent
  場所: パッケージ com.sho5nn.sample.data
/Users/shogo/dev/code/android/myrepos/android-tasting-dagger2/sample/src/main/java/com/sho5nn/sample/Main.java:6: エラー: シンボルを見つけられません
import com.sho5nn.sample.data.DaggerRedComponent;
                             ^
  シンボル:   クラス DaggerRedComponent
  場所: パッケージ com.sho5nn.sample.data
/Users/shogo/dev/code/android/myrepos/android-tasting-dagger2/sample/src/main/java/com/sho5nn/sample/Main.java:9: エラー: シンボルを見つけられません
import com.sho5nn.sample.domain.DaggerColorComponent;
                               ^
  シンボル:   クラス DaggerColorComponent
  場所: パッケージ com.sho5nn.sample.domain
/Users/shogo/dev/code/android/myrepos/android-tasting-dagger2/sample/src/main/java/com/sho5nn/sample/domain/ColorComponent.java:10: エラー: @com.sho5nn.sample.domain.ColorScope com.sho5nn.sample.domain.ColorComponent depends on more than one scoped component:
@Component(dependencies = {BlueComponent.class, RedComponent.class})
^
      @com.sho5nn.sample.data.BlueScope com.sho5nn.sample.data.BlueComponent
      @com.sho5nn.sample.data.RedScope com.sho5nn.sample.data.RedComponent
エラー4個

depends on more than one scoped component


Android で複数非同期処理のシーケンス制御をライブラリに依存せず Pure Java で実装しなければならない場合の一案

もし、 Android 開発において、ライブラリに依存せず Pure Java を前提とし、複数の非同期処理をシーケンス制御する必要が出てきた場合に、どう解決すればよいか、の問題に対する一案を残した記録文章です。

背景

直近の仕事で、 Android 向け社内ライブラリの開発に携わっていたのですが、いくつかの社内事情があり、そのライブラリが依存するライブラリは appcompat-v7 のみという制約がありました。

f:id:sho5nn:20170506202536j:plain

あまり詳しくは書けないですが、そのライブラリは SSO を実現する機能を備えていて、いくつかのログインシーケンス制御があり、そのシーケンス中にあるそれぞれのステップが非同期処理で実行され、前の非同期処理の結果を元に次の非同期処理を行うような処理を実装する必要がありました。

最近のトレンドに習えば、 RxJava ライブラリなどを使用して精神的安定を維持しつつ開発・保守したいところですが、先述した制約のため Pure Java で実装するしかない状態でした。

その状態のまま開発した場合は言わずもがな、先行き怪しく最悪死人も待ったなしなのでどうにかしないとその先は地獄だぞという気持ちがあり、他のメンバーと議論したところ、 JavaScript の Promise からヒントを得てみてはどうだろうかということになり、 Promise like なインターフェースをまず実装し、そのインターフェースを用いてシーケンス制御の実装を進めました。

以下、そのときに実装した Promise like なインターフェースのコード(実際は、そのときに書いたコードを元に更に修正した)と、それを使用したサンプルコードを雑に残します。

参考

コード

基本的には Rx と似ていて、何らかのデータがどのようにして伝達していってその結果をどのように扱うか、のデータフローを先にコードで表現し、実行時に何らかのデータがそのデータフローに則って実行結果を取得します。

// sample
ExecutorService executor = Executors.newCachedThreadPool();

Promise
  .when(executor, Promise.single(new FooTask()))
  .then(new FulfillCallbackThenAll<String>() {
    @Override
    public PromiseTask.All onFulfilled(String value) {
      return Promise.all(new BarTask(), new BazTask());
    }
  })
  .then(new FulfillCallbackThenSingle<Object[], Void>() {
    @Override
    public PromiseTask.Single<Void> onFulfilled(Object[] value) {
      return Promise.single(new QuxTask());
    }
  }, new RejectCallbackDone<Throwable[]>() {
    @Override
    public void onRejected(Throwable[] value) {
      Toast.makeText(SimpleActivity.this, "failure", Toast.LENGTH_SHORT).show();
      progress.setVisibility(View.GONE);
    }
  }).atMain()
  .done(new FulfillCallbackDone<Void>() {
    @Override
    public void onFulfilled(Void value) {
      progress.setVisibility(View.GONE);
      updateViewWhenSuccess();
    }
  }, new RejectCallbackDone<Throwable>() {
    @Override
    public void onRejected(Throwable value) {
      progress.setVisibility(View.GONE);
      updateViewWhenFailure();
    }
  }).atMain();


// with lambda
Promise
  .when(executor, Promise.single(new FooTask()))
  .then((FulfillCallbackThenAll<String>) value -> Promise.all(new BarTask(), new BazTask()))
  .then(
    (FulfillCallbackThenSingle<Object[], Void>) value -> {
      return Promise.single(new QuxTask());
    },
    (RejectCallbackDone<Throwable[]>) value -> {
      Toast.makeText(SimpleActivity.this, "failure", Toast.LENGTH_SHORT).show();
      progress.setVisibility(View.GONE);
    }).atMain()
  .done(
    value -> {
      progress.setVisibility(View.GONE);
      updateViewWhenSuccess();
    },
    value -> {
      progress.setVisibility(View.GONE);
      updateViewWhenFailure();
    }).atMain();

非同期処理は Callable もしくは Runnable インターフェースで実装し、 Promise オブジェクトに渡します。

Promise.when() を呼び出して Promise オブジェクトを取得し、 Promise.then() でチェーンしつつ次の Promise オブジェクトを取得し、最後に Promise.done() を呼び出して最初の Promise オブジェクトの非同期処理を実行します。

チェーンする場合、 Promise.then() に渡すコールバックインターフェースの返り値として、次に実行したい非同期処理を表す Callable もしくは Runnable インターフェースオブジェクトを渡すことで、その次の Promise.then() に渡したコールバックインターフェースが呼ばれます。

Promise.single()/all()/race()

Callable もしくは Runnable を Promise オブジェクトに渡す際に、実行方法を指定します。

  • Promise.single(Callable/Runnable)
    • 1 つの非同期処理を実行する
  • Promise.all(Callable[]/Runnable[])
    • 全て成功した場合のみ成功時コールバックインターフェースを呼ぶ
    • どれか 1 つでも失敗した場合には失敗時コールバックインターフェースを呼ぶ
  • Promise.race(Callable[]/Runnable[])
    • どれか 1 つの非同期処理が成功/失敗に関係なく終了した時点で、成功時/失敗時コールバックインターフェースを呼ぶ

Promise.atMain()/at(Handler)

成功時/失敗時コールバックインターフェースの呼び出されるスレッドを指定します。 Promise オブジェクトごとに指定するので、 View 更新等、ステップごとに細かく制御が可能。

雑感

余程のことがないかぎりは、世に存在する便利なライブラリを使うべきなのは言うまでもなく、しかし、個人的にはまあまあ楽しめたとしとこうというのが本音だった。


Photo by Jamison McAndie | Unsplash

DroidKaigi 2017 で良いキッカケを

@sho5nn です。 DroidKaigi 2017 運営メンバーの一人として、協賛企業さんへの連絡や調整などを担当しています。

DroidKaigi は今回で 3 回目の開催。

僕は 1 回目は一般参加者として、2, 3 回目は運営メンバーとして参加しています。この記事では、運営メンバーとして DroidKaigi から得てほしいものと、運営メンバーとしてこういうことしてるよー的な内容を書いてみます。

DroidKaigi で良いキッカケを

f:id:sho5nn:20170215030124j:plain

DroidKaigi は Android の技術に特化した、エンジニアが主役のカンファレンスです。

登壇者、一般参加者はほぼエンジニア、運営スタッフもほぼエンジニアです。企業関係者さんもエンジニアさんが多いことでしょう。

そんな方たちが 1 年に 1 度、わいわいと集まってしまいます。それも 1000 人規模で。すごーい!

これだけの規模になれば、

  • 今まで知らなかった Android エンジニアと話すキッカケ
  • TwitterGitHub のアイコンだけは知ってるけど話したことない方と交流できるキッカケ
  • 聴いたセッションの内容をさっそく実践に導入してみようかなというキッカケ
  • 普段抱えているあの問題、他の方はどう解決してるんだろう?と色んな人に直接聞いて答えを探せるキッカケ
  • 最近流行ってるアレ、どうですか?と会話してトレンドの感触を掴めるキッカケ
  • 公式アプリ 、ここ修正したらもっと良くなるんじゃ?というコントリビュートのキッカケ

github.com

などなど、何かしらプラスになる良いキッカケがあることでしょう。あります。

是非 DroidKaigi に参加していただき、なんでもいいので何かの “キッカケ” を得て、

次なるステップへの糧にしてほしい、と思います。

スポンサー担当こういうことしてるよ

f:id:sho5nn:20170215030159j:plain

イベントがこれだけの規模となると、会場準備などでまとまったお金が必要になってきます。

DroidKaigi 2017 の準備に必要なお金は、チケット販売代金の他に、協賛企業さんからいただいた協賛金も合わせてやりくりしています。

スポンサー担当としては、協賛金含むスポンサープランの計画立案と各企業さんとのやり取りが主な運営作業です。やり取りの内容は、例えば、公式サイトに掲載する企業ロゴの提出依頼、企業ブース出展にともなう会場の準備とそのご案内、請求書の発行・送付などです。

それらのやり取りはだいたいメールで行っています。が、一応僕も Android エンジニア、普段メールとはあまり縁がないためか、文章を考えるだけで数十分かかったりしていました。

最近は、慣れもあってかそんなにかからなくなり、少し格好付けた言葉…例えば「承りました」の使いドコロをなんとなく掴めてきた気がします。社会人として圧倒的成長です。

今回は zendesk というカスタマーサポートツールを導入してみて、各企業さんからのご質問への回答や依頼メールの返信を行なっています。まだまだうまいこと回せず、知らないうちにチケットが溜まって アワワワ となってたりしますが、普通に生活していたら経験できない貴重な体験をしているんだなーという漠然とした気持ちで頑張っています、ハイ。

さいごに

参加チケットはまだ数十枚ほど余っています。まだ登録に迷っている方は是非この機会を逃さないように。 :pray:

droidkaigi.connpass.com