2015年5月19日火曜日

未選択状態を持つデータをどう表現するか悩んだ話

*追記1:enum の場合について最後に追記しました。

*追記2:まずは int 型じゃねーよ、enumだろって vvakameさんに怒られたけど、もともとは、とあるプロジェクトでサーバーに意図しない値がきてるんだけど、、、みたいなことがあって、サーバーに渡す値を静的に制限するにはどうするのがいいのかな、というのが出発点だったのです。なんで最初が int かっつーと、そのときのコードが int だったからだよっ(つまり初心者がやりがちってこと)



例えば T シャツのサイズをユーザーに選択してもらう画面があったとします。
Tシャツのサイズは L, M, S で、サーバーに投げるときはそれぞれ int値 の 1, 2, 3 として渡します。

まずは int 型で、ってなりますよね。 private int size; @Override public void onSizeSelected(int size) { this.size = size; } @OnClick(R.id.send_button) void onSendButtonClicked() { send(size); } @POST("/tshirt-size") boolean send(@Query("size") int size); これだと、ユーザーが選択していない状態で送信ボタンを押すと 0 が送られてしまうので、メッセージを表示して送信をブロックしましょう。
そのためには未選択状態の値を定義しないといけません。よくあるのは -1 で、こんな感じになるでしょう。 private int size = -1; @Override public void onSizeSelected(int size) { this.size = size; } @OnClick(R.id.send_button) void onSendButtonClicked() { if (size == -1) { Toast.makeText(context, "サイズを選択してください", Toast.LENGTH_SHORT).show(); } else { send(size); } } 悪くないんですが、-1 を弾くよりサーバーに送るデータ を 1, 2, 3 に制限したほうがよさそうです。
そこで @IntDef を使って次のようにしてみます。 public static final int SIZE_L = 1; public static final int SIZE_M = 2; public static final int SIZE_S = 3; @Retention(RetentionPolicy.SOURCE) @IntDef({SIZE_L, SIZE_M, SIZE_S}) public @interface ValidSize { } private int size = -1; @Override public void onSizeSelected(@ValidSize int size) { this.size = size; } @OnClick(R.id.send_button) void onSendButtonClicked() { if (size == -1) { Toast.makeText(this, "サイズを選択してください", Toast.LENGTH_SHORT).show(); } else { send(size); // ここでエラーになる } } @POST("/tshirt-size") boolean send(@ValidSize int size) { return true; } これだと send(size); のところでエラーがでます。size が @ValidSize ではないからですね。
次のように値をチェックすればエラーは出なくなりますが、int から int 変換ですしどうもいまいちです。 @OnClick(R.id.send_button) void onSendButtonClicked() { if (size == -1) { Toast.makeText(this, "サイズを選択してください", Toast.LENGTH_SHORT).show(); } else { switch (size) { case SIZE_L: send(SIZE_L); break; case SIZE_M: send(SIZE_M); break; case SIZE_S: send(SIZE_S); break; } } } そこで @ValidSize の int 値を保持するクラスを用意してみます。
未設定かどうかを保持する boolean 値も持たせます。 private static class Size { public static final int SIZE_L = 1; public static final int SIZE_M = 2; public static final int SIZE_S = 3; @Retention(RetentionPolicy.SOURCE) @IntDef({SIZE_L, SIZE_M, SIZE_S}) public @interface ValidSize { } @ValidSize private int size; private boolean isValid = false; public void setSize(@ValidSize int size) { this.size = size; this.isValid = true; } @ValidSize public int getSize() { return size; } public boolean isValid() { return isValid; } } private Size size = new Size(); @Override public void onSizeSelected(@Size.ValidSize int size) { this.size.setSize(size); } @OnClick(R.id.send_button) void onSendButtonClicked() { // ここのチェックを強制できない if (!size.isValid()) { Toast.makeText(this, "サイズを選択してください", Toast.LENGTH_SHORT).show(); } else { send(size.getSize()); } } @POST("/tshirt-size") boolean send(@Size.ValidSize int size) { return true; } これで不細工なswitch文にさよならできましたが、問題があります。
size.isValid() でチェックすることを利用側に強制できません。

そこで、null を未設定状態として扱うようにしてみます。 private static class Size { public static final int SIZE_L = 1; public static final int SIZE_M = 2; public static final int SIZE_S = 3; @Retention(RetentionPolicy.SOURCE) @IntDef({SIZE_L, SIZE_M, SIZE_S}) public @interface ValidSize { } public static Size valueOf(@ValidSize int size) { return new Size(size); } @ValidSize private final int size; private Size(@ValidSize int size) { this.size = size; } @ValidSize public int getValue() { return size; } } @Nullable private Size size = null; @Override public void onSizeSelected(@Size.ValidSize int size) { this.size = Size.valueOf(size); } @Override public void onSizeCleared() { this.size = null; } @OnClick(R.id.send_button) void onSendButtonClicked() { if (size == null) { Toast.makeText(this, "サイズを選択してください", Toast.LENGTH_SHORT).show(); } else { send(size.getValue()); } } @POST("/tshirt-size") boolean send(@Size.ValidSize int size) { return true; } これで null じゃないときに取得できる値を @Size.ValidSize に制限できます。

size に @Nullable をつければ、null チェックをしないで size.getValue() を呼び出そうとしたところで Lint の警告が出てくれます。
size に null を代入することでサイズ選択のクリアもできます。


未選択状態に特定の値を割り当てる場合、その値が絶対使われないならいいのですが、使われる場合もありえます。 例えば、透明度を含む色を選択してもらいたい場合では #ffffffff が -1 なので、-1を未選択状態に割り当てるのは不適当になります。
null を未選択状態に割り当てる方法はこういう場合にも適用できます。



追記 :

@vvakame から enum 使えよおらーって言われたので、@zaki50 さんの提案をもとに enum版も載せておきます。

前提として、send()に渡される引数の値を制限したいというのが目的です。 retrofit で引数のパラメータに enum を渡すと toString() の値が利用されるようで、なにかしらコンバーターを仕込まないといけなさそうです。 なので、send() に渡す値に @ValidSize をつけるのはそのままにしたいと思います。

こんな感じになります。ほぼ同じですね。
違いは、onSizeSelected() の引数が @ValidSize int size から Size になったので、この部分の値の制限が Lint からコンパイラになったという点と、 enum なので == で比較できるという点くらいでしょうか。
ちなみにこの書き方だと SIZE_L などを static import する必要があります。Ctrl + space2回 で候補がでます。 public enum Size { L(SIZE_L), M(SIZE_M), S(SIZE_S); @Retention(RetentionPolicy.SOURCE) @IntDef({SIZE_L, SIZE_M, SIZE_S}) public @interface ValidSize { int SIZE_L = 1; int SIZE_M = 2; int SIZE_S = 3; } @ValidSize private final int size; Size(@ValidSize int size) { this.size = size; } @ValidSize public int getValue() { return size; } } @Nullable private Size size = null; @Override public void onSizeSelected(Size size) { this.size = size; } @Override public void onSizeCleared() { this.size = null; } @OnClick(R.id.send_button) void onSendButtonClicked() { if (size == null) { Toast.makeText(this, "サイズを選択してください", Toast.LENGTH_SHORT).show(); } else { send(size.getValue()); } } @POST("/tshirt-size") boolean send(@Size.ValidSize int size) { return true; }



0 件のコメント:

コメントを投稿