2023年10月31日火曜日

プログラミングにおいてフラグを多用すると怒られるのですが、なぜフラグを使ってはいけないのか実例を用いて説明して頂けませんか?

·
フォロー

2値フラグを

個使うと

通りの組み合わせができ、これをすべてテストしなければならないですね。

一方、そのようなフラグの組み合わせで表現したとしても、実際の条件成立パターンは QM 法 (クワイン・マクラスキー法

) で大きく簡略化・情報圧縮できることが多いわけです。そして簡略化・圧縮後の論理式というのは、人間が意味論的に理解しやすい論理であったりします。

わざわざ網羅テストを複雑化させて、人間が解りにくい形にする必要はないよね、ということになります。


加えて …

なぜ QM 法で圧縮できるかというと、各条件・各フラグが完全独立ではなく同時には成立しないケースや後続条件を考慮しなくてよい早期リターン可能なケースなどがあるからです。

論理的に成立しない/使われないパターンまで等しくテストしなければならない状況に持ち込むのは非効率だという遅延評価戦略的な見方もできます。

·
フォロー

もし〇〇なら✕✕をする

という振る舞いをフラグ変数を使わずに素直に書き下せば、

  1. if 〇〇 { 
  2. ✕✕をする 
  3. } 

こんな風に書くことでしょうね。

フラグ変数を導入するというのは、上を例えば

  1. var フラグ変数x 
  2. if 〇〇 { 
  3. フラグ変数x = y 
  4. } 
  5.  
  6. if フラグ変数xがy { 
  7. ✕✕をする 
  8. } 

と書くということです。フラグ変数を介在させて、条件判断処理と、条件判断の結果に対応する処理の結びつきを間接化してるのです。

間接化しないほうがわかりやすく、バグも少ないだろう、というのはわかると思います。理由はフラグを使うと変数の値という状態を持ち込んでいて、その状態を意図しない箇所が変更する可能性を導入しているからです。特に上の場合、★のところに処理が挟まったりした場合などですね。変数が増えてくればなおさらですが、たとえ一個でも酷い話です。

しかしそもそも、間接化したかったのは、★に処理を挟みたいから、とかなのではないでしょうか。他に、フラグ変数を使いたくなる理由もあるかもしれませんが、できれば回避したほうが良くて、そのために回避方法をとことん考えるべきです。

上の場合であれば、フラグ変数は〇〇の判断の結果を保存するために用いられているので、

  1. function is〇〇(....) { 
  2.  return 〇〇 
  3. } 
  4.  
  5.  
  6. if is〇〇(....) { 
  7. ✕✕ 
  8. } 

のように結果を表す変数を導入するのではなく、結果を得る関数に切り出すことで、フラグ変数を排除できます。またこうすることで条件分岐が減り、複雑さが減り、カバレッジを上げるのが楽になり単体試験が容易になります。式を求めるために必要な情報が関数の引数の形で明確になり理解しやすくなります。長いブロックが処理単位で分割され読みやすくなります。関数名を適切につけることでコメントに頼らずに意味もわかりやすくなります。

ここでは、フラグ変数を参照している箇所を、フラグ変数を計算する関数呼び出しに置き換えています。その場合デメリットになるかもしれないのは、計算タイミングが遅延されたこと、また複数回のフラグ変数の参照が複数回の呼び出しになるので、計算にコストがかかる場合計算量が増えてしまう可能性があること、副作用を伴なう場合意味がかわることなどです。

副作用に関していえば、上記のような見易くなる変換を邪魔してスパゲッティを生じさせているなら、なおさら副作用や値の依存性の制御が必要だと言えるでしょう。計算コストに関してはメモ化を検討するのも良いでしょう。

メモ化 - Wikipedia
メモ化 ( 英 : memoization )とは、 プログラム の高速化のための 最適化 技法の一種であり、 サブルーチン 呼び出しの結果を後で再利用するために保持し、そのサブルーチン(関数)の呼び出し毎の再計算を防ぐ手法である。メモ化は 構文解析 などでも使われる(必ずしも高速化のためだけとは限らない)。 キャッシュ はより広範な用語であり、メモ化はキャッシュの限定的な形態を指す用語である。 メモ化という用語は 1968年 に ドナルド・ミッキー が ラテン語 の memorandum (覚えておく)から作った造語である [1] 。 memorization (記憶、暗記)は 同根語 であってよく似ているが、メモ化という言葉は情報工学では特別な意味を持つ。 メモ化された関数は、以前の呼び出しの際の結果をそのときの引数と共に記憶しておき、後で同じ引数で呼び出されたとき、計算せずにその格納されている結果を返す。メモ化可能な関数は 参照透過性 を備えたものに限られる。すなわち、メモ化されたことで副作用が生じない場合に限られる。メモ化の実装では ルックアップテーブル などの参照方式が使われるが、メモ化では参照されるテーブルの内容は必要に応じて格納されるという点で、通常のテーブル参照とは異なる。 メモ化は関数の時間コストを領域コストに交換して、時間コストを低減させる手段である。すなわち、メモ化された関数は速度の面で最適化され、 記憶装置 の領域という面ではより多く消費する。 計算複雑性理論 は、 アルゴリズム の時間と領域のコストを扱う。全ての関数には時間と領域についての 複雑性 が定義される。 このように トレードオフ が発生するが、同様のトレードオフがある最適化とは異なる。というのも、 演算子強度低減 などの 最適化 はコンパイル時のものだが、メモ化は実行時の最適化であるためである。さらに言えば、演算子強度低減は例えば、乗算を加算の組合せで表すことで性能を改善しようとするもので、プラットフォームのアーキテクチャに深く依存している。一方、メモ化はプラットフォームからは独立した戦略である。 次の 擬似コード は n の 階乗 を計算する関数である。 function factorial ( n ) // n は非負整数 if n == 0 then return 1 else return factorial(n - 1) * n // 再帰呼び出し end if end function n ≥ 0 であるような全ての 整数 n について、関数 factorial の結果は 不変 である。つまり、 x = factorial(3) としたとき、 x の値は 常に 6 である。上記のメモ化する前のコードでは、結果を得るのに n + 1 回の 再帰呼び出し が必要であり、それが計算における時間コストに相当する。プラットフォームのアーキテクチャにも依存するが、時間コストは以下のコストの総和である。 関数の スタックフレーム を設定するのにかかるコスト n と 0 を比較するのにかかるコスト n から 1 を引くのにかかるコスト 再帰呼び出しのためのスタックフレームを設定するのにかかるコスト 再帰呼び出しの結果と n の乗算を行うコスト 結果を呼び出し側に返すのにかかるコスト メモ化していない実装では、これらのコストのうち 2 番から 6番が n に比例した回数かかることになる。 factorial をメモ化したバージョンを以下に示す。 function factorial ( n ) // n は非負整数 if n == 0 then return 1 else if (テーブルに n の階乗が格納されている) then return (テーブルに格納された n の階乗の値) else x = factorial(n - 1) * n // 再帰呼び出し x を n の階乗の値としてテーブルに格納する return x end if end function このメモ化バージョンでは、「テーブル」は広域の永続性のある変数であり、 n をキーとする 連想配列 のようなものである。この ルックアップテーブル が領域(すなわちコンピュータのメモリ)を使うことによって、 factorial を同じ引数で繰り返し呼び出したときに要する時間が削減される。 例えば、 factorial を最初に引数 5 で呼び出すと、その後 5 以下の引数で呼び出したとき、それらの値は既にメモ化されている。なぜなら、 factorial は引数 5、4、3、2、1、0 で再帰的に呼び出され、それぞれの呼び出しについて結果が記憶されるからである。また、引数 5 で呼び出された後に引数 7 で呼び出されると、再帰呼び出しは 2 回で済み(7 と 6)、5! の値は既に記憶されているのでそれが使われる。このように、メモ化された関数は呼び出される度により効率化されていき、結果として全体の高速化が図られる。 自動メモ化 [ 編集 ] 上述の factorial の例のように、メモ化は一般に プログラマ が関数内部に明示的に施すものであるが、 参照透過性 のある関数は外部で自動的にメモ化することもできる [2] 。 ピーター・ノーヴィグ が採用した技法は Common Lisp (ノーヴィグの論文で自動メモ化の例に使われていた言語)だけでなく、様々な プログラミング言語 で応用可能である。自動メモ化は 項書き換え [3] や 人工知能 [4] の研究でも応用が模索されている。 関数が 第一級か第二級オブジェクト であるようなプログラミング言語(例えば Lua )では、自動メモ化は関数を特定の引数付きで実行するたびに、その結果の値で(実行時に)置き換えてやることで実装される。このような置き換えを行う汎用関数を本来の関数を呼び出す代わりに呼び出せばよい。 擬似コード を以下に示す(この例では関数は第一級オブジェクトであると仮定している)。 function memoized-call ( F ) // F は関数オブジェクト if ( F には対応する配列 values がない) then allocate an associative array called values ; attach values to F ; end if; if F. values[arguments] is empty then F. values[arguments] = F (arguments); end if; return F. values[arguments] ; end function factorial を自動メモ化する場合もこの戦略を使い、 facto
広告
·
フォロー

こちら↓の 山本 聡 様の回答が、とーーーっても参考になったので共有いたします。

山本 聡
· 1月13日
最近、DRY原則のやりすぎや安易な共通化は良くないと聞きます。しかしその解決策が後のことや目的、ドメインを考えるなど曖昧です。明確な基準はないのですか?
長文です。長くなってしまいました。 冒頭に追記、DRY原則って、オブジェクト指向の設計的な目的とかドメインとかの事だったらしいので、別の質問して自己回答してみました。 誰かのご参考になるかもなのでリンクしておきます。 システムの設計についての質問です。顧客クラスを多数の関連部署でそれぞれで実装を行うと処理がぶつかって破綻すると聞きますが、それに対しての解決策はどのようにするのがよいのですか?に対する山本 聡 (Satoshi Yamamoto)さんの回答 「最近、DRY原則のやりすぎや安易な共通化は良くないと聞きます。」 そんなの聞きます? やりすぎDRYって俺以外に言ってる人ほぼ聞いたことなんだけど...... そんなこと気がついているくらいに技術レベル高い人、そんなにいるかなあ。お友達になれそうだわ。 あ、ここにある程度書いているか...。この記事見て、私も気がついたんだったです。 記事には次のように書いています。 > DRYは素晴らしい考えですが、やり過ぎると密結合を生んでしまう。 んーー、ちょっと表現が微妙かな。それに解決策も示されてないなー。この記事。 私が気が付いたことと私が思う解決策をここの回答で書いておきます。 「DRY原則のやりすぎ」「安易な共通化」についてです。 これはつまりは「DRY原則を正確に理解してない間違った形でのDRY原則の使用」「考えられていない形での共通化」ということです。 私の日本語もあやしいもんですが、正しく表現すると、DRYは素晴らしいんですが、まちがったDRYのやり過ぎによって、DRYに違反して、だめになる。 そんな感じです。 やりすぎなDRYって、いうのは、実際には正しいDRYをやりすぎているのではなく、DRYに見えるけど、本当はDRYじゃないよ、ってことになります もし、いつもうだうだと長文かいてしまう自分の発言で「DRY原則のやりすぎや安易な共通化は良くないと聞きます」という感じをうけていただけてたら、なんらか、こういうことに疑問持ってくれたら、それはとっても感謝です。 日本の技術力の底上げ的になったら、嬉しいかなと思ってます。 極めて単純に見えることでも、こういう積み重ねが大事というか、リアルワールドでお仕事する人の中で、自称上級者の人も、これをわかっている人は皆無でこういうのがわかっていないことで、スパゲティ(コード)を茹でて(作って)しまうことに通じてくるので、お気をつけくださいませ、と思います。 ということで、DRY原則のやりすぎ、と呼んでいる、実際にはDRY違反の事例について細かく書いておきます。 概念的ですが、下記のようなコードを100回くらいは見ました。いや、ちょっと大げさかな、目立つところでガッツリ使われてたのを見たのは2,30回くらいかも。 プロジェクトのコードみてたら、枝葉末端みていると、ほぼ100%みかけますし、根幹部分にこういうのがあって、目くじらたてなくてもいいんだけど、ちょっと見苦しいな、まだまだコードの整理整頓が足りんな、と自分は思います。 根幹部分はきれいにしておきたいものねー。なるべく。 普通、人は下記コードがまだ整理整頓できてない、ってことに気が付きもしないです。これが汚いコードだって見分けられる人いるのかねぇ。 人はよく、こういうコード書いてしまうと思うんですよ。というのが下記ね。 const onClickButton1 = () => { getData(`button1`); } const onClickButton2 = () => { getData(`button2`); } const onClickButton3 = () => { getData(`button3`); } const onClickButton4 = () => { getData(`button4`); } const getData = (param) => { // 前処理 100行 if (param === `button1`) { // 処理A 50行 } else if (param === `button2`) { // 処理B 50行 } else if (param === `button3`) { // 処理C 50行 } else if (param === `button4`) { // 処理D 50行 } // 後処理 100行 } 雑な概念コードですが、こういうの書いていたとします。 これ、共通化じゃなく、DRY原則違反なんです。でも、ほんとーーーーーに多くの人がこれがDRYだと思ってる。共通化しているからいいじゃない!と思ってる。 当社調べでは140%くらいの人が、これがDRYだとかって思ってるんじゃないかいね。 Webで調べました。 DRY原則 同じ意味や機能を持つ情報を複数の場所に重複して置くことをなるべく避けるべきとする考え方。 このようにあります。 「getDataで1ヶ所にまとめているから、共通化じゃないか!」ではないんです。 buttonが分散(分岐)しているのを、getDataで集約しているのは、別に悪くないわけですがgetDataの内部で分離しているから、わざわざ集めたものを分散(分岐)しているから、不要な分散コードがまぎれているんす。 やるならこう。下記のように。 こんなコード例のように理想的にいくかどうか、いろいろ微妙ですが、突き詰めて正しくDRYを行うと、こうです。 const onClickButton1 = () => { getDataBefore(); // 処理A 50行 getDataAfter(); } const onClickButton2 = () => { getDataBefore(); // 処理B 50行 getDataAfter(); } const onClickButton3 = () => { getDataBefore(); // 処理C 50行 getDataAfter(); } const onClickButton4 = () => { getDataBefore(); // 処理D 50行 getDataAfter(); } const getDataBefore = () => { // 前処理 100行 } const getDataAfter = () => { // 後処理 100行 } これならgetData内の分散分岐処理がなくなっています。 そもそもgetData内の分岐処理がいらなかったことがわかりますよね。 getBeforeをたくさん書いているから分散しているとか、そういう話じゃねーんですよ。 ifの分岐がなくなっているところを見て欲しいわけで。 より多段階にリファクタリングかけて、JSのパワーを使ってよりリファクタリングするとこうかな。 const onClickButton1 = () => { getData(()=> { // 処理A 50行 }) } const onClickButton2 = () => { getData(()=> { // 処理B 50行 }) } const onClickButton3 = () => { getData(()=> { // 処理C 50行 }) } const onClickButton4 = () => { getData(()=> { // 処理D 50行 }) } const getData = (f) => { // 前処理 100行 f(); // 後処理 100行 } 最初のコードと上のコードくらべてみると、最初のコードが、DRYに沿ってないってのわかりますかねー。最初のコード内にあるgetData内部のifの分岐は、DRYしてるのに仕方なく分岐しているのじゃなくて、書かなくてよい余計な分岐なんですわ。 ifを関数引数に置き換えているだけじゃないか、ってことじゃないですよ。分散しているものをわざわざ集めたのにその集めたところで分散するな、ってことね。 で、その、最初のコードにあるgetData内の分岐処理が、あとあと複雑になってきて何が共通化になるのかどうなのか、わけわかんなくなっていくわけですがそういうのを予防するのが、正しいコードの整理整頓、正しいリファクタリング、ということです。 もう一個、例をあげておきます。 if (a) { // 処理A 10行 // 処理X 10行 } else if (b) { // 処理A 10行 // 処理Y 10行 // 処理B 10行 } else { // 処理Z 10行 // 処理B 10行 } 元は上記コードだったとしましょう。DRYを行おうとして、間違ったDRY=やりすぎなDRYをすると、次のようになります。「安易な共通化」とも呼べますね。 const commonProcess = (param) => { if (param === 'X' || param === 'Y') { // 処理A 10行 } if (param === 'X') { // 処理X 10行 } else if (param === 'Y') { // 処理Y 10行 } else if (param === 'Z') { // 処理Z 10行 } if (param === 'Y' || param === 'Z') { // 処理B 10行 } } // メイン処理 commonProcess( a ? 'X' : b ? 'Y' : 'Z' ); 上記は「共通化しなかったほうがよかったじゃないか!」と思わせるような意味で「やりすぎDRY」です。でも、それは決して「やりすぎ」なのではなく、やり方が間違っているということなんです。 「安易な共通化」をスべきではない、のですが、共通化の方法を間違っているということです。 正しく共通化を行うには次のようにするべきです。 const funcA = () => { // 処理A 10行 } const funcB = () => { // 処理B 10行 } if (a) { funcA(); // 処理X 10行 } else if (b) { funcA(); // 処理Y 10行 funcB(); } else { // 処理Z 10行 funcB(); } 共通化する位置が違うんですよ。正しく共通化するにはどのようにするかを正確に見抜いてコード書かないといけません。 > しかしその解決策が後のことや目的、ドメインを考えるなど曖昧です。明確な基準はないのですか? 明確な基準がない、って、まあ、よくわかりませんが、これで何かあいまいなところはときほぐれますでしょうか? それともそういう話とは違ってたかな? 私もDRY原則の原理主義とかじゃないので、コードに妥協せずになんでもかんでもリファクタリングしろ、ということではないです。 DRYは原則だけど、このプロジェクトの今のコードでは、リファクタリングが大変だからコードは理想形ではないからおいておこう、という場面があるのはしっています。 リファクタリングは簡単ではないです。 でも、こういうやり過ぎなDRY(まちがったDRY)という共通化した内部で分岐するというものが理想形ではないと知り、これが積み重なってくるとやっかいなことをいろいろ引き起こすものだということも知っておくとよいんじゃないかなー、 共通化した内部で分岐する、が望ましくないことだ、と知った上で、コードをリファクタリングするともっともっときれいなコードを書けるようになります。 ということで、だらだら書いてしまいました。 長々書いたので、YouTuber的な定番のセリフですが 最後までご視聴いただきありがとうございます。 もしこの動画がいいなと感じましたら、高評価、チャンネル登録、ぜひよろしくお願いします! まあ、こんなマニアックなコードの良し悪しとか、わかってもらえないと思うので、いいですけどね。別に。高評価なんか。1件ももらえなくても!他の人に理解されなくても!俺は仕事がんばるよ…

この回答の中で言及はされていませんが、やりすぎDRYの例として提示されているコードがまさしくフラグ引数のアンチパターンに相当しています。

フラグ引数のリファクタリング方法は上記の回答を参考にしていただくとして、ここではご質問に沿って、フラグで処理を切り替える場合(下記、コード1)でどのような不都合があるか見てみましょう。

  1. // コード1 
  2. onButtonAClick() 
  3. { 
  4. onButtonClick(Flags.A); 
  5. } 
  6.  
  7. void onButtonBClick() 
  8. { 
  9. onButtonClick(Flags.B); 
  10. } 
  11.  
  12. void onButtonCClick() 
  13. { 
  14. onButtonClick(Flags.C); 
  15. } 
  16.  
  17. void onButtonClick(Flags flg) 
  18. { 
  19. // 前処理 P 
  20.  
  21. if(flg == Flags.A) 
  22. { 
  23. // 処理 A 
  24. } 
  25. else if(flg == Flags.B) 
  26. { 
  27. // 処理 B 
  28. } 
  29. else if(flg == Flags.C) 
  30. { 
  31. // 処理 C 
  32. } 
  33. else 
  34. { 
  35. // 想定外処理 X(ログ出しなど) 
  36. } 
  37.  
  38. // 後処理 N 
  39. } 

よくあるフラグ引数のアンチパターンとして示されるコードです(処理 P, A, B, C, N ともそれなりのボリュームがあるものとします)。これをベースにご説明いたします。

このコードの着想は『buttonA~buttonCのいずれのクリックイベントに対しても 前処理 Pと 後処理 Nを実施するのだから、「P → A or B or C → N」という流れを共通化しよう』という思いつきによるものです。

確かに「P → 何かの処理 → N」の流れだけで言えば共通化できていそうに見えますが、現実的な実装はこんなに単純にはなりません。どんどん実装を進めていったときにコードの脆さが露見していきます。まあ見ていてください(ドヤ顔)。

例えば「処理 A, Bの実行結果によって後処理 Nの前に処理 Eを実行する」といった仕様が発生した場合、コードを以下のように書き変えることになるでしょう。

  1. // コード2 
  2. onButtonAClick() 
  3. { 
  4. onButtonClick(Flags.A); 
  5. } 
  6.  
  7. void onButtonBClick() 
  8. { 
  9. onButtonClick(Flags.B); 
  10. } 
  11.  
  12. void onButtonCClick() 
  13. { 
  14. onButtonClick(Flags.C); 
  15. } 
  16.  
  17. void onButtonClick(Flags flg) 
  18. { 
  19. // 前処理 P 
  20.  
  21. bool flgE = false; 
  22.  
  23. if(flg == Flags.A) 
  24. { 
  25. ResultA resultA; 
  26.  
  27. // 処理 A(resultAに何かを代入) 
  28.  
  29. flgE = resultA.NeedE(); 
  30. } 
  31. else if(flg == Flags.B) 
  32. { 
  33. ResultB resultB; 
  34.  
  35. // 処理 B(resultBに何かを代入) 
  36.  
  37. flgE = resultB.NeedE(); 
  38. } 
  39. else if(flg == Flags.C) 
  40. { 
  41. // 処理 C 
  42. } 
  43. else 
  44. { 
  45. // 想定外処理 X(ログ出しなど) 
  46. } 
  47.  
  48. if(flgE) 
  49. { 
  50. // 処理 E 
  51. } 
  52.  
  53. // 後処理 N 
  54. } 

さらに、処理 Cでも同様の仕様変更(今度は処理 F)が必要になりました。コードを修正します。

  1. // コード3 
  2. onButtonAClick() 
  3. { 
  4. onButtonClick(Flags.A); 
  5. } 
  6.  
  7. void onButtonBClick() 
  8. { 
  9. onButtonClick(Flags.B); 
  10. } 
  11.  
  12. void onButtonCClick() 
  13. { 
  14. onButtonClick(Flags.C); 
  15. } 
  16.  
  17. void onButtonClick(Flags flg) 
  18. { 
  19. // 前処理 P 
  20.  
  21. bool flgE = false; 
  22.  
  23. if(flg == Flags.A) 
  24. { 
  25. ResultA resultA; 
  26.  
  27. // 処理 A(resultAに何かを代入) 
  28.  
  29. flgE = resultA.NeedE(); 
  30. } 
  31. else if(flg == Flags.B) 
  32. { 
  33. ResultB resultB; 
  34.  
  35. // 処理 B(resultBに何かを代入) 
  36.  
  37. flgE = resultB.NeedE(); 
  38. } 
  39. else if(flg == Flags.C) 
  40. { 
  41. bool flgF = false; 
  42.  
  43. // 処理 C(flgEに何かを代入) 
  44.  
  45. if(flgF) 
  46. { 
  47. // 処理 F 
  48. } 
  49. } 
  50. else 
  51. { 
  52. // 想定外処理 X(ログ出しなど) 
  53. } 
  54.  
  55. if(flgE) 
  56. { 
  57. // 処理 E 
  58. } 
  59.  
  60. // 後処理 N 
  61. } 

ヤバさがわかってきましたか?

これでもかなりヤバめですが、buttonCの類似機能を持つ buttonC2が追加となったとします。C2の処理は Cと共通部分(C')があるようです。ちょっとコード化してみます。

  1. // コード4 
  2. onButtonAClick() 
  3. { 
  4. onButtonClick(Flags.A); 
  5. } 
  6.  
  7. void onButtonBClick() 
  8. { 
  9. onButtonClick(Flags.B); 
  10. } 
  11.  
  12. void onButtonCClick() 
  13. { 
  14. onButtonClick(Flags.C); 
  15. } 
  16.  
  17. void onButtonClick(Flags flg) 
  18. { 
  19. // 前処理 P 
  20.  
  21. bool flgD = false; 
  22.  
  23. if(flg == Flags.A) 
  24. { 
  25. ResultA resultA; 
  26.  
  27. // 処理 A(resultAに何かを代入) 
  28.  
  29. flgD = resultA.NeedD(); 
  30. } 
  31. else if(flg == Flags.B) 
  32. { 
  33. ResultB resultB; 
  34.  
  35. // 処理 B(resultBに何かを代入) 
  36.  
  37. flgD = resultB.NeedD(); 
  38. } 
  39. else if(flg == Flags.C || flg == Flags.C2) 
  40. { 
  41. // 処理 C' 
  42.  
  43. bool flgF = false; 
  44.  
  45. if(flg == Flags.C) 
  46. { 
  47. // 処理 C1(flgFに何かを代入) 
  48. } 
  49. else 
  50. { 
  51. // 処理 C2(flgFに何かを代入) 
  52. } 
  53.  
  54. if(flgF) 
  55. { 
  56. // 処理 F 
  57. } 
  58. } 
  59. else 
  60. { 
  61. // 想定外処理 X(ログ出しなど) 
  62. } 
  63.  
  64. if(flgE) 
  65. { 
  66. // 処理 E 
  67. } 
  68.  
  69. // 後処理 N 
  70. } 

そろそろ読むのも嫌になってくるかと思います。

それでは・・・ここで、問題です(デデンッ!)

  1. // コード5 
  2. onButtonAClick() 
  3. { 
  4. onButtonClick(Flags.A); 
  5. } 
  6.  
  7. void onButtonBClick() 
  8. { 
  9. onButtonClick(Flags.B); 
  10. } 
  11.  
  12. void onButtonCClick() 
  13. { 
  14. onButtonClick(Flags.C); 
  15. } 
  16.  
  17. void onButtonClick(Flags flg) 
  18. { 
  19. // 前処理 P 
  20.  
  21. bool flgE = false; 
  22.  
  23. if(flg == Flags.A) 
  24. { 
  25. ResultA resultA; 
  26.  
  27. // 処理 A(resultAに何かを代入) 
  28.  
  29. flgE = resultA.NeedE(); 
  30. } 
  31. else if(flg == Flags.B) 
  32. { 
  33. ResultB resultB; 
  34.  
  35. // 処理 B(resultBに何かを代入) 
  36.  
  37. flgE = resultB.NeedE(); 
  38. } 
  39. else if(flg == Flags.C || flg == Flags.C2) 
  40. { 
  41. // 処理 C' 
  42.  
  43. bool flgF = false; 
  44.  
  45. if(flg == Flags.C) 
  46. { 
  47. // 処理 C1(flgFに何かを代入) 
  48. } 
  49. else 
  50. { 
  51. // 処理 C2(flgEに何かを代入) 
  52. } 
  53.  
  54. if(flgE) 
  55. { 
  56. // 処理 F 
  57. } 
  58. } 
  59. else 
  60. { 
  61. // 想定外処理 X(ログ出しなど) 
  62. } 
  63.  
  64. if(flgE) 
  65. { 
  66. // 処理 E 
  67. } 
  68.  
  69. // 後処理 N 
  70. } 

上記コード5は明確なコーディングミスがあります。どこでしょうか?

もしフラグ変数の何が悪いかわからない方は、ぜひとも間違い探しをしてみてください。その不毛さを身をもって実感するといいと思います。

さて、いかがでしょうか。

間違い見つかりましたか?

それでは適当な行稼ぎも済んだので・・・

正解発表です!(ジャジャンッ!)

まあ、そんなにもったいぶることでもないんですけどね。

51行目の

  1. // 処理 C2(flgEに何かを代入) 

と、54行目の

  1. if(flgE) 

が間違いでした。本当はここの処理では flgFが使われるべきところです。ちょっとずるいかもしれませんが、間違いが1つとは申し上げませんでした。

実際の開発も不具合の原因が1つとは限りません。そして、上記のコーディングミスは実行するまでその存在に気づけません

フラグを多用する設計はコーディングミスを生みやすく、なまじビルドエラーなどが出ないため、実行しないと分からない(テストケースが甘い場合にはテストですら発覚しない)のです。

onButtonClickが肥大化しすぎてテストコードが書けないか、書けたとしてもメンテナンスに手間が掛かりすぎます。テストケースの作り込みも難儀することでしょう。結局、ブレークポイントで1行ずつ追っていくか、分岐の端々でログを出すかしかありません。

この例の場合 「// 処理 A」とかで省略していますが、実際のコードは数百行に肥大化したものになります。if文の分岐もこの比ではないほど複雑になります。その数百行の中からたった1文字のミスを探し出すことを考えてみてください。

やりたくないでしょ?

それがフラグ引数が敬遠される理由です。

広告
·
フォロー

フラグを使うのが問題なのではなく「多用」するのが問題なのではないでしょうか。

フラグにより状態が変わるわけですから、10個のフラグがあると「1024通り」の状態が発生します。

テストケースもその全てのパスを確認しないといけないわけですから最低でも1024は書かないといけません。

全てのフラグの成立条件を変えて評価をされていますでしょうか?

また、関数・メソッドの中にフラグがあり、複数の箇所でフラグにより処理が変わるのであれば「元々1つにすべきでは無かった」可能性があります。

·
フォロー

フラグがいけないのではなく多用がいけない気がします。

フラグがあるってことは、フラグによる条件分岐があるってことですよね。条件分岐がひとつ増えるとプログラムの複雑度は1増えますので、KISS(単純にしておけ)原則に違反します。あとは熱力学の法則に従ってエントロピーは増大する一方、プログラム全体が汚部屋やゴミ屋敷のようになっていきます。

実例を示せとのことですが、ちょっと良い例は示せません。プログラムソースコードがわかりにくい例を掲げても結局わかりにくいままだと思いますので。今まで見たことのある、長い条件式や深いネストの嫌なif文を思い出して嫌な気分になってみてください。それがフラグを追加して悪くなる実例です。

フラグを追加しても複雑さが増えなければ良い訳です。

  • フラグによって条件分岐する箇所が少なければ複雑化を抑えられる
  • フラグ間に依存関係がなければ、条件分岐の複雑化を抑えられる

新しく追加するフラグを良く分析して、安易な条件分岐の増大を抑えましょう。

  • コンパイル時静的に決まるものはコンパイルオプションにする
  • feature toggle のような役割のものなら、そういうのを一元管理しているところにまかせる
  • フラグで分岐するのではなく、型の抽象化を使って型で分岐する

ああもうこれは誰が見てもフラグにするしかないよねっていう王道フラグを実装して、怒った人を見返してやりましょう。

·
フォロー

フラッグを多用して、訳の分からぬ物をつくる前に、キチンと分析せよ、状態遷移モデルを描けという事です。

Finite-state machine - Wikipedia
Mathematical model of computation "SFSM" redirects here. For the Italian railway company, see Circumvesuviana . Classes of automata (Clicking on each layer gets an article on that subject) A finite-state machine ( FSM ) or finite-state automaton ( FSA , plural: automata ), finite automaton , or simply a state machine , is a mathematical model of computation . It is an abstract machine that can be in exactly one of a finite number of states at any given time. The FSM can change from one state to another in response to some inputs ; the change from one state to another is called a transition . [1] An FSM is defined by a list of its states, its initial state, and the inputs that trigger each transition. Finite-state machines are of two types— deterministic finite-state machines and non-deterministic finite-state machines . [2] For any non-deterministic finite-state machine, an equivalent deterministic one can be constructed. The behavior of state machines can be observed in many devices in modern society that perform a predetermined sequence of actions depending on a sequence of events with which they are presented. Simple examples are: vending machines , which dispense products when the proper combination of coins is deposited; elevators , whose sequence of stops is determined by the floors requested by riders; traffic lights , which change sequence when cars are waiting; combination locks , which require the input of a sequence of numbers in the proper order. The finite-state machine has less computational power than some other models of computation such as the Turing machine . [3] The computational power distinction means there are computational tasks that a Turing machine can do but an FSM cannot. This is because an FSM's memory is limited by the number of states it has. A finite-state machine has the same computational power as a Turing machine that is restricted such that its head may only perform "read" operations, and always has to move from left to right. FSMs are studied in the more general field of automata theory . Example: coin-operated turnstile [ edit ] State diagram for a turnstile A turnstile An example of a simple mechanism that can be modeled by a state machine is a turnstile . [4] [5] A turnstile, used to control access to subways and amusement park rides, is a gate with three rotating arms at waist height, one across the entryway. Initially the arms are locked, blocking the entry, preventing patrons from passing through. Depositing a coin or token in a slot on the turnstile unlocks the arms, allowing a single customer to push through. After the customer passes through, the arms are locked again until another coin is inserted. Considered as a state machine, the turnstile has two possible states: Locked and Unlocked [4] There are two possible inputs that affect its state: putting a coin in the slot coin and pushing the arm push In the locked state, pushing on the arm has no effect; no matter how many times the input push is given,
·
フォロー

フラグも「大域変数になってて(後個からでも参照可能で)数が多い」とかだと訳わからなくなるんでアレなだけですね。

オブジェクトの整理がしっかりなされてて、そのインスタンス内で非公開メンバとして扱われて、尚且つ状態遷移がしっかりまとまってたら別にフラグが多くても問題にならないです。

Aというインスタンスの状態がXXの場合、Bのインスタンスが如何な状態であろうと無視できる。…とか、オブジェクト間の関係性を示す仕様の定義で整理できます。テストケースはクラスBの中でだけ完結するので、クラスAのテストケース分析に関し、クラスBの当該フラグの状態ケース全てを網羅する必要はないわけです。

単独で存在するフラグを無軌道に増やしたり、その責任範囲が特定しにくい(値の操作をあらゆる場所で行うとか…)等の設計の拙さが問題なのです。

多用することは別に間違いとはいえません。そのようなシステムはたくさんあります。

·
フォロー

フラグの質が問題なんじゃないでしょうかね。指摘側も本質分かってなくてフラグ多用するな言ってるだけとかね。アクションをフラグ使ってディスパッチする位なら、関数分けろが一般論。メッセージキューとかはディスパッチいるかもしれんけど、プロセス跨がないなら関数ポインタとかデリゲートなどの動的クロージャをキューに渡せばよいってなりますよね。

怒ると褒めるをフラグ化する必要が本当にあるのか、まずは怒るメソッドと褒めるメソッドを用意して、DBやユーザによる入力に基づいて切り替えが必要な場合だけ、フラグをディスパッチする構造にするのがいい。こうするとディスパッチ関数もシンプルになっていいよね。ディパッチ項目が100とか行っても大丈夫になる。本当にフラグによる指定が必要?コードに怒る()、褒める()とか書いて済むならそっちの方がいいの。

コーディングルールなんて本質分かってない雑魚が適当に作ってるだけのもの多いからね。

テスト項目増えるほうは気にしてないな。自分は。本当に必要ならやるだけ。不要なテスト項目増やされるのは嫌だけどね。

·
フォロー

例えば自動運転システムの開発。前を走行中の自動車が速度を落としたら自分もブレーキを踏んで速度を落とす仕様があったとします。フラグでいきましょう。仮に減速フラグで管理するとします。

次に、目の前の車が加速したら、ある程度までそれに追随する仕様があったとします。加速フラグの管理とします。

減速フラグtrueの場合、減速の処理を、加速フラグtrueの場合、加速の処理をするとします。

初期値は以下です

減速フラグ=false

加速フラグ=false

目の前の車が加速しました。

減速フラグ=false

加速フラグ=true

目の前の車が減速してきました。

減速フラグ=true

加速フラグ=false

前の車と同じ速度です。

減速フラグ=false

加速フラグ=false

…こんな車に乗りたいでしょうか?

加速フラグと減速フラグが両方立つことは絶対ないのでしょうか?

両方立った場合ちゃんと減速が優先されるのでしょうか?

仮に減速が優先されて、減速が終わった時に、加速が立ったままになっていて、突然加速し始めないのでしょうか?

停止中と、加速も減速もしてない走行中が、どちらも減速、加速フラグfalseで判別できませんね。停止中フラグと定速走行フラグを新設しますしょうか?

2の4乗通りで16パターンの考慮が必要ですね。タイミングの事とか考えると無数に組み合わせありますね。派遣会社から外注のエンジニア沢山雇って試験やってもらいましょうか!

いいですね、フラグいっぱいにしてスパゲティにしたほうが雇用が増えます。

走行状態変数を作って停止中、加速中、減速中、定速走行中の4状態を管理してシンプルにするとか、余計なことは考えてはだめです。

あくまでフラグにしましょう。全部のフラグが立ってる場合のテストとかして、車ブルブルバグらせて、それを治すためにさらにフラグがフラグを参照するようにして、グローバルなフラグとローカルなフラグの入れ子構造にして、徹底的にスパゲティにして、みんなで残業代稼ぎましょう。なんのこっちゃ

広告
·
フォロー

コンピュータのメモリは0番地から始まるのでそのほうがアラインとか考えたときに考えやすい(コンパイラが作りやすい)からです。

·
フォロー

昨今のプログラミング言語は真偽値を用意しているからでしょう。

例えば、

  1. delete_flg = 0 

とかはよく使われますが、delete_flgが0=削除されていない、1=削除された、とわざわざマッピングするくらいなら、

  1. deleted = false 

と書けばいい話です。

言語の表現力を使いきらずに、別の機能で再現していては、そのたびに上記の"マッピング"のような脳内変換が必要になるので、その分だけ無駄が出るということかと思います。

·
フォロー

とりあえず、そういうことは怒ってくる人に聞くのがスジじゃないかなあ

さておき

フラグを多用すると怒られる

まあ、これは分かるでしょう

この状態はロジックが煩雑になり、読むのも改修するのも面倒になるってことス

機能分割を考え直せって話です

なぜフラグを使ってはいけないのか

使うなとは言ってないんじゃないですか?

フラグでごちゃごちゃやってるヒマあるんだったら、条件直指定で抜けるなり元にもどるなりした方がいいんじゃないですかねえ

おそらく実際のコードを見ないと正確な評価や助言はできません

ふわっとした話で一般的な話を聞いて、それを実際のコードに活かせるとはとても思えません

再度いいますが、怒られたらすぐに「その人に」理由を聞きましょう

·
フォロー

昔、知人が関わっていたゲーム、NOëL NOT DIGITALというPlay Station用のゲームのデバッグをお手伝いしたことがあります。その時に見つけたバグです。

このゲームは女の子に電話をかけ、その時の会話とかで好感度を上げながらストーリーを進めていくアドベンチャーゲームです。

夜中に電話すると、寝ているところを起こしてしまうので好感度が下がります。

さて、夜中の電話を繰り返していると、好感度が-127と最低の値になります。その状態になった時に、また夜中に電話をすると、好感度は-128にならずに、128になります。

プログラム的にいうと、符号付き8ビット変数を使っていて、アンダーフローするということになります。

そうすると、130回以上夜中に電話をかけ続けた男に急にデレるのです。好感度最高ですからね。

通称「ストーカープレイ」と名付けられました。製品版はやってないので修正されたかどうかは覚えていませんが、誰の助けも得ず、ソースコードも見ずに一発でそのバグを見つけたのは凄かったと自分でも思います。

0 コメント:

コメントを投稿