マスターテーブルとenum
昨日、三浦カズヒトさんと Twitter でやりとりさせてもらった。
俺、設計とか技術とかそういうのに疎いから…うまく言語化できるか解らんけど。「マスター(リソースとも言う?)テーブルで「コードと名前」くらい持ってるけど、結局「そのコードの意味はプログラムが理解せねばならん」から「マスタと同数のenum持ってる」とか、許すならenumに落としたい。
— 三浦カズヒト(英語9点の生きにくさ) (@kazuhito_m) 2016年5月26日
あまり深く考えずにリプライしてしまい中途半端にしか答えられなかったので、ツイッターの文字数制限に縛られない場所で自分の考えをまとめてみた。
問題
データベースのマスターテーブルにキーと名称のペアがあったとして、それと同じペアをenumや定数でプログラムに持つのが、果たして良いことなのか。
どうせマスターにペアを追加するときはそれに対応するプログラムをデプロイするし、マスターとプログラムの両方を手直しするならマスターなんて持たずにプログラムだけで対応すれば良いのではないだろうか。
マスターに名称を持つ価値は、せいぜい急な名称変更に対応出来る程度なのではないか。
マスターとenumの両方を持つ理由
マスターには値の名称を持ち、enumではそのマスターに応じた処理を分岐させるための定数とするよう、両方を持つのだと思う。
例えば、マスターに以下のようなものがあったとする。
種別番号 | 種別名 |
---|---|
0 | 大 |
1 | 中 |
2 | 小 |
それに応じたenumを用意すると、こうなる。
public enum Kind { DAI, // 種別番号 0「大」 を表す CYU, // 種別番号 1「中」 を表す SYO // 種別番号 2「小」 を表す }
で、種別ごとに何らかの処理を実装すると、こうなる。
public void someProcess(Kind kind) { switch (kind) { case DAI: // 種別番号 0「大」 の時の処理 break; case CYU: // 種別番号 1「中」 の時の処理 break; case SYO: // 種別番号 2「小」 の時の処理 break; } }
このような実装をしていたら、マスターに種別を増やした場合にそれに応じたenumを増やして、更にそのenumを使っている箇所に処理を追加しないとならない。
例えば「極小」を追加したらマスターは下記のようになる。
種別番号 | 種別名 |
---|---|
0 | 大 |
1 | 中 |
2 | 小 |
3 | 極小 |
それに応じてenumの「GOKUSYO」も増やす。
public enum Kind { DAI, // 種別番号 0「大」 を表す CYU, // 種別番号 1「中」 を表す SYO, // 種別番号 2「小」 を表す GOKUSYO // 種別番号 3「極小」 を表す }
さらに「case GOKUSYO」の処理を増やす。
public void someProcess(Kind kind) { switch (kind) { case DAI: // 種別番号 0「大」 の時の処理 break; case CYU: // 種別番号 1「中」 の時の処理 break; case SYO: // 種別番号 2「小」 の時の処理 break; case GOKUSYO: // 種別番号 3「極小」 の時の処理 break; } }
種別が変更になるたびにマスターとenumと処理を増やすのは、正直しんどい。
だったらマスターを捨ててenumだけで良いのでは?というのも分かる。
マスターだけにする
種別に応じた処理をマスターに基いてやるようにすれば、enumも処理も不要になるケースがある。
例えばマスターに処理をするのに必要なパラメータを登録するというやり方。
種別番号 | 種別名 | パラーメータ1 | パラーメータ2 |
---|---|---|---|
0 | 大 | 789 | JKL |
1 | 中 | 456 | GHI |
2 | 小 | 123 | DEF |
3 | 極小 | 0 | ABC |
処理はこのようになる。
public void someProcess(int kind) { int param1 = getParam1(kind); // マスターから種別番号に応じたパラメータ1を取得 String param2 = getParam2(kind); // マスターから種別番号に応じたパラメータ2を取得 // param1とparam2を基に処理する }
処理は同じだが種別によってパラメータが違うという場合なら、これで十分対応が可能と思う。
これならマスターだけをメンテすれば万事OK。
マスターもenumも持たない
マスターがキーと値のペアだけだったら、マスターを持たないという選択もありだと思う。
かと言って、enumと処理がバラバラだとそれはそれでしんどいので、もうマスターもenumも持たないようにしたい。
マスターをなくしたので、種別に応じた種別名を取得する仕組みを作る。
ひとつの答えはインターフェースかと思う。
public interface Kind { public String getName(); // 種別名を返す }
で、種別に応じた実装クラスを用意する。
public class Kind0 implements Kind { public String getName() { return "大"; } } public class Kind1 implements Kind { public String getName() { return "中"; } } public class Kind2 implements Kind { public String getName() { return "小"; } } public class Kind3 implements Kind { public String getName() { return "極小"; } }
これで元々マスターにあった「キーに応じた値」を得ることは出来る。
せっかくインターフェースにしたなら、冒頭にあった
public void someProcess(Kind kind) { switch (kind) { case DAI: // 種別番号 0「大」 の時の処理 break; case CYU: // 種別番号 1「中」 の時の処理 break; case SYO: // 種別番号 2「小」 の時の処理 break; } }
この種別が増えるたびに分岐が増える汚コードもすっきりさせられる。
インターフェースに、この時にするべき処理を定義する。
public interface Kind { public String getName(); // 種別名を返す public void someProcess(); // するべき処理 }
で、種別に応じた「するべき処理」を実装する。
public class Kind0 implements Kind { public String getName() { return "大"; } public void someProcess() { // "大"の時にするべき処理 } } public class Kind1 implements Kind { public String getName() { return "中"; } public void someProcess() { // "中"の時にするべき処理 } } public class Kind2 implements Kind { public String getName() { return "小"; } public void someProcess() { // "小"の時にするべき処理 } } public class Kind3 implements Kind { public String getName() { return "極小"; } public void someProcess() { // "極小"の時にするべき処理 } }
そうすれば、汚コードもすっきりする。
public void someProcess(Kind kind) { kind.someProcess(); }
呼び出し元はインターフェースしか見えないので実際に何が実行されるかは、渡ってきたKindの実装クラスに応じて変わってくる。
変数kindはKind1のインスタンス参照かもしれないし、Kind2のインスタンス参照かもしれない。
実装はすっきりしたので、あとはインスタンスの生成だが、ここは局所化すれば汚コードでも良いと思う。
public class KindFactory { public static Kind create(int id) { switch (id) { case 0: return new Kind0(); case 1: return new Kind1(); case 2: return new Kind2(); case 3: return new Kind3(); } throw new IllegalArgumentException(id+"って何よ?"); } }
マジックナンバーが嫌だというなら、内部的なenumを使うのも良いかもしれない。
public class KindFactory { private enum K { DAI, // 種別番号 0「大」 を表す CYU, // 種別番号 1「中」 を表す SYO, // 種別番号 2「小」 を表す GOKUSYO // 種別番号 3「極小」 を表す } public static Kind create(int id) { if (K.DAI.ordinal()==id) { return new Kind0(); } else if (K.CYU.ordinal()==id) { return new Kind1(); } else if (K.SYO.ordinal()==id) { return new Kind2(); } else if (K.GOKUSYO.ordinal()==id) { return new Kind3(); } throw new IllegalArgumentException(id+"って何よ?"); } }
いやいやそれでも条件分岐が多くて汚コードは嫌だと言うなら、idに応じたクラスをプロパティファイルに書いて、リフレクションでインスタンスを作ると条件分岐がなくて尚良しかと。
まとめ
そうは言っても状況によって求められることは違うので、ここで書いたことが絶対に正しいとも思わないし、プロダクトによって答えは変わってくるかと。