Interfaceの実体クラスにまたInterfaceがある場合のDI(Dagger2)
背景
以下の構成を考える。
ドメイン層は他の層と依存関係を持たないが、ドメインのメソッドで通信を行いたい。
そのためにinterfaceを設定してリポジトリ層と疎結合にする。
ただしリポジトリ層でも通信をする場合とテスト用にローカルファイルを返却する場合で切り替えたい。
そのためInterfaceの実体クラス内にまたInterfaceがある状況になっている。
この構成時にDagger2でDIしたい。
環境
- Android Studio 3.2
- kotlin 1.3.11
- Dagger2.19
実装
以下の4クラスを実装する
RepositoryComponent
package c.offwhite.novel.di import c.offwhite.novel.domain.ISearchRepository import dagger.Component @Component(modules = [RepositoryModule::class, ApiModule::class]) interface RepositoryComponent { fun inject(): ISearchRepository }
RepositoryModule
@Module class RepositoryModule { @Provides fun provideNarouRepository(narouApi: INarouOpenApi): ISearchRepository { return NarouRepository(narouApi) } }
ApiModule
@Module class ApiModule { @Provides fun provideNarouApi(narouOpenApi: NarouOpenApi): INarouOpenApi { return narouOpenApi } }
class WebSite (val domain: String) { @Inject lateinit var searchRepository: ISearchRepository fun search(word: String): List<NovelIntroduction> { searchRepository = DaggerRepositoryComponent.create().inject() return searchRepository.search(word) } }
解説
Dagger2が依存性を解決できないパターンが複数存在する - Interface(何が具体化されるかわからないため) - サードパーティ製のクラス(@Injectが書いてないため)
これらのDagger2だけで自動的に解決できない依存性を満たすために、こちらから供給するクラス(Factoryクラス)を指定する必要がある。
図のように「もしISearchRepositoryというinterfaceが登場したら、NarouRepositoryを注入する」といった形である。
供給するクラスを指定するクラスをmoduleと呼び、以下のように定義する。
@Module class RepositoryModule { @Provides fun provideNarouRepository(narouApi: INarouOpenApi): ISearchRepository { return NarouRepository(narouApi) } }
モジュールクラスには@Moduleを記述し、供給メソッドには@Providesを記述する。
@Providesで指定するメソッドは対象のinterfaceを返却するメソッドとし、注入する実体クラスを指定する。
もし供給するRepositoryを切り替えたくなった場合はここのreturnの値を切り替えれば良い。
今回のケースでは実体クラスNarouRepositoryはApIインスタンスを注入する必要がある。 もちろんここで以下のように実体クラスを指定しても良い。
実体クラスを指定した場合
@Module class RepositoryModule { @Provides fun provideNarouRepository(): ISearchRepository { return NarouRepository(NarouOpenApi()) } }
しかし今回はApiインスタンスも条件によって切り替えたいため、このモジュール一つにまとめてしまうと責務が分割できない。 よってここのパラメータをInterface指定としている。 こうなるとまたこのInterfaceの依存を指定する必要があるので、Apiクラスのモジュールも作成する。
ApiModule
@Module class ApiModule { @Provides fun provideNarouApi(narouOpenApi: NarouOpenApi): INarouOpenApi { return narouOpenApi } }
あとはComponentクラスに「依存注入がわからなくなったらこのmoduleを参照するように」と指定するだけで良い。
RepositoryComponent
package c.offwhite.novel.di import c.offwhite.novel.domain.ISearchRepository import dagger.Component @Component(modules = [RepositoryModule::class, ApiModule::class]) interface RepositoryComponent { fun inject(): ISearchRepository }
以下の@Coomponent(modules = ...)が該当箇所にあたる。ここはリストでの指定が必要なので一つでもリスト形式で記述する。 この指定によって、Daggerが依存性を解決できなかった時に参照する先を指定できる。
@Component(modules = [RepositoryModule::class, ApiModule::class])
あとは以下のドメインクラスにて指定して利用するだけになる。
class WebSite (val domain: String) { @Inject lateinit var searchRepository: ISearchRepository fun search(word: String): List<NovelIntroduction> { searchRepository = DaggerRepositoryComponent.create().inject() return searchRepository.search(word) } }
これでInterfaceの実体クラス内にまたinterfaceがある場合でもDIできた。
個人的に詰まったところ
- kolint-kapt がないとkotlinがjavaのannotation processingをサポートしてくれないので、DaggerComponentが作成されない。 そのためgradleに以下の記述が必須。
apply plugin: 'kotlin-kapt'
Android開発のマージリクエストで最低限確認するべき項目まとめ
このドキュメントについて
本ドキュメントはAndroid開発において最低限確認するべき項目をまとめたものです。
作成理由
マージリクエスト時のレビューは基本的にはレビューアの技術力に左右されるものだと思いますが、 よく忙しさやモチベーションに比例して指摘できる事項が異なってきたりしてしまいます。
そのため毎回必ず確認すべき項目についてチェックリストを作成することにしました。
コードレビューでチェックする観点は多数存在するのですが、 あまりにチェック項目が多すぎるとレビューコストが高くなり、おざなりになりがちです。 そのため今回は必ずチェックすべき項目のみピックアップしたつもりです。
レビュー項目
設計確認
- [ ] 設計(要件)を満たすように実装されていること
- [ ] 設計や要件に記載がない箇所についてはTODOが残されていること
UI
- [ ] 各解像度でレイアウトが確認されていること(スクショが上がってなかったら貼ってもらう)
コーディング
- [ ] 重複したコードが存在しないこと
- [ ] クラスの責務が明確になっていること(メソッドが存在するクラスは適切に選択されていること)
- [ ] メソッド名から想定できない処理をメソッド内で実施していないこと
- [ ] 一目でわからない処理をする場合は"目的(理由)"や"思想"についてのコメントが書かれていること
- [ ] 不要なコード/リソースが残っていないこと
-
- [ ] クラス名・変数名・定数名・メソッド名が適切に命名されていること
RecyclerViewにLiveDataをDataBindingしたい
状況
- List型のデータを取得し、画面にRecyclerViewで表示させたい。
- だけどobserve書きたくないし、RecyclerViewのAdapterクラスをViewModelで持ちたくないし。
- LiveDataに値をpostしたらいい感じに紐づいてくれないだろうか
実装
以下のように実装した。 主に4つのクラスで構成される。
- ViewModel
- Fragment
- RecyclerView.Adapter
- Fragmentのxml
ViewModel
/** * メイン画面のViewModel */ class MainViewModel constructor(private val showNovelListUseCase: ShowNovelListUseCase) : ViewModel() { // 小説リスト val novelList = MutableLiveData<List<NovelIntroduction>>() // onCreate時に呼び出す想定 fun onCreate() = GlobalScope.launch { val novel = showNovelListUseCase.getNovelList("") novelList.postValue(novel) } // ViewModel()にDIする場合はFactoryクラスを作成する必要がある。 class Factory(private val showNovelListUseCase: ShowNovelListUseCase) : ViewModelProvider.NewInstanceFactory() { override fun <T : ViewModel?> create(modelClass: Class<T>): T { return MainViewModel(showNovelListUseCase) as T } } }
Fragment
class MainFragment : Fragment() { companion object { fun newInstance() = MainFragment() } // ViewModel private lateinit var viewModel: MainViewModel // binding private lateinit var binding: MainFragmentBinding override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { // レイアウトをinflateする binding = DataBindingUtil.inflate(inflater, R.layout.main_fragment, container, false) // LiveDataとxmlをbindする binding.setLifecycleOwner(this) // inflateしたViewを返却する return binding.root } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) // viewModel作成 viewModel = ViewModelProviders .of(this, MainViewModel.Factory(ShowNovelListUseCase())) .get(MainViewModel::class.java) // 生成したviewModelをbindする binding.viewModel = viewModel // 横向き val manager = LinearLayoutManager(context) manager.orientation = LinearLayoutManager.HORIZONTAL binding.recyclerView.layoutManager = manager binding.recyclerView.adapter = NovelListAdapter() viewModel.onCreate() } }
RecyclerView.Adapter
class NovelListAdapter : RecyclerView.Adapter<NovelListAdapter.ViewHolder>() { // 表示アイテム private var novelList: List<NovelIntroduction> = emptyList() override fun onBindViewHolder(holder: ViewHolder, position: Int) { if (holder is ItemViewHolder && novelList.size > position) { holder.bind(novelList[position]) } } override fun getItemCount(): Int { return novelList.size } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NovelListAdapter.ViewHolder { return ItemViewHolder(parent) } /** * データをセットしてアップデートする */ fun update(novelList: List<NovelIntroduction>) { this.novelList = novelList notifyDataSetChanged() } abstract class ViewHolder(view: View) : RecyclerView.ViewHolder(view) class ItemViewHolder( private val parent: ViewGroup, private val binding: MainItemBinding = DataBindingUtil.inflate( LayoutInflater.from(parent.context), R.layout.main_item, parent, false ) ) : ViewHolder(binding.root) { fun bind(item: NovelIntroduction) { binding.item = item } } companion object { @JvmStatic @BindingAdapter("items") fun RecyclerView.bindItems(items: List<NovelIntroduction>?) { // まだ情報が取得できていない場合はitemsがnullになる可能性があるため、nullチェック必須。 if (items == null) { return } // RecyclerView.Adapterを継承しているので、RecyclerViewに設定されているadapterを取得できる val adapter = adapter as NovelListAdapter adapter.update(items) } } }
main_fragment.xml
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <variable name="viewModel" type="c.offwhite.sampledi.ui.main.MainViewModel"/> </data> <android.support.constraint.ConstraintLayout android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ui.main.MainFragment"> <android.support.v7.widget.RecyclerView android:id="@+id/recyclerView" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent" app:items="@{viewModel.novelList}"/> </android.support.constraint.ConstraintLayout> </layout>
解説
まずはLiveDataの設定。ここは通常のLiveDataのコーディングと同じである。 showNovelListUseCase.getNoveList()は通信が発生する可能性があるのでバックグラウンドでの実行とし、LiveDataはpostValueで値を設定する
ViewModel
// 小説リスト val novelList = MutableLiveData<List<NovelIntroduction>>() // onCreate時に呼び出す想定 fun onCreate() = GlobalScope.launch { val novel = showNovelListUseCase.getNovelList("") novelList.postValue(novel) }
次にxmlのRecyclerViewにDataBindingを追加する。app:items="@{viewModel.novelList}"の行がそれにあたる。
main_fragment.xml
<android.support.v7.widget.RecyclerView android:id="@+id/recyclerView" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent" app:items="@{viewModel.novelList}"/>
しかし、本来RecyclerViewにList
自作セッターの定義方法は@BindingAdapter("対象のattribute")を指定することで可能。今回はapp:itemsに対する自作セッターのため、@BindingAdapter("items")となる。
なお、staticメソッドでないとダメな模様。
RecyclerView.Adapter
companion object { @JvmStatic @BindingAdapter("items") fun RecyclerView.bindItems(items: List<NovelIntroduction>?) { // まだ情報が取得できていない場合はitemsがnullになる可能性があるため、nullチェック必須。 if (items == null) { return } // RecyclerView.Adapterを継承しているので、RecyclerViewに設定されているadapterを取得できる val adapter = adapter as NovelListAdapter adapter.update(items) } }
これでViewModel.novelListが更新されるたびに adapter.update()が呼び出されるため、adapter内のデータ変更が通知される。
RecyclerView.Adapter
/** * データをセットしてアップデートする */ fun update(novelList: List<NovelIntroduction>) { this.novelList = novelList notifyDataSetChanged() }
はまったところ
Android DataBinding error. Could not find accessorが発生
- apply plugin: 'kotlin-kapt'がないと@BindingAdapterが動かない。しかしDataBindingはなくても大丈夫という罠。
Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter itemsが発生
- @BindingAdapter(items)を指定していたRecyclerView.bindItemsのパラメータをnon-nullにしていたが、非同期処理中にnullのLiveDataをbindしてしまい、non-nullを違反してしまっていた。nullableにしてnullチェックを導入することで回避している。
kotlinでSmart cast to xxx is impossible because response.body() is a complex expressionが発生した場合
状況
Retrofitを使った以下のソースコードでスマートキャストできないとエラーが発生した。
override fun search(word: String): List<NovelIntroduction> { val response = searchService.getNovelList(word).execute() if (response.isSuccessful && response.body()!=null) { return response.body().mapNotNull { r -> NovelIntroductionTranslator().toNovelIntroduction(r) } // <-- Smart cast impossible } }
発生しているエラー
Smart cast to xxx is impossible because response.body() is a complex expression
response.body()はList
対応方法
とりあえず!!をつければエラーは消えるのだが、せっかくのkotlinなのにnull pointer exceptionが発生しうる状況は避けたい。
smat castは同じプロパティに対して何回アクセスしても同じ値を返すことが保証されている場合可能らしい。
つまりvarであったらそもそもsmart castできない。
RetrofitのResponseクラスを覗いてみると以下のようにコーディングされていた。
private final @Nullable T body; ・ ・ ・ public @Nullable T body() { return body; }
よく考えたらRetrofitはjavaでコーディングされているのでvalでない。 (javaでいくらfinalがついていてもListであればimmutableにしない限り内容書き換えられるのでたぶんダメ)
ということは一旦response.body()をvalに移し変えてからnullチェックを行えばよい。
if (response.isSuccessful) { val body = response.body() if (body!= null) return body.mapNotNull { r -> NovelIntroductionTranslator().toNovelIntroduction(r) } }
これでSmart Castできた。
APIレスポンス(json)が一項目目と二項目目以降でレスポンス項目が異なって困った話
状況
- 利用しようとしたAPIレスポンス(json)が一項目目と二項目目以降でレスポンス項目が異なるという鬼畜仕様。
- そのため全てのレスポンス項目を含むレスポンスクラスを作成し、一旦レスポンスを受け取る。
- Translatorクラスを作成し、レスポンスをドメインオブジェクトに変換する際に一項目目を取り除きたい。
レスポンスJSONはこんな感じ
[{"allcount":617021},{"title":"\u5143\u30fb\u9b54\u738b\u8ecd\u306e\u7adc\u9a0e\u58eb\u304c\u7d4c\u55b6\u3059\u308b\u731f\u5175\u56e3\u3002","ncode":"N6147EU"
コーディング
一項目目はallcountしか返却されず、その後のレスポンスでは各項目のデータが返却されるようなので、 いったん一項目目の場合はnullを返すTranslatorクラスとする。
fun toNovelIntroduction(response : NarouNovelIntroductionResponse) : NovelIntroduction? { if (response.allcount == null) { return null } return NovelIntroduction( response.title ?: "", response.ncode?: "", response.userid?: "", response.writer?: "", response.story?: "", response.biggenre?: 0, response.genre?: 0, response.gennsaku?: "", response.keyword?: "", response.general_firstup?: "", response.general_lastup?: "", response.novel_type?: 0, response.end?: 0, response.general_all_no?: 0, response.length?: 0, response.time?: 0, response.isstop?: 0, response.isbl?: 0, response.isgl?: 0, response.iszankoku?: 0, response.istensei?: 0, response.pc_or_k?: 0, response.global_point?: 0, response.fav_novel_cnt?: 0, response.review_cnt?: 0, response.all_point?: 0, response.all_hyoka_cnt?: 0, response.sasie_cnt?: 0, response.kaiwaritu ?: 0, response.novelupdated_at?: "", response.updated_at?:"") }
返却データをfilterかけて対応。
override fun search(word : String) : List<NovelIntroduction> { return searchService.getNovelList(word).map{response -> NovelIntroductionTranslator().toNovelIntroduction(response)}.filterNotNull() }
これで対応できた。
追記
kotlinにはmapNotNullというものがあるらしい。これを使うと以下のようにコーディングできる。便利。
override fun search(word : String) : List<NovelIntroduction> { return searchService.getNovelList(word).mapNotNull{response -> NovelIntroductionTranslator().toNovelIntroduction(response)} }
最小構成でDagger2の使い方の初歩を理解する with kotlin
対象者
- DIはもうわかったけどDagger2の使い方がわからない人
- javaのサンプルは多いけどkotlinのサンプルが少なくて困惑している人
- @Moduleとか@Provideとか@Componentとか@Injectってあるけど最小で動かすとしたらどれが必要なんだと困惑している人
最小構成(パターン①)
以下の3クラスで構成する
CoffeeMaker.kt
import javax.inject.Inject class CoffeeMaker @Inject constructor(private val heater: Heater) { fun getTemperature() : String { return heater.getTemperature() } }
Heater.kt
import javax.inject.Inject class Heater @Inject constructor() { fun getTemperature() : String { return "38度" } }
MainActivity.kt
import dagger.Component class MainActivity : AppCompatActivity() { private lateinit var coffeeShop :CoffeeShop override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val dataBinding = DataBindingUtil.setContentView<MainActivityBinding>(this, R.layout.main_activity) // DaggerCoffeeShopはComponent interfaceを継承したクラス。 // Dagger2によってビルド時に自動生成される。 coffeeShop = DaggerCoffeeShop.create() // ビルド時に自動生成されたクラスがDIしてくれているので、 // maker()実行だけで、メンバにインジェクト済みのインスタンスが取得できる val coffeeMaker = coffeeShop.maker() // 38度が返ってくる dataBinding.textView.text = coffeeMaker.getTemperature() } } @Component interface CoffeeShop { fun maker(): CoffeeMaker }
解説:パターン①
Dagger2は一行で書くと
"@Componentが記載されているinterfaceに対する、実体Factoryクラスを自動生成してくれる"
ツールだと理解した。
今回自動生成されたDaggerCoffeeShopは以下のような形になる。
public final class DaggerCoffeeShop implements CoffeeShop { private DaggerCoffeeShop(Builder builder) {} public static Builder builder() { return new Builder(); } public static CoffeeShop create() { return new Builder().build(); } @Override public CoffeeMaker maker() { return new CoffeeMaker(new Heater()); } public static final class Builder { private Builder() {} public CoffeeShop build() { return new DaggerCoffeeShop(this); } } }
以下行でわかるとおり、@Componentを付けたinterfaceに対する実体クラスが自動生成されている。
public final class DaggerCoffeeShop implements CoffeeShop {
また、以下の行ではmaker()の返却インスタンスであるCoffeeMakerインスタンスを生成する コードが自動生成されている。
@Override public CoffeeMaker maker() { return new CoffeeMaker(new Heater()); }
これによってDIを考慮して設計されたクラスをインスタンス化したいときに、クラス生成地獄を隠蔽することが可能になっている。
最小構成(パターン②)
"@Componentが記載されているinterfaceに対する、実体Factoryクラスを自動生成してくれる" のはわかった。 しかしActivityが複数のメンバ変数を持つような場合は一つ一つ上記のような書き方で注入していく必要があるのだろうか。
Dagger2ではもうひとつ書き方が用意されている。 "自分自身の@Injectが記載されているメンバ変数全てにインスタンスを設定してくれる"方法である。
以下に対象コードを記載する。
class OtherActivity : AppCompatActivity() { @Inject lateinit var coffeeMaker : CoffeeMaker override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = DataBindingUtil.setContentView<OtherActivityBinding>(this,R.layout.other_activity) DaggerActivityComponent.create().inject(this) binding.textView.text = coffeeMaker.getTemperature() } } @Component interface ActivityComponent { fun inject(activity:OtherActivity) }
解説:パターン②
先ほどと異なるのは @Componentが記載されているインターフェースのメソッドに自分自身が設定されていることである。 このような場合、以下のようなクラスが自動生成される。
public final class DaggerActivityComponent implements ActivityComponent { private DaggerActivityComponent(Builder builder) {} public static Builder builder() { return new Builder(); } public static ActivityComponent create() { return new Builder().build(); } private CoffeeMaker getCoffeeMaker() { return new CoffeeMaker(new Heater()); } @Override public void inject(OtherActivity activity) { injectOtherActivity(activity); } private OtherActivity injectOtherActivity(OtherActivity instance) { OtherActivity_MembersInjector.injectCoffeeMaker(instance, getCoffeeMaker()); return instance; } public static final class Builder { private Builder() {} public ActivityComponent build() { return new DaggerActivityComponent(this); } } }
injectメソッドを実行すると、injectOtherActivityが呼ばれている。 ここでは以下の処理が行われている。
OtherActivity_MembersInjector.injectCoffeeMaker(instance, getCoffeeMaker());
コードを追うと以下のようになっている。
public final class OtherActivity_MembersInjector implements MembersInjector<OtherActivity> { private final Provider<CoffeeMaker> coffeeMakerProvider; public OtherActivity_MembersInjector(Provider<CoffeeMaker> coffeeMakerProvider) { this.coffeeMakerProvider = coffeeMakerProvider; } public static MembersInjector<OtherActivity> create(Provider<CoffeeMaker> coffeeMakerProvider) { return new OtherActivity_MembersInjector(coffeeMakerProvider); } @Override public void injectMembers(OtherActivity instance) { injectCoffeeMaker(instance, coffeeMakerProvider.get()); } public static void injectCoffeeMaker(OtherActivity instance, CoffeeMaker coffeeMaker) { instance.coffeeMaker = coffeeMaker; } }
呼び出されている以下のメソッドを見ると、指定したインスタンスに生成されたインスタンスを設定している。
public static void injectCoffeeMaker(OtherActivity instance, CoffeeMaker coffeeMaker) {
instance.coffeeMaker = coffeeMaker;
}
このような仕組みで自身のメンバ変数にDIする書き方も可能である。
まとめ
・ ひとまずDagger2を利用してインスタンス生成までは実行できた。
・ しかしDagger2を利用して本来やりたいことは自動テスト時に対象クラス以外をモッククラスに置き換えることである。 そのためにはModuleとprovideを理解する必要がある。
おまけ:自分が理解するうえで詰まった(困惑した)ポイントまとめ
1. なんかいろいろ書き方があってわからない
→指定したクラスの依存性を満たしたインスタンスを返却する方法と、自分自身を指定してメンバに注入する方法が混ざって混乱していた。
2. 注入される側(今回の例で言えばHeaterクラス)のコンストラクタには@Injectいらなくない?注入されるだけだし。
→記入しないと注入するクラスだと認識してくれなくてコードが自動生成されないみたい。
3. RebuildしてもDaggerXXXクラスが自動生成されない....
→MakeProjectしたら動作するようになった。謎。
4. なぜかフィールドインジェクション作成時ビルドが成功しなかった。
→メンバ変数がprivateだとinjectするタイミングで参照できないために、自動生成に失敗していたみたい。privateメンバ変数に値を入れたい場合はコンストラクタインジェクションしかないかも。
Clean ArchitectureでAndroidアプリのクラス設計をしてみる
結論
こんな感じになった。
こんなシステムを想定した
- Webサイトで公開されている小説情報を表示したい
- Webサイトの検索機能を使えば、小説情報一覧を取得できる
- 小説情報一覧に含まれるコードを利用すれば、小説の中身が取得できる
Clean Architectureとは
ネットにいくらでも転がっているので割愛。
今回はRobert C. MartinさんのClean Architecture本を参考にした。
クラス設計をしていく上で気になったこと
疑問その1
Webから取得したレスポンスデータを処理してドメインモデルに変換するクラスはどこにあるべきなのか
結論
円の内側のレイヤーは外側レイヤーを知るべきではない。 内側のレイヤーは一番変えてはいけない(変わらない)レイヤー= domain層 のため、externalはdomainを知っていてよいが、domainはexternalを知ってはいけない。そのためexternal層でレスポンスデータを処理し、ドメインモデルを生成する。
疑問その2
リポジトリを知ってはいけないとすると、ドメインで通信処理がしたくなったときどうすればよいのか
結論
インタフェースをかませるしかない。インターフェース自体の置き場所は、「ドメインは外側を知らない」ルールのため、ドメイン内に置く。
疑問その3
genreは101,102,201などが返却される。
この値は101=異世界[恋愛], 102=現実世界[恋愛]などと対応している。101の値などは基本利用しないため、レスポンスデータからドメインモデルに変換する際に置換して保持すべきではないか
結論
実際の業務に合わせた方がよいと思う。今回の場合は101で実際に管理しているため、ドメイン側でgenreName()を用意し、そこで対応表に合わせた名前を返すようにする。
感想
下位レイヤーと上位レイヤーの考え方が今までと異なっていた(業務ロジックが上で、通信周りは下のイメージだった)ので、そこの切り替えに時間がかかった気がする。
完成してみるとドメインがきれいに切り離されていていい感じ。