面向对象设计之开闭原则
设计模式专栏: http://t.csdnimg.cn/4Mt4u
目录
1.引言
2.如何理解“对扩展开放、对修改关闭”
3.修改代码就意味着违反开闭原则吗
4.如何做到“对扩展开放、对修改关闭”
5.如何在项目中灵活应用开闭原则
6.总结
1.引言
开闭原则(Open Closed Principle,OCP),又称为“对扩展开发、对修改关闭”原则。开闭原则既是 SOLID 原则中最难理解、最难掌握的,又是最有用的。之所以说开闭原则难理解,是因为“怎样的代码改动才被定义为'扩展’?怎样的代码改动才被定义为'修改’?怎么才算满足或违反'开闭原则’?修改代码就一定意味着违反'开闭原则’吗?”等问题都比较难理解。之所以说开闭原则难掌握,是因为“如何做到'对扩展开发、对修改关闭’?如何在项目中灵活应用'开闭原则’,避免在追求高扩展性的同时影响代码的可读性?”等问题都比较难掌握。
之所以说开闭原则最有用,是因为扩展性是代码质量的重要衡量标准。在22种经典设计模式中,大部分设计模式都是为了解决代码的扩展性问题而产生的,它们主要遵守的设计原则就是开闭原则。
2.如何理解“对扩展开放、对修改关闭”
开闭原则的英文描述是:software enities(modules,classes,fiuncions,etc.)should be open for extension but closed for modification。对应的中文为:软件实体(模块、类和方法等)应该“对扩展开放、对修改关闭”,详细表述为: 添加一个新功能时应该是在已有代码基础上扩展代码(新类和方法等),而非修改已有代码(修改模块、类和方法等)。
为了让读者更好地理解开闭原则,我们举例说明。
下面是一段AR(应用程序编程接口)监控告警的代码。其中,AlertRule 类存储告警规则:Notification类负责告警通知,支持电子邮件、短信和微信等多种通知渠道;NotificationEmergencyLevel类表示告警通知的紧急程度,包括SEVERE(严重)、URGENCY(紧急)、NORMAL(普通)和 TRIVIAL(无关紧要),不同的紧急程度对应不同的通知渠道。
public class Alert{private AlertRule rule;private Notifcation notification;public Alert(AlertRule rule, Notification notification){this.rule = rule;this.notifcation = notifcation;}public void check(String api,long requestCount, long errorCount, long duration){long tps=requestCount/duration;if (tps > rule.getMatchedRule(api).getMaxTps()) { notification.notify(NotificationEmergencyLevel.URGENCY,"...");}if(errorCount>rule.getMatchedRule(api).getMaxErrorCount()){notification.notify(NotifcationEmergencyLevel.SEVERE,"...");}}
}
上面这段代码的业务逻辑主要集中在check()函数中。当接口的TPS(Transactions PerSecond,每秒事务数)超过预先设置的最大值时,或者当接口请求出错数大于最大允许值时就会触发告警,通知接口的相关负责人或团队。
如果我们需要添加更多的告警规则:“当每秒接口超时请求个数超过预先设置的最大值时也要触发告警并发送通知”,那么如何改动代码呢?代码的主要改动有两处:第一处是修改check()函数的入参,添加一个新的统计数据 timeoutCount,表示超时接口请求数; 第二处是有check()函数中添加新的告警逻辑。具体的代码改动如下所示。
public class Alert{//...省略AlertRule/Notifcation属性和构造函数...//改动一:添加参数timeoutCountpublic void check(String api, long regueatcount, long erorcount, long timeoutCount, long duration){long tps=reqestCount /duration;if (tps > rule.getMatchedRule(api) .getMaxTps()){notification.notify(NotificationEmergencyLevel.URGENCY, "...");}if(errorCount> rule.getMatchedRule(api).getMaxErrorCount())){notification.notify(NotificationEmergencyLevel.SEVERE, "...");}//改动二:添加接口超时处理逻辑long timeoutTps=timeoutCount /duration;if(timeoutTps > rule.getMatchedRule(api).getMaxTimeoutTps()) {notification.notify(NotificationEmergencyLevel.URGENCY, "...");}}
}
上述代码的改动带来下列两方面的问题。一方面,对接口进行了修改,调用这个接口的代码就要做相应的修改。另一方面,修改了check()函数,相应的单元测试需要修改。
上述代码改动是基于“修改”方式增加新的告警。如果我们遵守开闭原则,也就是“对扩展开放、对修改关闭”,那么如何通过“扩展”方式增加新的告警呢?
我们先重构添加新的告警之前的 Alert类的代码,让它的扩展性更好。重构的内容主要包含两部分:第一部分是将check()函数的多个入参封装成ApiStatInfo类;第二部分是引入handler(告警处理器),将if判断逻辑分散到各个handler 中。具体的代码实现如下。
public class Alert{private list<AlertHandler> alertHandlers = new ArrayList<>();public void addAlertHandler(AlertHandler alertHandler) { this.alertHandlers.add(alertHandler);}public void check(ApiStatInfo apistatInfo){for(AlertHandler handler:alertHandlers){handler.check(apistatInfo);}}public class ApistatInfo{//省略constructor、getter和setter方法private string api;private long requestCount;private long errorCount;private long duration;}public abstract class AlertHandler(protected AlertRule rule;protected Notification notification;public AlertHandler(AlertRule rule, Notifcation notification){this.rule rule;this.notification=notifcation;}public abstract void check(ApiStatInfo apistatInfo) ;}public class TpsAlertHandler extends AlertHandler {public TpsAlertHandler(AlertRule rule, Notification notification){super(rule, notifcation);}}@Overridepublic void check(ApiStatInfo apistatInfo){long tps = apiStatInfo.getReguestcount () / apiStatInfo.getpuration();if (tps > rule.getMatchedRule(apiStatInfo.getApi()).getMaxTps()){ notification.notify(NotifcationEmergencyLevel.URGENCY, "...");}}}public class ErrorAlertHandler extends AlertHandler{public ErrorAlertHandler (AlertRule rule, Notifcation notification){super(rule, notifcation);}@Overridepublic void check (ApiStatInfo apistatInfo) {if (apistatInfo.getErrorCount() > rule.getMatchedRule(apistatInfo.getApi()).getMaxErrorCount()){notifcation.notify(NotifcationEmergencyLevel.SEVERE,"...");}}
}
接下来,我们看一下重构之后的Alent类的具体使用方式,如下列代码所示。其中ApplicationContext是一个单例类,负责Alert类的创建、组装(alertRule和notification 的依赖注入)和初始化(添加handler)。
public class ApplicationContext{private AlertRule alertRule;private Notifcation notification;private Alert alert;public void initializeBeans(){alertRule=new AlertRule(/*.省略参数,*/);//省略一些初始化代码notifcation=new Notification(/*.省略参数.*/);//省略一些初始化代码alert=new Alert();alert.addAlertHandler(new TpsAlertHandler(alertRule,notifcation));alert.addAlertHandler(new ErrorAlertHandler(alertRule, notifcatíon));}public Alert getAlert(){ return alert; )//“饿汉式”单例private static final Applicationcontext instance = new ApplicationContext();private ApplicationContext(){initializeBeans();}public static ApplicationContext getInstance(){return instance;}
}public class Demo{public static void main(string[] args){ApiStatInfo apistatInfo = new ApiStatInfo();//...省略设置apistatInfo数据值的代码Applicationcontext.getinstance().getAlert().check(apistatInfo)}
}
对于重构之后的代码,如果添加新的告警:“如果每秒接口超时请求个数超过最大值,就告警”,那么如何改动代码呢?主要的改动有下面4处。
改动一: 在 ApiStatInfo 类中添加新属性 timeoutCount。
改动二:添加新的 TimeoutAlertHander 类。
改动三:在ApplicationContecxt 类的 initializeBeans()方法中,向 alert对象中注册
Timeout-AlertHandler。
改动四:使用 Alert 类时,需要给check()函数的入参 apiStatInfo对象设置timeoutCount属性值。
改动之后的代码如下所示:
public class Alert{//代码未改动}public class ApistatInfo{//省略constructorgetter和setter方法private String api;private long requegtCount;private long errorCount;private long duratlon; private 1ong timeoutCount;//改动一:添加新属性timeoutcount
}public abstract class AlertHandler{//代码未改动
public class TpsAlertHandler extends AlertHandler{ //代码未改动}
public class ErrorAlertHandler extends AlertHandler { //代码未改动}
//改动二:添加新的TimeoutAlertHander类
public class TimeoutAlertHandler extends AlertHandler {//省略代码public class ApplicationContext{private AlertRule alertRule;private Notification notifcation;private Alert alert;public void initinlizeBeanst{alertRule = new AlertRule(/*.省路参数。*/);//省略一些初始化代码 notification = new Notification(/*.省略参数.*/);//省路一些初始化代码alert = now Alert();alert.addAlertHandler(new psAlertHandler(alertRule, notification ));alert.addAlertHandler(new ErrorAlertHandler(alertRule, notification));//改动三:向alert对象中注册TimeoutAlertHandleralert.addAlertHandler(new imeoutAlertHandler(alertRule, notification));}//...省略其他未改动代码
}public class Demo{public static void main(Stringl] args){ApiStatInfo apiStatInfo = new ApistatInfo();apistatInfo.setTimeoutCount(289);//改动四:设置timeoutCount值//...省路apistatInfo的set字段代码ApplicationContext.getInstance().getAlert().check(apiStatInfo);
}
重构之后的代码更加灵活,更容易扩展。如果想要添加新的告警,那么只需要基于扩展的方式创建新的handler类,不需要改动check()函数。不仅如此,我们只需要为新的handler头添加新的单元测试,旧的单元测试都不会失败,也不用修改。
3.修改代码就意味着违反开闭原则吗
读者可能对上面重构之后的代码产生疑间:在添加新的告警时,尽管改动二(添加新的TimeoutAlertHander类)是基于扩展而非修改的方式完成的,但改动一、改动三和改动四是基于修改而非扩展的方式完成的,改动一、改动三和改动四不违反开闭原则吗?
我们先分析一下改动一: 在 ApiStatInfo类中添加新属性 timeoutCount。
在改动一中,我们不仅在ApiStatInfo的类中添加了新的属性,还添加了对应的getter和setter方法。那么,上述问题就转化为:在类中添加新的属性和方法属于“修改”还是“扩展”?
我们回忆一下开闭原则的定义: 软件实体(模块、类和方法等)应该“对扩展开放、对修改关闭”。从定义中可以看出,开闭原则作用的对象可以是不同粒度的代码,如模块、类和方法(及其属性)。对于同一代码改动,在粗代码粒度下,可以被认定为“修改”,在细代码粒度下,可以被认定为“扩展”。例如,“改动一”中添加属性和方法相当于修改类,在类这个层面,这个代码改动可以被认定为“修改”;但这个代码改动并没有修改已有的属性和方法,在方法(及其属性)这一层面,它又可以被认定为“扩展”。
实际上,我们没有必要纠结某个代码改动是“修改”还是“扩展”,更没有必要纠结它是否违反“开闭原则”。回到开闭原则的设计初衷:只要代码改动没有破坏原有代码的正常运行和原有的单元测试,我们就可以认为这是一个合格的代码改动。
我们再来分析一下改动三和改动四:在ApplicationContext类的initializeBeans()方法中向alert 对象中注册 TimeoutAlertHandler;使用Alert类时,给check()函数的入参 apiStatInfo对象设置 timeoutCount 属性值。
这两处改动是在方法内部进行的,无论从哪个层面(模块、类、方法)来看,都不能算是“扩展”,而是“修改”。不过,有些修改是在所难免的,是可以接受的。
在重构之后的 Alent 类代码中,核心逻辑集中在Alent类及其各个handler类中。当添加的告警时,Alen类完全不需要修改,而只需要扩展(新增)一个handler类。如果把 Alent类及其各个handler 类看作一个“模块”,那么,从模块这个层面来说,向模块添加新功能时只需要扩展,不需要修改,完全满足开闭原则。
我们也要认识到,添加一个新功能时,不可能做到任何模块、类和方法的代码都不“修改”。类需要创建、组装,并且会进行一些初始化操作,这样才能构建可运行的程序,这部分代码的修改在所难免。我们努力的方向是尽量让修改操作集中在上层代码中,尽量让核心、复杂、通用、底层的那部分代码满足开闭原则。
4.如何做到“对扩展开放、对修改关闭”
在上面的 Alert类的例子中,我们通过引入一组 handler 类的方式满足了开闭原则。如果读者没有太多复杂代码的设计和开发经验,就可能有这样的疑问:这样的代码设计思路我怎么想不到呢?你是怎么想到的呢?
实际上,之所以作者能够想到,依靠的是扎实的理论知识和丰富的实战经验,这需要读者慢慢学习和积累。对于如何做到“对扩展开放、对修改关闭”,作者有一些指导思想和具体方法分享给读者。
实际上,开闭原则涉及的就是代码的扩展性问题,该原则是判断一段代码是否易扩展的“金标准”。如果某段代码在应对未来需求变化时,能够到“对扩展开放、对修改关闭”,就说明这段代码的扩展性很好。
为了写出扩展性好的代码,我们需要具备扩展意识、抽象意识和封装意识。这些意识可能比任何开发技巧都重要。
在编写代码时,我们需要多花点时间思考: 对于当前这段代码,未来可能有哪些需求变更。如何设计代码结构,事先预留了扩展点,在未来进行需求变更时,不需要改动代码的整体结构,新的代码能够灵活地插入到扩展点上,完成需求变更,从而实现代码的最小化改动。
我们还要善于识别代码中的可变部分和不可变部分。我们将可变部分封装,达到隔离变化的效果,并提供抽象化的不可变接口给上层系统使用。当具体的实现发生变化时,只需要基于相同的抽象接口扩展一个新的实现,替换旧的实现,上层系统的代码几乎不需要修改。
为了实现开闭原则,除在写代设时,我们需要时间具备扩展意识、抽象意识、封装意识以外,我们还有一些具体的方法可以使用。
代码的扩展性是评判代码质量的重要标准。实际上,本文涉及的大部分知识点都是围绕如何提高高代码的扩展性来展开讲解的,本文提到的大部分设计原则和设计模式都是以提高代码的扩展性为最终目的。22种经典设计模式中的大部分都是为了解决代码的扩展性问题而总结出来的,都是以开闭原则为指导原则而设计的。
有众多的设计原则和设计模式中,常用来提高代码扩展性的方法包括多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(如策略模式、模板方法模式和职责链模式等)设计模式这一部分的内容较多,后面也会详细讲解。本节通过一个简单例子来介绍如何利用多态、依赖注入、基于接口而非实现编程实现开闭原则。
例如,我们希望实现通过Kafka发送异步消息。对于这样一个功能的开发,我们抽象定义一组与具体消息队列(Kafka)无关的异步消息发送接口。所有上层系统都依赖这组抽象的接口编程,并且通过依赖注入的方式来调用。当需要替换消息队列或消息格式时,如将Kafka替换成RocketMQ或将消息的格式从JSON替换为XML,因为代码设计满足开闭原则,所以替换起来非常轻松。具体的代码实现如下所示。
//这一部分代码体现了抽象意识
public interface MessageQueue(...)
public class KafkaMessageQueue implements MessageQueue {...)
public class RocketMQMessageQueue implements MessageQueue (...)public interface MessageFromatter{...}
public class JsonMessagerromatter implements MessageFromatter {..)
public class ProtoBufMessageFromatter implements MessageFromatter { ... }public class Demo(private MessageQueue msgQueue;//基于接口而非实现编程public Demo(MessageQueue msgQueue){ //依赖注入this.msqQueue = msgQueue;}public void send (Notification notification, Messageformatter msgforatter) {...}
}
5.如何在项目中灵活应用开闭原则
上文提到,写出支持开闭原则(扩展性好)的代码的关键是预留扩展点。如何才能识别出有可能的扩展点呢?
如果我们开发的是业务系统,如金融系统、电商系统和物流系统等,要想识别出尽可能多的扩展点,就要对业务有足够的了解。只有这样,才能预见未来可能要支持的业务需求。如要我们开发的是与业务无关的、通用的、偏底层的功能模块,如框架、组件和类库,如果想设出尽可能多的扩展点,就需要了解它们会被如何使用和使用者未来会有哪些功能需求等。
但是,即使我们对业务和系统有足够的了解,也不可能识别出所有的扩展点。即便我们的够识别出所有的扩展点,但为了预留所有扩展点而付出的开发成本往往是不可接受的。因此我们没必要为一些未来不一定需要实现的需求提前“买单”,也就是说,不要进行过度设计。
推荐的做法是,对于一些短期内可能进行的扩展,需求改动对代码结构影响比较大的扩展,或者实现成本不高的扩展,在编写代码时,我们可以事先进行可扩展性设计;但对于一些不确定未来是否要支持的需求,或者实现起来比较复杂的扩展,我们可以等到有需求驱动时,再通过重构的方式来满足扩展的需求。
除此之外,我们还要认识到,开闭原则并不是“免费”的。代码的扩展性往往与代码的可读性冲突。例如上文提供的 Alert类的例子,为了更好地支持扩展性,我们对代码进行了重构,重构之后的代码比原始代码复杂很多,理解难度也增加不少。因此,在平时的开发中,我们需要权衡代码的扩展性和可读性。在一些场景下,代码的扩展性更重要,我们就适当地“牺牲”一些代码的可读性;在一些场景下,代码的可读性更重要,我们就适当地“牺牲”一些代码的扩展性。
在上文提到的 Alert类的例子中,如果告警规则不是很多,也不复杂,那么check()函数中的if分支就不会有很多,对应的代码逻辑不会太复杂,代码行数也不会太多,因此,使用最初的代码实现即可。相反,如果告警规则多且复杂,那么check()函数中的if分支就会有很多对应的代码逻辑就会变复杂,代码行数也会增加,check()函数的可维护性和扩展性就会变差此时,重构代码就变得合理了。
6.总结
开闭原则是面向对象设计中最基础的设计原则之一。其核心思想是:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。这意味着,当应用的需求改变时,在不修改软件实体的源代码或二进制代码的前提下,可以拓展模块的功能,使其满足需求。换句话说,强调的是用抽象构建框架,用实现扩展细节,从而提高软件系统的可复用性及可维护性。
开闭原则并不是要求所有代码都不能修改,而是要求将变化的部分尽可能地封装和抽象出来。通过遵循这一原则,开发者可以构建出更加稳定、灵活且易于维护的软件系统。
相关文章:
面向对象设计之开闭原则
设计模式专栏: http://t.csdnimg.cn/4Mt4u 目录 1.引言 2.如何理解“对扩展开放、对修改关闭” 3.修改代码就意味着违反开闭原则吗 4.如何做到“对扩展开放、对修改关闭” 5.如何在项目中灵活应用开闭原则 6.总结 1.引言 开闭原则(Open Closed Principle&…...
【项目技术介绍篇】若依项目代码文件结构介绍
作者介绍:本人笔名姑苏老陈,从事JAVA开发工作十多年了,带过大学刚毕业的实习生,也带过技术团队。最近有个朋友的表弟,马上要大学毕业了,想从事JAVA开发工作,但不知道从何处入手。于是࿰…...
实现DevOps需要什么?
实现DevOps需要什么? 硬性要求:工具上的准备 上文提到了工具链的打通,那么工具自然就需要做好准备。现将工具类型及对应的不完全列举整理如下: 代码管理(SCM):GitHub、GitLab、BitBucket、SubV…...
Linux小程序: 手写自己的shell
注意: 本文章只是为了理解shell内部的工作原理, 所以并没有完成shell的所有工作, 只是完成了shell里的一小部分工作 #include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include &l…...
javaSwing租户管理系统
简介 欢迎阅读本篇博客,今天我将为大家介绍一个基于Java Swing开发的租户管理系统。该系统具有登录、注册、添加租户、查询租户信息、修改租户信息、删除租户、修改密码、退出登录等功能模块,旨在提供一个便捷的租户管理解决方案。 一、项目介绍 该租…...
cesium实现竖立的圆
cesium中的圆是平行于地面的,想实现竖起来的圆可以使用ellipsoid,设置其中一个轴的radii值为一个很小的值,比如0.00001,则这个轴上的宽度就会非常小,看起来就是一个圆面。 一、画圆ellipse,此处也把画圆的代…...
汽车电子行业知识:智能汽车电子架构
文章目录 3.智能汽车电子架构3.1.汽车电子概念及发展3.2.汽车电子架构类型3.2.1.博世汽车电子架构3.2.2.联合电子未来汽车电子架构3.2.3.安波福汽车电子架构3.2.4.丰田汽车电子架构3.2.5.华为汽车电子架构 3.智能汽车电子架构 3.1.汽车电子概念及发展 汽车电子是车体汽车电子…...
LeetCode146:LRU缓存
leetCode:146. LRU 缓存 题目描述 请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类: LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存 int get(int key) 如果关键字 key 存在于缓存中&#x…...
【Unity音游制作】你玩过节奏大师吗?(Koreographe插件导入游戏主体)【一】
👨💻个人主页:元宇宙-秩沅 👨💻 hallo 欢迎 点赞👍 收藏⭐ 留言📝 加关注✅! 👨💻 本文由 秩沅 原创 👨💻 收录于专栏:Uni…...
高效解决Ubuntu Server 18.04.1 LTS 64bit更新gdb8.1.1到gdb12.1
文章目录 问题解决步骤 问题 因为需要用到gdb一些指令,但是gdb8.x好像存在普遍的问题,实现不了某些指令,比方说set detach-on-fork on,升级版本也没有比较好的教程 经过我不断的试错,我终于升级成功了!&a…...
【公示】2023年度青岛市级科技企业孵化器拟认定名单
根据《青岛市科技企业孵化器管理办法》(青科规〔2023〕1号)(以下简称《管理办法》)、《关于开展2023年度市级科技企业孵化器认定申报工作的通知》,经申报受理、区市推荐、形式审查、专家评审及现场核查等程序ÿ…...
【软件安装】(十四)Ubuntu22.04安装Psensor硬件监视器
一个愿意伫立在巨人肩膀上的农民...... Ubuntu系统硬件运行查询输入指令太繁琐,终端展示不直观,因此这款具有可视化监控Ubuntu系统下当前电脑的硬件CPU(中央处理器)、GPU(显卡)和硬盘等温度等功能ÿ…...
数组合并小程序
题目: 输入有序数组a, b, 不使用排序算法,及额外数组,按大小顺序合并a, b数组,元素不重复; 思路: 1. 如果比插入的数组大,那么往后插入,如果继续有大的,就移动位置插入…...
python练习二
# Demo85def pai_xu(ls_test):#创建一个列表排序函数命名为pai_xu# 对创建的函数进行注释"""这是一个关于列表正序/倒序排列的函数:param ls_test: 需要排序的列表:return:"""ls1 [int(ls_test[i]) for i in range(len(ls_test))]#对input输入的…...
专升本-数字媒体
数字媒体 概念: 媒体:是信息的载体,传播信息的媒介,能为信息的传播提供平台 数字媒体:多重媒体,使用文字,数据,图像,声音等各种媒体 数字媒体技术:利用计…...
蓝桥杯算法题-发现环
问题描述 小明的实验室有N台电脑,编号1~N。原本这N台电脑之间有N-1条数据链接相连,恰好构成一个树形网络。在树形网络上,任意两台电脑之间有唯一的路径相连。 不过在最近一次维护网络时,管理员误操作使得某两台电脑之间增…...
Oracle存数字精度问题number、binary_double、binary_float类型
--表1 score是number(10,5)类型 create table TEST1 (score number(10,5) ); --表2 score是binary_double类型 create table TEST2 (score binary_double ); --表3 score是binary_float类型 create table TEST3 (score binary_float );实验一:分别往三张表插入 小数…...
Java封装最佳实践:打造高内聚、低耦合的优雅代码~
个人主页:秋风起,再归来~ 文章专栏:javaSE的修炼之路 个人格言:悟已往之不谏,知来者犹可追 克心守己,律己则安! 1、封装 1.1 封装的概念 面向对象程序三大…...
开源,微信小程序-超级计算器T3000 简介
笔者于四年前自学微信小程序开发,这个超级计算器T3000就是当时的练习作品。超级计算器T3000的功能有很多,其中的核心技术是矩阵计算,使用的工具库是math.js,其次是复杂运算和分式运算。关于math.js的使用,可以参考另一…...
Dimitra:基于区块链、AI 等前沿技术重塑传统农业
根据 2023 年联合国粮食及农业组织(FAO)、国际农业发展基金(IFAD)等组织联合发布的《世界粮食安全和营养状况》报告显示,目前全球约有 7.35 亿饥饿人口,远高于 2019 年的 6.13 亿,这意味着农业仍…...
web vue 项目 Docker化部署
Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段: 构建阶段(Build Stage):…...
在软件开发中正确使用MySQL日期时间类型的深度解析
在日常软件开发场景中,时间信息的存储是底层且核心的需求。从金融交易的精确记账时间、用户操作的行为日志,到供应链系统的物流节点时间戳,时间数据的准确性直接决定业务逻辑的可靠性。MySQL作为主流关系型数据库,其日期时间类型的…...
调用支付宝接口响应40004 SYSTEM_ERROR问题排查
在对接支付宝API的时候,遇到了一些问题,记录一下排查过程。 Body:{"datadigital_fincloud_generalsaas_face_certify_initialize_response":{"msg":"Business Failed","code":"40004","sub_msg…...
Python爬虫(二):爬虫完整流程
爬虫完整流程详解(7大核心步骤实战技巧) 一、爬虫完整工作流程 以下是爬虫开发的完整流程,我将结合具体技术点和实战经验展开说明: 1. 目标分析与前期准备 网站技术分析: 使用浏览器开发者工具(F12&…...
MySQL 8.0 OCP 英文题库解析(十三)
Oracle 为庆祝 MySQL 30 周年,截止到 2025.07.31 之前。所有人均可以免费考取原价245美元的MySQL OCP 认证。 从今天开始,将英文题库免费公布出来,并进行解析,帮助大家在一个月之内轻松通过OCP认证。 本期公布试题111~120 试题1…...
佰力博科技与您探讨热释电测量的几种方法
热释电的测量主要涉及热释电系数的测定,这是表征热释电材料性能的重要参数。热释电系数的测量方法主要包括静态法、动态法和积分电荷法。其中,积分电荷法最为常用,其原理是通过测量在电容器上积累的热释电电荷,从而确定热释电系数…...
LINUX 69 FTP 客服管理系统 man 5 /etc/vsftpd/vsftpd.conf
FTP 客服管理系统 实现kefu123登录,不允许匿名访问,kefu只能访问/data/kefu目录,不能查看其他目录 创建账号密码 useradd kefu echo 123|passwd -stdin kefu [rootcode caozx26420]# echo 123|passwd --stdin kefu 更改用户 kefu 的密码…...
在树莓派上添加音频输入设备的几种方法
在树莓派上添加音频输入设备可以通过以下步骤完成,具体方法取决于设备类型(如USB麦克风、3.5mm接口麦克风或HDMI音频输入)。以下是详细指南: 1. 连接音频输入设备 USB麦克风/声卡:直接插入树莓派的USB接口。3.5mm麦克…...
LCTF液晶可调谐滤波器在多光谱相机捕捉无人机目标检测中的作用
中达瑞和自2005年成立以来,一直在光谱成像领域深度钻研和发展,始终致力于研发高性能、高可靠性的光谱成像相机,为科研院校提供更优的产品和服务。在《低空背景下无人机目标的光谱特征研究及目标检测应用》这篇论文中提到中达瑞和 LCTF 作为多…...
DBLP数据库是什么?
DBLP(Digital Bibliography & Library Project)Computer Science Bibliography是全球著名的计算机科学出版物的开放书目数据库。DBLP所收录的期刊和会议论文质量较高,数据库文献更新速度很快,很好地反映了国际计算机科学学术研…...
