从“高内聚,低耦合”说起

news2024/11/16 0:30:42

从“高内聚,低耦合”说起

记得在上学的时候,⽼师就说过“⾼内聚,低耦合”,但当初对这句话的理解⽐较浅显。⼯作之后,为了说服别⼈采⽤⾃⼰设计的⽅案,常常说“……这样就做到了⾼内聚,低耦合……”。

随着⼯作经验越来越丰富,学到的内容越来越多,在解释设计⽅案的时候,可能会这样说:

“……此处使⽤了策略模式,从⽽保证了模块相对的稳定性,和较强的扩展性……”;
“……这个聚合维护了A、 B和C之间的固定规则……”。

⽽之前经常说的“⾼内聚,低耦合”却不会经常挂在嘴边了。那什么是“⾼内聚,低耦合”呢?

内聚性和耦合性,都是软件度量。内聚性是指功能相关的程序组合成⼀个模块的程度,或是各机能凝聚的状态或程度。耦合性是指,⼀个程序中模块及模块之间信息或参数依赖的程度。它们都是早期结构化分析的重要概念之⼀,⽽且是相对的概念。⼀般内聚性⾼的程序,通常是低耦合的。虽然我们不再使⽤严格的“结构化分析”步骤,但是它依然适⽤于现在⼀直存在的模块关系中。在⾯向对象的分析和设计中,⼀个类可以看成最⼩的模块,那么内聚性和耦合性也可以表达为对象之间的关系。“高内聚,低耦合”代表着这程序更健壮、更易扩展。

它似乎并未过时,但是在讨论问题的时候,我们为什么不再经常使⽤?或者说你在什么时候使⽤呢?或者我们把问题再扩⼤⼀点——你依据什么“设计⽅法”去指导架构设计⼯作呢?是SOLID?是设计模式?还是DDD呢?

在回答这个问题之前,我们看看还有哪些“设计⽅法”出现在分析和设计⼯作中,以及使⽤时遇到的问题。

令人迷茫的设计方法

故事一 SOLID不够明确?

对OO设计熟悉的读者肯定知道著名的SOLID原则。当时我对OO的认识还只有“封装、继承和多态”,读到这些理论的时候,收获颇多。直到有同事问我:“单⼀职责原则规定每个类都有单⼀的功能,那么到底⼀个类有多少功能算是单⼀呢?如果⼀个类有多个成员⽅法,是不是⼀定要拆分成多个类呢? ”

我想了⼀下,如果⼀个类中只有CRUD⽅法,是不是要把类拆成Creator、 Retriver、 Updater和Deleter才能满⾜这个原则呢?⾃认为对SOLID了如指掌的我⼀时⽆语凝噎。

故事二 设计模式违反“原则”?

接触到设计模式后,“策略模式”中的每⼀个算法被封装到⼀个类,算是满⾜了“单⼀职责”,并且“针对了接⼝编程,⽽不是针对实现”,顿时有种发现新⼤陆的感觉。此时,感觉SOLID⼀点都不“Solid”,反⽽很虚。设计模式才是指导开发的王道,策略模式也是我最常⽤的模式之⼀(可能它最简单吧)。

后来遇到更复杂的情况:不同的条件下,需要使⽤不同的“策略组”(算法组);并且随着状态的流转,“策略组”也跟着变化。使⽤“状态模式”,问题迎刃⽽解。此时的我认为,设计模式是指导设计的另⼀套原则,是对SOLID原则的拓展和应⽤,是对于模糊的原则做了⼀次详细的解释说明,⽽SOLID本身很难指导开发设计。

状态模式

问题一:状态模式中的坏味道

直到一次Code Review时,同事提出了以下疑问:

“虽然增加⼀个新的状态(State)只需改变很少已有代码,但是如果增加⼀个新动作(action),是不是所有状态⼦类都实现这个动作?这样就会把已有的State⼦类全部更改⼀遍。 ”

“如果有必要,是的。但⽬前这个动作只有在StateX下才会真正地使Context的状态发⽣改变。我会在State中⽀持默认的实现,只有ConcreteStateX才会完成状态变化的逻辑。这样⼀来就不需要改动每⼀个State了。 ”

“如果这个‘新动作’只在某⼀个具体的State中才⽣效,那么相当于‘兄弟State’不得不⽀持这个对⾃⼰毫⽆意义的动作,⽽且基类可能有越来越多类似的默认实现,此时会出现‘DivergentChange’这个坏味道。⽽且当我们使⽤State基类的时候,我们并不清楚哪些⽅法在哪个具体的State⼦类有特殊的实现,给读代码的⼈也带来很多困难。 ”

在此之前,我从未怀疑过设计模式会遭到挑战,⽽且对⽅说的貌似有点道理。

问题二:组合模式违反“单一职责”原则

再来看组合模式,Component承担了Leaf和Composite两种职责,明显违背了“单一职责”原则,以后还要不要用呢?


组合模式

故事三 拍脑袋决定方案?

经历过以上故事的我,对于设计上的优缺点已经有了一定的认识,但是在代码层面竟然也遇到了问题。

在⼀次重构⼯作中,遇到了“重复代码(Duplicated Code) ”的坏味道,我直接使⽤“PullUp Method”的⼿法,将重复代码推⼊了超类;⽽同事却认为使⽤继承不如使⽤组合,建议使⽤“Extra Class”,将重复代码抽到⼀个⽆关的类。我俩的⽅法都能解决问题,但是为了说服对⽅花了很多时间。后来想了想,对于两个都能解决问题的⽅案,我是否还要花时间去争论?如果两个⽅案都⾏,拍脑袋决定岂不美哉?

故事四 DDD如何指导设计开发?

随着在项⽬中使⽤DDD,⾃认为积累了不少经验。⼀次在DDD Community的讨论中,涉及了聚合,便有了下⾯的对话:

问题一:聚合和一致性的关系

“……聚合是为了维护模型对象间的固定业务规则⽽存在,所以A、 B和C在同⼀个聚合⾥⾯。 ”

“等等, A和D之间也有关系,这种关系难道不是业务规则吗?为什么要把D排除在聚合边界之外? ”


聚合边界

“这⾥说的‘固定业务规则’是强⼀致性的, A和D之间的业务规则是‘最终⼀致性’的。 ”

“‘最终⼀致性’是不是牵强附会的概念呢?你的意思是, A和D之间的业务规则可以‘不⼀致’,也就是不固定?你问问业务⽅同意吗? ”

“我的意思是它们之间可以有‘短暂的不⼀致’,⽐如发邮件的那⼀刻,并不期望邮件在⼀秒内发出去,它可能在邮件服务的队列⾥等着呢,只要在规定的时间内,⽐如五分钟,发出去即可。所以我们经常把‘邮件系统’作为⼀个独⽴的系统,故⽽不会把‘邮件’和‘发送邮件的对象’作为⼀个聚合。 ”

“听起来有道理。但是我也⻅过业界很多情况下把‘下单’和‘财务’做成两个微服务系统的,所以‘订单’和‘账单’肯定不属于同⼀个聚合。按照上⾯的说法,‘订单’和‘账单’之间肯定没有强⼀致性的业务规则。但是你点外卖的时候,难道不是付款成功之后才会告诉你下单成功吗?它既不会让你等五分钟,也不会没收到钱就给你准备饭菜。这种规则难道不是你所谓的‘强⼀致性’? ”

讨论到这个地方,通常会以“具体问题具体分析”结尾,或者还没结尾就转到了另一个相关话题:

问题二:实体只有一部分需要聚合维护固定规则

“……前⾯说到,聚合维系了内部对象的固定规则,所以操作聚合内的对象要通过聚合根。但是聚合内某些实体的状态更新通过聚合根操作效率太低……”

“不通过聚合根操作会破坏业务规则吧? ”

“打个⽐⽅,在订单这个聚合⾥,直接更改某个订单项的价格可能会破坏订单的业务规则,⽐如‘总价限额’。但是更改订单项的备注并不会破坏任何规则。如果还有更多类似‘更改备注’这样的操作,都要通过聚合根这个‘代理’完成。这跟重构中的坏味道‘中间⼈(Middle Man) ’有点像。 ”

“那能不能把订单项中‘没有固定规则’的部分和‘有固定规则’的部分分开,做成两个实体对象呢? ”

“⾸先,在当前上下⽂⾥,订单项是⼀个⾮常明确的概念,分开后怎样对应业务概念呢?其次,如果真的分开,这个新的实体对象⽣命周期的维护也需要成本。 ”


部分需要维护固定规则

讨论到此处,又陷入了僵局。但讨论一旦发散起来,根本刹不住车。

问题三:实体只有某个生命周期需要聚合维护固定规则

“上⾯讨论的是‘实体只有⼀部分需要聚合维护规则’。我现在遇到的问题是‘实体只有某个⽣命周期需要聚合维护固定规则’。这种情况下,相当于实体的其他⽣命周期的操作也要受限于聚合。 ”

“说来听听? ”

“还是拿‘订单’举例。在创建订单的时候,所有订单项之间才会维护固定规则,⽐如‘总价限额’。⼀旦创建完毕,业务规定不能更改订单项的价格,也就不再需要聚合维护任何固定规则了。《领域驱动设计》并没有给出这种情况下对应的明确答案,如果按照书中对聚合处理,同样也会遇到前⾯讨论的问题。 ”


只在某个生命周期维护规则

诸如此类的讨论还有很多,⼼中也有⾃⼰模糊的答案,但是也产⽣了更多的疑问——聚合是不是⼀个业务概念?它是⼀个令⼈随意打扮的,还是⼀个客观存在的个体?如果“令⼈随意打扮”,那该如何打扮呢?如果客观存在,该如何才能准确地找到它呢?在DDD中有⾮常多的概念,它们⼀直在讨论中存在争议,那么DDD该如何指导设计和开发呢?

设计中的妥协

不知道各位读者是否也遇到过上⾯类似的问题。每当读到⼀个“新”的“设计⽅法”的时候,总免不了“得矣得矣”⽽沾沾⾃喜;⽽在实践中,很难得到⼀个完美的解决⽅案。

我也⼀直在思考,到底什么样的架构才是“完美”的?

后来我在《架构整洁之道》里面找到了答案:“软件架构的终极⽬标是,⽤最⼩的⼈⼒成本来满⾜构建和维护该系统的需求”(以下简称“最⼩⼈⼒成本原则”)。如果我们把这句话当作架构的⽬标,那么很多设计问题都会迎刃⽽解了。

在“故事二”的“问题一”中,新的解决方案可以是“把每种状态的动作处理做成可配置的,配置项通过多个策略模式完成”。这样只拓展新增加的aciton策略,相同的策略只需要相同的配置即可,并且还将state的控制权还给了Context。


状态模式替代方案之一

但是这种方案是不是“最小人力成本原则”呢?未必!状态模式作为成熟的设计模式,更容易被开发者理解和应用。而且我们还要面对团队开发人员的技术水平问题:当前的开发团队能否理解新的解决方案呢?我们需要花多大代价普及这个方案呢?这个方案是否可以作为一个新的模式在团队推广呢?

在“故事二”的“问题二”中,组合模式也是经过千千万万开发人员采用并验证过的,它是处理树型结构的典型方案。虽然它违背了某些原则,但是如果“妥协”一下,它还是非常好用的工具。相反,如果相似的问题不采用组合模式,新的方案能否满足组合模式的所有优点呢?

在“故事四”的“问题一”中,虽然D和A、B、C之间有业务规则,但是如果放在一个聚合里面维护,会不会因为聚合内过于复杂而无法满足“最小人力成本原则”呢?如果是,我们只能通过“妥协”,把相对联系不太紧密的D排除在聚合之外。其他问题的分析也是同理。

讨论到这里,所有的问题还是没有确切的答案,一切都是依赖具体的“环境”做出的妥协。

如果我们把“最小人力成本原则”来作为设计原则或者方法,那什么是“最小”呢?如何衡量最小呢?难道软件的设计原则真的是“具体问题具体分析”?而且它听起来更像是一个条管理原则,那我们学习SOLID和DDD还有什么用?

原则和模式

三年前,我以DDD专家身份去客户现场,几个同事在讨论具体概念的时候发生了分歧。这时,一位资深的同事问我们:“你认为架构的设计原则是什么?”我思考了一下:“高内聚,低耦合”。虽然当时得到了大佬的认同,但我并没有对自己的答案有多少信心。

如果我们把“高内聚,低耦合”作为原则对前面的问题进行分析,似乎也能得到类似的答案,毕竟“内聚性高,耦合性低”确实能降低构建和维护系统需求的成本。但是它的缺点也同样存在——到底内聚性达到什么程度才算“高内聚”呢?毕竟这两个指标没法量化。如果我们在进行OO建模,此时我们会发现,SOLID原则会告诉你怎样做到“高内聚,低耦合”。单一职责原则要求“一个类或者模块应该有且只有一个改变的原因”,其实就是对“高内聚,低耦合”的一个应用。

所以,我们可以把之前提到的所有“原则”都作为设计的指导,只不过在不同层级,不同粒度上会有不同。

那么,设计模式和“聚合”也是设计原则吗?在我们的讨论中,它们不是原则,而是模式。

模式可以理解为以原则为指导,针对一类问题提出的可复用的解决方案。设计模式很多情况下都是印证了SOLID原则。既然“针对一类问题”,它必然有严格的使用条件。

在“故事三”中,针对“消除重复代码”的原则,提出了多个“模式”——“Pull Up Method”和“Extract Class”。《重构:改善既有代码的设计》也给出了使用条件——如果两个类不相干,那么使用“Extract Class”;如果两个类有很多共性,或者本来就属于同一个继承结构,那么使用“Pull Up Method”。所以当我们搞清楚模式的使用条件时,就不用拍脑袋决定了。

在“故事四”的“问题三”中,聚合模式似乎不能满足“实体只在某个生命周期才需要一个组合结构维护固定规则,而其他生命周期和这个组合结构解耦”的问题。那么聚合模式是错误的吗?并不是,它仍然满足了大多数情况下的设计要求。很显然,我们此时的特殊需求,超越了聚合模式的使用范围。

既然模式不再适用,那我们就通过原则来指导设计。

首先,根据“最小人力成本原则”,使用聚合模式和“疑似中间人(Middle Man)坏味道”两者之间,哪个给团队带来的成本最小呢?如果使用聚合代价更小,那我们欣然接受聚合模式;反之,考虑“聚合”的设计原则。

在《领域驱动设计:软件核心复杂性应对之道》里讨论聚合的时候反复提到“局部和整体”和“一致性”,可以认为这是聚合模式的理论来源,也就是设计聚合的原则。事实上,它把复杂的变化封装在了一起,也符合我们说的“高内聚,低耦合”的原则。如果我们放弃聚合模式,那么就用前面说的原则进行重新设计。这样无论设计出的怎样的模型,他都是符合DDD设计思想的。如果这种模型能解决一类问题,它甚至可以命名为新的模式。所以,一旦分清了原则和模式之间的关系,更利于我们做设计工作。

因此:

软件的设计原则是分层级的:

  • 高层原则是抽象的,难以指导设计工作;
  • 低层原则是对高层原则在某个方面的细化,但它不能违背高层原则。

模式以原则为指导,针对一类问题提出的可复用的解决方案,并且有明确的使用条件。

做设计时,优先以满足条件的模式为指导,当模式无法满足设计时,以对应层次的原则作为指导。当低层原则无法指导设计时,向高层依次寻找原则。当新的设计方案能解决某一类问题时,它可能就是一种新的模式。

由上面的总结可以看出,模式作为设计的武器,当武器库中的武器都不能满足设计要求时,要么选择“妥协”,找一个最趁手的武器用起来,要么根据“原则”对武器进行升级打造,丰富武器库。(古代著名军事家戚继光在抗倭时,以长矛为基础发明狼筅,以克制倭刀。)

回到“高内聚,低耦合”

如果现在有人问我:“你的设计原则是什么?”我的回答可能是“高内聚,低耦合”,也可能是“封装、继承和多态”,但答案不再唯一。如果我们此刻在讨论“划分限界上下文”的原则,上面的答案就跟下面的问答类似:

问:“为什么铁球会落地?”

答:“因为万有引力。”

在物理界中,所有物理学家的终极梦想是发现一个宇宙通用的公式(就像“万有引力”能够解释“重力”一样),这个公式能够解释一切物理现象。且不论这个公式是否存在,即便存在,应该也是极其复杂的。在我们的设计过程中,不必用最高层的原则指导一切,那样指导具体设计时就会变得模糊。

推荐阅读

  • 如何实现系统解耦
  • 如何理解SOLID原则?
  • 代码的简单设计五原则
  • 用DDD指导微服务拆分
  • DDD的战略设计和战术设计
  • 写了这么多年代码,你真的了解设计模式么?
  • 《Head First设计模式》
  • 《重构:改善既有代码的设计》
  • 《领域驱动设计:软件核心复杂性应对之道》
  • 《架构整洁之道》

文/Thoughtworks 王万徳
原文链接:https://insights.thoughtworks.cn/architecture-design-principles-patterns/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/420244.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C语言 - 初阶指针 概念、类型、野指针、指针运算】

C语言 - 初阶指针一 指针概念注意:指针变量的大小:(与指向的数据类型无关)二 指针类型2.1指针类型的含义:2.1.1 不同指针类型决定解引用时候权限不同:总结:2.1.2 任何类型的指针变量都能存放地址…

10 个高级 Python 面试问题

随着 Python 最近变得越来越流行,你们中的许多人可能正在接受与 Python 打交道的技术面试。在这篇文章中,我将列出十个高级 Python 面试问题和答案。 这些内容可能会令人困惑,并且针对的是中级开发人员,他们需要对 Python 作为一…

RB-PEG-COOH,罗丹明聚乙二醇羧基化学试剂简介;RhodamineB-PEG-acid

RB-PG-COOH, 罗丹明聚乙二醇羧基 中文名称:罗丹明-聚乙二醇-羧基 英文名称:RhodamineB-PEG-acid RB-PEG-COOH 性状:固体或粘性液体,取决于分子量 溶剂:溶于水和DCM、DMF、DMSO等常规性有机溶剂 分子量&#xff1…

归排、计排深度理解

归并排序:是创建在归并操作上的一种有效的排序算法。算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。归并排序思路简单,速度仅次于快速排序,为稳定排序算法&#…

ICMP隧道技术实现防火墙穿透

1.在mac os的虚拟机里准备三台kali 三台主机ip地址分别是 192.168.1.15,192.168.1.16,192.168.1.17, 为方便描述 依次把他们暂且命名为主机A,主机B,主机C 2.在主机C 上打开终端,输入 cd /usr/local/src 然后新建一个hello.txt 文…

最新版人脸识别小程序 图片识别 生成二维码签到 地图上选点进行位置签到 计算签到距离 课程会议活动打卡日常考勤 上课签到打卡考勤口令签到

技术选型 1,前端 小程序原生MINA框架 css JavaScript Wxml 2,管理后台 云开发Cms内容管理系统 web网页 3,数据后台 小程序云开发 云函数 云开发数据库(基于MongoDB) 云存储 4,人脸识别算法 基于百度…

xxl-job定时任务调度中心的配置以及整合到自己的项目中实现远程调用

目录 前言: xxl-job配置与启动: xxl-job-admin: xxl-job-execultor--sample-springboot: 启动任务调度中心: ​编辑 调用定时任务: ​编辑 在自己的项目中配置xxl-job: 项目结构: ​编辑 AdUp…

【CSS】更改用户界面样式 ② ( 取消轮廓线 outline | 取消轮廓线设置方式 outline: 0; | 代码示例 )

文章目录一、更改轮廓线 outline二、轮廓线代码示例三、取消轮廓线代码示例一、更改轮廓线 outline 轮廓线 是 元素 边框 外面 的一条线 , 其作用是 选中后突出元素 ; 一般情况下都会去掉 轮廓线 显示 ; outline 样式后可设置 1 ~ 3 个参数 , 按照顺序分别是 : outline-color…

JavaScript-DOM基础

DOM介绍 事件介绍 文档加载 DOM查询 DOM介绍 DOM&#xff0c;全称Document Object Model文档对象模型。浏览器已经提供了文档节点对象 时window属性&#xff0c;可以在页面中直接使用&#xff08;document文档节点代表整个网页&#xff09; <button id "btn&qu…

【Linux 裸机篇(三)】I.MX6ULL 启动方式

目录一、启动方式选择1. 串行下载2. 内部 BOOT 模式2.1 BOOT ROM 初始化内容2.2 启动设备二、镜像烧写1. IVT2. Boot data3. DCD数据一、启动方式选择 I.MX6ULL 芯片上电后&#xff0c;芯片会根据 BOOT_MODE[1:0]的设置来选择 BOOT 方式。BOOT_MODE[1:0]的值是可以改变的&#…

【ONE·C++ || list (一)】

总言 主要介绍list的基本函数使用及部分函数接口模拟实现(搭框架)。 文章目录总言1、常用接口与举例演示1.1、接口总览1.2、部分例子1.2.1、头删、头插、尾删、尾插、遍历1.2.2、pos插入删除、迭代器失效问题1.2.3、一些相对陌生接口简介&#xff08;std::sort和list::sort比较…

D. Captain Flint and Treasure(拓扑排序 + 贪心)

Problem - D - Codeforces 芬特队长参与了另一个寻宝活动&#xff0c;但只发现了一个奇怪的问题。这个问题可能与宝藏的位置有关&#xff0c;也可能不是。这就是为什么弗林特船长决定把解决问题的工作交给他的船员&#xff0c;并提供了一个高得离谱的奖励:休息一天。问题本身听…

【日常】我的扬马最后一小时

文章目录1 Approxmation, Regularization and Relaxation赛前风波惨痛的主场之战释然的痛苦之路后记1 Approxmation, Regularization and Relaxation 在算法理论研究中&#xff0c;为了使得降低问题的求解复杂度&#xff0c;常常会选择牺牲算法的选择求解精度&#xff0c;这种…

Redis-----什么是Redis?

什么是Redis&#xff1f; redis是一个基于内存的key-value结构数据库。 基于内存存储&#xff0c;读写性能高适合存储热点数据&#xff08;热点商品、资讯、新闻&#xff09;企业应用广泛 Redis入门 redis简介 redis是一个开源的内存中的数据结构存储系统&#xff0c;数据库…

ASP宿舍管理系统设计与实现

学生宿舍的管理工作也将成为一项十分繁重的工作&#xff0c;建立一个学生宿舍管理系统是非常必要的&#xff0c;可行的。计算机能够极大地提高学生宿舍管理的办事效率&#xff0c;学校要想与先进科学技术接轨&#xff0c;就得科学化、正规化的进行管理。随着社会信息化步伐的加…

使用Unity模拟人群疏散的资料整理

本文地址&#xff1a;https://blog.csdn.net/t163361/article/details/130136283 UnityDemo Evacuation Simulator Unity_EvacuationSimulator Crowd-Simulation-and-Visualization-in-Unity Multi-agent-simulation-program-for-evacuation Crowd-Evacuation-Simulatio…

Android SQLite插入float类型浮点数小数位数异常(四舍五入过的两位小数变成13位小数)的原因和解决方法

浮点数异常截图&#xff1a; 说明&#xff1a; 正常保留两位小数并正确插入的记录是通过db.execSQL(sql);方法插入的&#xff0c;而浮点数异常的是通过ContentValues db.insert() 方式插入的,可以发现问题出在db.insert()方法上&#xff0c;我又试过在put的时候直接输入类似16…

zabbix代理服务器部署

分布式监控的作用&#xff1a; ●分担 server 的集中式压力 ●解决多机房之间的网络延时问题 部署zabbix代理服务器 1、关闭防火墙、修改主机名 systemctl disable --now firewalld setenforce 0 hostnamectl set-hostname zbx-proxy su 2、设置zabbix的下载源&#xff0c;按…

7.2 模拟乘法器及其在运算电路中的应用

模拟乘法器是实现两个模拟量相乘的非线性电子器件&#xff0c;利用它可以方便地实现乘、除、乘方和开方运算电路。此外&#xff0c;由于它还能广泛地应用于广播电视、通讯、仪表和自动控制系统&#xff0c;进行模拟信号的处理&#xff0c;所以发展很快&#xff0c;称为模拟集成…

【微信小程序-原生开发】添加自定义图标(以使用阿里图标库为例)

方式一 &#xff1a; 下载svg导入 优点&#xff1a;操作方便&#xff0c;支持多彩图标缺点&#xff1a;会增加源代码大小 下载 svg 格式的图标图片&#xff0c;放入源码中使用 小程序项目中的路径为 assets\icon\美食.svg 使用时-代码范例 <image class"imgIcon"…