レガシーアンチパターン
About
過去有用と思われていて、モダンな環境では役に立たないパターンを紹介します。現在でも組み込みなどの一部の環境では役に立つものがあります。C++以降の言語を使い、1GB以上のメモリが使えるような富豪的プログラミングでは面倒なだけなのに、なぜか老害に教え込まれて今でも有用だと思っている若い人がいて災いを起こしているので紹介していきます。
なんでもConstantsファイル
概要
プロジェクトを横断するConstantsファイルやクラスを作り、定数はなんでもかんでもそこに入れてしまう。
駄目な理由
もっとスコープが狭くて良いものをプロジェクト全体に広げ、Constantsファイルは肥大化し見通しが悪くなる。
改善策
型を定義できる言語を使用しているなら、そもそもtypesafe enumを使うべきで、数字や文字の定数を定義してはいけない。ユーザーに関する定数はUser.UserName.MAX_LENGTH, 注文に関するものはOrder.Quantity.MINIMUMのように関連するパッケージの下にまとめる。どうしてもApplication全体を通したいものだけApplication.PORTのようにApplication配下に定義する。
解説
数値・文字では型が同じなら他の箇所で使うことができてしまう。どちらもintならusername.maxLength > Qualtity.MINIMUMのような事ができるが、静的型付言語であれば型を異なるものにしておけば、コンパイル時点でこのような間違いが検出できる。
例えばJava SDKにはColor.REDなどの定数クラスや、swingのWindowConstantsなどnamespaceを区切った定数はあるが、全体をスコープとしたjava.lang.Constantsのようなクラスは無い。どこからでもアクセスできるという意味では環境変数が近いが、やはりドット区切りでnamespaceを表す習慣はある。
プロジェクト内に名前がConstants.拡張子
だけのファイルがあったら危険信号だ。老害警戒令を発令し、レガシーコード改善 or プロジェクト離脱 or die.
歴史的経緯
C言語の時代はすべてがグローバルであり、端末1画面(コマンドプロンプト1ウィンドウがモニタに映るすべてだと思ってもらえば差し支えない)でコード補完もなく開発するにあたって、参照するヘッダファイルをすべて紙に印刷して探していたので、定数が一か所にまとまっていると探しやすかったのである。現代ではシステムも複雑化し、IDEは候補を自動でサジェストしてくれ、コントロールクリック一発で参照先に飛べるので無意味な工夫である。
前時代的なenumを使う
概要
typesafe enumを使うべき時に数値・文字列型のenumを使う。列挙型が適している場所を定数にしてしまうとさらに致命的。前項の'なんでもConstants'の亜種。
解説
なんでこんな事が2010年代にも起きているの?C言語でenumがバグを生む原因とされ、C++でclassを使ったtypesafe enumが発明され、JavaやC#でenumがキーワード登録され(Java1.5からだっけ?)、当たり前のようにenumを使うようになった結果、数値型enumのつらさを知らない世代によって再量産されているのだろうか?
歴史的経緯
C言語では下記のようにenumを定義した。
enum {
January = 1,
February,
March,
April,
...
}
各項目は任意の値を設定でき、値を設定しなかった場合は自動的に一つ前の項目+1となる。上記の例ならFebruary=2
,March=3
となる。当時はインターネットは普及しておらず、OSSはviなどごく一部に限られ12、各種ライブラリはITベンダから高値で買っていた。ソースは非公開でヘッダファイル(.h)とコンパイル済みバイナリ(.soなど)だけを提供してもらい、ヘッダをincludeしてコンパイル後、リンカを使って提供されたバイナリと結合する。この時、enumはコンパイル時に数値に置き換えられてしまう。昔Octoberは8月だったのが10月に変更されたが3、コンパイル済バイナリではOctoberは8月を指したままである。もちろん変更に合わせて元ソースを変更して再コンパイルすれば10月を指すようにはなる。しかしベンダとの連絡は電話だし、プログラムの納品は分厚いKING JIM FILEと納品書・検収書へのハンコが当たり前の時代に、最新の状態のバイナリを手配してもらい最新であり続ける事が如何に難しいかは容易に想像できよう4。また普通のintであるため、前項のconstantsと同じく他の項目との比較や代入に使われてしまう事があった。コピペプログラマは「コピーしてとりあえずそれっぽく動いたら完成」なので値に入っている意味までは考えないのである。
このような状況下で「enumはバグの原因になるから使うな」と言われたのが90年代。c++ではenumキーワードの使い方は変わらなかったが、型安全に列挙型を宣言できるtypesafe enumが発明された。c++でのサンプルコードを示す。
class Month {
public:
Month(int v){
value = v;
}
int getValue(){
return value;
}
friend boolean operator==(const Month& m){
return value == m.value;
}
private:
int value;
} January(1), February(2), ...
今回はclass宣言と同時にインスタンスを作っているが、この辺は流派によって異なる。
std::cout << January.getValue();
とすれば標準出力に1
が表示される。
こうして置くとif(month==January){...}
のようなコードがあった時に、monthを違う型と比較しようものならそこでコンパイルエラーになる。間違ってもif(month.getValue()==January.getValue())
などとしてはいけない。そのためにoperator==
があり、引数の型がMonth
なのである(正確にはconst Month&
だけど)。これがtypesafeでない数値型enumの場合、コンパイラで間違いを検出できず「テスト対象ではたまたま同じ値だった」場合にテストをすり抜け、取りうる値によって正常に動作したりしなかったりするやっかいなバグになる。自動テストもなければ、テスト手法も確立されていなかった時代なので品管をすり抜けてリリースされる事があったのである。
Javaの時代になりtypesafe enumが評価されて当たり前のように使われだすと、このenumを標準化しようという機運が高まりJSR161で議論され、jdk1.5でやっとキーワードとなった。筆者は1.4以前からtypesafe enumを自力で書いてxxx.enumパッケージに入れていたので、1.5でいきなりenumがキーワードになってパッケージ名に使えなくなり、全部enumsに変更した覚えがある。
このように使う。
public enum Month{ January, February, March, ... }
valueの数値として必要なければ、これだけでmonth.name
は文字列でそれぞれ"January","February",...
を返すようになり、January.equals(month)
やMonth.values()
, Month.valueOf(String)
が機能する。上記c++のサンプルと同じようにするならこうだ。
public enum Month{
January(1), February(2),... ;
Month(int v){
value = v;
}
private int value;
しかしほとんどのケースではname
,valueOf(String)
,equals(Object)
ができれば事足りる5ので、これを書くケースはそう多くない。列挙するだけで型安全なtypesafe enumが手に入るので、classで真面目に書くのに比べてかなり楽できる。筆者はjdk1.4以前から設定ファイル読んでenumを自動生成するツールを作っていたが6、1.5対応でソースがかなり短くなった覚えがある。javaでenumが採用された時に、ハゲのおっさんが「enumは(よくわからないけど)バグの原因なるから使っちゃいけないんだよ、ぷんぷん(若い頃にそう教わったけど理由は知らないよ)」とのたまわっていた。90年代のまま時間が止まっているようなので化石と一緒に埋めておいた。
時代は流れ2010年代になると、なぜだかtypesafe enumを知らずに気軽にenumを使うお茶目さんやconstを使うheadlessが復活している気がする。確かに今の時代、enumのためにわざわざc++で例示したようなコードを書くのはタルい。しかしそれはtypesafe enumすらないような前時代的な言語7を使っているのが悪いのであって、列挙型を使うべきところではtypesafe enumを定義すべきだ。君の(無能な)上司8や納期が許せばだが。なに?そもそも型がない?そんな・・・馬鹿な・・・9
PKが全部シーケンス
概要
どんなテーブルにもPKとして連番のIDがある。
改善策
PKが文字列の方がわかりやすいマスタデータは文字列PKにする。2カラムでユニークを担保するテーブルは複合PKにする。
解説
都道府県マスタみたいのようなマスタデータならPKをhokkaido
,okinawa
のような文字列にしておけばSQLログを見ていちいち「何番がなんなの?」って悩まなくてすむ。表示順はsort_orderとして別カラムを用意しておけば変更があったときにも柔軟に対応できる。また、適切に複合PKを組めば楽なのにわざわざ別カラムとしてシーケンスPKを定義して複雑度をあげ、constraintや余計な読み込みSQL発行しているケースも散見される。
古いORMなどでは連番PKが前提のようなものがあるが、そんなものは捨ててしまえ。
歴史的経緯
過去1バイトでもデータを節約したかったときに、PKのように参照元の各テーブルが持つデータ量を減らすためにintにしたかった。またC言語には文字列がなくchar型のポインタを持ちnull文字終端を探す処理を行っていて、バグが混入しやすかったので文字列を扱うこと自体を避けるのが好ましかった。電文は常に固定長、データベースもvarchar型やtext型などはなく固定長文字列型であるため、固定長になるように文字列の後ろに空白を追加する必要があり、全レコードでそのサイズになって空白分はHDDの肥やしとなってしまうため、スキーマ設計時に慎重に最大文字数を決定する必要があった。そんな時代だからintが好まれたのである。現代人は手を出してはいけない。
無効フラグ定数
概要
有効な範囲にある特定の値(FFや99999)を「このレコードは無効」という意味で使う
改善策
statusやis_validなどの別カラムで管理する。
解説
正の整数しか使わないレコードなら-1やnullにするという手もないこともないが、余計なコメントを残す必要があったり、nullはデータベース上でインデックスが効かなかったりソート順を気にしないといけないことがあったりでおすすめできない。
歴史的経緯
長らく符号付intでの-1が0xFFにあたるから(8bit CPUの場合)そこが起源かなと思っていたが、先日より古い歴史を知る人から興味深い話が聞けたので紹介したい。
その昔、プログラマが設計したアセンブラをパンチャーが紙テープにパンチで穴を開けてコンピュータに読み込ませて入力していた。穴が開いていない状態を0
、開いている状態を1
として2進数で読み込ませるのである。穴を開ける箇所が1行あたりに8個あった場合、1行が8ビットとして読み込まれる。パンチャーが打ち間違いをした際、一度開けた穴は埋めることはできないが、まだ開けていない場所を開けることはできる。そこで、打ち間違いをした場合はその行すべて穴を開けてしまい、その場合はこの行を無効としてスキップするという処理になっていた。したがってすべてのビットが1
であるFは無効である、という認識が生まれた。
もし現代にこのような無効フラグを設定しようとする老害がいたら、「このシステム、入力はパンチカードっすか?」と聞いてみよう。
ビットフラグ
概要
複数のステータスを同時に管理する箱をintにし、ビット表記したときの0
,1
でON/OFFを表す。
改善案
クラスやEnumのコレクションにする。DB上ならカンマ区切り文字列など。
解説
例えば動画のタグで作者は病気
,病院に行け
,病院が来い
,ここに病院を建てよう
というのがあり、このうち1個目と4個目だけ保持するなら[作者は病気, ここに病院を建てよう]
という風に持つ。
組み込み系などメモリを節約したいシステムでは現代でも有効であり、プログラマなら知っておくべきテクニックである。今ではintはほぼ32bitに固定された感があるが、基本的にCPUはこのアドレス幅単位でデータをやりとりするので、64bit CPUであればintを一つだけ扱おうとしても64bitのデータがやりとりされる。であればlongを使ってもコストは変わらず、最小限のコストで64種類の状態のON/OFFを管理できて非常にお得である。
が、一方で理由もわからないまま見よう見まねでやってしまう人がいる。筆者が新人の頃出会ったSIerでVBerの人は、ステータスを管理する変数を文字列でstatus="00000000"
と定義し、n桁めのフラグが立っているかどうか、if status.charAt(n)=='0'
のように文字型で判断していた。可読性を犠牲にしてまで速度やメモリ節約をするために使うテクニックなのに、これでは文字数xバイト数の領域が必要となってしまう。'生兵法は大怪我のもと'とはこのことである。
歴史的経緯
言うまでもないがメモリが1バイトでももったいない、CPUの処理を1ステップでも削減したいという時代の名残である。読みやすいコードにするためにも、意味がわからないまま継承されるのを避けるためにも、現代的な環境では最低でも文字列の配列で管理してほしい。
最後に
皆さんが遭遇したレガシーパターンの報告をお待ちしています。コメントや編集リクエストを頂けると大変うれしいです。
-
理由はviを開発したマシンにカーソルキーが無かったからだが、結果としてホームポジションのままカーソル移動できる事が生産性の高さに寄与する事が認知され、今日でも各種IDEでvimプラグインやブラウザでvimiumが使われることとなった。 ↩ ↩2
-
Octは8を表す接頭語(オクトパス、オクタコア等)であるが、1年の始まりが3月から1月に変更されたためOctoberが10月になった、らしい 。 ↩
-
むしろ、それでどうやってITの仕事をするのか想像すること自体が難しい。 ↩
-
もちろんDBにはenum名の文字列で入っている。enumにわざわざ番号振ってintで保存したりするなよ、おじさんと約束だぞ。 ↩
-
enumだけじゃなくて、Entiry,Dao,Service,Controller,strutsでのrouting定義やjspファイルも生成してcrudは楽できるようにしてた。n+1問題もネットで見る前に自分のソースで「これきついな」ってなってたし、Entity同士が相互参照して初期化時にloadしようとしてselect処理が無限ループしたりして関連クラスを自動loadするのやめたりとか、今ならしなくていい苦労をいっぱいした。 ↩
-
なぜtypescriptは2010年代に定義された静的型付け言語のくせにtypesafe enumがないのか?アホなの?死ぬの?typescriptの開発にヘルスバーグが関わったとされているが、彼は.NETで忙しくてtypescriptはろくにレビューしてないんじゃないの?javascriptとの互換性のために既存のenumはとっておくとしても ↩
-
上司がtypesafe enumも知らないようだとご愁傷様。上司との巡りあわせは自分の努力だけではどうにもならない運もあるから。来世では頑張ろうね。 ↩
-
javascriptやPHPにも(動的だけど)型は、ありまぁす。 ↩