2015年5月19日火曜日

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

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

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



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

まずは int 型で、ってなりますよね。
  1. private int size;  
  2.   
  3. @Override  
  4. public void onSizeSelected(int size) {  
  5.     this.size = size;  
  6. }  
  7.   
  8. @OnClick(R.id.send_button)  
  9. void onSendButtonClicked() {  
  10.     send(size);  
  11. }  
  12.   
  13. @POST("/tshirt-size")  
  14. boolean send(@Query("size"int size);  
これだと、ユーザーが選択していない状態で送信ボタンを押すと 0 が送られてしまうので、メッセージを表示して送信をブロックしましょう。
そのためには未選択状態の値を定義しないといけません。よくあるのは -1 で、こんな感じになるでしょう。
  1. private int size = -1;  
  2.   
  3. @Override  
  4. public void onSizeSelected(int size) {  
  5.     this.size = size;  
  6. }  
  7.   
  8. @OnClick(R.id.send_button)  
  9. void onSendButtonClicked() {  
  10.     if (size == -1) {  
  11.         Toast.makeText(context, "サイズを選択してください", Toast.LENGTH_SHORT).show();  
  12.     } else {  
  13.         send(size);  
  14.     }  
  15. }  
悪くないんですが、-1 を弾くよりサーバーに送るデータ を 1, 2, 3 に制限したほうがよさそうです。
そこで @IntDef を使って次のようにしてみます。
  1. public static final int SIZE_L = 1;  
  2. public static final int SIZE_M = 2;  
  3. public static final int SIZE_S = 3;  
  4.   
  5. @Retention(RetentionPolicy.SOURCE)  
  6. @IntDef({SIZE_L, SIZE_M, SIZE_S})  
  7. public @interface ValidSize {  
  8. }  
  9.   
  10. private int size = -1;  
  11.   
  12. @Override  
  13. public void onSizeSelected(@ValidSize int size) {  
  14.     this.size = size;  
  15. }  
  16.   
  17. @OnClick(R.id.send_button)  
  18. void onSendButtonClicked() {  
  19.     if (size == -1) {  
  20.         Toast.makeText(this"サイズを選択してください", Toast.LENGTH_SHORT).show();  
  21.     } else {  
  22.         send(size); // ここでエラーになる  
  23.     }  
  24. }  
  25.   
  26. @POST("/tshirt-size")  
  27. boolean send(@ValidSize int size) {  
  28.     return true;  
  29. }  
これだと send(size); のところでエラーがでます。size が @ValidSize ではないからですね。
次のように値をチェックすればエラーは出なくなりますが、int から int 変換ですしどうもいまいちです。
  1. @OnClick(R.id.send_button)  
  2. void onSendButtonClicked() {  
  3.     if (size == -1) {  
  4.         Toast.makeText(this"サイズを選択してください", Toast.LENGTH_SHORT).show();  
  5.     } else {  
  6.         switch (size) {  
  7.             case SIZE_L:  
  8.                 send(SIZE_L);  
  9.                 break;  
  10.             case SIZE_M:  
  11.                 send(SIZE_M);  
  12.                 break;  
  13.             case SIZE_S:  
  14.                 send(SIZE_S);  
  15.                 break;  
  16.         }  
  17.     }  
  18. }  
そこで @ValidSize の int 値を保持するクラスを用意してみます。
未設定かどうかを保持する boolean 値も持たせます。
  1. private static class Size {  
  2.     public static final int SIZE_L = 1;  
  3.     public static final int SIZE_M = 2;  
  4.     public static final int SIZE_S = 3;  
  5.   
  6.     @Retention(RetentionPolicy.SOURCE)  
  7.     @IntDef({SIZE_L, SIZE_M, SIZE_S})  
  8.     public @interface ValidSize {  
  9.     }  
  10.   
  11.     @ValidSize  
  12.     private int size;  
  13.     private boolean isValid = false;  
  14.   
  15.     public void setSize(@ValidSize int size) {  
  16.         this.size = size;  
  17.         this.isValid = true;  
  18.     }  
  19.   
  20.     @ValidSize  
  21.     public int getSize() {  
  22.         return size;  
  23.     }  
  24.   
  25.     public boolean isValid() {  
  26.         return isValid;  
  27.     }  
  28. }  
  29.   
  30. private Size size = new Size();  
  31.   
  32. @Override  
  33. public void onSizeSelected(@Size.ValidSize int size) {  
  34.     this.size.setSize(size);  
  35. }  
  36.   
  37. @OnClick(R.id.send_button)  
  38. void onSendButtonClicked() {  
  39.     // ここのチェックを強制できない  
  40.     if (!size.isValid()) {  
  41.         Toast.makeText(this"サイズを選択してください", Toast.LENGTH_SHORT).show();  
  42.     } else {  
  43.         send(size.getSize());  
  44.     }  
  45. }  
  46.   
  47. @POST("/tshirt-size")  
  48. boolean send(@Size.ValidSize int size) {  
  49.     return true;  
  50. }  
これで不細工なswitch文にさよならできましたが、問題があります。
size.isValid() でチェックすることを利用側に強制できません。

そこで、null を未設定状態として扱うようにしてみます。
  1. private static class Size {  
  2.     public static final int SIZE_L = 1;  
  3.     public static final int SIZE_M = 2;  
  4.     public static final int SIZE_S = 3;  
  5.   
  6.     @Retention(RetentionPolicy.SOURCE)  
  7.     @IntDef({SIZE_L, SIZE_M, SIZE_S})  
  8.     public @interface ValidSize {  
  9.     }  
  10.   
  11.     public static Size valueOf(@ValidSize int size) {  
  12.         return new Size(size);  
  13.     }  
  14.   
  15.     @ValidSize  
  16.     private final int size;  
  17.   
  18.     private Size(@ValidSize int size) {  
  19.         this.size = size;  
  20.     }  
  21.   
  22.     @ValidSize  
  23.     public int getValue() {  
  24.         return size;  
  25.     }  
  26. }  
  27.   
  28. @Nullable  
  29. private Size size = null;  
  30.   
  31. @Override  
  32. public void onSizeSelected(@Size.ValidSize int size) {  
  33.     this.size = Size.valueOf(size);  
  34. }  
  35.   
  36. @Override  
  37. public void onSizeCleared() {  
  38.     this.size = null;  
  39. }  
  40.   
  41. @OnClick(R.id.send_button)  
  42. void onSendButtonClicked() {  
  43.     if (size == null) {  
  44.         Toast.makeText(this"サイズを選択してください", Toast.LENGTH_SHORT).show();  
  45.     } else {  
  46.         send(size.getValue());  
  47.     }  
  48. }  
  49.   
  50. @POST("/tshirt-size")  
  51. boolean send(@Size.ValidSize int size) {  
  52.     return true;  
  53. }  
これで 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回 で候補がでます。
  1. public enum Size {  
  2.     L(SIZE_L), M(SIZE_M), S(SIZE_S);  
  3.   
  4.     @Retention(RetentionPolicy.SOURCE)  
  5.     @IntDef({SIZE_L, SIZE_M, SIZE_S})  
  6.     public @interface ValidSize {  
  7.         int SIZE_L = 1;  
  8.         int SIZE_M = 2;  
  9.         int SIZE_S = 3;  
  10.     }  
  11.   
  12.     @ValidSize  
  13.     private final int size;  
  14.   
  15.     Size(@ValidSize int size) {  
  16.         this.size = size;  
  17.     }  
  18.   
  19.     @ValidSize  
  20.     public int getValue() {  
  21.         return size;  
  22.     }  
  23. }  
  24.   
  25. @Nullable  
  26. private Size size = null;  
  27.   
  28. @Override  
  29. public void onSizeSelected(Size size) {  
  30.     this.size = size;  
  31. }  
  32.   
  33. @Override  
  34. public void onSizeCleared() {  
  35.     this.size = null;  
  36. }  
  37.   
  38. @OnClick(R.id.send_button)  
  39. void onSendButtonClicked() {  
  40.     if (size == null) {  
  41.         Toast.makeText(this"サイズを選択してください", Toast.LENGTH_SHORT).show();  
  42.     } else {  
  43.         send(size.getValue());  
  44.     }  
  45. }  
  46.   
  47. @POST("/tshirt-size")  
  48. boolean send(@Size.ValidSize int size) {  
  49.     return true;  
  50. }  




0 件のコメント:

コメントを投稿