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

DDD整理(概念篇)

DDD


为什么需要DDD

微服务架构的演进

​ 不赘述,可以查看我之前的分布式架构演进博客。

微服务架构划分的痛点

​ 那进入微服务架构时代以后,微服务确实也解决了原来采用集中式架构的单体应用的很多问题,比如扩展性、弹性伸缩能力、小规模团队的敏捷开发等等。这是微服务带来的好处,但是道理我都懂,怎么做呢?怎么拆分微服务?微服务的粒度应该多大呀?微服务到底应该如何拆分和设计呢?微服务的边界应该在哪里?

​ 可以说微服务拆分困境产生的根本原因就是不知道业务或者微服务的边界到底在什么地方。

DDD

​ DDD 是一种处理高度复杂领域的设计思想,它试图分离技术实现的复杂性,并围绕业务概念构建领域模型来控制业务的复杂性,以解决软件难以理解,难以演进的问题。DDD 不是架构,而是一种架构设计方法论,它通过边界划分将复杂业务领域简单化,帮我们设计出清晰的领域和应用边界,可以很容易地实现架构演进。

​ DDD 核心思想是通过领域驱动设计方法定义领域模型,从而确定业务和应用边界,保证业务模型与代码模型的一致性。

DDD与微服务

​ DDD 是一种架构设计方法,微服务是一种架构风格,两者从本质上都是为了追求高响应力,而从业务视角去分离应用系统建设复杂度的手段。两者都强调从业务出发,其核心要义是强调根据业务发展,合理划分领域边界,持续调整现有架构,优化现有代码,以保持架构和代码的生命力,也就是我们常说的演进式架构。

DDD 主要关注:从业务领域视角划分领域边界,构建通用语言进行高效沟通,通过业务抽象,建立领域模型,维持业务和代码的逻辑一致性。

微服务主要关注:运行时的进程间通信、容错和故障隔离,实现去中心化数据管理和去中心化服务治理,关注微服务的独立开发、测试、构建和部署。


战略设计和战术设计

DDD 包括战略设计和战术设计两部分。

战略设计

战略设计主要从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界。

DDD 战略设计会建立领域模型,领域模型可以用于指导微服务的设计和拆分。事件风暴是建立领域模型的主要方法,它需要多方参与,往往会有业务专家、设计人员、开发人员。它是一个从发散到收敛的过程。它通常采用用例分析、场景分析和用户旅程分析,尽可能全面不遗漏地分解业务领域,并梳理领域对象之间的关系,这是一个发散的过程。事件风暴过程会产生很多的实体、命令、事件等领域对象,我们将这些领域对象从不同的维度进行聚类,形成如聚合、限界上下文等边界,建立领域模型,这就是一个收敛的过程。

我们可以用三步来划定领域模型和微服务的边界。

第一步:在事件风暴中梳理业务过程中的用户操作、事件以及外部依赖关系等,根据这些要素梳理出领域实体等领域对象。

第二步:根据领域实体之间的业务关联性,将业务紧密相关的实体进行组合形成聚合,同时确定聚合中的聚合根、值对象和实体。在这个图里,聚合之间的边界是第一层边界,它们在同一个微服务实例中运行,这个边界是逻辑边界,所以用虚线表示。

第三步:根据业务及语义边界等因素,将一个或者多个聚合划定在一个限界上下文内,形成领域模型。在这个图里,限界上下文之间的边界是第二层边界,这一层边界可能就是未来微服务的边界,不同限界上下文内的领域逻辑被隔离在不同的微服务实例中运行,物理上相互隔离,所以是物理边界,边界之间用实线来表示。

在这里插入图片描述

战术设计

经过战略设计,我们划分建立了领域模型,划定了业务领域的边界,建立了通用语言和限界上下文,确定了领域模型中各个领域对象的关系。那么接下来就要根据领域模型,去落地。而战术设计就是技术视角出发,侧重于领域模型的技术实现,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计和实现。


事件风暴

事件风暴是建立领域模型的主要方法,但是在 DDD 领域建模和系统建设过程中,有很多的参与者,包括领域专家、产品经理、项目经理、架构师、开发经理和测试经理等。对同样的领域知识,不同的参与角色可能会有不同的理解,那大家交流起来就会有障碍,怎么办呢?因此,在 DDD 中就出现了“通用语言”和“限界上下文”这两个重要的概念。

通用语言

​ 通用语言定义上下文含义。在事件风暴过程中,通过团队交流达成共识的,能够简单、清晰、准确描述业务涵义和规则的语言就是通用语言。也就是说,通用语言是团队统一的语言,不管你在团队中承担什么角色,在同一个领域的软件生命周期里都使用统一的语言进行交流。

​ 通用语言包含术语和用例场景,并且能够直接反映在代码中。通用语言中的名词可以给领域对象命名,如商品、订单等,对应实体对象;而动词则表示一个动作或事件,如商品已下单、订单已付款等,对应领域事件或者命令。

限界上下文

​ 语言都有它的语义环境,同样,通用语言也有它的上下文环境。为了避免同样的概念或语义在不同的上下文环境中产生歧义,DDD 在战略设计上提出了“限界上下文”这个概念,用来确定语义所在的领域边界。限界上下文就是用来定义领域边界,以确保每个上下文含义在它特定的边界内都具有唯一的含义,领域模型则存在于这个边界之内。

​ 限界上下文的作用就是:用来封装通用语言和领域对象,提供上下文环境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。这个边界定义了模型的适用范围,使团队所有成员能够明确地知道什么应该在模型中实现,什么不应该在模型中实现。

​ 比如说商品在不同的阶段有不同的术语,在销售阶段是商品,而在运输阶段则变成了货物。同样的一个东西,由于业务领域的不同,赋予了这些术语不同的涵义和职责边界,这个边界就可能会成为未来微服务设计的边界。那么,领域边界就是通过限界上下文来定义的。

​ 限界上下文是微服务设计和拆分的主要依据。在领域模型中,如果不考虑技术异构、团队沟通等其它外部因素,一个限界上下文理论上就可以设计为一个微服务。

完整的流程

下面我带你看一张图,这张图描述了从事件风暴建立通用语言到领域对象设计和代码落地的完整过程。

在这里插入图片描述

  1. 在事件风暴的过程中,领域专家会和设计、开发人员一起建立领域模型,在领域建模的过程中会形成通用的业务术语和用户故事。事件风暴也是一个项目团队统一语言的过程。
  2. 通过用户故事分析会形成一个个的领域对象,这些领域对象对应领域模型的业务对象,每一个业务对象和领域对象都有通用的名词术语,并且一一映射。
  3. 微服务代码模型来源于领域模型,每个代码模型的代码对象跟领域对象一一对应。

事件风暴的小tips

设计过程中我们可以用一些表格,来记录事件风暴和微服务设计过程中产生的领域对象及其属性。比如,领域对象在 DDD 分层架构中的位置、属性、依赖关系以及与代码模型对象的映射关系等。

在这里插入图片描述


DDD 的知识体系提出了很多的名词,像:领域、子域、核心域、通用域、支撑域、限界上下文、聚合、聚合根、实体、值对象等等,非常多。这些名词,都是关键概念。但是,这些名词在你的微服务设计和开发过程中不一定都用得上,但它可以帮你理解 DDD 的核心设计思想和理念,你可以根据你的业务场景去具体分析你需要哪些,不需要生硬的全部套上去。

领域和子域

领域具体指一种特定的范围或区域,领域就是用来确定范围的,范围即边界。DDD 的领域就是这个边界内要解决的业务问题域

领域可以进一步划分为子领域。我们把划分出来的多个子领域称为子域,每个子域对应一个更小的问题域或更小的业务范围。

如何划分

举个例子:如何给桃树建立一个完整的生物学知识体系?

  • 第一步:确定研究对象,即研究领域,这里是一棵桃树。
  • 第二步:对研究对象进行细分,将桃树细分为器官,器官又分为营养器官和生殖器官两种。其中营养器官包括根、茎和叶,生殖器官包括花、果实和种子。桃树的知识体系是我们已经确定要研究的问题域,对应 DDD 的领域。根、茎、叶、花、果实和种子等器官则是细分后的问题子域。这个过程就是 DDD 将领域细分为多个子域的过程。
  • 第三步:对器官进行细分,将器官细分为组织。比如,叶子器官可细分为保护组织、营养组织和输导组织等。这个过程就是 DDD 将子域进一步细分为多个子域的过程。
  • 第四步:对组织进行细分,将组织细分为细胞,细胞成为我们研究的最小单元。细胞之间的细胞壁确定了单元的边界,也确定了研究的最小边界。

核心域、通用域和支撑域

在领域不断划分的过程中,领域会细分为不同的子域,子域可以根据自身重要性和功能属性划分为三类子域,它们分别是:核心域、通用域和支撑域。

核心域:它是业务成功的主要因素和公司的核心竞争力

通用域:没有太多个性化的诉求,同时被多个子域使用的通用功能子域是通用域

支撑域:有一种功能子域是必需的,但既不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域,就是支撑域

总结

这里总结一下,就是说每一个细分的领域都会有一个知识体系,也就是 DDD 的领域模型。在所有子域的研究完成后,我们就建立了全域的知识体系了,也就建立了全域的领域模型。

领域的核心思想就是将问题域逐级细分,来降低业务理解和系统实现的复杂度。通过领域细分,逐步缩小微服务需要解决的问题域,构建合适的领域模型,而领域模型映射成系统就是微服务了。

核心域、支撑域和通用域的主要目标是:通过领域划分,区分不同子域在公司内的不同功能属性和重要性,从而公司可对不同子域采取不同的资源投入和建设策略,其关注度也会不一样。


领域中的对象

实体

在 DDD 中有这样一类对象,它们拥有唯一标识符,且标识符在历经各种状态变更后仍能保持一致。对这些对象而言,重要的不是其属性,而是其延续性和标识,对象的延续性和标识会跨越甚至超出软件的生命周期。我们把这样的对象称为实体。

实体的业务形态

在 DDD 不同的设计过程中,实体的形态是不同的。在战略设计时,实体是领域模型的一个重要对象。领域模型中的实体是多个属性、操作或行为的载体。在事件风暴中,我们可以根据命令、操作或者事件,找出产生这些行为的业务实体对象,进而按照一定的业务规则将依存度高和业务关联紧密的多个实体对象和值对象进行聚类,形成聚合。你可以这么理解,实体和值对象是组成领域模型的基础单元。

实体的运行形态

实体以 DO(领域对象)的形式存在,每个实体对象都有唯一的 ID。我们可以对一个实体对象进行多次修改,修改后的数据和原来的数据可能会大不相同。但是,由于它们拥有相同的 ID,它们依然是同一个实体。比如商品是商品上下文的一个实体,通过唯一的商品 ID 来标识,不管这个商品的数据如何变化,商品的 ID 一直保持不变,它始终是同一个商品。

实体的数据库形态

与传统数据模型设计优先不同,DDD 是先构建领域模型,针对实际业务场景构建实体对象和行为,再将实体对象映射到数据持久化对象。

在领域模型映射到数据模型时,一个实体可能对应 0 个、1 个或者多个数据库持久化对象。大多数情况下实体与持久化对象是一对一。在某些场景中,有些实体只是暂驻静态内存的一个运行态实体,它不需要持久化。比如,基于多个价格配置数据计算后生成的折扣实体。

而在有些复杂场景下,实体与持久化对象则可能是一对多或者多对一的关系。比如,用户 user 与角色 role 两个持久化对象可生成权限实体,一个实体对应两个持久化对象,这是一对多的场景。再比如,有些场景为了避免数据库的联表查询,提升系统性能,会将客户信息 customer 和账户信息 account 两类数据保存到同一张数据库表中,客户和账户两个实体可根据需要从一个持久化对象中生成,这就是多对一的场景。

值对象

​ 《实现领域驱动设计》一书中对值对象的定义:通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体。在 DDD 中用来描述领域的特定方面,并且是一个没有标识符的对象,叫作值对象。

​ 也就说,值对象描述了领域中的一件东西,这个东西是不可变的,它将不同的相关属性组合成了一个概念整体。当度量和描述改变时,可以用另外一个值对象予以替换。它可以和其它值对象进行相等性比较,且不会对协作对象造成副作用。这部分在后面讲“值对象的运行形态”时还会有例子。

值对象的业务形态

​ 值对象是 DDD 领域模型中的一个基础对象,它跟实体一样都来源于事件风暴所构建的领域模型,都包含了若干个属性,它与实体一起构成聚合。

​ 实体是看得到、摸得着的实实在在的业务对象,实体具有业务属性、业务行为和业务逻辑。而值对象只是若干个属性的集合,只有数据初始化操作和有限的不涉及修改数据的行为,基本不包含业务逻辑。值对象的属性集虽然在物理上独立出来了,但在逻辑上它仍然是实体属性的一部分,用于描述实体的特征。在值对象中也有部分共享的标准类型的值对象,它们有自己的限界上下文,有自己的持久化对象,可以建立共享的数据类微服务,比如数据字典。

​ 比如我们之前在用DDD建模时,将用户的住址就设计为了值对象——住址是用户的属性的一部分,它包含国家、省份、城市、区、街道等。

值对象的代码形态

​ 值对象在代码中有这样两种形态。如果值对象是单一属性,则直接定义为实体类的属性;如果值对象是属性集合,则把它设计为 Class 类,Class 将具有整体概念的多个属性归集到属性集合,这样的值对象没有 ID,会被实体整体引用。

值对象的运行形态

​ 实体实例化后的 DO 对象的业务属性和业务行为非常丰富,但值对象实例化的对象则相对简单和乏味。除了值对象数据初始化和整体替换的行为外,其它业务行为就很少了。值对象嵌入到实体的话,有这样两种不同的数据格式,也可以说是两种方式,分别是属性嵌入的方式和序列化大对象的方式。

值对象的数据库形态

​ DDD 引入值对象是希望实现从“数据建模为中心”向“领域建模为中心”转变,减少数据库表的数量和表与表之间复杂的依赖关系,尽可能地简化数据库设计,提升数据库性能。在领域建模时,我们可以将部分对象设计为值对象,保留对象的业务涵义,同时又减少了实体的数量;在数据建模时,我们可以将值对象嵌入实体,减少实体表的数量,简化数据库设计。

值对象的优势和局限

优势
  • ​ 简化数据库设计,提升数据库性能
  • ​ 值对象采用序列化大对象的方法简化了数据库设计,减少了实体表的数量
  • ​ 值对象采用属性嵌入的方法提升了数据库的性能
  • ​ 简单、清晰地表达业务概念
劣势
  • ​ 无法满足基于值对象的快速查询,会导致搜索值对象属性值变得异常困难。
  • ​ 如果实体引用的值对象过多,则会导致实体堆积一堆缺乏概念完整性的属性,这样值对象就会失去业务涵义

实体和值对象的关系

实体和值对象是微服务底层的最基础的对象,一起实现实体最基本的核心领域逻辑。DDD 提倡从领域模型设计出发,而不是先设计数据模型。前面讲过了,传统的数据模型设计通常是一个表对应一个实体,一个主表关联多个从表,当实体表太多的时候就很容易陷入无穷无尽的复杂的数据库设计,领域模型就很容易被数据模型绑架。可以说,值对象的诞生,在一定程度上,和实体是互补的。

举个例子:

在这里插入图片描述

​ 在领域模型中人员是实体,地址是值对象,地址值对象被人员实体引用。在数据模型设计时,地址值对象可以作为一个属性集整体嵌入人员实体中,组合形成上图这样的数据模型;也可以以序列化大对象的形式加入到人员的地址属性中,前面表格有展示。

聚合

​ 在事件风暴中,我们会根据一些业务操作和行为找出实体(Entity) 或值对象(ValueObject),进而将业务关联紧密的实体和值对象进行组合,构成聚合,再根据业务语义将多个聚合划定到同一个限界上下文(Bounded Context)中,并在限界上下文内完成领域建模。

​ 聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。聚合有一个聚合根和上下文边界,这个边界根据业务单一职责和高内聚原则,定义了聚合内 部应该包含哪些实体和值对象,而聚合之间的边界是松耦合的。按照这种方式设计出来的微 服务很自然就是“高内聚、低耦合”的。

​ 聚合在 DDD 分层架构里属于领域层,领域层包含了多个聚合,共同实现核心业务逻辑。聚 合内实体以充血模型实现个体业务能力,以及业务逻辑的高内聚。跨多个实体的业务逻辑通过领域服务来实现,跨多个聚合的业务逻辑通过应用服务来实现。比如有的业务场景需要同 一个聚合的 A 和 B 两个实体来共同完成,我们就可以将这段业务逻辑用领域服务来实现; 而有的业务逻辑需要聚合 C 和聚合 D 中的两个服务共同完成,这时你就可以用应用服务来组合这两个服务。

聚合根

​ 聚合根的主要目的是为了避免由于复杂数据模型缺少统一的业务规则控制,而导致聚合、实 体之间数据不一致性的问题。如果把聚合比作组织,那聚合根就是这个组织的负责人。聚合根也称为根实体,它不仅是实体,还是聚合的管理者。

​ 首先它作为实体本身,拥有实体的属性和业务行为,实现自身的业务逻辑。

​ 其次它作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则协同完成 共同的业务逻辑。

​ 最后在聚合之间,它还是聚合对外的接口人,以聚合根 ID 关联的方式接受外部任务和请 求,在上下文内实现聚合之间的业务协同。也就是说,聚合之间通过聚合根 ID 关联引用, 如果需要访问其它聚合的实体,就要先访问聚合根,再导航到聚合内部实体,外部对象不能 直接访问聚合内实体。

聚合的一些设计原则

  • 在一致性边界内建模真正的不变条件。聚合用来封装真正的不变性,而不是简单地将对象组合在一起。聚合内有一套不变的业务规则,各实体和值对象按照统一的业务规则运行, 实现对象数据的一致性,边界之外的任何东西都与该聚合无关,这就是聚合能实现业务高内 聚的原因。
  • 设计小聚合。如果聚合设计得过大,聚合会因为包含过多的实体,导致实体之间的管理 过于复杂,高频操作时会出现并发冲突或者数据库锁,最终导致系统可用性变差。而小聚合设计则可以降低由于业务过大导致聚合重构的可能性,让领域模型更能适应业务的变化
  • 通过唯一标识引用其它聚合。聚合之间是通过关联外部聚合根 ID 的方式引用,而不是直接对象引用的方式。外部聚合的对象放在聚合边界内管理,容易导致聚合的边界不清晰,也 会增加聚合之间的耦合度。
  • 在边界之外使用最终一致性。聚合内数据强一致性,而聚合之间数据最终一致性。在一次事务中,最多只能更改一个聚合的状态。如果一次业务操作涉及多个聚合状态的更改,应 采用领域事件的方式异步修改相关的聚合,实现聚合之间的解耦(相关内容我会在领域事件 部分详解)。
  • 通过应用层实现跨聚合的服务调用。为实现微服务内聚合之间的解耦,以及未来以聚合为单位的微服务组合和拆分,应避免跨聚合的领域服务调用和跨聚合的数据库表关联。

总结

聚合的特点:高内聚、低耦合,它是领域模型中最底层的边界,可以作为拆分微服务的最小 单位,但我不建议你对微服务过度拆分。但在对性能有极致要求的场景中,聚合可以独立作 为一个微服务,以满足版本的高频发布和极致的弹性伸缩能力。

一个微服务可以包含多个聚合,聚合之间的边界是微服务内天然的逻辑边界。有了这个逻辑 边界,在微服务架构演进时就可以以聚合为单位进行拆分和组合了,微服务的架构演进也就 不再是一件难事了。

聚合根的特点:聚合根是实体,有实体的特点,具有全局唯一标识,有独立的生命周期。一 个聚合只有一个聚合根,聚合根在聚合内对实体和值对象采用直接对象引用的方式进行组织 和协调,聚合根与聚合根之间通过 ID 关联的方式实现聚合之间的协同。

实体的特点:有 ID 标识,通过 ID 判断相等性,ID 在聚合内唯一即可。状态可变,它依附 于聚合根,其生命周期由聚合根管理。实体一般会持久化,但与数据库持久化对象不一定是 一对一的关系。实体可以引用聚合内的聚合根、实体和值对象。

值对象的特点:无 ID,不可变,无生命周期,用完即扔。值对象之间通过属性值判断相等 性。它的核心本质是值,是一组概念完整的属性组成的集合,用于描述实体的状态和特征。 值对象尽量只引用值对象。


领域中的事件

​ 在事件风暴(Event Storming)时,我们发现除了命令和操作等业务行为以外,还有一种 非常重要的事件,这种事件发生后通常会导致进一步的业务操作,在 DDD 中这种事件被称为领域事件。领域事件是领域模型中非常重要的一部分,用来表示领域中发生的事件。一个领域事件将导致进一步的业务操作,在实现业务解耦的同时,还有助于形成完整的业务闭环。

​ 如果一个领域事件导致的业务操作涉及多个聚合状态的更改, 应采用领域事件的最终一致性。

​ 在具体落地时,有可能领域事件发生在微服务内的聚合之间,有的则发生在微服务之间,还有两者皆有的场景,一般来说跨微服务的领域事件处理居多。微服务内的领域事件呢,可以采用事件总线的方式更新多个聚合;微服务之间则可以使用消息中间件,保证最终一致性即可。不过也要根据业务需求具体分析,稍微做一下变通也无可厚非。

领域事件总体架构

​ 领域事件的执行需要一系列的组件和技术来支撑。领域事件处理包括:事件构建和发布、事件数据持久化、事件总线、消息中间件、事件接收和处理等。

事件构建和发布

​ 事件基本属性至少包括:事件唯一标识、发生时间、事件类型和事件源,其中事件唯一标识 应该是全局唯一的,以便事件能够无歧义地在多个限界上下文中传递。事件基本属性主要记录事件自身以及事件发生背景的数据。事件中还有一项更重要,那就是业务属性,用于记录事件发生那一刻的业务数据,这些 数据会随事件传输到订阅方,以开展下一步的业务操作。

​ 事件基本属性和业务属性一起构成事件实体,事件实体依赖聚合根。领域事件发生后,事件 中的业务数据不再修改,因此业务数据可以以序列化值对象的形式保存,这种存储格式在消 息中间件中也比较容易解析和获取。

事件数据持久化

​ 事件数据持久化可用于系统之间的数据对账,或者实现发布方和订阅方事件数据的审计。当遇到消息中间件、订阅方系统宕机或者网络中断,在问题解决后仍可继续后续业务流转,保 证数据的一致性。

事件总线 (EventBus)

​ 事件总线是实现微服务内聚合之间领域事件的重要组件,它提供事件分发和接收等服务。事件总线是进程内模型,它会在微服务内聚合之间遍历订阅者列表,采取同步或异步的模式传 递数据。事件分发流程大致如下:

  • 如果是微服务内的订阅者(其它聚合),则直接分发到指定订阅者;
  • 如果是微服务外的订阅者,将事件数据保存到事件库(表)并异步发送到消息中间件;
  • 如果同时存在微服务内和外订阅者,则先分发到内部订阅者,将事件消息保存到事件库 (表),再异步发送到消息中间件。

消息中间件

​ 跨微服务的领域事件大多会用到消息中间件,实现跨微服务的事件发布和订阅。

事件接收和处理

​ 微服务订阅方在应用层采用监听机制,接收消息队列中的事件数据,完成事件数据的持久化 后,就可以开始进一步的业务处理。领域事件处理可在领域服务中实现。


具体落地实现后续会整理。

本文部分内容摘自《实现领域驱动设计》与极客网《DDD实战课》

相关文章:

  • DDD的分层架构设计
  • 面试记录之synchronized的惨败经历
  • 面试复盘整理
  • Go语言基础_数据类型、基本语法篇
  • Go学习笔记_环境搭建
  • Markdown学习
  • Markdown下载客户端
  • JDK,JRE,JVM三者的区别
  • 2020-12-01
  • 2020-12-02
  • 比较三个数字,求出最大值
  • Scanner限制次数猜数字
  • ArrayList,随机抽取6个数字在【1-33】中的随机数,并且遍历
  • 利用ArrayList遍历集合
  • 用一个大集合存入20个随机数字,然后筛选其中的偶数元素,放到小集合中
  • 【140天】尚学堂高淇Java300集视频精华笔记(86-87)
  • 【5+】跨webview多页面 触发事件(二)
  • C# 免费离线人脸识别 2.0 Demo
  • crontab执行失败的多种原因
  • Java知识点总结(JDBC-连接步骤及CRUD)
  • JS+CSS实现数字滚动
  • Python socket服务器端、客户端传送信息
  • tensorflow学习笔记3——MNIST应用篇
  • UEditor初始化失败(实例已存在,但视图未渲染出来,单页化)
  • 不上全站https的网站你们就等着被恶心死吧
  • 推荐一个React的管理后台框架
  • 我看到的前端
  • 一个完整Java Web项目背后的密码
  • (delphi11最新学习资料) Object Pascal 学习笔记---第7章第3节(封装和窗体)
  • (pytorch进阶之路)CLIP模型 实现图像多模态检索任务
  • (第27天)Oracle 数据泵转换分区表
  • (动手学习深度学习)第13章 计算机视觉---图像增广与微调
  • (非本人原创)史记·柴静列传(r4笔记第65天)
  • (强烈推荐)移动端音视频从零到上手(上)
  • (淘宝无限适配)手机端rem布局详解(转载非原创)
  • (续)使用Django搭建一个完整的项目(Centos7+Nginx)
  • (一)UDP基本编程步骤
  • (转)ABI是什么
  • (转)ORM
  • .NET Compact Framework 3.5 支持 WCF 的子集
  • .net core 实现redis分片_基于 Redis 的分布式任务调度框架 earth-frost
  • .Net IE10 _doPostBack 未定义
  • .net php 通信,flash与asp/php/asp.net通信的方法
  • .net 简单实现MD5
  • .NET 使用 JustAssembly 比较两个不同版本程序集的 API 变化
  • .NET/ASP.NETMVC 深入剖析 Model元数据、HtmlHelper、自定义模板、模板的装饰者模式(二)...
  • /boot 内存空间不够
  • [Electron]ipcMain.on和ipcMain.handle的区别
  • [jquery]this触发自身click事件,当前控件向上滑出
  • [leetcode]Clone Graph
  • [LeetCode]—Roman to Integer 罗马数字转阿拉伯数字
  • [Linux] Boot分区满了的处理方法 The volume boot has only 0 bytes disk space remaining
  • [NOI 2016]循环之美
  • [PAT] 1041 Be Unique (20 分)Java
  • [Python]闭包