D'où viennent les branches complexes ?
Tout d'abord, la première question dont nous voulons discuter est de savoir pourquoi il y a souvent autant de branches complexes dans l'héritage code. Ces branches complexes n'existent souvent pas dans la première version du code. En supposant que le concepteur ait encore une certaine expérience, il doit prévoir les domaines qui pourraient devoir être étendus dans le futur et réserver les interfaces abstraites.
Mais après plusieurs itérations du code, notamment après plusieurs ajustements des détails des exigences, des branches complexes apparaîtront. Les ajustements détaillés des exigences ne sont souvent pas reflétés dans UML, mais directement dans le code. Par exemple, les messages étaient initialement divisés en deux catégories : les messages de discussion et les messages système. Lors de la conception, ceux-ci ont naturellement été conçus comme deux sous-catégories de la catégorie des messages. Mais un jour, les exigences sont ajustées en détail. Certains messages système sont importants et leurs titres doivent être affichés en rouge. À ce moment-là, les programmeurs effectuent souvent les modifications suivantes :
Ajouter un attribut important à la classe de message système.
Ajoutez une branche sur l'attribut important dans la méthode de rendu correspondante pour contrôler la couleur du titre
Pourquoi le programmeur a-t-il fait un tel changement ? C'est peut-être parce qu'il n'a pas réalisé que cela devait être abstrait. Parce que l'exigence dit "certains messages système sont importants", pour les programmeurs qui ont reçu plus de formation dans les langages de programmation impératifs, la première chose à laquelle ils peuvent penser est le bit d'indicateur - un bit d'indicateur peut faire la distinction entre les messages importants et non importants. . important. Il ne s'attendait pas à ce que cette exigence puisse être interprétée d'une autre manière : « Les messages système sont divisés en deux catégories : importants et sans importance. En l'interprétant de cette façon, il savait que les messages du système devaient être abstraits.
Bien sûr, il est également possible que le programmeur sache que l'abstraction est possible, mais pour une raison quelconque, il choisit de ne pas le faire. Une situation très courante est que quelqu'un oblige les programmeurs à sacrifier la qualité du code en échange de la rapidité de progression du projet. L'ajout d'une propriété et d'une branche est beaucoup plus simple que la refactorisation abstraite. Si vous souhaitez effectuer 10 modifications de cette forme, est-il plus rapide d'en créer 10. branches ou 10 abstractions ? La différence est évidente.
Bien sûr, s'il y en a trop, certaines personnes intelligentes se lèveront et diront "Pourquoi ne pas le changer pour changer de cas". Dans certains cas, cela peut réellement améliorer la lisibilité du code, en supposant que chaque branche s'exclut mutuellement. Mais lorsque le nombre de cas de commutation augmente, le code devient également illisible.
Quels sont les inconvénients des branches complexes
Quels sont les inconvénients des branches complexes ? Permettez-moi de prendre comme exemple une section de l'ancien code de la version Web de Baidu Hi.
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;
Ce code n'est pas difficile à comprendre, je pose donc une question simple, quelle branche effectue le hit JSON suivant :
{ "result": "ok", "command": "message", "content": { "from": "CatChen", "content": "Hello!" } }
Vous pouvez facilement obtenir la bonne réponse : Ceci JSON atteint la branche /* render chat message */ (afficher le message de discussion). Alors je veux savoir comment vous avez porté ce jugement ? Tout d'abord, vous devez voir s'il correspond au cas "ok": branch, et le résultat est un succès. Ensuite, vous devez voir s'il correspond au cas "message": branch, et le résultat est également un succès, donc ; il n'est pas nécessaire de regarder le cas "systemmessage": ; Ensuite, il ne remplit pas la condition dans if ; et il ne remplit pas la condition dans else if, donc il atteint la branche else.
Vous voyez le problème ? Pourquoi ne pouvez-vous pas regarder autre chose et simplement dire que ce JSON atteint cette branche ? Parce que sinon lui-même ne contient aucune condition, il implique seulement la condition ! La condition de each else est le résultat de la négation puis de l’opération AND de each if et else if avant lui. En d'autres termes, juger du succès de this else équivaut à un ensemble de conditions complexes pour juger du succès :
!(json.content.from == "" && json.content.content == "kicked") && !(json.command == "systemmessage" || json.content.type == "sysmsg")
Appliquez ensuite les deux cas de commutation externes, et les conditions de cette branche sont comme ceci :
json.result == "ok" && (json.command == "message" || json.command == "systemmessage") && !(json.content.from == "" && json.content.content == "kicked") && !(json.command == "systemmessage" || json.content.type == "sysmsg")
Il y a une logique répétitive ici. Après l'avoir omis, cela ressemble à ceci :
json.result == "ok" && json.command == "message" && !(json.content.from == "" && json.content.content == "kicked") && !(json.content.type == "sysmsg")
Combien d'efforts avons-nous déployé pour déduire cela à partir des simples quatre lettres else Long ? chaîne d'expressions logiques ? De plus, si on n’y regarde pas bien, on ne comprend vraiment pas ce que dit cette expression.
C'est là que les branches complexes deviennent difficiles à lire et à gérer. Imaginez que vous soyez confronté à un cas de commutation avec un if else. Il y a 3 cas au total, et chaque cas a 3 elses, cela vous suffit pour étudier - chaque branche et condition contient toutes ses conditions préalables. toutes les branches ancêtres sont le résultat de non-then-AND.
Comment éviter les branches complexes
Tout d'abord, les opérations logiques complexes sont inévitables. Le résultat de la refactorisation devrait être une logique équivalente. Tout ce que nous pouvons faire est de rendre le code plus facile à lire et à gérer. Par conséquent, nous devons nous concentrer sur la manière de rendre les opérations logiques complexes faciles à lire et à gérer.
Résumé en classes ou usines
Pour ceux qui sont habitués à la conception orientée objet, cela peut signifier diviser des opérations logiques complexes et les répartir dans différentes classes :
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 上的每一处修改都是独立的,修改一个条件并不影响其他条件。
最后,如何编写一个这样的模式匹配模块,这已经超出了本文的范围。
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!