2017年9月1日金曜日

SharedPreferences を使ったデータアクセス部分を Kotlin のカスタムアクセサ で実装する

ユーザーの血液型を保存したいとします。

SharedPreferences に保存するとして、保存・読み出しでキーを間違えたり、対象の SharedPreferences を間違えたりしないためには、SharedPreferences への保存と読み出しを行うためのクラスを用意するとよいです。

例えば次のような Utils クラスを用意したとしましょう。 public class ProfileSettingUtils { private static final String PREF_KEY_BLOOD_TYPE = "blood_type"; private static SharedPreferences getPref(@NonNull Context context) { return PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); } @Nullable public static String getBloodType(@NonNull Context context) { return getPref(context).getString(PREF_KEY_BLOOD_TYPE, null); } public static void setBloodType(@NonNull Context context, @Nullable String bloodType) { getPref(context) .edit() .putString(PREF_KEY_BLOOD_TYPE, bloodType) .apply(); } } この Utils クラスで保存・読み出しを行えば、キーを間違えたり対象の SharedPreferences を間違えたりはしません。
しかし、"a" がA型を意味するなど、利用側が返される文字列の意味を知っている必要がありますし、"-" など意図しない文字列も保存できてしまいます。

血液型のような取り得る値が決まっているものは enum で定義して、保存・読み出し部分も enum でやりとりするべきです。 public enum BloodType { A("a"), B("b"), O("o"), AB("ab"); @NonNull public final String value; BloodType(@NonNull String value) { this.value = value; } @Nullable public static BloodType from(@Nullable String value) { if (value != null) { for (BloodType bloodType : values()) { if (bloodType.value.equals(value)) { return bloodType; } } } return null; } } public class ProfileSettingUtils { private static final String PREF_KEY_BLOOD_TYPE = "blood_type"; private static SharedPreferences getPref(@NonNull Context context) { return PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); } @Nullable public static BloodType getBloodType(@NonNull Context context) { return BloodType.from(getPref(context).getString(PREF_KEY_BLOOD_TYPE, null)); } public static void setBloodType(@NonNull Context context, @Nullable BloodType bloodType) { getPref(context) .edit() .putString(PREF_KEY_BLOOD_TYPE, bloodType != null ? bloodType.value : null) .apply(); } } Robolectric を使えばテスト時に SharedPreferences の動きをモック化できますが、テストのセットアップとして SharedPreferences に値をセットするよりは、BloodType を返す部分をモック化できたほうが柔軟性があります。

では Utils クラスをやめてみましょう。 public class ProfileSetting { private static final String PREF_KEY_BLOOD_TYPE = "blood_type"; @NonNull private final SharedPreferences pref; public ProfileSetting(@NonNull Context context) { pref = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); } @Nullable public BloodType getBloodType() { return BloodType.from(pref.getString(PREF_KEY_BLOOD_TYPE, null)); } public void setBloodType(@Nullable BloodType bloodType) { pref .edit() .putString(PREF_KEY_BLOOD_TYPE, bloodType != null ? bloodType.value : null) .apply(); } } このクラスを使って、読み出した BloodType を判断・加工するロジック部分があるとします。
このままだとロジック部分がデータアクセス部分に依存しています。
そこで、依存関係逆転の原則(DIP)を適用してロジックが抽象に依存できるように interface を用意します。 public interface Profile { @Nullable BloodType getBloodType(); void setBloodType(@Nullable BloodType bloodType); } public class ProfileSetting implements Profile { private static final String PREF_KEY_BLOOD_TYPE = "blood_type"; @NonNull private final SharedPreferences pref; public ProfileSetting(@NonNull Context context) { pref = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); } @Override @Nullable public BloodType getBloodType() { return BloodType.from(pref.getString(PREF_KEY_BLOOD_TYPE, null)); } @Override public void setBloodType(@Nullable BloodType bloodType) { pref .edit() .putString(PREF_KEY_BLOOD_TYPE, bloodType != null ? bloodType.value : null) .apply(); } } ロジック部分は ProfileSetting ではなく interface の Profile を外部から渡してもらうようにします。

さて、これを Kotlin 化してみましょう。 enum class BloodType(val value: String) { A("a"), B("b"), O("o"), AB("ab"); companion object { fun from(value: String?): BloodType? = value?.let { values().firstOrNull { it.value == value } } } } interface Profile { var bloodType: BloodType? } class ProfileSetting(context: Context) : Profile { companion object { private const val PREF_KEY_BLOOD_TYPE = "blood_type" } private val pref: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.applicationContext) override var bloodType: BloodType? get() = BloodType.from(pref.getString(PREF_KEY_BLOOD_TYPE, null)) set(bloodType) = pref.edit().putString(PREF_KEY_BLOOD_TYPE, bloodType?.value).apply() } Kotlin ではプロパティの定義にカスタムアクセサを書けるので、同じキーに対する保存と読み込みを一箇所に書けて対応がわかりやすくなりますね。


0 件のコメント:

コメントを投稿