如何优雅的在业务中使用设计模式

news/2025/2/21 7:12:14/


/   今日科技快讯   /

8月25日,美图公司发布2021年中期业绩报告。截至6月30日,公司已购买的比特币和以太坊公允价值分别约为6520万美元、3220万美元。上半年比特币公允价值减少1.119亿元人民币,以太坊增加9490万元人民币,虚拟货币投资总计亏损1700万元人民币。

/   作者简介   /

明天是周六啦,又到了休息的时候,我们下周再见!

本篇文章来自小呆呆666的投稿,结合业务详细地分析了两种设计模式,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章!

小呆呆666的博客地址:

https://juejin.cn/user/2840793776393847/posts

/   前言   /

有段时间没写文章了,最近沉迷Rust,无法自拔,锈儿有毒;这真是门非常有趣的语言,很多地方的设计,真的是满足了我所有的向往。

当然,这也不是一门简单的语言,提出所有权的概念,引入了极多符号:mut、&mut、ref mut、&、*、as_mut、as_ref。。。让人头秃。。。

之前看到过一句话,觉得很不错:学习Rust并不会给你带来智商上的优越感,但或许会让你重新爱上编程

大家如果阅读过一些开源框架的源码,可能会发现其中数不尽的抽象类,设计模式拈手而来,在功能框架中,可以使用设计模式随心所欲的解耦;在实际的复杂业务中,当然也可以应用合适的设计模式。

这篇文章,我会结合较为常见的实际业务场景,探讨如何使用合适的设计模式将业务解耦

  • 此处的应用绝不是生搬硬套,是我经过深思熟虑,并将较为复杂的业务进行全面重构后,得出的一套行之有效的思路历程

  • 任何一个设计模式都是一个伟大的经验及其思想总结,千人千面,如果对文章中内容,有不同的意见,希望你能在评论中提出,我们共同探讨,共同进步

本文章是一篇弱代码类型文章,我会画大量的图片向大家展示,引用设计模式后,会对原有的业务流程,产生什么样的影响。

/   前置知识   /

这里,需要了解下基础知识,什么是责任链模式和策略模式

责任链模式,在很多开源框架中都是有所应用,你如果听到啥啥拦截器,基本就是责任链模式,责任链模式的思想很简单,但是有很多种实现方式

  • 最简单的链表实现就和OkHttp的拦截器实现大相径庭

  • OkHttp的拦截器实现和Dio拦截器实现结构相同,但遍历方式不一样

  • 很多骚操作:我喜欢OkHttp的实现方式,喜欢dio的Api设计,结尾会给出一个结合这俩者思想的通用拦截器

策略模式,或是天生适合业务,同一模块不同类型业务,如果行为相同,或许就可以考虑使用策略模式去解耦了

责任链模式

这边用Dart写一个简单的拦截器,dart和java非常像

  • 为了减少语言差异,我就不使用箭头语法了

  • 下划线表示私有

用啥语言不重要,这边只是用代码简单演示下思想

此处实现就用链表了;如果,使用数组的形式,需要多写很多逻辑,数组的优化写法在结尾给出,此处暂且不表

  • 结构

责任链的结构,通常有俩种结构

  • 链表结构:链表构建责任链,十分便捷的就能和下一节点建立联系

  • 数组结构:数组,用通用的List即可,方便增删,不固定长度(别费劲的用固定长度Array了,例如:int[]、String[])

实现一个链表实体很简单

abstract class InterceptChain<T> {InterceptChain? next;void intercept(T data) {next?.intercept(data);}
}
  • 实现

拦截器实现

/// 该拦截器以最简单的链表实现
abstract class InterceptChain<T> {InterceptChain? next;void intercept(T data) {next?.intercept(data);}
}class InterceptChainHandler<T> {InterceptChain? _interceptFirst;void add(InterceptChain interceptChain) {if (_interceptFirst == null) {_interceptFirst = interceptChain;return;}var node = _interceptFirst!;while (true) {if (node.next == null) {node.next = interceptChain;break;}node = node.next!;}}void intercept(T data) {_interceptFirst?.intercept(data);}
}

使用

  • 调整add顺序,就调整了对应逻辑的节点,在整个责任链中的顺序

  • 去掉intercept重写方法中的super.intercept(data),就能实现拦截后续节点逻辑

void main() {var intercepts = InterceptChainHandler<String>();intercepts.add(OneIntercept());intercepts.add(TwoIntercept());intercepts.intercept("测试拦截器");
}class OneIntercept extends InterceptChain<String> {@overridevoid intercept(String data) {data = "$data:OneIntercept";print(data);super.intercept(data);}
}class TwoIntercept extends InterceptChain<String> {@overridevoid intercept(String data) {data = "$data:TwoIntercept";print(data);super.intercept(data);}
}

打印结果

测试拦截器:OneIntercept
测试拦截器:OneIntercept:TwoIntercept

策略模式

  • 结构

策略模式最重要的:应该就是对抽象类的设计,对行为的抽象

  • 实现

定义抽象类,抽象行为

/// 结合适配器模式的接口适配:抽象必须实现行为,和可选实现行为
abstract class BusinessAction {///创建相应资源:该行为必须实现void create();///可选实现void dealIO() {}///可选实现void dealNet() {}///可选实现void dealSystem() {}///释放资源:该行为必须实现void dispose();
}

实现策略类

//Net策略
class NetStrategy extends BusinessAction {@overridevoid create() {print("创建Net资源");}@overridevoid dealNet() {print("处理Net逻辑");}@overridevoid dispose() {print("释放Net资源");}
}///IO策略
class IOStrategy extends BusinessAction {@overridevoid create() {print("创建IO资源");}@overridevoid dealIO() {print("处理IO逻辑");}@overridevoid dispose() {print("释放IO资源");}
}

使用

void main() {var type = 1;BusinessAction strategy;//不同业务使用不同策略if (type == 0) {strategy = NetStrategy();} else {strategy = IOStrategy();}//开始创建资源strategy.create();//......... 省略N多逻辑(其中某些场景,会有用到Net业务,和上面type是关联的)//IO业务:开始处理业务strategy.dealIO();//......... 省略N多逻辑//释放资源strategy.dispose();
}

结果

创建IO资源
处理IO逻辑
释放IO资源

/   适合的业务场景   /

这边举一些适合上述设计模式的业务场景,这些场景是真实存在的!

这些真实的业务,使用设计模式解耦和纯靠if else怼,完全是俩种体验!

代码如诗,这并不是一句玩笑话。

连环弹窗业务

  • 业务描述

连环弹窗夺命call来袭。。。

好家伙,套娃真是无所不在,真不是我们代码套娃,实在是业务套娃,手动滑稽.png

图示弹窗业务(连环弹窗业务1)

  • 直接开搞

看到这个业务,大家会去怎么做呢?

有人可能会想,这么简单的业务还需要想吗?直接写啊!

  • A:在确定回调里面,跳转B弹窗

  • B:查看详情按钮跳转C弹窗

  • 。。。

好一通套后,终于写完了

产品来了,加需求B和C弹窗之间要加个预览G弹窗,点击B的查看详情按钮,跳转预览G弹窗;预览G弹窗只有一个确定按钮,点击后跳转C弹窗

你心里可能要想了,这特么不是坑爹?

业务本来就超吉尔套,我B弹窗里面写的跳转代码要改,传参要改,而且还要加弹窗!

先要去找产品撕比,撕完后

  • 然后继续在屎山上,小心翼翼的再拉了坨shit

  • 这座克苏鲁山初成规模

连环弹窗业务2

产品又来了,第一稿需求不合理,需要调整需求

交换C和D弹窗位置,D弹窗点击下一步的时候,需要加一个校验请求,通过后才能跳转到C弹窗

你眉头一皱,发现事情没有表面这么简单

  • 由于初期图简单,几乎都写在一个文件里,眼花缭乱弹窗回调太多,而且弹窗样式也不一样

  • 现在改整个流程,导致你整个人脑子嗡嗡响

心中怒气翻涌,找到产品说

回来,坐在椅子上,心里想:

  • 老夫写的代码天衣无缝,这什么几把需求

  • 可恶,这次测试,起码要给我多提十几个BUG

克苏鲁山开始狰狞(连环弹窗业务3)

产品飘来,加改需求:如此,如此,,,这般,这般,,,

产品:改下,,,然后,扔给你几十页的PRD

你看了看这改了几十版的克苏鲁山,这几十个弹窗逻辑居然都写在一个文件里,快一万行的代码。。。

心里不禁想:

  • 本帅比写的代码果然牛批,或许这就是艺术!艺术总是曲高和寡,难被人理解!而我的代码更牛批,连我自己都看不懂了!

  • 这代码行数!这代码结构!不得拍个照留念下,传给以后的孩子当传家宝供着!

心里不禁嘚瑟:

  • 这块业务,除了我,还有谁敢动,成为头儿的心腹,指日可待!

但,转念深思后:事了拂衣去,深藏功与名

  • 重构

随着业务的逐渐复杂,最初的设计缺点会逐渐暴露;重构有缺陷的代码流程,变得势在必行,这会极大的降低维护成本

如果心中对责任链模式有一些概念的话,会发现上面的业务,极其适合责任链模式!

对上面的业务进行分析,可以明确一些事

  • 这个业务是一个链式的,有着明确的方向性:单向,从头到尾指向

  • 业务拆分开,可以将一个弹窗作为单颗粒度,一个弹窗作为节点

  • 上级的业务节点可以对下级节点拦截(点击取消,拒绝按钮,不再进行后续业务)

重构上面的代码,只要明确思想和流程就行了

第一稿业务 :责任链

代码:简写

void main() {var intercepts = InterceptChainHandler<String>();intercepts.add(AIntercept());intercepts.add(BIntercept());intercepts.add(CIntercept());intercepts.add(DIntercept());intercepts.add(EIntercept());intercepts.add(FIntercept());intercepts.intercept("测试拦截器");
}

第二稿业务:责任链

代码:简写

void main() {var intercepts = InterceptChainHandler<String>();intercepts.add(AIntercept());intercepts.add(BIntercept());intercepts.add(GIntercept());intercepts.add(CIntercept());intercepts.add(DIntercept());intercepts.add(EIntercept());intercepts.add(FIntercept());intercepts.intercept("测试拦截器");
}

第三稿业务:责任链

代码:简写

void main() {var intercepts = InterceptChainHandler<String>();intercepts.add(AIntercept());intercepts.add(BIntercept());intercepts.add(GIntercept());intercepts.add(DIntercept());intercepts.add(CIntercept());intercepts.add(EIntercept());intercepts.add(FIntercept());intercepts.intercept("测试拦截器");
}

总结:经过责任链模式重构后,业务节点被明确的区分开,整个流程从代码上看,都相当的清楚,维护将变的异常轻松;或许,此时能感受到一些,编程的乐趣了

花样弹窗业务

  • 业务描述

来描述一个新的业务:这个业务场景真实存在某办公软件

  • 进入APP首页后,和后台建立一个长连接

  • 后台某些工单处理后,会通知APP处理,此时app会弹出处理工单的弹窗(app顶部)

  • 弹窗类型很多:工单处理弹窗,流程审批弹窗,邀请类型弹窗,查看工单详情弹窗,提交信息弹窗。。。

  • 弹窗弹出类型,是根据后台给的Type进行判断:从而弹出不同类型弹窗、点击其按钮,跳转不同业务,传递不同参数。

  • 分析

1. 确定设计

这个业务,是一种渐变性的引导你搭建克苏鲁代码山

  • 在前期开发的时候,一般只有俩三种类型弹窗,前期十分好做;根本不用考虑如何设计,抬手一行代码,反手一行代码,就能搞定

  • 但是后来整个业务会渐渐的鬼畜,不同类型会慢慢加到几十种之多!!!

首先这个业务,使用责任链模式,肯定是不合适的,因为弹窗之间的耦合性很低,并没有什么明确的上下游关系

但是,这个业务使用策略模式非常的合适!

  • type明确:不同类型弹出不同弹窗,按钮执行不同逻辑

  • 抽象行为明确:一个按钮就是一种行为,不同行为的实现逻辑大相径庭


2. 抽象行为

多样弹窗的行为抽象,对应其按钮就行了

确定、取消、同意、拒绝、查看详情、我知道了、提交

直接画图来表示吧

  • 实现

来看下简要的代码实现,代码不重要,重要的是思想,这边简要的看下代码实现流程

抽象基类

/// 默认实现抛异常,可提醒未实现方法被误用
abstract class DialogAction {///确定void onConfirm() {throw 'DialogAction:not implement onConfirm()';}///取消void onCancel() {throw 'DialogAction:not implement onCancel()';}///同意void onAgree() {throw 'DialogAction:not implement onAgree()';}///拒绝void onRefuse() {throw 'DialogAction:not implement onRefuse()';}///查看详情void onDetail() {throw 'DialogAction:not implement onDetail()';}///我知道了void onKnow() {throw 'DialogAction:not implement onKnow()';}///提交void onSubmit() {throw 'DialogAction:not implement onSubmit()';}
}

实现逻辑类

class OneStrategy extends DialogAction {@overridevoid onConfirm() {print("确定");}@overridevoid onCancel() {print("取消");}
}class TwoStrategy extends DialogAction{@overridevoid onAgree() {print("同意");}@overridevoid onRefuse() {print("拒绝");}
}//........省略其他实现

使用

void main() {//根据接口获取var type = 1;DialogAction strategy;switch (type) {case 0:strategy = DefaultStrategy();break;case 1:strategy = OneStrategy();break;case 2:strategy = TwoStrategy();break;case 3:strategy = ThreeStrategy();break;case 4:strategy = FourStrategy();break;case 5:strategy = FiveStrategy();break;default:strategy = DefaultStrategy();break;}//聚合弹窗按钮触发事件(不同弹窗的确定按钮,皆可聚合为一个onConfirm事件,其它同理)BusinessDialog(//通过传入的type,显示对应类型的弹窗type: type,//确定按钮onConfirm: () {strategy.onConfirm();},//取消按钮onCancel: () {strategy.onCancel();},//同意按钮onAgree: () {strategy.onAgree();},//拒绝按钮onRefuse: () {strategy.onRefuse();},//查看详情按钮onDetail: () {strategy.onDetail();},//我知道了按钮onKnow: () {strategy.onKnow();},//提交按钮onSubmit: () {strategy.onSubmit();},);
}

/   复杂业务场景演变   /

我们看下,一个简单的提交业务流,怎么逐渐变的狰狞

我会逐渐给出一个合适的解决方案,如果大家有更好的想法,务必在评论区告诉鄙人

业务描述:我们的车子因不可抗原因坏了,要去维修厂修车,工作人员开始登记这个损坏车辆。。。

业务的演变

第一稿:初始业务

登记一个维修车辆的流程,实际上还是满麻烦的

  • 登记一个新车,需要将车辆详细信息登记清楚:车牌、车架、车型号、车辆类型、进出场时间、油量、里程。。。

  • 还需要登记一下用户信息:姓名、手机号、是否隶属公司。。。

  • 登记车损程度:车顶、车底、方向盘、玻璃、离合器、刹车。。。

  • 车内物品:车座皮套、工具。。。

  • 以及其他我没想到的。。。

  • 最后:提交所有登记好的信息

第一稿,业务流程十分清晰,细节复杂,但是做起来不难

第二稿(实际是多稿聚合):增加下述几个流程

  • 外部登记:外部登记了一个维修车辆部分信息(后台,微信小程序,H5等等),需要在app上完善信息,提交接口不同(必带车牌号)

  • 快捷洗车:洗车业务极其常见,快捷生成对应信息,提交接口不同

  • 预约订单登记:预约好了车辆一部分一些信息,可快捷登记,提交接口不同(必带车牌号)

因为登记维修车辆流程,登记车辆信息流程极其细致繁琐,我们决定复用登记新车模块

  • 因为此处逻辑大多涉及开头和结尾,中间登记车辆信息操作几乎未改动,复用想法是可行的

  • 如果增加车辆登记项,新的三个流程也必须提交这些信息;所以,复用势在必行

因为这一稿需求,业务也变得愈加复杂

第三稿

现在要针对不同的车辆类型,做不同的处理;车类型分:个人车,集团车

不同类型的登记,在提交的时候,需要校验不同的信息;校验不通过,需要提示用户,并且不能进行提交流程

提交后,需要处理下通用业务,然后跳转到某个页面

第三稿的描述不多,但是,大大的增加了复杂度

  • 尤其是不同类型校验过程还不同,还能中断后续提交流程

  • 提交流程后,还需要跳转通用页面

开发探讨

  • 第一稿

正常流程开发、、、

  • 第二稿

对于第二稿业务,可以好好考虑下,怎么去设计?

开头和结尾需要单独写判断,去处理不同流程的业务,这至少要写俩个大的判断模块,接受数据的入口模块可能还要写判断

这样就非常适合策略模式去做了

开头根据执行的流程,选择相应的策略对象,后续将逻辑块替换抽象的策略方法就OK了,大致流程如下

  • 第三稿

第三稿的需求,实际上,已经比较复杂了

  • 整个流程中掺杂着不同业务流程处理,不同流程逻辑又拥有阻断下游机制(绿色模块)

  • 下游逻辑又会合流(结尾)的多种变换

在这一稿的需求

  • 使用策略模式肯定是可以的

  • 阻断那块(绿色模块)需要单独处理下:抽象方法应该拥有返回值,外层根据返回值,判断是否进行后续流程

  • 但!这!也太不优雅了!

思考上面业务一些特性

  • 拦截下游机制

  • 上游到下游、方向明确

  • 随时可能插入新的业务流程。。。

可以用责任链模式!但,需要做一些小改动!这地方,我们可以将频繁变动的模块用责任链模式全都隔离出来

看下,使用责任链模式改造后流程图

浏览上述流程图可发现,本来是极度杂乱糅合的业务,可以被设计相对更加平行的结构

对于上述流程,可以进一步分析,并进一步简化:对整体业务分析,我们需要去关注其变或不变的部分

  • 不变:整体业务变动很小的是,登记信息流程(主体逻辑这块),此处的相关变动是很小的,对所有流程也是共用的部分

  • 变:可以发现,开头和结尾是变动更加频繁的部分,我们可以对此处逻辑进行整体的抽象

抽象多变的开头和结尾

所以我们抽象拦截类,可以做一些调整

abstract class InterceptChainTwice<T> {InterceptChainTwice? next;void onInit(T data) {next?.onInit(data);}void onSubmit(T data) {next?.onSubmit(data);}
}

来看下简要的代码实现,代码不重要,主要看看实现流程和思想

抽象拦截器

abstract class InterceptChainTwice<T> {InterceptChainTwice? next;void onInit(T data) {next?.onInit(data);}void onSubmit(T data) {next?.onSubmit(data);}
}class InterceptChainTwiceHandler<T> {InterceptChainTwice? _interceptFirst;void add(InterceptChainTwice interceptChain) {if (_interceptFirst == null) {_interceptFirst = interceptChain;return;}var node = _interceptFirst!;while (true) {if (node.next == null) {node.next = interceptChain;break;}node = node.next!;}}void onInit(T data) {_interceptFirst?.onInit(data);}void onSubmit(T data) {_interceptFirst?.onSubmit(data);}
}

实现拦截器

/// 开头通用拦截器
class CommonIntercept extends InterceptChainTwice<String> {@overridevoid onInit(String data) {//如果有车牌,请求接口,获取数据//.................//填充页面super.onInit(data);}
}/// 登记新车拦截器
class RegisterNewIntercept extends InterceptChainTwice<String> {@overridevoid onInit(String data) {//处理开头针对登记新车的单独逻辑super.onInit(data);}@overridevoid onSubmit(String data) {var isPass = false;//如果校验不过,拦截下游逻辑if (!isPass) {return;}// ......super.onSubmit(data);}
}/// 省略其他实现

使用

void main() {var type = 0;var intercepts = InterceptChainTwiceHandler();intercepts.add(CommonIntercept());intercepts.add(CarTypeDealIntercept());if (type == 0) {//登记新车intercepts.add(RegisterNewCarIntercept());} else if (type == 1) {//外部登记intercepts.add(OutRegisterIntercept());} else if (type == 2) {//快捷洗车intercepts.add(FastWashIntercept());} else {//预约订单登记intercepts.add(OrderRegisterIntercept());}intercepts.add(TailIntercept());//业务开始intercepts.onInit("传入数据源");//开始处理N多逻辑//............................................................//经历了N多逻辑//提交按钮触发事件SubmitBtn(//提交按钮onSubmit: () {intercepts.onSubmit("传入提交数据");},);
}

总结

关于代码部分,关键的代码,我都写出来,用心看看,肯定能明白我写的意思

也不用找我要完整代码了,这些业务demo代码写完后,就删了

本栏目这个业务,实际上是非常常见的的一个业务,一个提交流程与很多其它的流程耦合,整个业务就会慢慢的变的鬼畜,充满各种判断,很容易让人陷入泥泞,或许,此时可以对已有业务进行思考,如何进行合理的优化

该业务的演变历程,和开发改造是本人的一次思路历程,如大家有更好的思路,还请不吝赐教。

/   通用拦截器   /

我结合OkHttp的思想和Dio的API,封装了俩个通用拦截器,这边贴下代码,如果哪里有什么不足,请及时告知本人

说明下:这是Dart版本的

抽象单方法

///一层通用拦截器,T的类型必须一致
abstract class InterceptSingle<T> {void intercept(T data, SingleHandler handler) => handler.next(data);
}///添加拦截器,触发拦截器方法入口
class InterceptSingleHandler<T> {_InterceptSingleHandler _handler = _InterceptSingleHandler(index: 0,intercepts: [],);void add(InterceptSingle intercept) {//一种类型的拦截器只能添加一次for (var item in _handler.intercepts) {if (item.runtimeType == intercept.runtimeType) {return;}}_handler.intercepts.add(intercept);}void delete(InterceptSingle intercept) {_handler.intercepts.remove(intercept);}void intercept(T data) {_handler.next(data);}
}///------------实现不同处理器 参照 dio api设计 和 OkHttp实现思想---------------
abstract class SingleHandler {next(dynamic data);
}///实现init处理器
class _InterceptSingleHandler extends SingleHandler {List<InterceptSingle> intercepts;int index;_InterceptSingleHandler({required this.index,required this.intercepts,});@overridenext(dynamic data) {if (index >= intercepts.length) {return;}var intercept = intercepts[index];var handler =_InterceptSingleHandler(index: index + 1, intercepts: intercepts);intercept.intercept(data, handler);}
}

抽象双方法

///俩层通用拦截器,T的类型必须一致
abstract class InterceptTwice<T> {void onInit(T data, TwiceHandler handler) => handler.next(data);void onSubmit(T data, TwiceHandler handler) => handler.next(data);
}///添加拦截器,触发拦截器方法入口
class InterceptTwiceHandler<T> {_TwiceInitHandler _init = _TwiceInitHandler(index: 0, intercepts: []);_TwiceSubmitHandler _submit = _TwiceSubmitHandler(index: 0, intercepts: []);void add(InterceptTwice intercept) {//一种类型的拦截器只能添加一次for (var item in _init.intercepts) {if (item.runtimeType == intercept.runtimeType) {return;}}_init.intercepts.add(intercept);_submit.intercepts.add(intercept);}void delete(InterceptTwice intercept) {_init.intercepts.remove(intercept);_submit.intercepts.remove(intercept);}void onInit(T data) {_init.next(data);}void onSubmit(T data) {_submit.next(data);}
}///------------实现不同处理器 参照 dio api设计 和 OkHttp实现思想---------------
abstract class TwiceHandler {next(dynamic data);
}///实现init处理器
class _TwiceInitHandler extends TwiceHandler {List<InterceptTwice> intercepts;int index;_TwiceInitHandler({required this.index,required this.intercepts,});@overridenext(dynamic data) {if (index >= intercepts.length) {return;}var intercept = intercepts[index];var handler = _TwiceInitHandler(index: index + 1, intercepts: intercepts);intercept.onInit(data, handler);}
}///实现submit处理器
class _TwiceSubmitHandler extends TwiceHandler {List<InterceptTwice> intercepts;int index;_TwiceSubmitHandler({required this.index,required this.intercepts,});@overridenext(dynamic data) {if (index >= intercepts.length) {return;}var intercept = intercepts[index];var handler = _TwiceSubmitHandler(index: index + 1, intercepts: intercepts);intercept.onSubmit(data, handler);}
}

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

使用OpenGL挑战抖音蓝线特效

PermissionX 1.5发布,支持申请Android特殊权限啦

欢迎关注我的公众号

学习技术或投稿

长按上图,识别图中二维码即可关注


http://www.ppmy.cn/news/787286.html

相关文章

c 自动打印的服务器,clodop云打印服务器(c_lodop打印机不打印)

我的也一样&#xff0c;等等吧 &#xff0c;再看看别人怎么说的。 方法有二种&#xff1a;本地打印和网络打印.a&#xff1a;本地打印&#xff0c;在服务器(或共享的电脑主机)上装好打印机的驱动&#xff0c;在云终端上插入打印机的usb 接口&#xff0c;即可使用。b&#xff1a…

江阳职高计算机应用教改实验,高职《计算机应用基础》教改探讨

摘 要&#xff1a;高职教育中的“计算机应用基础”是一门公共基础且又具备操作技能实践性的课程&#xff0c;由于电脑的家庭普及、智能手机的个人普及、中小学信息技术课程的开设等原因&#xff0c;一部分学生已经具备了一些计算机操作技能。针对计算机科技技术发展飞速的今天&…

11. exercise练习

文章目录 01_图片的列表02_京东左侧导航条03_网易新闻的右侧的列表04_w3导航条05_京东的轮播图06_京东顶部导航条07_背景重复08_按钮练习09_按钮练习10_电影卡片11_米兔的动画11_米兔的动画13.钟表14.复仇者联盟15.再做导航条16.淘宝导航17.移动端页面18.美图 01_图片的列表 &…

HTML5+CSS3的学习(六)

HTML5CSS3的学习(六) 2018版李立超htmlcss基础 103集教程&#xff0c;哔哩哔哩链接&#xff1a;https://www.bilibili.com/video/BV1sW411T78k?spm_id_from333.999.0.0 2019版李立超前端html5css3 148集教程&#xff0c;哔哩哔哩链接&#xff1a;https://www.bilibili.com/v…

什么是场景化需求分析法?如何有效使用这个客户需求分析最有效的方法?

根据美国工业协会统计&#xff0c;产品失败的原因核心有6个&#xff0c;如下图&#xff0c;其中不适当的市场分析排名第一&#xff0c;独占产品失败原因的32%&#xff0c;客户需求分析又是市场分析中非常重要的内容&#xff0c;可以这样说&#xff0c;能否有效洞察客户需求是产…

面试测试开发工程师:用例篇

目录 1. 测试用例的基本要素 2. 测试用例的给我们带来的好处 3. 测试用例的设计方法 3.1 测试用例的总体设计方法 基于需求的设计 3.2 具体的设计方法 3.2.2 等价类 3.2.3 边界值 3.2.4 因果图 3.2.5 正交排列 3.2.6 场景设计法 3.2.7 错误猜测法 4. 什么是测试用…

基于零代码搭建你自己的设备管理系统

在信息化技术高速发展的今天&#xff0c;产品呈现傻瓜化趋势&#xff0c;不是设计师也能用美图秀秀把自己的相片处理得美美的&#xff1b;不是摄影师也能用抖音、手机剪辑软件制作出很燃的视频。当然&#xff0c;不会编程也能在零代码平台上搭建出一个设备管理系统。 一、设备…

git:只clone或fetch某个分支最新版本的内容

参考&#xff1a; 【解决】Git如何只克隆远程仓库最新的一个版本&#xff08;不拷贝其他所有历史版本&#xff09;_克隆github远程仓库代码的当前版本_COCO56&#xff08;徐可可&#xff09;的博客-CSDN博客