基于ABP落地领域驱动设计-02.聚合和聚合根的最佳实践和原则

前言

上一篇 基于ABP落地领域驱动设计-01.全景图 概述了DDD理论和对应的解决方案、项目组成、项目引用关系,以及基于ABP落地DDD的通用原则。从这本篇开始,会更加深入地介绍在基于 ABP Framework 落地DDD过程中的最佳实践和原则

围绕DDDABP Framework两个核心技术,后面还会陆续发布核心构件实现综合案例实现系列文章,敬请关注! ABP Framework 研习社(QQ群:726299208) ABP Framework 学习及实施DDD经验分享;示例源码、电子书共享,欢迎加入!

领域对象是DDD的核心,我们会依次分析聚合/聚合根、仓储、规约、领域服务的最佳实践和规则。内容较多,会拆分成多个章节单独展开。

本文重点讨论领域对象——聚合和聚合根的最佳实践和原则

首先我们需要一个业务场景,例子中会用到 GitHub 的一些概念,如:Issue(建议)、Repository(代码仓库)、Label(标签)和User(用户)。

下图显示了业务场景对应的聚合、聚合根、实体、值对象以及它们之间的关系。

Issue 聚合是由 Issue(聚合根)、Comment(实体)和 IssuelLabel(值对象)组成的集合。因为其他聚合相对简单,所以我们重点分析 Issue 聚合

聚合

正如前面所讲,一个聚合是一系列对象(实体和值对象)的集合,通过聚合根将所有关联对象绑定在一起。本节将介绍与聚合相关的最佳实践和原则。

我们对聚合根子集合实体都使用实体这个术语,除非明确写出聚合根或子集合实体。

聚合和聚合根原则

包含业务原则

•实体负责实现与其自身属性相关的业务规则。•聚合根还负责其子集合实体状态管理。•聚合应该通过实现领域规则规约来保持自身的完整性有效性。这意味着,与数据传输对象(DTO)不同,实体具有实现业务逻辑的方法。实际上,我们应该尽可能在实体中实现业务规则

单个单元原则

聚合及其所有子集合,作为单个单元被检索和保存。例如:如果向 Issue 添加 Comment,需要这样做:

•从数据库中获取 Issue 包含所有子集合:Comments (该问题的评论列表) 和 IssueLabels (该问题的标签集合)。•在 Issue 类中调用方法添加一个新的 Comment,比如: Issue.AddCommnet(...)•作为一个单一的数据库更新操作,将 Issue(包括所有子集合)保存到数据库。

对于习惯使用 EF Core 和 关系数据的开发者来说,这看起来似乎有些奇怪。获取 Issue 的所有数据是没有必要低效的。为什么我们不直接执行一个SQL插入命令到数据库,而不查询任何数据呢?

答案是,我们应该在代码中实现业务规则并保持数据的一致性和完整性。如果我们有一个业务规则,如:用户不能对锁定的 Issue 进行评论,我们如何不通过检索数据库中数据的情况下,检查 Issue 的锁定状态呢?所以,只有当应用程序代码中的相关对象可用时,即获取到聚合及其所有子集合数据时,我们才能执行该业务规则。

另一方面,MongoDB开发者会发现这个规则非常自然。因为在 MongoDB 中,一个聚合对象(包括子集合)被保存在数据库中的一个集合中,而在关系型数据库中,它被分布在数据库中几个表中。因此,当你得到一个聚合时,所有的子集合已经作为查询的一部分被检索出来了,不需要任何额外配置。

ABP框架有助于在您的应用程序中实现这一原则。

示例:添加 Comment 到 Issue

public class IssueAppService : ApplicationService ,IIssueAppService
{private readonly IRepository<Issue,Guid> _issueRepository;public IssueAppService(IRepository<Issue,Guid> issueRepository){_issueRepository = issueRepository;}[Authorize]public async Task CreateCommentAsync(CreateCommentDto input){var issue = await _issueRepository.GetAsync(input.IssueId);issue.AddComment(CurrentUser.GetId(),input.Text);await _issueRepository.UpdateAsynce(issue);}
}

_issueRepository.GetAsync(...)方法默认作为单个单元检索 Issue 对象并包含所有子集合。对于 MongoDB 来说这个操作开箱即用,但是使用 EF Core 需要配置聚合与数据库映射,配置后 EF Core 仓储实现 会自动处理。_issueRepository.GetAsync(...)方法提供一个可选参数includeDetails,可以传递值 false 禁用该行为,不包含子集合对象,只在需要时启用它。

Issue.AddComment(...)传递参数 userId 和 text ,表示用户ID评论内容,添加到 Issue 的 Comments 集合中,并实现必要的业务逻辑验证。

最后,使用 _issueRepository.UpdateAsync(...) 保存更改到数据库。

EF Core 提供 变更跟踪(Change Tracking)功能,实际上你不需要调用 _issueRepository.UpdateAsync(...) 方法,会自动进行保存。这个功能是由 ABP 工作单元系统 提供,应用服务的方法作为一个单独的工作单元,在执行完之后会自动调用 DbContext.SaveChanges()。当然,如果使用 MongoDB 数据库,则需要显示地更新已经更改的实体。所以,如果你想要编写独立于数据库提供程序的代码,应该总是为要更改的实体调用UpdateAsync()方法。

事务边界原则

一个聚合通常被认为是一个事务边界。如果用例使用单个聚合,读取并保存为单个单元,那么对聚合对象所做的所有更改,将作为原子操作保存,而不需要显式地使用数据库事务。

当然,我们可能需要处理将多个聚合实例作为单一用例更改的场景,此时需要使用数据库事务确保更新操作的原子性数据一致性。正因为如此,ABP框架为一个用例(即一个应用程序服务方法)显式地使用数据库事务,一个应用程序服务方法,就是一个工作单元。

可序列化原则

聚合(包含根实体和子集合)应该是可序列化的,并且可以作为单个单元在网络上进行传输。举个例子,MongoDB序列化聚合为Json文档保存到数据库,反序列化从数据库中读取的Json数据。

当您使用关系数据库和ORM时,没有必要这样做。然而,它是领域驱动设计的一个重要实践。

聚合和聚合根最佳实践

以下最佳实践确保实现上述原则。

只通过ID引用其他聚合

一个聚合应该只通过其他聚合的ID引用聚合,这意味着你不能添加导航属性到其他聚合。

•这条规则使得实现可序列化原则得以实现。•可以防止不同聚合相互操作,以及将聚合的业务逻辑泄露给另一个聚合。

我们来看一个例子,两个聚合根:GitRepository 和 Issue :

public class GitRepository:AggregateRoot<Guid>
{public string Name {get;set;}public int StarCount{get;set;}public Collection<Issue> Issues {get;set;} //错误代码示例
}public class Issue:AggregateRoot<Guid>
{public tring Text{get;set;}public GitRepository Repository{get;set;} //错误代码示例public Guid RepositoryId{get;set;} //正确示例
}

GitRepository 不应该包含 Issue 集合,他们是不同聚合。•Issue 不应该设置导航属性关联 GitRepository ,因为他们是不同聚合。•Issue 使用 RepositoryId 关联 Repository 聚合,正确。

当你有一个 Issue 需要关联的 GitRepository 时,那么可以从数据库通过 RepositoryId 直接查询。

用于 EF Core 和 关系型数据库

在 MongoDB 中,自然不适合有这样的导航属性/集合。如果这样做,在源集合的数据库集合中会保存目标集合对象的副本,因为它在保存时被序列化为JSON,这样可能会导致持久化数据的不一致。

然而,EF Core 和关系型数据库的开发者可能会发现这个限制性的规则是不必要的,因为 EF Core 可以在数据库的读写中处理它。

但是我们认为这是一条重要的规则,有助于降低领域的复杂性防止潜在的问题,我们强烈建议实施这条规则。然而,如果你认为忽略这条规则是切实可行的,请参阅前面基于ABP落地领域驱动设计-01.全景图[2]关于数据库独立性原则的讨论部分。

保持聚合根足够小

一个好的做法是保持一个简单而小的聚合。这是因为一个聚合体将作为一个单元被加载和保存,读/写一个大对象会导致性能问题。

请看下面的例子:

public class UserRole:ValueObject
{public Guid UserId{get;set;}public Guid RoleId{get;set;}
}public class Role:AggregateRoot<Guid>
{public string Name{get;set;}public Collection<UserRole> Users{get;set;} //错误示例:角色对应的用户是不断增加的
}
public class User:AggregateRoot<Guid>
{public string Name{get;set;}public Collection<UserRole> Roles{get;set;}//正确示例:一个用户拥有的角色数量是有限的
}

Role聚合 包含 UserRole 值对象集合,用于跟踪分配给此角色的用户。注意,UserRole 不是另一个聚合,对于规则仅通过Id引用其他聚合没有冲突。

然而,实际却存在一个问题。在现实生活中,一个角色可能被分配给数以千计(甚至数以百万计)的用户,每当你从数据库中查询一个角色时,加载数以千计的数据项是一个重大的性能问题。记住:聚合是由它们的子集合作为一个单一单元加载的

另一方面,用户可能有角色集合,因为实际情况中用户拥有的角色数量是有限的,不会太多。当您使用用户聚合时,拥有一个角色列表可能会很有用,且不会影响性能。

如果你仔细想想,当使用非关系型数据库(如MongoDB)时,当RoleUser都有关系列表时还有一个问题:在这种情况下,相同的信息会在不同的集合中重复出现,将很难保持数据的一致性,每当你在User.Roles中添加一个项,你也需要将它添加到Role.Users中。

因此,根据以下因素来确定聚合边界和大小:

•考虑对象关联性,是否需要在一起使用。•考虑性能,查询(加载/保存)性能和内存消耗。•考虑数据的完整性、有效性和一致性。

而实际:

•大多数聚合根没有子集合。•一个子集合最多不应该包含超过100-150个条目。如果您认为集合可能有更多项时,请不要定义集合作为聚合的一部分,应该考虑为集合内的实体提取为另一个聚合根。

聚合根/实体中的主键

•一个聚合根通常有一个ID属性作为其标识符(主键,Primark Key: PK)。推荐使用 Guid 作为聚合根实体的PK。•聚合中的实体(不是聚合根)可以使用复合主键

示例:聚合根和实体

//聚合根:单个主键
public class Organization
{public Guid Id{get;set;}public string Name{get;set;}//...
}
//实体:复合主键
public class OrganizationUser
{public Guid OrganizationId{get;set;} //主键public Guid UserId{get;set;}//主键public bool IsOwner{get;set;}//...
}

Organization 包含 Guid 类型主键 IdOrganizationUser 是 Organization 中的子集合,有复合主键:OrganizationId 和 UserId 。

这并不意味着子集合实体应该总是有复合主键,只有当需要时设置;通常是单一的ID属性。

复合主键实际上是关系型数据库的一个概念,因为子集合实体有自己的表,需要一个主键。另一方面,例如:在MongoDB中,你根本不需要为子集合实体定义主键,因为它们是作为聚合根的一部分来存储的。

聚合根/实体构造函数

构造函数是实体的生命周期开始的地方。一个设计良好的构造函数,担负以下职责:

•获取所需的实体属性参数,来创建一个有效的实体。应该强制只传递必要的参数,并可以将非必要的属性作为可选参数。•检查参数的有效性。•初始化子集合。

示例:Issue(聚合根)构造函数

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Volo.Abp;
using Volo.Abp.Domain.Entities;namespace IssueTracking.Issues
{public class Issue:AggregateRoot<Guid>{public Guid RepositoryId{get;set;}public string Title{get;set;}public string Text{get;set;}public Guid? AssignedUserId{get;set;}public bool IsClosed{get;set;}pulic IssueCloseReason? CloseReason{get;set;} //枚举public ICollection<IssueLabel> Labels {get;set;}public Issue(Guid id,Guid repositoryId,string title,string text=null,Guid? assignedUserId = null):base(id){//属性赋值RepositoryId=repositoryId;//有效性检测Title=Check.NotNullOrWhiteSpace(title,nameof(title));Text=text;AssignedUserId=assignedUserId;//子集合初始化Labels=new Collection<IssueLabel>();}private Issue(){/*反序列化或ORM 需要*/}}
}

Issue类通过其构造函数参数,获得属性所需的值,以此创建一个正确有效的实体。•在构造函数中验证输入参数的有效性,比如:Check.NotNullOrWhiteSpace(...) 当传递的值为空时,抛出异常ArgumentException。•初始化子集合,当使用 Labels 集合时,不会获取到空引用异常。•构造函数将参数id传递给base类,不在构造函数中生成 Guid,可以将其委托给另一个 Guid生成服务,作为参数传递进来。•无参构造函数对于ORM是必要的。我们将其设置为私有,以防止在代码中意外地使用它。

实体属性访问器和方法

上面的示例代码,看起来可能很奇怪。比如:在构造函数中,我们强制传递一个不为nullTitle。但是,我们可以将 Title 属性设置为 null,而对其没有进行任何有效性控制。这是因为示例代码关注点暂时只在构造函数。

如果我们用 public 设置器声明所有的属性,就像上面的Issue类中的属性例子,我们就不能在实体的生命周期中强制保持其有效性和完整性。所以:

•当需要在设置属性时,执行任何逻辑,请将属性设置为私有private。•定义公共方法来操作这些属性。

示例:通过方法修改属性

namespace IssueTracking.Issues
{public Guid RepositoryId {get; private set;} //不更改public string Title { get; private set; } //更改,需要非空验证public string Text{get;set;} //无需验证public Guid? AssignedUserId{get;set;} //无需验证public bool IsClosed { get; private set; } //需要和 CloseReason 一起更改public IssueCloseReason? CloseReason { get;private set;} //需要和 IsClosed 一起更改public class Issue:AggregateRoot<Guid>{//...public void SetTitle(string title){Title=Check.NotNullOrWhiteSpace(title,nameof(title));}public void Close(IssueCloseReason reason){IsClosed = true;CloseReason =reason;}public void ReOpen(){IsClosed=false;CloseReason=null;}}
}

RepositoryId 设置器设置为私有private,因为 Issue 不能将 Issue 移动到另一个 Repository 中,该属性创建之后无需更改。•Title 设置器设置为私有,当需要更改时,可以使用 SetTitle 方法,这是一种可控的方式。•Text 和 AssignedUserId 都有公共设置器,因为这两个字段并没有约束,可以是null或任何值。我们认为没有必要定义单独的方法来设置它们。如果以后需要,可以添加更改方法并将其设置器设置为私有。领域层是内部项目,并不会暴露给客户端使用,所以这种更改不会有问题。•IsClosed 和 IssueCloseReason 是成对修改的属性,分别定义 Close 和 ReOpen 方法一起修改他们。通过这种方式,可以防止在没有任何理由的情况下关闭一个问题。

业务逻辑和实体中的异常处理

当你在实体中进行验证和实现业务逻辑,经常需要管理异常:

•创建特定领域异常。•必要时在实体方法中抛出这些异常。

示例:

public class Issue:AggregateRoot<Guid>
{//..public bool IsLocked {get;private set;}public bool IsClosed{get;private set;}public IssueCloseReason? CloseReason {get;private set;}public void Close(IssueCloseReason reason){IsClose = true;CloseReason =reason;}public void ReOpen(){if(IsLocked){throw new IssueStateException("不能打开一个锁定的问题!请先解锁!");}IsClosed=false;CloseReason=null;}public void Lock(){if(!IsClosed){throw new IssueStateException("不能锁定一个关闭的问题!请先打开!");}}public void Unlock(){IsLocked = false;}
}

这里有两个业务规则:

•锁定的Issue不能重新打开•不能锁定一个关闭的Issue

Issue 类在这些业务规则中抛出异常 IssueStateException 。

namespace IssueTracking.Issues
{public class IssueStateException : Exception{public IssueStateException(string message):base(message){}}
}

抛出此类异常有两个潜在问题:

1.在这种异常情况下,终端用户是否应该看到异常(错误)消息?如果是,如何实现本地化异常消息?因为不能在实体中注入和使用IStringLocalizer,导致不能使用本地化系统。2.对于 Web 应用程序或 HTTP API,应该给客户端返回什么 HTTP Status Code?

ABP框架 Exception Handing 系统处理了这些问题。

示例:抛出业务异常

using Volo.Abp;
namespace IssuTracking.Issues
{public class IssueStateException : BuisinessException{public IssueStateExcetipn(string code): base(code){}}
}

IssueStateException 类继承 BusinessException 类。ABP框架在请求禁用时默认返回 403 HTTP 状态码;发生内部错误是返回 500 HTTP 状态码。•code 用作本地化资源文件中的一个,用于查找本地化消息。

现在,我们可以修改 ReOpen 方法:

public void ReOpen()
{if(IsLocked){throw new IssueStateException("IssueTracking:CanNotOpenLockedIssue");}IsClosed=false;CloseReason=null;
}

建议:使用常量代替魔术字符串"IssueTracking:CanNotOpenLockedIssue"

然后在本地化资源中添加一个条目,如下所示:

"IssueTracking:CanNotOpenLockedIssue":"不能打开一个锁定的问题!请先解锁!"

•当抛出异常时,ABP自动使用这个本地化消息(基于当前语言)向终端用户显示。•异常Code("IssueTracking:CanNotOpenLockedIssue")被发送到客户端,因此它可以以编程方式处理错误情况。

实体中业务逻辑需要用到外部服务

当业务逻辑只使用该实体的属性时,在实体方法中实现业务规则是很简单的。如果业务逻辑需要查询数据库或使用任何应该从依赖注入系统中获取的外部服务时,该怎么办?请记住,实体不能注入服务

有两个方式实现:

•在实体方法上实现业务逻辑,并将外部依赖项作为方法的参数。•创建领域服务(Domain Service)

领域服务在后面介绍,现在让我们看看如何在实体类中实现它。

示例:业务规则:一个用户不能同时分配超过3个未解决的问题

public class Issue:AggregateRoot<Guid>
{//..public Guid? AssignedUserId{get;private set;}//问题分配方法public async Task AssignToAsync(AppUser user,IUserIssueService userIssueService){var openIssueCount = await userIssueService.GetOpenIssueCountAsync(user.Id);if(openIssueCount >=3 ){throw new BusinessException("IssueTracking:CanNotOpenLockedIssue");}AssignedUserId=user.Id;}public void CleanAssignment(){AssignedUserId=null;}
}

AssignedUserId 属性设置器设置为私有,通过 AssignToAsync 和 CleanAssignment 方法进行修改。•AssignToAsync 获取一个 AppUser 实体,实际上只用到 user.Id,传递实体是为了确保参数值是一个存在的用户,而不是一个随机值。•IUserIssueService 是一个任意的服务,用于获取分配给用户的问题数量。如果业务规则不满足,则抛出异常。所有规则满足,则设置 AssignedUserId 属性值。

此方法完全实现了应用业务逻辑,然而,它有一些问题:

•实体变得复杂,因为实体类依赖外部服务。•实体变得难用,调用方法时需要注入依赖的外部服务 IUserIssueService 作为参数。

==聚合和聚合根的最佳实践和原则部分完结!==

学习帮助

围绕DDDABP Framework两个核心技术,后面还会陆续发布核心构件实现综合案例实现系列文章,敬请关注!

ABP Framework 研习社(QQ群:726299208) 专注 ABP Framework 学习及DDD实施经验分享;示例源码、电子书共享,欢迎加入!

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

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

相关文章

每日一笑 | 实名举报校长拿两份工资!

全世界只有3.14 % 的人关注了数据与算法之美&#xff08;图源网络&#xff0c;侵权删&#xff09;

159个故事串起三千年大历史!这套“儿童版史记”太无敌了!

▲数据汪特别推荐点击上图进入玩酷屋17世纪英国哲学家培根说“读史使人明智”&#xff0c;意思是真实、鲜活的历史&#xff0c;不仅能极大拓宽孩子眼界&#xff0c;更能让孩子以古人为鉴&#xff0c;树立远大的志向&#xff0c;对成长大有助益。在我们的学生时代&#xff0c;认…

再见,REST,你好,gRPC

gRPC是一个开源的远程过程调用框架&#xff0c;用于服务之间的高性能通信。服务之间的通信可以使用各种语言&#xff0c;通过可插拔的负载均衡、追踪、健康检查和身份验证&#xff0c;这让它被认为是一种非常高效的方法。在默认情况下&#xff0c;gRPC使用协议缓冲&#xff08;…

全校师生放6天春假;清华大学设立天文系;郭守敬望远镜光谱数突破千万;《自然》发表最新发现;百度败诉需道歉;这就是今天的大新闻...

今天是3月29日农历二月廿三今天星期五早上上班等了N趟车愣是没挤上去下面是今天的大新闻全校师生放6天“春假”&#xff08;中国青年网&#xff09;3月28日&#xff0c;四川西南航空职业学院发布了《关于“泛美春假”的放假通知》&#xff0c;要求在放假时间总量不变的情况下&a…

jenkins java反序列化_Jenkins “Java 反序列化”过程远程命令执行漏洞

###漏洞原理反序列化是指特定语言中将传递的对象序列化数据重新恢复为实例对象的过程&#xff0c;而在这个过程中会执行一系列的字节流解析和对象实例化操作用于恢复之前序列化时的对象。在原博文所提到的那些 Java 应用里都有特定的接口用于传递序列化对象数据&#xff0c;而在…

WPF DataGrid 在Header中显示行号

在Datagrid中显示行号&#xff0c;如果你绑定的datacontext中没有序号&#xff0c;又想要显示序号的时候&#xff0c;可以按照本文的方法显示喽~效果如下图&#xff1a;来看看代码吧~MainWindow.xaml<Window x:Class"wpfcore.MainWindow"xmlns"http://schema…

每日一笑 | 爱的魔力转圈圈~

全世界只有3.14 % 的人关注了数据与算法之美&#xff08;素材源网络&#xff0c;侵权删&#xff09;

怎样维护成功的开源项目

开源可不仅仅是将代码扔到网上就万事大吉了&#xff0c;将开源项目变成能让自己引以为豪的东西才算成功。那么&#xff0c;你需要注意哪些方面呢&#xff1f; 写好指导性文字 每一个开源项目有三样东西是少不了的&#xff1a;项目目标和方法的简要说明、如何参与和授权许可。最…

技术管理中的手段——奖励和惩罚

对于技术出身的同学来说&#xff0c;一旦晋升为管理岗&#xff0c;则意味着面临很多棘手的问题&#xff0c;例如&#xff1a;手下的人不听管教&#xff0c;无法按时完成任务等等。一般凭技术晋升的同学&#xff0c;自然是技术中的佼佼者&#xff0c;作为团队中的骨干&#xff0…

用科学实验玩“坏”二十四节气,中华传统还能这么学,科学、节气、民俗三重启蒙!...

▲数据汪特别推荐点击上图进入玩酷屋之前我们“迪比的冒险之旅”和“delightmom魔幻科学实验”做完团购活动之后&#xff0c;很多妈妈都跟小木说拿回去孩子特别喜欢&#xff0c;每个实验做了好几遍还不尽兴&#xff0c;经常追问小木有没有新的实验更新。看下图孩子玩的多么开心…

java 1.8 vm_HotSpot虚拟机在java 1.8中的新实现

HotSpotJava HotSpot 虚拟机是 Java SE 平台的一个核心组件。它实现 Java 虚拟机规范&#xff0c;并作为 Java 运行时环境中的一个共享库来提供。作为 Java 字节码执行引擎&#xff0c;它在多种操作系统和架构上提供 Java 运行时设施&#xff0c;如线程和对象同步。它包括自适应…

用BenchmarkDotNet看Method

在前面的文章中看了Property的几种不同访问方式《用BenchmarkDotNet看Property》&#xff0c;性能调用上的差别明显&#xff0c;那同样作为class里重要成员&#xff0c;Method性能如何呢&#xff1f;下面是被测试方法public class MyClass{public string MyMethod(){return Dat…

美国老师用的思维导图书,真正培养孩子的思维能力!

▲数据汪特别推荐点击上图进入玩酷屋小木读大学的时候&#xff0c;无意间在图书馆接触到了东尼博赞的《思维导图》。当时有好几个朋友和我推荐过这本书&#xff0c;我就借来看了。阅读之后&#xff0c;我发现对我来说&#xff0c;这真是不可多得一本好书——它改变了我20多年的…

filesystemwatch java_C#方法的委托和java中的回调

先看个效果20130415.C#监视文件夹,显示文件夹操作到listView上代码实现,以前在学校生活写的,就几句代码using System;using System.Collections.Generic;using System.ComponentModel;using System.Data;using System.Drawing;using System.Text;using System.Windows.Forms;us…

每日一笑 | 为什么男生追到一半就不追了?

全世界只有3.14 % 的人关注了数据与算法之美&#xff08;图源网络&#xff0c;侵权删&#xff09;

转:超越设计模

转&#xff1a;http://www.ibm.com/developerworks/cn/java/j-lo-beyondpattern/刘 旭进, 软件开发工程师, IBM 中国软件开发中心简介&#xff1a; 可复用面向对象软件的基础 -- 设计模式&#xff0c;以其可复用的设计初衷、精巧的逻辑思维被广大面向对象程序设计所追捧。但不少…

为什么中国天才都往美国跑,可美国人的数学那么槽糕

中国天才少年尹希&#xff0c;17岁时收到哈佛大学博士offer&#xff0c;31岁成为哈佛最年轻华人正教授。中国年轻科学家、未来科学大奖数学与计算机奖获得者许晨阳&#xff0c;于2018年加入美国麻省理工&#xff0c;选择去世界顶尖的地方看看。22岁中国“神童”曹原&#xff0c…

如何使用VIM的Help

很多时候在用到vim的命令的时候&#xff0c;都会去网上搜索&#xff0c;殊不知&#xff0c;如果熟练使用VIM的help&#xff0c;可以达到事半功倍的效果。 下面介绍如何使用VIM的help&#xff1a; 1. 在vim的一般模式中输入&#xff1a;help可以进入vim的help界面 这里面注…

可编程智能小车,100种玩法,从3岁玩到15岁,培养孩子“最强大脑”

▲数据汪特别推荐点击上图进入玩酷屋毫无疑问&#xff0c;数学、科学和计算机科学是解决21世纪现代问题的三大支柱。当现在各式各样的兴趣班和教育辅导班快要呈现饱和状态时&#xff0c;一种新兴的教育活动正如火如荼地进行着那就是少儿编程。少儿编程奇迹般的红火&#xff0c;…

php 数组什么情况下是空的?

转载于:https://www.cnblogs.com/persist/p/3183819.html