はじめに
こんにちは。これは Hamee Advent Calendar 2020 6日目の記事です。
これって何の話?
私の現在のお仕事は主に Android アプリの開発なのですが、その中では Dependency Injection (DI) を多用しています。
そのこと自体は今となってはそれほど珍しくないことだと思いますが、DI は少しとっつきにくい概念でもあるので、「DI って何がおいしいの?」とか「よくわからんけど DI が良いらしいから使うべ」という人もいるんじゃないかなと勝手に思ったりしています。
だって「Dependency Injection」でググっても概念的な解説が多くて、なかなか頭に入らないというか、分かったような分からないようなだったりしません?私だけかな。。
というわけでこの記事では、概念的な話からではなく、具体的な例から DI とは何なのかを紐解いてみようかなと思います。ついでに Android のいわゆるモダンな開発での活用方法みたいな話まで持っていこうと思います。
あ、具体的な例から解説していくと、どうしても網羅的な説明は難しくなってしまいます(DI は解説するなら○○も書くべきだ的な)。そこはどうかご理解いただきたいです。。。1
例題1: 円柱の体積を計算する機能
「半径と高さから円柱の体積を計算する」アプリを作ることを考えてみたいと思います。
機能の分解
昨今のソフトウェア開発では複雑な機能をなるべく単純な機能に分解して開発していくのが一般的です。
今回の例題では
- 機能A : 半径と高さから円柱の体積を計算する
という機能が求められていますが、この機能は少し複雑な気がしないでもないような気がしますので(異論は認めない!)2、以下の2つに機能に分解しましょう。
- 機能B : 半径から円の面積を計算する
- 機能C : 面積と高さから体積を計算する
コードにするとこんな感じでしょうか。
object CylinderVolumeCalculator {
// 機能A: 半径と高さから円柱の体積を計算する
fun calcCylinderVolume(radius: Double, height: Double): Double {
return calcVolume(calcCircleArea(radius), height)
}
// 機能B: 半径から円の面積を計算する
private fun calcCircleArea(radius: Double): Double {
return radius * radius * Math.PI
}
// 機能C: 面積と高さから体積を計算する
private fun calcVolume(area: Double, height: Double): Double {
return area * height
}
}
このコードは問題なく動くはずです。本体プログラムから CylinderVolumeCalculator.calcCylinderVolume()
静的メソッドを呼べば正しく動作するでしょう。ですがこういう書き方は、以下のような観点からあまりよろしくないと言われています。
- 問題1: 分解された機能B、機能Cが非公開のためテストできない。
- 問題2: 公開されている機能Aもテストしにくい。
問題1: 分解された機能ごとのテストができない
複雑さを嫌って機能を分解したのですから、単体テストも分解された単純な機能ごとに行うべきでしょう。それなのに機能Bと機能Cのメソッドが非公開(private
フィールド)になっていてはテストのしようがありません。まずはこれらを公開メソッドにしてあげましょう。
object CylinderVolumeCalculator {
// 機能A: 半径と高さから円柱の体積を計算する
fun calcCylinderVolume(radius: Double, height: Double): Double {
return calcVolume(calcCircleArea(radius), height)
}
// 機能B: 半径から円の面積を計算する
fun calcCircleArea(radius: Double): Double {
return radius * radius * Math.PI
}
// 機能C: 面積と高さから体積を計算する
fun calcVolume(area: Double, height: Double): Double {
return area * height
}
}
これで機能B, Cの単体テストは問題なく書けるでしょう。
問題2: 公開されている機能Aもテストしにくい
calcCylinderVolume()
メソッドは元から公開されていますからテストを書くことはできます。が、ちょっと厄介な問題を抱えています。
このメソッドの機能は、本来的には「入力された半径と高さから円柱の体積を計算して返却する」ですが、機能を分解した結果、単体としての機能(スペック)は以下のように変わっています。
- 機能Bに半径を渡して円の面積を受け取る。
- 機能Cに面積と高和を渡して体積を受け取る。
- 受け取った体積を返却する。
どういうことかというと、仮に機能Bの実装にミスがあって円の面積が間違っていたとしてもそれは機能B側の問題であって機能Aの問題ではありません。同様に機能Cの実装にミスがあって体積が間違っていたとしてもそれは機能Cの問題であって機能Aの問題ではありません。機能Aの役割はあくまで、受け取ったデータを機能Bや機能Cに渡したり受け取ったり返却したりすることだけです。言い換えれば、機能Aの単体テストは、受け取ったデータを正しく機能Bや機能Cに渡したり返却したりできているかだけを確認できれば良いことになります(もちろん機能Bと機能Cは十分にテストされていることが前提となります)。
このような場合、機能B、機能Cの想定される正しい挙動を模したモックを用意して機能Aのテストを行うのが一般的です。しかし calcCylinderVolume()
の実装は、calcCircleArea()
メソッドや calcVolume()
メソッドを直接呼んでしまっているため、モックの使いようがありません。どうしたものでしょうか?
モックを使えるようにする
機能B、機能Cのモックを作成し、また使えるようにするには、一般に各機能のインタフェースを用意します。3
// 機能Bのインタフェース
interface CalcCircleAreaFunction {
companion object {
fun newInstance(): CalcCircleAreaFunction = CalcCircleAreaFunctionImpl()
}
fun calcCircleArea(radius: Double): Double
}
// 機能Bの実装
class CalcCircleAreaFunctionImpl : CalcCircleAreaFunction {
override fun calcCircleArea(radius: Double): Double {
return radius * radius * Math.PI
}
}
// 機能Cのインタフェース
interface CalcVolumeFunction {
companion object {
fun newInstance(): CalcVolumeFunction = CalcVolumeFunctionImpl()
}
fun calcVolume(area: Double, height: Double): Double
}
// 機能Cの実装
class CalcVolumeFunctionImpl : CalcVolumeFunction {
override fun calcVolume(area: Double, height: Double): Double {
return area * height
}
}
こんな感じですね。ちなみに各インタフェースにファクトリメソッド(newInstance()
)を用意しているのは、これらの機能を利用する側が実装クラス(CalcCircleAreaFunctionImpl
とCalcVolumeFunctionImpl
)を意識しなくて済むようにするためです。こうすれば、機能Aはとりあえず以下のように書き換えることができます。
// 機能Aの実装(円柱の体積を計算する)
fun calcCylinderVolume(
radius: Double,
height: Double,
calcCircleAreaFunction: CalcCircleAreaFunction = CalcCircleAreaFunction.newInstance(),
calcVolumeFunction: CalcVolumeFunction = CalcVolumeFunction.newInstance()
): Double {
return calcVolumeFunction.calcVolume(calcCircleAreaFunction.calcCircleArea(radius), height)
}
機能Aのメソッド calcCylinderVolume()
に引数を追加して機能B、機能Cの関数オブジェクトを受け取れるように拡張しています。デフォルトで各機能の通常の実装が渡されるようになっているため calcCylinderVolume()
メソッドの使い方に変化はありませんが、単体テストではこれらの引数に各機能のモックオブジェクトを渡すことで calcCylinderVolume()
メソッドをテストできます。たとえば以下のように書けるでしょう。45
class CylinderVolumeCalculatorTest {
@Test
fun calcCylinderVolume_success() {
// テストで使うパラメータ
val requestRadius = 2.0
val requestHeight = 3.0
// 機能Bのモックが返す値
val expectArea = requestRadius * requestRadius * 3.14
// 機能Cのモックが返す値
val expectVolume = expectArea * requestHeight
// 機能Bのモック
val calcCircleAreaFunctionMock = object : CalcCircleAreaFunction {
override fun calcCircleArea(radius: Double): Double {
// 機能Aは機能Bに対して requestRadius の値を渡していなければならない
assertThat(radius, `is`(requestRadius))
return expectArea
}
}
// 機能Cのモック
val calcVolumeFunctionMock = object : CalcVolumeFunction {
override fun calcVolume(area: Double, height: Double): Double {
// 機能Aは機能Cに対して requestHeight の値を渡していなければならない
assertThat(height, `is`(requestHeight))
// 同様に機能Aは機能Cに、機能Bが返した expectArea の値を渡していなければならない
assertThat(area, `is`(expectArea))
return expectVolume
}
}
// テスト対象のメソッドを実行する
// その際、機能B、機能Cのモックを渡す
val volume = CylinderVolumeCalculator.calcCylinderVolume(
requestRadius,
requestHeight,
calcCircleAreaFunctionMock,
calcVolumeFunctionMock
)
// 機能Aは機能Cが返した expectVolume の値を返却しなければならない
assertThat(volume, `is`(expectVolume))
}
}
依存する機能の「注入」とは
ここでもう一度、機能A(calcCylinderVolume
メソッド)の実装を見てみましょう。
// 機能Aの実装(円柱の体積を計算する)
fun calcCylinderVolume(
radius: Double,
height: Double,
calcCircleAreaFunction: CalcCircleAreaFunction = CalcCircleAreaFunction.newInstance(),
calcVolumeFunction: CalcVolumeFunction = CalcVolumeFunction.newInstance()
): Double {
return calcVolumeFunction.calcVolume(calcCircleAreaFunction.calcCircleArea(radius), height)
}
機能Aは機能B(calcCircleAreaFunction
)と機能C(calcVolumeFunction
)を、引数として受け取り、そしてそれらを利用して、自分自身の機能を実現しています。機能Aは機能Bと機能Cに依存していて、それら「依存する機能」が外部から送り込まれているようにも見えます。そのためこのような設計パターンは Dependency Injection (DI) と呼ばれています(個人的にはこの名称はあまり好きではないのですが…)。
ただし、一般に DI と言った場合、上記の実装のように依存する機能を直接メソッドに渡すことはあまりありません。というのも、機能Aを使う側(calcCylinderVolume
メソッドを呼び出す側)から見ると、本来なら半径と高さの2つのパラメータさえ渡せば良いはずのメソッドなのに、なんだかよく分からない引数を余計に渡さないといけないように見えてしまうからです。それに第3引数と第4引数を渡さなかった場合、つまりデフォルト値が利用された場合、CalcCircleAreaFunctionImpl
クラスとCalcVolumeFunctionImpl
クラスがインスタンス化されてしまうので、機能Aが連続して呼ばれるようなシーンではパフォーマンス面でも問題になるかも知れません。
そのため依存する機能はメソッドに直接渡すのではなく、そのメソッドが属するクラスに渡すのが一般的だと思います。
class CylinderVolumeCalculator(
private val calcCircleAreaFunction: CalcCircleAreaFunction = CalcCircleAreaFunction.newInstance(),
private val calcVolumeFunction: CalcVolumeFunction = CalcVolumeFunction.newInstance()
) {
// 機能A: 半径と高さから円柱の体積を計算する
fun calcCylinderVolume(radius: Double, height: Double): Double {
return calcVolumeFunction.calcVolume(calcCircleAreaFunction.calcCircleArea(radius), height)
}
}
この例ではクラスのコンストラクタで依存する機能を渡すようにしていますが、クラスがインスタンス化されたあとに渡すようにすることもあります。
ちなみにこうした場合、テストコードは以下のように書き換える必要があります(変更箇所のみ抜粋)。
// テスト対象のメソッドを実行する
// その際、機能B、機能Cのモックを渡す
val volume = CylinderVolumeCalculator(calcCircleAreaFunctionMock, calcVolumeFunctionMock)
.calcCylinderVolume(requestRadius, requestHeight)
例題2: BGM の再生と停止をする機能
次に、もう少し Android アプリ開発っぽい例を考えてみましょう。
アプリ内で BGM を再生したり停止したりする機能を作成してみようと思います。
ざっと実装
Android で音楽を再生するにはMediaPlayer
クラスを使うのが一般的だと思います。そこでMediaPlayer
クラスを簡単に使うためのラッパークラスを書いてみましょう。
interface BgmPlayer {
companion object {
fun newInstance(context: Context): BgmPlayer =
BgmPlayerImpl(context)
}
// 再生
fun play()
// 停止
fun stop()
}
class BgmPlayerImpl(context: Context) : BgmPlayer {
private val mediaPlayer = MediaPlayer.create(context, R.raw.battle_bgm).apply {
isLooping = true
}
// 再生
override fun play() {
mediaPlayer.start()
}
// 停止
override fun stop() {
mediaPlayer.stop()
}
}
BgmPlayerImpl
クラスは問題なく動作するはずです。ですがこの実装にはいくつか問題があります。一つずつ見ていきましょう。
問題1: BgmPlayerImpl
クラス自体をテストしにくい
これは例題1でも問題になった「依存する機能を内包してしまっている」ことに起因します。今回の例題ではBgmPlayerImpl
クラスが内部的にMediaPlayer
クラスに依存しており、かつ private メンバとして保持しているためモックに置き換えることができません。そこで例題1でやったのと同じように、MediaPlayer
をコンストラクタで渡せるようにしてみましょう。
interface BgmPlayer {
companion object {
fun newInstance(mediaPlayer: MediaPlayer): BgmPlayer =
BgmPlayerImpl(mediaPlayer)
fun newInstance(context: Context): BgmPlayer =
BgmPlayerImpl(
MediaPlayer.create(context, R.raw.battle_bgm).apply {
isLooping = true
}
)
}
// 再生
fun play()
// 停止
fun stop()
}
class BgmPlayerImpl(
private val mediaPlayer: MediaPlayer
) : BgmPlayer {
// 再生
override fun play() {
mediaPlayer.start()
}
// 停止
override fun stop() {
mediaPlayer.stop()
}
}
BgmPlayerImpl
クラスのコンストラクタはMediaPlayer
を受け取るようにして、「Context
からMediaPlayer
を生成する」という処理はファクトリメソッド側で行うよう修正しました。6
これでBgmPlayerImpl
クラスにMediaPlayer
を注入できるようになりましたね。
こうすればBgmPlayerImpl
クラスの単体テストは、たとえば以下のように書けそうです。
class BgmPlayerTest {
@Test
fun start_and_stop_success() {
var isStartCalled = false
var isStopCalled = false
// MediaPlayer のモックを作る
val mediaPlayerMock = object : MediaPlayer() {
override fun start() {
isStartCalled = true
}
override fun stop() {
isStopCalled = true
}
}
// テスト対象のクラスをインスタンス化する
// ただし MediaPlayer にはモックを使う
val bgmPlayer = BgmPlayerImpl(mediaPlayerMock)
// play メソッドのテスト: MediaPlayer の start メソッドが呼ばれること
bgmPlayer.play()
assertTrue(isStartCalled)
// stop メソッドのテスト: MediaPlayer の stop メソッドが呼ばれること
bgmPlayer.stop()
assertTrue(isStopCalled)
}
}
問題2: 複数個所から呼ばれる場合どうするか
BGM の再生/停止は、アプリの色々な箇所から呼ばれる可能性があります。画面が複数あるアプリでは、たとえば画面Aで BGM の再生を開始し、画面Bに遷移してから BGM を停止するということも十分考えられます。そのような場合、たとえば以下のように実装しても良いものでしょうか?
private val bgmPlayer: BgmPlayer by lazy { BgmPlayer.newInstance(requireContext()) }
// 画面Aの Fragment で BGM 再生開始
private fun playBgm() {
bgmPlayer.play()
}
private val bgmPlayer: BgmPlayer by lazy { BgmPlayer.newInstance(requireContext()) }
// 画面Bの Fragment で BGM 停止
private fun stopBgm() {
bgmPlayer.stop()
}
現在の Android アプリの開発では、多くの場合、画面ごとに異なる Fragment を用意してその Fragment ごとに動作を制御するのが一般的です。上記の例でもそれに倣って、画面Aの Fragment (FragmentA
) と画面Bの Fragment (FragmentB
) にそれぞれ再生と停止のコードを入れてみました。ですがこれでは正しく動作しません。なぜならそれぞれの箇所でBgmPlayerImpl
クラスの新しいインスタンスを生成してしまっているためです。正常に動作させるにはFragmentA
とFragmentB
で同じインスタンスを共有する必要があります。異なる Fragment 間でオブジェクトを共有するにはどうすれば良いでしょうか?
その一つの方法として、共有したいオブジェクトをApplication
クラスの派生クラスに保持させることが考えられます7。たとえば以下のようにします。
// アプリ内で共有するオブジェクトのコンテナ
class DIContainer(context: Context) {
val bgmPlayer: BgmPlayer by lazy { BgmPlayer.newInstance(context) }
}
class MainApplication : Application() {
lateinit var diContainer: DIContainer
private set
override fun onCreate() {
super.onCreate()
diContainer = DIContainer(applicationContext)
}
}
こうしておけば、各 Fragment で同一インスタンスの BgmPlayer
を以下のように取得できます。
class FragmentA : Fragment() {
private lateinit var diContainer: DIContainer
private lateinit var bgmPlayer: BgmPlayer
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
diContainer = (requireActivity().application as MainApplication).diContainer
bgmPlayer = diContainer.bgmPlayer
}
}
これが DI コンテナを使った DI となります。これまで見てきた、コンストラクタを介しての「機能の注入」とは少し勝手が違いますが、「特定の機能を外部から受け取っている」という意味で同じ意味合いを持ちます。
Hilt の導入
ここまで DI の基本的な考え方を書いてきました。単体テストを書く上で多くのメリットがある手法であることは確かなようですね。
その一方で DI を導入するには少し面倒くさいコードを書かなければならないというデメリットがあることも分かったと思います。例題2で見たDIContainer
の実装と、それを使えるようにするためのApplication
クラスやFragment
へのコードの追加がそれです。
ですがこれらの面倒くさいコードは、便利な DI コンテナライブラリを導入することで多くの部分を省略できるようになります。ここでは Hilt というライブラリを導入して例題2のコードを書き換えてみます。
なお、この記事では Hilt のさわり程度しか解説しません。より詳しく知りたい方はこちら等を参考にしてみてください。
Hilt の初期設定
まずは開発環境に Hilt を組み込みます。
buildscript {
dependencies {
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
}
}
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation "com.google.dagger:hilt-android:2.28-alpha"
kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}
Application
クラスにアノテーションを付ける
次にApplication
クラス(の派生クラス)に@HiltAndroidApp
のアノテーションを付けます。
また Hilt の導入によって自前の DI コンテナは不要になるためDIContainer
のインスタンス化やそれを保持するためのインスタンス変数は不要になります。つまりMainApplication
クラスの定義は以下だけで十分となります。
@HiltAndroidApp
class MainApplication : Application()
Activity
やFragment
にもアノテーションを付ける
さらにActivity
やFragment
(の派生クラス)に@AndroidEntryPoint
のアノテーションを付けます。
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
// 省略
}
@AndroidEntryPoint
class FragmentA : Fragment() {
// 省略
}
@AndroidEntryPoint
class FragmentB : Fragment() {
// 省略
}
Hilt モジュールを作る
Hilt を使うには DI して使いたいクラス、つまり例題2でいうBgmPlayer
のインスタンス化の方法を Hilt に知らせる必要があります。そのためには Hilt モジュールと呼ばれる object を定義する必要があります。この object は名前は何でも構いませんが@Module
, @InstallIn
のアノテーションを付ける必要があります。また@Provides
アノテーションを付けたメソッドを定義する必要もあります。
@Module
@InstallIn(ApplicationComponent::class)
object BgmPlayerModule {
@Provides
fun provideBgmPlayer(
@ApplicationContext context: Context
): BgmPlayer = BgmPlayer.newInstance(context)
}
これでBgmPlayer
の機能が要求されたときのBgmPlayer
のインスタンス化の方法を Hilt に教えることができました。
BgmPlayerImpl
クラスをシングルトンにする
上記の設定でBgmPlayer
のインスタンス化の方法を規定できましたが、このままだとBgmPlayer
を注入するたびに新しいBgmPlayerImpl
インスタンスが生成されてしまいます。例題2でも見たように、このクラスは複数個所から同じインスタンスを使われる想定になっています。つまりシングルトンである必要があります。
Hilt ではシングルトンにしたいクラスに@Singleton
アノテーションを付けるだけでそれを実現できます。
@Singleton
class BgmPlayerImpl(
private val mediaPlayer: MediaPlayer
) : BgmPlayer {
// 省略
}
これでお膳立ては整いました。
BgmPlayer
の注入
以上で Hilt を使ってBgmPlayer
を DI できるようになりました。
たとえば例題2で見たのと同じように Fragment でBgmPlayer
を使いたい場合は以下のように注入できます。
@AndroidEntryPoint
class FragmentA : Fragment() {
// BgmPlayerを注入する
@Inject
lateinit var bgmPlayer: BgmPlayer
// BGMを再生する
fun playBgm() {
bgmPlayer.play()
}
}
インスタンス変数bgmPlayer
は初期化されていないように見えますが、@Inject
アノテーションを付けることで Hilt が自動的に初期化してくれます。その際 Hilt モジュール(上で定義したBgmPlayerModule
object)が参照され、適切にBgmPlayerImpl
クラスがインスタンス化されます。あるいは、すでにBgmPlayerImpl
のインスタンスが存在する場合はそれが利用されます(@Singleton
アノテーションがあるため)。
例題2では DI の仕組みを自前で実装しましたが Hilt を使えば「お作法」に従って書いていくだけで済みます。自分でごちゃごちゃ考える必要がなくなりますし、コードもすっきりして見通しが良くなります。例題2でやった自前実装とほぼ同じことを Hilt が自動的にやってくれているわけですね。
おまけ:ViewModel への注入
MVVM で開発する場合は ViewModel に注入することも可能です。
android {
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation "androidx.fragment:fragment-ktx:1.2.5"
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02'
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02'
}
class AViewModel @ViewModelInject constructor(
// BgmPlayerを注入する
private val bgmPlayer: BgmPlayer
) : ViewModel() {
// BgmPlayerを使ってBGMを再生する
fun playBgm() {
bgmPlayer.play()
}
}
@AndroidEntryPoint
class FragmentA : Fragment() {
private val viewModel: AViewModel by viewModels()
// AViewModel#playBgm()メソッドを呼んでBGMを再生する
fun playBgm() {
viewModel.playBgm()
}
}
おわりに
初めて Dependency Injection という言葉を耳にしたとき皆さんはどう感じたでしょうか?「はぁ?なんだそりゃ?」と思った人が多いのではないでしょうか?私もそのうちの一人です。私は中毒患者じゃないぞ。
DI のとっつきにくさはその名称にも原因があるんじゃないかなと思っています。依存性の注入…本当になんだそりゃですよ。。。
なのでこの記事では DI という言葉に振り回されないよう、どんなところからこういう考え方が生まれたのかという「ことの発端」みたいなところから書いてみたつもりです。またその話を「ふーん、概念はなんとなく分かった」で終わらせず、最新のモダンな開発で活かせるよう Hilt の使い方まで引っ張りました。執筆時間をあまり取れなかったのでだいぶ端折ってしまいましたが、参考にしてくれる人が少しでもいてくれたら嬉しいです。
-
マサカリはいったん床に置きましょう! ↩
-
言うまでもないと思いますが、こんな簡単な処理をわざわざ分解するなんて、普通はやらないと思います。 ↩
-
Kotlin ならインタフェースじゃなく高階関数を使えば良いんじゃない?というツッコミが飛んできそうですが、はい、この例ではそうですよね。ですがモック化する対象のオブジェクトは通常、複数のメソッドを持っていますので、インタフェースを使うということでご理解いただきたく…(例が悪いという説) ↩
-
この記事ではモックをすべて自前で実装していますが Mockito や MockK を使えばもっと簡単に実装できます。 ↩
-
このテストコード、「円柱の体積の計算なんてめっちゃ単純な処理のためにこんな面倒なテスト書かなきゃいけないんかい!」というツッコミをもらえそうですね。もちろんこれは「DI とは何か」を解説するために単純な処理を題材にしたからなのですが、ある意味、「必要以上に機能を分解してもデメリットが多い」ことを示す例にもなっていますね。 ↩
-
「ContextからMediaPlayerを生成する」処理を
BgmPlayerImpl
のセカンダリコンストラクタとして実装する方法もありますが、「MediaPlayerの生成処理はテスト対象外である」ことが明示的になるようファクトリメソッド側に書いてしまうのが個人的には好みです。 ↩ -
アプリ全体で共有するオブジェクトを Application クラスに保持させる理由は以前書いた記事をご参照いただければ! ↩