当前位置: 首页 > news >正文

【读书笔记】架构整洁之道 P6 实现细节

第6部分 实现细节

第30章 数据库只是实现细节

从系统架构的角度来看,数据库并不重要——它只是一个实现细节,在系统架构中并不占据重要角色。

关系型数据库

但不管关系型数据库的设计有多么有智慧,多么精巧,多么符合数学原理,它仍然也只是一种技术。换句话说,它终究只是一种实现细节。

虽然关系型数据的表模型设计对某一类数据访问需要来说可能很方便,但是把数据按行组织成表结构本身并没有什么系统架构意义上的重要性。应用程序的用例不应该知道,也不应该关心这么低层次的实现细节,需要了解数据表结构的代码应该被局限在系统架构的最外圈、最低层的工具函数中。

很多数据访问框架允许将数据行和数据表以对象的形式在系统内部传递。这么做在系统架构上来说是完全错误的,这会导致程序的用例、业务逻辑、甚至UI与数据的关系模型相互绑定在一起。

为什么数据库系统如此流行

为什么数据库系统在软件系统和企业软件领域如此流行?Oracle、MySQL和SQL Server这些产品广泛流行的原因是什么?答案是硬盘。

以磁感应方式读取数据的硬盘在过去五十年成为数据存储的主流手段,以至于最近几代软件工程师对其他类型的数据存储几乎一无所知。但是在硬盘的整个发展过程中,程序员们始终被一个限制困扰着:磁盘的访问速度太慢了!

在磁盘上,数据是按照环形轨道存储的。这些轨道又会进一步被划分成一系列扇区,这些扇区的大小通常是4 KB。而每个盘片上都有几百条轨道,整个硬盘可能由十几个盘片组成。如果要从硬盘上读取某一个特定字节,需要将磁头挪到正确的轨道上,等待盘片旋转到正确的位置上,再将整个扇区读入内存中,从内存中查询对应的字节。这些过程当然需要时间,所以硬盘的访问速度一般在毫秒级。

为了应对硬盘访问速度带来的限制,必须使用索引、缓存以及查询优化器等技术。同时,我们还需要一种数据的标准展现格式,以便让索引、缓存及查询优化器来使用。概括来说,我们需要的就是某种数据访问与管理系统。过去几十年内,业界逐渐发展出了两种截然不同的系统:文件系统与关系型数据库系统(RDBMS)。

  • 文件系统是基于文档格式的,它提供的是一种便于存储整个文档的方式。当需要按照名字存储数据和查找一系列文档时,文件系统很有用,但当我们需要检索文档内容时,它就没那么有用了。
  • 数据库系统则主要关注的是内容,它提供的是一种便于进行内容检索的存储方式。其最擅长的是根据某些共同属性而检索一系列记录。然而,它对存储和访问内容不透明的文档的支持就没那么强了。

假设磁盘不存在会怎样

虽然硬盘现在还是很常见,但其实已经在走下坡路了。很快它们就会和磁带、软盘、CD一样成为历史,RAM正在替代一切。

如果所有的数据都存在内存中,应该如何组织它们呢?需要按表格存储并且用SQL查询吗?需要用文件形式存储,然后按目录查找吗?

当然不,我们会将数据存储为链表、树、哈希表、堆栈、队列等各种各样的数据结构,然后用指针或者引用来访问这些数据。

实现细节

上面所说的,就是为什么我们认为数据库只是一种实现细节的原因。数据库终究只是在硬盘与内存之间相互传输数据的一种手段而已,它真的可以被认为只是一个长期存储数据的、装满字节的大桶。我们通常并不会真的以这种形式来使用数据。

但性能怎么办呢

性能难道不是系统架构的一个考量标准吗?当然是——但当问题涉及数据存储时,这方面的操作通常是被封装起来,隔离在业务逻辑之外的。也就是说,我们确实需要从数据存储中快速地存取数据,但这终究只是一个底层实现问题。我们完全可以在数据访问这一较低的层面上解决这个问题,而不需要让它与系统架构相关联。

第31章 Web是实现细节

Web技术事实上并没有改变任何东西,或者说它也没有能力改变任何东西。这一次Web热潮只是软件行业从1960年来经历的数次振荡中的一次。这些振荡一会儿将全部计算资源集中在中央服务器上,一会儿又将计算资源分散到各个终端上。

一开始我们以为计算资源应该集中在服务器集群中,浏览器应该保持简单。但随后我们又开始在浏览器中引入Applets。再后来我们又改了主意,发明了Web 2.0,用Ajax和JavaScript将很多计算过程挪回浏览器中。我们先是非常兴奋地将整个应用程序挪到浏览器去执行,后来又非常开心地采用Node技术将那些JavaScript代码挪回服务器上执行。

第32章 应用程序框架是实现细节

单向婚姻

我们与框架作者之间的关系是非常不对等的。我们要采用某个框架就意味着自己要遵守一大堆约定,但框架作者却完全不需要为我们遵守什么约定。

请仔细想想这一关系,当我们决定采用一个框架时,就需要完整地阅读框架作者提供的文档。在这个文档中,框架作者和框架其他用户对我们提出进行应用整合的一些建议。一般来说,这些建议就是在要求我们围绕着该框架来设计自己的系统架构。譬如,框架作者会建议我们基于框架中的基类来创建一些派生类,并在业务对象中引入一些框架的工具。框架作者还会不停地催促我们将应用与框架结合得越紧密越好。

风险

框架自身的架构设计很多时候并不是特别正确的。框架本身可能经常违反依赖关系原则。譬如,框架可能会要求我们将代码引入到业务对象中——甚至是业务实体中。框架可能会想要我们将框架耦合在最内圈代码中。

框架可能会帮助我们实现一些应用程序的早期功能,但随着产品的成熟,功能要求很可能超出框架所能提供的范围。而且随着时间的推移,我们也会发现在应用的开发过程中,自己与框架斗争的时间要比框架帮助我们的时间长得多。

框架本身可能朝着我们不需要的方向演进。也许我们会被迫升级到一个并不需要的新版本,甚至会发现自己之前所使用的旧功能突然消失了,或悄悄改变了行为。

未来我们可能会想要切换到一个更新、更好的框架上。

解决方案

我们可以使用框架——但要时刻警惕,别被它拖住。我们应该将框架作为架构最外圈的一个实现细节来使用,不要让它们进入内圈。

**如果框架要求我们根据它们的基类来创建派生类,就请不要这样做!**我们可以创造一些代理类,同时把这些代理类当作业务逻辑的插件来管理。

以Spring为例,它作为一个依赖注入框架是不错的,也许我们会需要用Spring来自动连接应用程序中的各种依赖关系。这不要紧,但是千万别在业务对象里到处写@autowired注解。业务对象应该对Spring完全不知情才对

反之,我们也可以利用Spring将依赖关系注入到Main组件中,毕竟Main组件作为系统架构中最低层、依赖最多的组件,它依赖于Spring并不是问题。

不得不接受的依赖

有一些框架是避免不了使用的。例如,如果你在用C++,那么STL就是很难避免使用的。如果你在用Java,那么标准类库也是不太可能避免使用的。

第33章 案例分析:视频销售网站

产品

在这个案例分析中,我要讲的是一个我自己很熟悉的产品:线上收费视频网站。当然,这个有点像cleancoders.com,我在这个网站上出售我的软件开发教程视频。

这个案例的设计很简单,就是我们打算向一些个人或者企业提供一批收费的线上教学视频。个人用户既可以选择在线支付之后直接在线观看视频,也可以选择付一笔更高的费用将视频下载到本地,永久地拥有它们。而企业用户就只能在线播放,但他们可以选择批量购买,以此来获得一定折扣。

  • 个人用户通常既是购买者又是观看者。而企业用户则不同,他们购买视频通常是用来给其他人观看的。
  • 视频作者需要负责上传视频文件、写简介,并且提供视频附带的一系列习题、课后作业、答案、源代码以及其他各类资料。
  • 管理员需要负责增加新的视频播放列表,往视频播放列表里添加和删除视频,并且为各种许可类型设置价格。

系统架构设计中的第一步,是识别系统中的各种角色和用例。

用例分析

下面,我们通过图33.1来示范一次典型的用例分析。

图33.1:典型的用例分析

如你所见,图中显然存在着四个角色。根据单一职责原则(SRP),这四个角色将成为系统变更的主要驱动力。每当添加新功能,或者修改现有功能时,我们所做的一切都是在为这些角色服务。所以我们希望能够对系统进行分区处理,避免其中一个角色的变更需求影响其他角色。

另外,图33.1中的用例并不是一个完整的列表。例如,这里没有分析用于执行登录、注销的用例。

读者应该注意到图33.1中还有一些用虚线框起来的用例。我们称之为抽象用例[2],它们通常用来负责设置通用策略,然后交由其他具体用例来使用。

组件架构

既然我们弄清楚了系统中的各种角色和用例,接下来就可以构造一个初步的组件架构图了(如图33.2所示)。

图33.2:初步的组件架构图

在该图中,双实线代表了系统架构边界。可以看到这里将系统划分成视图、展示器、交互器以及控制器这几个组件,同时也按照对应的系统角色进行了分组。

值得注意的是,这里有两个特殊的组件:目录视图(Catalog View)和目录展示器(Catalog Presenter)。这就是我应对查看目录列表这个抽象用例的方法。我假设这些视图和展示器将会被编写为抽象类,而继承它们的组件将会包括它们的派生类。

但问题是,我们真的需要将系统拆分成这么多组件,然后以.jar或.dll文件的形式一个个交付吗?是,又不全是。我们确实要按照组件将编译和构建环境分开,以便单独构建对应的组件。但我们仍然可以考虑将所有的交付单元组合起来交付。例如,根据图33.2中的分组,我们可以很简单地将它们交付为5个.jar文件——视图、展示器、交互器、控制器和工具类,这样就可以分别单独部署这些被修改的组件了。

除此之外,还有另一种分组方式,就是将视图和展示器放在同一个.jar文件中,而将交互器、控制器以及工具类各自放在独立的.jar文件中。还有一种更简单的方式,就是将视图和展示器放在一个.jar文件中,而将其他所有的组件合并为另一个.jar文件。

依赖关系管理

如你所见,图33.2中的控制流是从右向左的。输入发生在控制器端,然后输入的数据经交互器处理后交由展示器格式化出结果,最后由视图来展示这个结果。

请注意,图中的箭头并不是一直从右向左的。事实上大部分的箭头都是从左向右的。这是因为该架构设计要遵守依赖关系原则。所有跨越边界的依赖关系都应该是同一个方向,而且都指向包含更高级策略的组件。

另外,还应该注意一下图中的“使用”关系(开放箭头),它和控制流方向是一致的;而“继承”关系(闭合箭头)则与之相反,它反映的是我们对开闭原则的应用,通过调整依赖关系,可以保证底层细节的变更不会影响到高层策略组件。

第34章 拾遗

下面我们再来看一个例子,假设正在构建一个在线书店,这个例子的任务是实现一个客户查看订单状态的用例。虽然这是一个Java程序的示例,但其所示范的原理适用于任何语言。现在,让我们暂时将整洁架构的概念放在一边,先来看一下如何具体安排代码设计和代码结构。

按层封装

我们首先想到的,也可能是最简单的设计方式,就是传统的水平分层架构。在这个架构里,我们将代码从技术角度进行分类。这通常被称为“按层封装”。图34.1用UML类图展示了这种设计。

在这种常见的分层架构中,Web代码分为一层,业务逻辑分为一层,持久化是另外一层。换句话说,我们对代码进行了水平分层,相同类型的代码在一层。在“严格的分层架构”中,每一层只能对相邻的下层有依赖关系。在Java中,分层的概念通常是用包来表示的。如图34.1所示,所有的分层(包)之间的依赖关系都是指向下的。这里包括了以下Java类。

  • OrdersController:Web控制器,类似Spring MVC控制器,负责处理Web请求。
  • OrderService:定义订单相关业务逻辑的接口。
  • OrderServiceImpl:Order服务的具体实现[3]。
  • OrdersRepository:定义如何访问订单持久信息的接口。
  • JdbcOrderRepository:持久信息访问接口的实现。

图34.1:按层封装

这种方式在项目初期之所以会很合适,是因为它不会过于复杂。但就像Martin指出的那样,一旦软件规模扩展了,我们很快就会发现将代码分为三大块并不够,需要进一步进行模块化。

如Bob所说,这里还存在另外一个问题是,分层架构无法展现具体的业务领域信息。把两个不同业务领域的、但是都采用了分层架构的代码进行对比,你会发现它们的相似程度极高:都有Web层、服务层和数据仓库层。这是分层架构的另外一个问题,后文会具体讲述。

按功能封装

另外一种组织代码的形式是“按功能封装”,即垂直切分,根据相关的功能、业务概念或者聚合根(领域驱动设计原则中的术语)来切分。在常见的实现中,所有的类型都会放在一个相同的包中,以业务概念来命名。

图34.2展示了这种方式,类和接口与之前类似,但是相比之前,这次它们都被放到了同一个Java包中。相比“按层封装”,这只是一个小变化,但是现在顶层代码结构至少与业务领域有点相关了。我们可以看到这段代码是与订单有关的,而不是只能看到Web、服务及数据访问。另外一个好处是,如果需要修改“查看订单”这个业务用例,比较容易找到相关代码,毕竟它们都在一个包中,而不是分散在各处。[4]

软件研发团队常常一开始采用水平分层方式(即“按层封装”),遇到困难后再切换到垂直分层方式(即“按功能封装”)。我认为,两种方式都很不好。看完本书,你应该意识到还有更好的分类方式——没错。

图34.2:按功能封装

端口和适配器

如Bob大叔所说,通过采用“端口和适配器”“六边形架构”“边界、控制器、实体”等,我们可以创造出一个业务领域代码与具体实现细节(数据库、框架等)隔离的架构。总结下来,如图34.3所示,我们可以区分出代码中的内部代码(领域,Domain)与外部代码(基础设施,Infrastructure)。

图34.3:区分内部代码和外部代码

内部区域包含了所有的领域概念,而外部区域则包含了与外界交互的部分(例如UI、数据库、第三方集成等)。这里主要的规则是,只有外部代码能依赖内部代码,反之则不能。图34.4展示了“查看订单”这个业务用例是如何用这种方式实现的。

这里com.mycompnay.myapp.domain包是内部代码,另外一个包是外部代码。注意这里的依赖关系是由外向内的。眼尖的读者可以注意到之前的OrderRepository类现在被改名为Orders。这个概念基于领域驱动设计理念,其中要求内部代码都应该用独特的领域语言来描述。换句话说,我们在业务领域里面讨论的应该是“Orders”,而不是“OrdersRepository”。

图34.4:“查看订单”业务用例

值得注意的是,这里是UML类图的一个简化版,这里缺少了交互器,以及跨边界调用时对应的数据编码解码对象。

按组件封装

虽然我对本书中的SOLID、REP、CCP、CRP以及其他大部分建议完全认同,我想提出对代码组织方式的一个不同看法——“按组件封装”。

我已经给出一些分层架构不好的理由,但这还不是全部。分层架构设计的目的是将功能相似的代码进行分组。处理Web的代码应该与处理业务逻辑的代码分开,同时也与处理数据访问的代码分开。正如我们在UML类图中所见,从实现角度讲,层就是代表了Java包。从代码可访问性角度来讲,如果需要OrdersController依赖OrderService接口,那么这个接口必须设置为public,因为它们在不同的包中。同样的,OrdersRepository接口也需要设置为public,这样才能被包外的类OrdersServiceImple使用。

在严格分层的架构中,依赖指向的箭头应该永远向下,每一层只能依赖相邻的下一层。通过引入一些代码互相依赖的规则,我们就形成了一个干净、漂亮的单向依赖图。这里有一个大问题——只要通过引入一些不应该有的依赖来作弊,依然可以形成漂亮的单向依赖图

假设新员工加入了团队,你给新人安排了一个订单相关的业务用例的实现任务。由于这个人刚刚入职,他想好好表现,尽快完成这项功能。粗略看过代码之后,新人发现了OrdersController这个类,于是他将新的订单相关的Web代码都塞了进去。但是这段代码需要从数据库查找一些订单数据。这时候这个新人灵机一动:“代码已经有了一个OrdersRepository接口,只需要将它用依赖注入框架引入控制器就行,我真机智!”几分钟之后,功能已经正常了,但是UML结构图变成了图34.5这样。

图34.5:宽松的分层架构

依赖关系箭头依然向下,但是现在OrdersController在某些情况下绕过了OrderService类。这种组织形式被称为宽松的分层架构,允许某些层跳过直接相邻的邻居。在有些情况下,这是意料之中的——例如,如果我们在遵循CQRS设计模式[6](在命令查询责任分离设计模式中,更新和读取数据的模式是不同的),这是合理的。但是更多的情况下,绕过业务逻辑层是不合理的,尤其是在业务逻辑层要控制权限的情况下。

这里我们有的其实只是一个规范——一个架构设计原则——内容是“Web控制器永远不应该直接访问数据层”。

那么,看一下“按组件封装”的做法。这种做法混合了我们之前讲的所有的方法,目标是将一个粗粒度组件相关的所有类放入一个Java包中。这就像是以一种面向服务的视角来构建软件系统,与微服务架构类似。这里,就像端口和适配器模式将Web视为一种交付手段一样,“按组件封装”将UI与粗粒度组件分离。图34.6展示了“查看订单”这个用例的设计图。

图34.6:“查看订单”业务用例

总的来说,这种方式将“业务逻辑”与“持久化代码”合并在一起,称为“组件”,Bob大叔在本书中对“组件”的定义如下:组件是部署单元。组件是系统中能够部署的最小单位,对应在Java里就是jar文件。

我对组件的定义稍有不同:“在一个执行环境(应用程序)中的、一个干净、良好的接口背后的一系列相关功能的集合”。这个定义来自我的“C4软件架构模型”[8](https://c4model.com/)。这个模型以一种层级模型讨论软件系统的静态结构,其中的概念包括容器、组件、类。这个模型认为,系统由一个或者多个容器组成(例如Web应用、移动App、独立应用、数据库、文件系统),每个容器包含一个或多个组件,每个组件由一个或多个类组成。每个组件具体存在于哪个jar文件中则是另外一个维度的事情。

The C4 model for visualising software architecture

这种“按组件封装”的方式的一个好处是,如果我们需要编写和订单有关的代码,只有一个位置需要修改——OrdersComponet。在这个组件中,仍然应该关注重点隔离原则,但这是组件内部问题,使用者不需要关心。这就有点像采用微服务架构,或者是面向服务架构的结果——独立的OrderService会将所有订单相关的东西封装起来。这里关键的区别是解耦的方式。我们可以认为,单体程序中的一个良好定义的组件,是微服务化架构的一个前提条件。

具体实现细节中的陷阱

我经常遇到的一个问题是,Java中public访问控制修饰符的滥用。我们作为程序员,好像天生就喜欢使用public关键词。这就好像是肌肉记忆一样。如果不信,请看一下各种书籍的代码示范、各种入门教程,以及GitHub上的开源框架。这个趋势是显而易见的,不管采用了哪种系统架构风格。

将所有的类都设置为public意味着就无法利用编程语言提供的封装手段。这样一来,没有任何东西可以阻碍某人写一段直接初始化具体实现类的代码,哪怕它违反了架构设计的要求。

组织形式与封装的区别

从另外一个角度来看,如果我们将Java程序中的所有类型都设置为public,那么包就仅仅是一种组织形式了(类似文件夹一样的分组方式),而不是一种封装方式。由于public类型可以在代码库的任何位置调用,我们事实上就可以忽略包的概念,因为它并不提供什么价值。最终,如果忽视包的概念(因为并不起到任何封装和隐藏的功能),那么想要采用的任何架构风格就都不重要了。我们回过头来看一下例子中的UML图,如果所有的类型都是public,那么Java包就成了一个无关紧要的细节信息。于是,所有四种架构方式事实上并没有任何区别(参见图34.7)。

我们再详细看一下图34.7中各个类之间的箭头:不论采用哪种架构设计风格,它们的指向都是一致的。虽然概念不同,但是语法上都是一致的。更进一步说,如果所有的类都是public的,那么其实我们就是在用四种不同的方式描述一个传统的分层架构设计方式。你会说当然没有人会将所有的Java类都设置为public,但是相信我,我见过。

图34.7:四种系统架构设计风格其实是等同的

虽然Java中的访问修饰符并不完美[9](例如在Java中,虽然我们倾向于认为包是具有层级关系的,但是其实我们并不能控制包和子包之间的访问关系。任何层级关系仅仅在磁盘目录结构上有意义。),但是忽略它们的存在就是在自找麻烦。Java类与包的组织形式其实可以很大程度决定这个类的可访问性(或者不可访问性)。如果我们将包的概念引入这幅图,同时标记(虚化的形式展示)应用到访问控制符的地方,这个图就很有意思了(参见图34.8)。

从左向右,在“按层封装”方式中,OrderService与OrderRepository需要 public 修饰符,因为包外的类需要依赖它们。然而,具体实现类(OrderServiceImpl和JdbcOrdersRepository)则可以设置更细致的访问权限(包范围内的protected)。不需要有人依赖它们,它们是具体的实现细节。

图34.8:带有访问修饰符的类型被虚化了

在“按功能封装”模式中,OrdersController是整个包的入口,所以其他的类都可以设置为包范围内的protected。这里的一个问题是,代码库中的其他代码都必须通过控制器才能访问订单信息——这可能是好处,也可能是坏处,视实际情况而定。

在端口与适配器模式中,OrderService与Orders接口都有来自包外的依赖关系,所以需要public修饰符。同样,实现类可以设置为包范围内protected,依赖在运行时注入。

最后,在“组件”封装模式中,OrdersComponet接口有来自Controller的依赖关系,但是其他类都可以设置为包protected。Public类型越少,潜在的依赖关系就越少。现在包外代码就不能再直接使用OrdersRepository接口或者其对应的实现[10],我们就可以利用编译器来维护架构设计原则了。

再澄清一点,这里描述的全都和单体程序有关,所有代码都存放在同一个代码树下。如果你在构建这种程序(大部分程序都是如此),那么我强烈建议利用编译器来维护架构设计原理,而不要依赖个人自律和编译过程之后的工具。

其他的解耦合模式

除编程语言自带的工具之外,通常还有其他方式可以进一步解耦源代码级别的依赖关系。在Java语言中,有模块化框架OSGi,以及最新的Java 9模块系统。正确利用模块系统,我们可以进一步区分public类型和对外发布的类型。例如,我们可以创建一个Orders模块,将所有的类型标记为public,但仅仅公布一小部分类供外部调用。

另外一个选择是将代码分散到不同的代码树中,以从源代码级别解耦依赖关系。以端口和适配器方式为例,我们会有三个代码树:

  • 业务代码(所有技术和框架无关的代码):OrdersService、OrderServiceImpl以及Orders。
  • Web源代码:OrdersController。
  • 持久化源代码:JdbcOrdersRepository。

后面两个源代码树对业务代码有编译期依赖关系,而业务代码则对Web和数据持久毫无所知。从实现角度来看,我们可以通过将这些代码在构建工具中组织成不同的模块或者项目(例如Maven、Gradle、MSBUILD等)来达到目的。理想情况下,我们可以用这种模式将所有组件都划分成不同的项目。

然而,这有点太理想化了,因为拆分代码库经常会带来性能、复杂度和维护性方面的问题。

有些人采用一个稍微简单的组织方式,仅使用两个代码树:

  • 业务(Domain)代码(内部)
  • 基础设施(Infrastructure)代码(外部)

这与图34.9完美对应,很多人都用这个方式来简化对端口和适配器架构的描述。基础设施部分对业务代码有一个编译期的依赖关系。

图34.9:业务与基础设施代码

将所有的基础设施代码放在同一个源代码树中,就有可能使得应用中一个区域的基础设施代码(Web控制器)直接调用另外一个区域的代码(数据库访问),而不经过领域代码。如果没有设置正确的访问修饰符,就更是如此了。

本章小结:本书拾遗

这一章的中心思想就是,如果不考虑具体实现细节,再好的设计也无法长久。必须要将设计映射到对应的代码结构上,考虑如何组织代码树,以及在编译期和运行期采用哪种解耦合的模式。保持开放,但是一定要务实,同时要考虑到团队的大小、技术水平,以及对应的时间和预算限制。最好能利用编译器来维护所选的系统架构设计风格,小心防范来自其他地方的耦合模式,例如数据结构。所有的实现细节都是关键的!

http://www.dtcms.com/a/411546.html

相关文章:

  • 古籍版面分析新SOTA:HisDoc-DETR如何助力AI赋能古籍数字化难题
  • 浙江省网站icp备案多久oa协同办公系统
  • 伊朗声称以色列核计划数据遭重大泄露
  • 自适应平台(Adaptive Platform)标准 ——Specification of Sensor Interfaces
  • LeetCode热题--200. 岛屿数量--中等
  • 营销型网站试运营调忧北京海淀房管局网站
  • 网站建设与制作与维护ppt网站百度排名怎么做快
  • SSM飞机售票管理系统63z52(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
  • [论文阅读] 人工智能 + 软件工程 | 当传统调试遇上LLM:CodeHinter为新手程序员打造专属辅助工具
  • 亚马逊网站做外贸网站是可以做的吗
  • Kimi推出全新Agent模式OK Computer,基于K2模型的端到端任务执行,已开启灰度测试
  • 用vs2010做网站视频教程高端网站制作报价
  • react-native集成PDF预览组件react-native-pdf
  • Dify笔记 知识库
  • 模板建站服务器网页打不开的解决方法
  • 女生做网站前台设置自动删除的wordpress
  • 苏州市吴江太湖新城建设局网站微信手机网站设计6
  • 单片机开发中的队列数据结构详解,队列数据结构在单片机软件开发中的应用详解,C语言
  • 邯郸网站推广wordpress 页面生成
  • 搭建本地代理服务器
  • USB4接口防护,ESD管与TVS管怎么选?-ASIM阿赛姆
  • LazyLLM部署日志
  • 祝贺职业教育网站上线网站的前端和后台
  • 第三人称:角色攻击
  • 怎么理解GO中的context
  • 国内永久免费建站哈尔滨网站设计有哪些步骤
  • 运动控制教学——5分钟学会样条曲线算法!(三次样条曲线,B样条曲线)
  • HTTP 错误 403.14 - Forbidden Web 服务器被配置为不列出此目录的内容——错误代码:0x00000000
  • 备案 多个网站上海网站制作建设是什么
  • 和的区别?