複雑な分岐はどこから来るのか
まず第一に、私たちが議論したい最初の疑問は、なぜレガシーコードには非常に多くの複雑な分岐が存在することが多いのかということです。これらの複雑な分岐は、コードの最初のバージョンには存在しないことがよくあります。設計者がまだある程度の経験を持っていると仮定すると、将来拡張する必要がある領域を予測し、抽象インターフェイスを予約する必要があります。
しかし、コードが数回反復された後、特に要件の詳細を数回調整した後、複雑な分岐が表示されます。要件に対する詳細な調整は、多くの場合、UML には反映されず、コードに直接反映されます。たとえば、メッセージは当初、チャット メッセージとシステム メッセージの 2 つのカテゴリに分類されていましたが、これらは自然にメッセージ カテゴリの 2 つのサブカテゴリとして設計されました。しかし、ある日、要件が詳細に調整されることになり、一部のシステム メッセージは重要なので、そのタイトルが赤色で表示されるようになります。このとき、プログラマは、次のような変更を行うことがよくあります。
システム メッセージ クラスに重要な属性を追加します。対応するタイトルの色を制御するために、render メソッドに重要な属性に関するブランチを追加するのはなぜでしょうか。おそらくそれは抽象的であるべきだと彼が気づいていなかったからでしょう。要件には「一部のシステム メッセージは重要である」と記載されているため、命令型プログラミング言語でより多くのトレーニングを受けたプログラマにとって、最初に思いつくのはフラグ ビットです。フラグ ビットは重要なものとそうでないものを区別できます。 。 重要。同氏は、この要件が「システム メッセージは 2 つのカテゴリ、重要なものと重要でないものに分類される」という別の方法で解釈される可能性があるとは予想していませんでした。このように解釈すると、システム メッセージは抽象化する必要があることがわかりました。
もちろん、プログラマは抽象化が可能であることを知っていても、何らかの理由で抽象化を行わないことを選択する可能性もあります。非常に一般的な状況は、プロジェクトの進行速度と引き換えにコードの品質を犠牲にすることをプログラマーに強いるというものです。プロパティとブランチを追加する方が、抽象的なリファクタリングよりもはるかに簡単です。この形式の変更を 10 個実行したい場合、10 個作成した方が早いでしょうか。分岐か 10 個の抽象化か?違いは明らかです。
もちろん、if else が多すぎると、「switch case に変更したらどうだろう」と立ち上がる賢い人もいるでしょう。場合によっては、各ブランチが相互に排他的であると仮定すると、これによりコードの可読性が実際に向上します。しかし、スイッチケースの数が増えると、コードも読めなくなります。
複雑なブランチの欠点は何ですか
複雑なブランチの欠点は何ですか?例として、Baidu Hi Web バージョンの古いコードのセクションを取り上げます。
switch (json.result) { case "ok": switch (json.command) { case "message": case "systemmessage": if (json.content.from == "" && json.content.content == "kicked") { /* disconnect */ } else if (json.command == "systemmessage" || json.content.type == "sysmsg") { /* render system message */ } else { /* render chat message */ } break; } break;
{ "result": "ok", "command": "message", "content": { "from": "CatChen", "content": "Hello!" } }
正しい答えを簡単に得ることができます: this JSON hit /* render chat message */ (show chatメッセージ) このブランチ。それで、どうやってこの判断を下したのか知りたいのですが?まず、case "ok": ブランチにヒットするかどうかを確認し、結果がヒットするかどうかを確認する必要があります。次に、case "message": ブランチにヒットするかどうかを確認し、結果もヒットするかどうかを確認する必要があります。 case "system message" を確認する必要はありません: ; 次に、if の条件にもヒットせず、else if の条件にもヒットしないため、else 分岐にヒットします。
問題がわかりましたか?なぜ他の部分を見て、この JSON がこのブランチにヒットすると言えないのでしょうか? else 自体には条件が含まれていないため、条件を意味するだけです。それぞれの else の条件は、その前の if と else if の否定とその後の AND 演算の結果です。言い換えれば、この else のヒットを判断することは、ヒットを判断する一連の複雑な条件と同等です:!(json.content.from == "" && json.content.content == "kicked") && !(json.command == "systemmessage" || json.content.type == "sysmsg")
json.result == "ok" && (json.command == "message" || json.command == "systemmessage") && !(json.content.from == "" && json.content.content == "kicked") && !(json.command == "systemmessage" || json.content.type == "sysmsg")
ここに重複があります。ロジックを省略すると、次のようになります。
json.result == "ok" && json.command == "message" && !(json.content.from == "" && json.content.content == "kicked") && !(json.content.type == "sysmsg")
単純な 4 文字の else からこのような長い一連の論理演算式を導き出すのに、どれだけの労力を費やしたでしょうか?しかも、この表現はよく見ないと何を言っているのか分かりません。
ここで、複雑なブランチの読み取りと管理が困難になります。 if else を含む switch ケースに直面していると想像してください。合計 3 つのケースがあり、それぞれのケースに 3 つの else があり、各分岐とその前提条件がすべて含まれています。すべての祖先分岐は非 then-AND の結果です。複雑な分岐を避ける方法
まず第一に、複雑な論理演算は避けることができません。リファクタリングの結果は同等のロジックになるはずです。私たちにできるのは、コードを読みやすく、管理しやすくすることだけです。したがって、複雑な論理演算を読みやすく管理しやすくする方法に焦点を当てる必要があります。 クラスまたはファクトリーに抽象化する
オブジェクト指向設計に慣れている人にとって、これは複雑な論理演算を分割し、それらを異なるクラスに分散することを意味する場合があります:
switch (json.result) { case "ok": var factory = commandFactories.getFactory(json.command); var command = factory.buildCommand(json); command.execute(); break; }
这看起来不错,至少分支变短了,代码变得容易阅读了。这个 switch case 只管状态码分支,对于 "ok" 这个状态码具体怎么处理,那是其他类管的事情。 getFactory 里面可能有一组分支,专注于创建这条指令应该选择哪一个工厂的选择。同时 buildCommand 可能又有另外一些琐碎的分支,决定如何构建这条指令。
这样做的好处是,分支之间的嵌套关系解除了,每一个分支只要在自己的上下文中保持正确就可以了。举个例子来说, getFactory 现在是一个具名函数,因此这个函数内的分支只要实现 getFactory 这个名字暗示的契约就可以了,无需关注实际调用 getFactory 的上下文。
抽象为模式匹配
另外一种做法,就是把这种复杂逻辑运算转述为模式匹配:
Network.listen({ "result": "ok", "command": "message", "content": { "from": "", "content": "kicked" } }, function(json) { /* disconnect */ }); Network.listen([{ "result": "ok", "command": "message", "content": { "type": "sysmsg" } }, { "result": "ok", "command": "systemmessage" }], function(json) { /* render system message */ }); Network.listen({ "result": "ok", "command": "message", "content": { "from$ne": "", "type$ne": "sysmsg" } }, func tion(json) { /* render chat message */ });
现在这样子是不是清晰多了?第一种情况,是被踢下线,必须匹配指定的 from 和 content 值。第二种情况,是显示系统消息,由于系统消息在两个版本的协议中略有不同,所以我们要捕捉两种不同的 JSON ,匹配任意一个都算是命中。第三种情况,是显示聊天消息,由于在老版本协议中系统消息和踢下线指令都属于特殊的聊天消息,为了兼容老版本协议,这两种情况要从显示聊天消息中排除出去,所以就使用了 "$ne" (表示 not equal )这样的后缀进行匹配。
由于 listen 方法是上下文无关的,每一个 listen 都独立声明自己匹配什么样的 JSON ,因此不存在任何隐含逻辑。例如说,要捕捉聊天消息,就必须显式声明排除 from == "" 以及 type == "sysmsg" 这两种情况,这不需要由上下文的 if else 推断得出。
使用模式匹配,可以大大提高代码的可读性和可维护性。由于我们要捕捉的是 JSON ,所以我们就使用 JSON 来描述每一个分支要捕捉什么,这比一个长长的逻辑运算表达式要清晰多了。同时在这个 JSON 上的每一处修改都是独立的,修改一个条件并不影响其他条件。
最后,如何编写一个这样的模式匹配模块,这已经超出了本文的范围。
以上がJavaScript でのさまざまな複雑な分岐ステートメントの使用法について話しましょうの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。