2値フラグを
通りの組み合わせができ、これをすべてテストしなければならないですね。
一方、そのようなフラグの組み合わせで表現したとしても、実際の条件成立パターンは QM 法 (クワイン・マクラスキー法
) で大きく簡略化・情報圧縮できることが多いわけです。そして簡略化・圧縮後の論理式というのは、人間が意味論的に理解しやすい論理であったりします。
わざわざ網羅テストを複雑化させて、人間が解りにくい形にする必要はないよね、ということになります。
加えて …
なぜ QM 法で圧縮できるかというと、各条件・各フラグが完全独立ではなく同時には成立しないケースや後続条件を考慮しなくてよい早期リターン可能なケースなどがあるからです。
論理的に成立しない/使われないパターンまで等しくテストしなければならない状況に持ち込むのは非効率だという遅延評価戦略的な見方もできます。
もし〇〇なら✕✕をする
という振る舞いをフラグ変数を使わずに素直に書き下せば、
- if 〇〇 {
- ✕✕をする
- }
こんな風に書くことでしょうね。
フラグ変数を導入するというのは、上を例えば
- var フラグ変数x
- if 〇〇 {
- フラグ変数x = y
- }
- ★
- if フラグ変数xがy {
- ✕✕をする
- }
と書くということです。フラグ変数を介在させて、条件判断処理と、条件判断の結果に対応する処理の結びつきを間接化してるのです。
間接化しないほうがわかりやすく、バグも少ないだろう、というのはわかると思います。理由はフラグを使うと変数の値という状態を持ち込んでいて、その状態を意図しない箇所が変更する可能性を導入しているからです。特に上の場合、★のところに処理が挟まったりした場合などですね。変数が増えてくればなおさらですが、たとえ一個でも酷い話です。
しかしそもそも、間接化したかったのは、★に処理を挟みたいから、とかなのではないでしょうか。他に、フラグ変数を使いたくなる理由もあるかもしれませんが、できれば回避したほうが良くて、そのために回避方法をとことん考えるべきです。
上の場合であれば、フラグ変数は〇〇の判断の結果を保存するために用いられているので、
- function is〇〇(....) {
- return 〇〇
- }
- ★
- if is〇〇(....) {
- ✕✕
- }
のように結果を表す変数を導入するのではなく、結果を得る関数に切り出すことで、フラグ変数を排除できます。またこうすることで条件分岐が減り、複雑さが減り、カバレッジを上げるのが楽になり単体試験が容易になります。式を求めるために必要な情報が関数の引数の形で明確になり理解しやすくなります。長いブロックが処理単位で分割され読みやすくなります。関数名を適切につけることでコメントに頼らずに意味もわかりやすくなります。
ここでは、フラグ変数を参照している箇所を、フラグ変数を計算する関数呼び出しに置き換えています。その場合デメリットになるかもしれないのは、計算タイミングが遅延されたこと、また複数回のフラグ変数の参照が複数回の呼び出しになるので、計算にコストがかかる場合計算量が増えてしまう可能性があること、副作用を伴なう場合意味がかわることなどです。
副作用に関していえば、上記のような見易くなる変換を邪魔してスパゲッティを生じさせているなら、なおさら副作用や値の依存性の制御が必要だと言えるでしょう。計算コストに関してはメモ化を検討するのも良いでしょう。
こちら↓の 山本 聡 様の回答が、とーーーっても参考になったので共有いたします。
この回答の中で言及はされていませんが、やりすぎDRYの例として提示されているコードがまさしくフラグ引数のアンチパターンに相当しています。
フラグ引数のリファクタリング方法は上記の回答を参考にしていただくとして、ここではご質問に沿って、フラグで処理を切り替える場合(下記、コード1)でどのような不都合があるか見てみましょう。
- // コード1
- onButtonAClick()
- {
- onButtonClick(Flags.A);
- }
- void onButtonBClick()
- {
- onButtonClick(Flags.B);
- }
- void onButtonCClick()
- {
- onButtonClick(Flags.C);
- }
- void onButtonClick(Flags flg)
- {
- // 前処理 P
- if(flg == Flags.A)
- {
- // 処理 A
- }
- else if(flg == Flags.B)
- {
- // 処理 B
- }
- else if(flg == Flags.C)
- {
- // 処理 C
- }
- else
- {
- // 想定外処理 X(ログ出しなど)
- }
- // 後処理 N
- }
よくあるフラグ引数のアンチパターンとして示されるコードです(処理 P, A, B, C, N ともそれなりのボリュームがあるものとします)。これをベースにご説明いたします。
このコードの着想は『buttonA~buttonCのいずれのクリックイベントに対しても 前処理 Pと 後処理 Nを実施するのだから、「P → A or B or C → N」という流れを共通化しよう』という思いつきによるものです。
確かに「P → 何かの処理 → N」の流れだけで言えば共通化できていそうに見えますが、現実的な実装はこんなに単純にはなりません。どんどん実装を進めていったときにコードの脆さが露見していきます。まあ見ていてください(ドヤ顔)。
例えば「処理 A, Bの実行結果によって後処理 Nの前に処理 Eを実行する」といった仕様が発生した場合、コードを以下のように書き変えることになるでしょう。
- // コード2
- onButtonAClick()
- {
- onButtonClick(Flags.A);
- }
- void onButtonBClick()
- {
- onButtonClick(Flags.B);
- }
- void onButtonCClick()
- {
- onButtonClick(Flags.C);
- }
- void onButtonClick(Flags flg)
- {
- // 前処理 P
- bool flgE = false;
- if(flg == Flags.A)
- {
- ResultA resultA;
- // 処理 A(resultAに何かを代入)
- flgE = resultA.NeedE();
- }
- else if(flg == Flags.B)
- {
- ResultB resultB;
- // 処理 B(resultBに何かを代入)
- flgE = resultB.NeedE();
- }
- else if(flg == Flags.C)
- {
- // 処理 C
- }
- else
- {
- // 想定外処理 X(ログ出しなど)
- }
- if(flgE)
- {
- // 処理 E
- }
- // 後処理 N
- }
さらに、処理 Cでも同様の仕様変更(今度は処理 F)が必要になりました。コードを修正します。
- // コード3
- onButtonAClick()
- {
- onButtonClick(Flags.A);
- }
- void onButtonBClick()
- {
- onButtonClick(Flags.B);
- }
- void onButtonCClick()
- {
- onButtonClick(Flags.C);
- }
- void onButtonClick(Flags flg)
- {
- // 前処理 P
- bool flgE = false;
- if(flg == Flags.A)
- {
- ResultA resultA;
- // 処理 A(resultAに何かを代入)
- flgE = resultA.NeedE();
- }
- else if(flg == Flags.B)
- {
- ResultB resultB;
- // 処理 B(resultBに何かを代入)
- flgE = resultB.NeedE();
- }
- else if(flg == Flags.C)
- {
- bool flgF = false;
- // 処理 C(flgEに何かを代入)
- if(flgF)
- {
- // 処理 F
- }
- }
- else
- {
- // 想定外処理 X(ログ出しなど)
- }
- if(flgE)
- {
- // 処理 E
- }
- // 後処理 N
- }
ヤバさがわかってきましたか?
これでもかなりヤバめですが、buttonCの類似機能を持つ buttonC2が追加となったとします。C2の処理は Cと共通部分(C')があるようです。ちょっとコード化してみます。
- // コード4
- onButtonAClick()
- {
- onButtonClick(Flags.A);
- }
- void onButtonBClick()
- {
- onButtonClick(Flags.B);
- }
- void onButtonCClick()
- {
- onButtonClick(Flags.C);
- }
- void onButtonClick(Flags flg)
- {
- // 前処理 P
- bool flgD = false;
- if(flg == Flags.A)
- {
- ResultA resultA;
- // 処理 A(resultAに何かを代入)
- flgD = resultA.NeedD();
- }
- else if(flg == Flags.B)
- {
- ResultB resultB;
- // 処理 B(resultBに何かを代入)
- flgD = resultB.NeedD();
- }
- else if(flg == Flags.C || flg == Flags.C2)
- {
- // 処理 C'
- bool flgF = false;
- if(flg == Flags.C)
- {
- // 処理 C1(flgFに何かを代入)
- }
- else
- {
- // 処理 C2(flgFに何かを代入)
- }
- if(flgF)
- {
- // 処理 F
- }
- }
- else
- {
- // 想定外処理 X(ログ出しなど)
- }
- if(flgE)
- {
- // 処理 E
- }
- // 後処理 N
- }
そろそろ読むのも嫌になってくるかと思います。
それでは・・・ここで、問題です(デデンッ!)
- // コード5
- onButtonAClick()
- {
- onButtonClick(Flags.A);
- }
- void onButtonBClick()
- {
- onButtonClick(Flags.B);
- }
- void onButtonCClick()
- {
- onButtonClick(Flags.C);
- }
- void onButtonClick(Flags flg)
- {
- // 前処理 P
- bool flgE = false;
- if(flg == Flags.A)
- {
- ResultA resultA;
- // 処理 A(resultAに何かを代入)
- flgE = resultA.NeedE();
- }
- else if(flg == Flags.B)
- {
- ResultB resultB;
- // 処理 B(resultBに何かを代入)
- flgE = resultB.NeedE();
- }
- else if(flg == Flags.C || flg == Flags.C2)
- {
- // 処理 C'
- bool flgF = false;
- if(flg == Flags.C)
- {
- // 処理 C1(flgFに何かを代入)
- }
- else
- {
- // 処理 C2(flgEに何かを代入)
- }
- if(flgE)
- {
- // 処理 F
- }
- }
- else
- {
- // 想定外処理 X(ログ出しなど)
- }
- if(flgE)
- {
- // 処理 E
- }
- // 後処理 N
- }
上記コード5は明確なコーディングミスがあります。どこでしょうか?
もしフラグ変数の何が悪いかわからない方は、ぜひとも間違い探しをしてみてください。その不毛さを身をもって実感するといいと思います。
さて、いかがでしょうか。
間違い見つかりましたか?
それでは適当な行稼ぎも済んだので・・・
正解発表です!(ジャジャンッ!)
まあ、そんなにもったいぶることでもないんですけどね。
51行目の
- // 処理 C2(flgEに何かを代入)
と、54行目の
- if(flgE)
が間違いでした。本当はここの処理では flgFが使われるべきところです。ちょっとずるいかもしれませんが、間違いが1つとは申し上げませんでした。
実際の開発も不具合の原因が1つとは限りません。そして、上記のコーディングミスは実行するまでその存在に気づけません。
フラグを多用する設計はコーディングミスを生みやすく、なまじビルドエラーなどが出ないため、実行しないと分からない(テストケースが甘い場合にはテストですら発覚しない)のです。
onButtonClickが肥大化しすぎてテストコードが書けないか、書けたとしてもメンテナンスに手間が掛かりすぎます。テストケースの作り込みも難儀することでしょう。結局、ブレークポイントで1行ずつ追っていくか、分岐の端々でログを出すかしかありません。
この例の場合 「// 処理 A」とかで省略していますが、実際のコードは数百行に肥大化したものになります。if文の分岐もこの比ではないほど複雑になります。その数百行の中からたった1文字のミスを探し出すことを考えてみてください。
やりたくないでしょ?
それがフラグ引数が敬遠される理由です。
フラグを使うのが問題なのではなく「多用」するのが問題なのではないでしょうか。
フラグにより状態が変わるわけですから、10個のフラグがあると「1024通り」の状態が発生します。
テストケースもその全てのパスを確認しないといけないわけですから最低でも1024は書かないといけません。
全てのフラグの成立条件を変えて評価をされていますでしょうか?
また、関数・メソッドの中にフラグがあり、複数の箇所でフラグにより処理が変わるのであれば「元々1つにすべきでは無かった」可能性があります。
フラグがいけないのではなく多用がいけない気がします。
フラグがあるってことは、フラグによる条件分岐があるってことですよね。条件分岐がひとつ増えるとプログラムの複雑度は1増えますので、KISS(単純にしておけ)原則に違反します。あとは熱力学の法則に従ってエントロピーは増大する一方、プログラム全体が汚部屋やゴミ屋敷のようになっていきます。
実例を示せとのことですが、ちょっと良い例は示せません。プログラムソースコードがわかりにくい例を掲げても結局わかりにくいままだと思いますので。今まで見たことのある、長い条件式や深いネストの嫌なif文を思い出して嫌な気分になってみてください。それがフラグを追加して悪くなる実例です。
フラグを追加しても複雑さが増えなければ良い訳です。
- フラグによって条件分岐する箇所が少なければ複雑化を抑えられる
- フラグ間に依存関係がなければ、条件分岐の複雑化を抑えられる
新しく追加するフラグを良く分析して、安易な条件分岐の増大を抑えましょう。
- コンパイル時静的に決まるものはコンパイルオプションにする
- feature toggle のような役割のものなら、そういうのを一元管理しているところにまかせる
- フラグで分岐するのではなく、型の抽象化を使って型で分岐する
ああもうこれは誰が見てもフラグにするしかないよねっていう王道フラグを実装して、怒った人を見返してやりましょう。
フラグも「大域変数になってて(後個からでも参照可能で)数が多い」とかだと訳わからなくなるんでアレなだけですね。
オブジェクトの整理がしっかりなされてて、そのインスタンス内で非公開メンバとして扱われて、尚且つ状態遷移がしっかりまとまってたら別にフラグが多くても問題にならないです。
Aというインスタンスの状態がXXの場合、Bのインスタンスが如何な状態であろうと無視できる。…とか、オブジェクト間の関係性を示す仕様の定義で整理できます。テストケースはクラスBの中でだけ完結するので、クラスAのテストケース分析に関し、クラスBの当該フラグの状態ケース全てを網羅する必要はないわけです。
単独で存在するフラグを無軌道に増やしたり、その責任範囲が特定しにくい(値の操作をあらゆる場所で行うとか…)等の設計の拙さが問題なのです。
多用することは別に間違いとはいえません。そのようなシステムはたくさんあります。
フラグの質が問題なんじゃないでしょうかね。指摘側も本質分かってなくてフラグ多用するな言ってるだけとかね。アクションをフラグ使ってディスパッチする位なら、関数分けろが一般論。メッセージキューとかはディスパッチいるかもしれんけど、プロセス跨がないなら関数ポインタとかデリゲートなどの動的クロージャをキューに渡せばよいってなりますよね。
怒ると褒めるをフラグ化する必要が本当にあるのか、まずは怒るメソッドと褒めるメソッドを用意して、DBやユーザによる入力に基づいて切り替えが必要な場合だけ、フラグをディスパッチする構造にするのがいい。こうするとディスパッチ関数もシンプルになっていいよね。ディパッチ項目が100とか行っても大丈夫になる。本当にフラグによる指定が必要?コードに怒る()、褒める()とか書いて済むならそっちの方がいいの。
コーディングルールなんて本質分かってない雑魚が適当に作ってるだけのもの多いからね。
テスト項目増えるほうは気にしてないな。自分は。本当に必要ならやるだけ。不要なテスト項目増やされるのは嫌だけどね。
例えば自動運転システムの開発。前を走行中の自動車が速度を落としたら自分もブレーキを踏んで速度を落とす仕様があったとします。フラグでいきましょう。仮に減速フラグで管理するとします。
次に、目の前の車が加速したら、ある程度までそれに追随する仕様があったとします。加速フラグの管理とします。
減速フラグtrueの場合、減速の処理を、加速フラグtrueの場合、加速の処理をするとします。
初期値は以下です
減速フラグ=false
加速フラグ=false
目の前の車が加速しました。
減速フラグ=false
加速フラグ=true
目の前の車が減速してきました。
減速フラグ=true
加速フラグ=false
前の車と同じ速度です。
減速フラグ=false
加速フラグ=false
…こんな車に乗りたいでしょうか?
加速フラグと減速フラグが両方立つことは絶対ないのでしょうか?
両方立った場合ちゃんと減速が優先されるのでしょうか?
仮に減速が優先されて、減速が終わった時に、加速が立ったままになっていて、突然加速し始めないのでしょうか?
停止中と、加速も減速もしてない走行中が、どちらも減速、加速フラグfalseで判別できませんね。停止中フラグと定速走行フラグを新設しますしょうか?
2の4乗通りで16パターンの考慮が必要ですね。タイミングの事とか考えると無数に組み合わせありますね。派遣会社から外注のエンジニア沢山雇って試験やってもらいましょうか!
いいですね、フラグいっぱいにしてスパゲティにしたほうが雇用が増えます。
走行状態変数を作って停止中、加速中、減速中、定速走行中の4状態を管理してシンプルにするとか、余計なことは考えてはだめです。
あくまでフラグにしましょう。全部のフラグが立ってる場合のテストとかして、車ブルブルバグらせて、それを治すためにさらにフラグがフラグを参照するようにして、グローバルなフラグとローカルなフラグの入れ子構造にして、徹底的にスパゲティにして、みんなで残業代稼ぎましょう。なんのこっちゃ
とりあえず、そういうことは怒ってくる人に聞くのがスジじゃないかなあ
さておき
フラグを多用すると怒られる
まあ、これは分かるでしょう
この状態はロジックが煩雑になり、読むのも改修するのも面倒になるってことス
機能分割を考え直せって話です
なぜフラグを使ってはいけないのか
使うなとは言ってないんじゃないですか?
フラグでごちゃごちゃやってるヒマあるんだったら、条件直指定で抜けるなり元にもどるなりした方がいいんじゃないですかねえ
おそらく実際のコードを見ないと正確な評価や助言はできません
ふわっとした話で一般的な話を聞いて、それを実際のコードに活かせるとはとても思えません
再度いいますが、怒られたらすぐに「その人に」理由を聞きましょう
昔、知人が関わっていたゲーム、NOëL NOT DIGITALというPlay Station用のゲームのデバッグをお手伝いしたことがあります。その時に見つけたバグです。
このゲームは女の子に電話をかけ、その時の会話とかで好感度を上げながらストーリーを進めていくアドベンチャーゲームです。
夜中に電話すると、寝ているところを起こしてしまうので好感度が下がります。
さて、夜中の電話を繰り返していると、好感度が-127と最低の値になります。その状態になった時に、また夜中に電話をすると、好感度は-128にならずに、128になります。
プログラム的にいうと、符号付き8ビット変数を使っていて、アンダーフローするということになります。
そうすると、130回以上夜中に電話をかけ続けた男に急にデレるのです。好感度最高ですからね。
通称「ストーカープレイ」と名付けられました。製品版はやってないので修正されたかどうかは覚えていませんが、誰の助けも得ず、ソースコードも見ずに一発でそのバグを見つけたのは凄かったと自分でも思います。
0 件のコメント:
コメントを投稿