银行转账业务场景的几种实现思路对比
ps: 由于本篇文章是我早期所写,文中的思想已经和我现在的想法完全不同了。目前我所理解的领域模型,是被调用的,应用层使用领域模型,调用领域模型中的各种构造块完成用例场景。另外,关于银行转账,我们还可以使用另一种更好的实现方案,即最终一致性的方案,通过事件驱动的流程的方式来实现转账。具体实现见ENode框架中的BankTransferSample中的代码实现:https://github.com/tangxuehua/enode
这篇文章希望通过一个银行转账的例子来和大家分享一些我最近想到的关于如何组织业务逻辑的心得和体会。当然,本人的能力和领悟有限,如有不正确之处,还希望各位看官能帮我指出来。我始终坚持一个信念,没有讨论就没有进步,任何一个非盈利为目的的人或组织始终应该把自己所学的知识共享出来与人讨论,这样不管对自己或对他人或对整个社会都大有好处。因为一个人的知识毕竟是有限的,但可以(并且也只能)和别人相互沟通交流学习来弥补这个缺陷。
转账过程简单描述银行转账的核心业务逻辑大家应该都很熟悉了,主要有这么几步:
源账户扣除转账金额,当然首先需要先判断源账户余额是否足够,如果不够,则无法转账; 目标账户增加转账金额; 为源账户生成一笔转账记录; 为目标账户生成一笔转账记录; 下面让我们来看看各种实现该业务场景的方法,并且来做一个对比。 事务脚本(Transaction Script、贫血模型)
这种方法的优缺点网上找一下一大堆,我这里也啰嗦列举一些:
容易理解,符合我们大脑过程化思考的习惯; 完全没有面向对象的思想,纯粹是面向过程式的一种组织业务逻辑的方式,所有的业务逻辑全部在一个方法中完成; 对象只包含数据而没有行为,对象只是用来被操作的“数据”,一般我们会设计很多的Item,以及ItemManager; 结构层次比较清晰,业务逻辑层和其他各层之间单项依赖;业务逻辑层中Item只代表数据,ItemManager则负责所有的业务逻辑实现,ItemManager只依赖于IDAL接口来完成持久化Item或重建Item; 由于所有的业务逻辑全部写在一个方法内,如果有另外一个需求也需要类似的业务逻辑,通常我们是写一个新的方法来实现,这样就很容易导致相同的业务逻辑出现在两个方法中,导致可维护性降低;虽然可以用一些重构的技巧或设计模式来解决重用的问题,但这往往需要开发人员具有很高的编码水平,并且往往很多时候因为时间紧迫导致不允许我们花很多时间去重构; 如果业务逻辑一旦改变,我们必须去修改实现该业务逻辑的方法,并且如果该业务逻辑在多个方法中出现,我们必须同时修改多个方法;演示代码:
![复制代码](http://common.cnblogs.com/images/copycode.gif)
2 {
3 private IBankAccountDAL bankAccountDAL;
4
5 public BankAccountManager(IBankAccountDAL bankAccountDAL)
6 {
7 this.bankAccountDAL = bankAccountDAL;
8 }
9
10 /// summary
11 /// 该方法完成转账业务逻辑
12 /// /summary
13 public void TransferMoney(Guid fromBankAccountId, Guid toBankAccountId, double moneyAmount)
14 {
15 var fromBankAccount = bankAccountDAL.GetById(fromBankAccountId);
16 var toBankAccount = bankAccountDAL.GetById(toBankAccountId);
17 if (fromBankAccount.MoneyAmount moneyAmount)
18 {
19 throw new NotSupportedException("账户余额不足。");
20 }
21 fromBankAccount.MoneyAmount -= moneyAmount;
22 toBankAccount.MoneyAmount += moneyAmount;
23
24 DateTime transferDate = DateTime.Now;
25 fromBankAccount.TransferHistories.Add(new TransferHistory
26 {
27 FromAccountId = fromBankAccountId,
28 ToAccountId = toBankAccountId,
29 MoneyAmount = moneyAmount,
30 TransferDate = transferDate
31 });
32 toBankAccount.TransferHistories.Add(new TransferHistory
33 {
34 FromAccountId = fromBankAccountId,
35 ToAccountId = toBankAccountId,
36 MoneyAmount = moneyAmount,
37 TransferDate = transferDate
38 });
39 }
40 }
41 /// summary
42 /// 银行帐号
43 /// /summary
44 public class BankAccount
45 {
46 public BankAccount() { this.TransferHistories = new List TransferHistory (); }
47 public Guid Id { get; set; }
48 public double MoneyAmount { get; set; }
49 public IList TransferHistory TransferHistories { get; set; }
50 }
51 /// summary
52 /// 转账记录
53 /// /summary
54 public class TransferHistory
55 {
56 public Guid FromAccountId { get; set; }
57 public Guid ToAccountId { get; set; }
58 public double MoneyAmount { get; set; }
59 public DateTime TransferDate { get; set; }
60 }
61 public interface IBankAccountDAL
62 {
63 BankAccount GetById(Guid bankAccountId);
64 }
![复制代码](http://common.cnblogs.com/images/copycode.gif)
这种方法的特点在网上也可以找到很多,但我也有一些其他自己的看法,见红色字体的部分:
基本是一种基于OO思想的开发方法,对象既有属性也有行为,对象之间通过相互引用和方法调用来完成对象之间的交互; 由于这是一种OO思想的设计方法,所以各种设计原则和模式都可以被充分利用; Evans对这种开发方法又作了进一步的完善,提出了:聚合、实体、值对象、服务、工厂、仓储、上下文,等这些概念;这确保我们在基于OO的思想组织业务逻辑时有了很好的指导思想; 需要特别指出的一点是,真正的Evans的DDD领域模型中的聚合根所内聚的所有值对象应该都是只读的,这一点特别重要。 基于Evans DDD的CQRS架构。这种架构的主要思想是将命令和查询分离,另一个重要的特点就是事件溯源,意思是领域对象不需要有公共的属性,只需要有行为即可,并且在任何一个行为发生后,都会触发一个事件。然后我们持久化的不是对象的状态,而是引起该对象状态改变的所有的事件。当我们需要重建一个领域对象时,只要先创建一个干净的只有唯一标识的对象,然后把和该对象相关的所有领域事件全部重新执行一遍,这样我们就得到了该对象的最终的状态了。说的简单点,就是我们不保存对象本身,而是只保存该对象的操作历史(或者叫操作日志),当我们需要重建该对象时只要”重演历史“即可。当然,为了避免性能的问题,比如因为一个对象可能会有很多的操作历史,如果每次重建该对象都是从头开始应用每个事件,那效率无疑是非常低的。因此我们使用了快照,快照保存了对象某个时刻的二进制形式(即被序列化过了)的状态。所以通常情况下,当我们要重建一个对象时都是从某个最近的快照开始回溯发生在快照之后的事件。 不管是Evans的DDD也好,CQRS架构也好,虽然都做到了让领域对象不仅有状态,而且有行为,但我觉得这还不够彻底。因为对象的行为总是“被调用”的,当现在有一个业务逻辑需要调用多个对象的一些行为来完成时,我们往往会一个一个地将对象从仓储中取出来,然后调用它们的方法。虽然Evans提出了领域服务(Service)的概念,并将一个领域对象不能完成的事情交给了领域服务去完成。但领域服务内部还是在一个个的取出对象然后调用它们的方法。这个做法在我看来和凭血模型没有本质区别,还是没有真正做到OO。因为贫血模型的情况下,对象是提供了数据让别人去操作或者说被别人使用;而充血模型的情况下,对象则是提供了数据和行为,但还是让别人去操作或者说被别人使用(数据被别人使用或方法被别人调用都是“被别人操作”的一种被动的方式)。所以从这个意义上来看对象时,我觉得贫血模型和充血模型没有本质区别。下面也给出一个实现了银行转账业务逻辑的充血模型实现:
![复制代码](http://common.cnblogs.com/images/copycode.gif)
2 /// 银行帐号, 它是一个Evans DDD中的实体, 并且是聚合根
3 /// /summary
4 public class BankAccount
5 {
6 private IList TransferHistory transferHistories;
7
8 public BankAccount() : this(Guid.NewGuid(), 0D, new List TransferHistory ()) { }
9 public BankAccount(Guid id, double moneyAmount, IList TransferHistory transferHistories)
10 {
11 this.Id = id;
12 this.MoneyAmount = moneyAmount;
13 this.transferHistories = transferHistories;
14 }
15 public Guid Id { get; private set; }
16 public double MoneyAmount { get; private set; }
17 public IList TransferHistory TransferHistories
18 {
19 get
20 {
21 return transferHistories.ToList().AsReadOnly();
22 }
23 }
24
25 public void TransferTo(Guid toBankAccountId, double moneyAmount, DateTime transferDate)
26 {
27 if (this.MoneyAmount moneyAmount)
28 {
29 throw new NotSupportedException("账户余额不足。");
30 }
31 this.MoneyAmount -= moneyAmount;
32 this.TransferHistories.Add(
33 new TransferHistory(this.Id, toBankAccountId, moneyAmount, transferDate));
34 }
35 public void TransferFrom(Guid fromBankAccountId, double moneyAmount, DateTime transferDate)
36 {
37 this.MoneyAmount += moneyAmount;
38 this.TransferHistories.Add(
39 new TransferHistory(fromBankAccountId, this.Id, moneyAmount, transferDate));
40 }
41 }
42 /// summary
43 /// 转账记录, 它是一个Evans DDD中的值对象
44 /// /summary
45 public class TransferHistory
46 {
47 public TransferHistory(Guid fromAccountId,
48 Guid toAccountId,
49 double moneyAmount,
50 DateTime transferDate)
51 {
52 this.FromAccountId = fromAccountId;
53 this.ToAccountId = toAccountId;
54 this.MoneyAmount = moneyAmount;
55 this.TransferDate = transferDate;
56 }
57
58 public Guid FromAccountId { get; private set; }
59 public Guid ToAccountId { get; private set; }
60 public double MoneyAmount { get; private set; }
61 public DateTime TransferDate { get; private set; }
62 }
63 /// summary
64 /// BankAccount聚合根对应的仓储
65 /// /summary
66 public interface IBankAccountRepository
67 {
68 BankAccount GetBankAccount(Guid bankAccountId);
69 }
70 /// summary
71 /// 转账服务, 它是一个Evans DDD中的领域服务
72 /// /summary
73 public class BankAccountService
74 {
75 private IBankAccountRepository bankAccountRepository;
76
77 public BankAccountService(IBankAccountRepository bankAccountRepository)
78 {
79 this.bankAccountRepository = bankAccountRepository;
80 }
81
82 /// summary
83 /// 该方法完成转账业务逻辑
84 /// /summary
85 public void TransferMoney(Guid fromBankAccountId, Guid toBankAccountId, double moneyAmount)
86 {
87 var fromBankAccount = bankAccountRepository.GetBankAccount(fromBankAccountId);
88 var toBankAccount = bankAccountRepository.GetBankAccount(toBankAccountId);
89
90 DateTime transferDate = DateTime.Now;
91 fromBankAccount.TransferTo(toBankAccountId, moneyAmount, transferDate);
92 toBankAccount.TransferFrom(fromBankAccountId, moneyAmount, transferDate);
93 }
94 }
![复制代码](http://common.cnblogs.com/images/copycode.gif)
这是一种根据我自己的想法而设计出来的一种设计与实现,但是离我理想中的设计还有一些距离。在我看来,真正理想的组织业务逻辑的方法或者说模型应该是这样的:
当外界需要领域逻辑的“实现模型”(简称领域模型)做某件事情时,会发出一个命令,这个命令可以理解为一个消息或者是一个事件。消息一旦创建出来后就是只读的,因为消息从某种程度上来说就是历史; 领域模型中的相关领域对象会主动响应该消息; 需要特别指出的是:我们不可以自己去获取一些相关的领域对象,然后进一步调用它们的方法而实现响应;而是应该所有可能被用到的领域对象必须好像永远已经存在于内存一样的永远在不停的在等待消息并作出响应。以银行转账作为例子,外界发出一个转账的消息,该消息会包含源帐号唯一标识、目标帐号唯一标识、转账金额这些信息。该消息的目的是希望两个两个银行帐号之间能进行转账。好了,外界要做的仅仅是发出这条消息即可。那么领域模型内部该如何去响应该消息呢?一种方法是将两个银行帐号先取出来,然后调用它们的转账方法(如TransferTo方法和TransferFrom方法)以实现转账的目的,前面的Evans的DDD的例子就是这样实现的。但这样做已经违反了我前面所说的理想的情况了。我的理想要求是,这两个银行帐号对象会像已经存在于内存一样可以直接主动去响应转账的消息,而不是转账的那两个方法(TransferTo方法和TransferFrom方法)被我们自己定义的领域服务所调用。 更加需要着重强调的是,我始终认为,真正的面向对象编程中的对象应该是一个”活“的具有主观能动性的存在于内存中的客观存在,它们不仅有状态而且还有自主行为。这里需要从两方面来解释:1)对象的状态可以表现出来被别人看到,但是必须是只读的,没有人可以直接去修改一个对象的状态,因为对象是一个在内存中的有主观意识的客观存在,它的状态必须是由它自己的行为导致自己的状态的改变。就好像现实生活中的动物或人一样,我不能强制你做什么事情,一定是我通知你(即发送消息给你),你才会做出响应并改变你自己的状态。2)对象的行为就是对象所具有的某种功能。对象的行为本质上应该是对某个消息的主动响应,这里强调的是主动,就是说对象的行为不可以被别人使用,而只能自己主动的去表现出该行为。另外,行为可以表现出来给别人看到,也可以不表现出来给别人看到。实际上,我们永远都不需要将对象的行为表现出来给别人看到,原因是别人不会去使用该行为的,行为永远只能是对象自己去表现出来。 领域模型这个生态系统中的各个领域对象在运行过程中如果需要和领域模型之外的东西(如数据持久层)交互,也应该通过消息来进行,因为只有这样才能确保领域对象是一个”活“的具有主观能动性的存在于内存中的客观存在。以上就是我心目中理想的如何设计对象来实现业务逻辑的方式。我想了很久,要完全实现上面的目标实在是太困难了。但也不是不可能,我按照我的能力,经过不断的设计、编码、测试、重构的反复循环过程。基本上设计出了一个令自己基本满意的基础框架出来,基于该框架,以银行转账为例子,我们可以以如下的方式来实现:
![复制代码](http://common.cnblogs.com/images/copycode.gif)
2 {
3 public TransferEvent(Guid fromBankAccountId, Guid toBankAccountId, double moneyAmount, DateTime transferDate)
4 {
5 this.FromBankAccountId = fromBankAccountId;
6 this.ToBankAccountId = toBankAccountId;
7 this.MoneyAmount = moneyAmount;
8 this.TransferDate = transferDate;
9 }
10 public Guid FromBankAccountId { get; private set; }
11 public Guid ToBankAccountId { get; private set; }
12 public double MoneyAmount { get; private set; }
13 public DateTime TransferDate { get; private set; }
14 }
15 public class BankAccount : DomainObject Guid
16 {
17 #region Private Variables
18
19 private List TransferHistory transferHistories;
20
21 #endregion
22
23 #region Constructors
24
25 public BankAccount(Guid customerId)
26 : this(customerId, 0D, new List TransferHistory ())
27 {
28 }
29 public BankAccount(Guid customerId, double moneyAmount, IEnumerable TransferHistory transferHistories)
30 : base(Guid.NewGuid())
31 {
32 this.CustomerId = customerId;
33 this.MoneyAmount = moneyAmount;
34 this.transferHistories = new List TransferHistory (transferHistories);
35 }
36
37 #endregion
38
39 #region Public Properties
40
41 public Guid CustomerId { get; private set; }
42 [TrackingProperty]
43 public IEnumerable TransferHistory TransferHistories
44 {
45 get
46 {
47 return transferHistories.AsReadOnly();
48 }
49 }
50 [TrackingProperty]
51 public double MoneyAmount { get; private set; }
52
53 #endregion
54
55 #region Event Handlers
56
57 private void TransferTo(TransferEvent evnt)
58 {
59 if (this.Id == evnt.FromBankAccountId)
60 {
61 DecreaseMoney(evnt.MoneyAmount);
62 transferHistories.Add(
63 new TransferHistory(
64 evnt.FromBankAccountId,
65 evnt.ToBankAccountId,
66 evnt.MoneyAmount,
67 evnt.TransferDate));
68 }
69 }
70 private void TransferFrom(TransferEvent evnt)
71 {
72 if (this.Id == evnt.ToBankAccountId)
73 {
74 IncreaseMoney(evnt.MoneyAmount);
75 transferHistories.Add(
76 new TransferHistory(
77 evnt.FromBankAccountId,
78 evnt.ToBankAccountId,
79 evnt.MoneyAmount,
80 evnt.TransferDate));
81 }
82 }
83
84 #endregion
85
86 #region Private Methods
87
88 private void DecreaseMoney(double moneyAmount)
89 {
90 if (this.MoneyAmount moneyAmount)
91 {
92 throw new NotSupportedException("账户余额不足。");
93 }
94 this.MoneyAmount -= moneyAmount;
95 }
96 private void IncreaseMoney(double moneyAmount)
97 {
98 this.MoneyAmount += moneyAmount;
99 }
100
101 #endregion
102 }
103 public class TransferHistory : ValueObject
104 {
105 #region Constructors
106
107 public TransferHistory(Guid fromAccountId,
108 Guid toAccountId,
109 double moneyAmount,
110 DateTime transferDate)
111 {
112 this.FromAccountId = fromAccountId;
113 this.ToAccountId = toAccountId;
114 this.MoneyAmount = moneyAmount;
115 this.TransferDate = transferDate;
116 }
117
118 #endregion
119
120 #region Public Properties
121
122 public Guid FromAccountId { get; private set; }
123 public Guid ToAccountId { get; private set; }
124 public double MoneyAmount { get; private set; }
125 public DateTime TransferDate { get; private set; }
126
127 #endregion
128
129 #region Infrastructure
130
131 protected override IEnumerable object GetAtomicValues()
132 {
133 yield return FromAccountId;
134 yield return ToAccountId;
135 yield return MoneyAmount;
136 yield return TransferDate;
137 }
138
139 #endregion
140 }
![复制代码](http://common.cnblogs.com/images/copycode.gif)
以上代码是转账事件、银行帐号(实体),以及转账记录(值对象)的实现代码,然后我们可以通过如下的方式来触发TransferEvent事件来让银行帐号”自动“响应。
1 EventProcesser.ProcessEvent(new TransferEvent(bankAccount1.Id, bankAccount2.Id, 1000, DateTime.Now));如果不需要增加其他的任何代码就OK了的话,那可就真美了,应该差不多可以实现我上面的目标了。但理想终归是理想,而现实的情况是:
1)领域对象的行为不可能做到别人不去调用它就能自己主动表现出来的地步,毕竟它不是一个真正的”活“的有主观能动性的人或动物;
2)领域对象并没有存在于内存中,而是在数据持久化介质中,如数据库,因此我们必须去把领域对象从数据库取出来;
那么难道我们只能放弃了吗?只能自己去做这两件事情了吗?不是,我们可以告诉基础框架如下一些信息,有了这些信息,基础框架就可以帮助我们完成上面的这两件事情了。
![复制代码](http://common.cnblogs.com/images/copycode.gif)
2 new GetDomainObjectIdEventHandlerInfo TransferEvent
3 {
4 GetDomainObjectId = evnt = evnt.FromBankAccountId,
5 EventHandlerName = "TransferTo"
6 },
7 new GetDomainObjectIdEventHandlerInfo TransferEvent
8 {
9 GetDomainObjectId = evnt = evnt.ToBankAccountId,
10 EventHandlerName = "TransferFrom"
11 }
12 );
![复制代码](http://common.cnblogs.com/images/copycode.gif)
上面的代码的意思是告诉框架1)BankAccount会去响应TransferEvent事件;2)BankAccount对象的唯一标识是从TransferEvent事件中的哪个属性中来的;3)因为这里BankAccount会有两个方法可能会响应TransferEvent事件,所以还指定了响应方法的名字从而可以区分。当然一般情况下,我们是不需要指定方法的名字的,因为大部分情况下一个对象对同一个事件只会有一个响应方法。比如下面的代码列出了很多中常见的事件与响应对象的映射信息:
![复制代码](http://common.cnblogs.com/images/copycode.gif)
2 {
3 protected override void InitializeObjectEventMappingItems()
4 {
5 //BankAccount Event Mappings.
6 RegisterObjectEventMappingItem DepositAccountMoneyEvent, BankAccount (evnt = evnt.BankAccountId);
7 RegisterObjectEventMappingItem WithdrawAccountMoneyEvent, BankAccount (evnt = evnt.BankAccountId);
8 RegisterObjectEventMappingItem TransferEvent, BankAccount (
9 new GetDomainObjectIdEventHandlerInfo TransferEvent
10 {
11 GetDomainObjectId = evnt = evnt.FromBankAccountId,
12 EventHandlerName = "TransferTo"
13 },
14 new GetDomainObjectIdEventHandlerInfo TransferEvent
15 {
16 GetDomainObjectId = evnt = evnt.ToBankAccountId,
17 EventHandlerName = "TransferFrom"
18 }
19 );
20
21 //Topic Event Mappings.
22 RegisterObjectEventMappingItem DomainObjectAddedEvent Reply , Topic (evnt = evnt.DomainObject.TopicId);
23 RegisterObjectEventMappingItem DomainObjectRemovedEvent Reply , Topic (evnt = evnt.DomainObject.TopicId);
24
25 //ForumUser Event Mappings.
26 RegisterObjectEventMappingItem PreAddDomainObjectEvent Topic , ForumUser (evnt = evnt.DomainObject.CreatedBy);
27 RegisterObjectEventMappingItem DomainObjectAddedEvent Topic , ForumUser (evnt = evnt.DomainObject.CreatedBy);
28
29 //Reply Event Mappings.
30 RegisterObjectEventMappingItem DomainObjectRemovedEvent Topic , Reply (evnt = Repository.Find Reply (new FindTopicRepliesEvent(evnt.DomainObject.Id)));
31 }
32 }
![复制代码](http://common.cnblogs.com/images/copycode.gif)
关于这种组织业务逻辑的方法,大家如果有仔细研究的兴趣,可以下载我的框架源代码和聚合演示例子源代码。
http://files.cnblogs.com/netfocus/EventBasedDDDExample.rar好了,大家觉得这三种组织业务逻辑的方法如何呢?很想听听大家的声音。我是一个喜欢思考问题、寻找真理的人,期望能和大家多多交流。
零售交易信息处理思路 零售行业的交易信息呈现无序杂乱,再加上数据采集难度大,业务人员繁琐,拉通不齐等问题,处理起来会相对费时,这里是我整理的一部分思路,可能有不全的地方,也欢迎大佬指正。
支付设计白皮书:支付系统的对账系统设计 可以说,对账是支付系统最头疼的事情。每一笔交易,都要做到各参与者的记录能够吻合,没有偏差。对账系统的工作,是发现有差异的记录,即轧帐;然后通过人工或者自动的方式,解决这些差异,即平帐。
美团点评智能支付核心交易系统的可用性实践(上) 每个系统都有它最核心的指标。比如在收单领域:进件系统第一重要的是保证入件准确,第二重要的是保证上单效率。清结算系统第一重要的是保证准确打款,第二重要的是保证及时打款。我们负责的系统是美团点评智能支付的核心链路,承担着智能支付100%的流量,内部习惯称为核心交易。因为涉及美团点评所有线下交易商家、用户之间的资金流转,对于核心交易来说:第一重要的是稳定性,第二重要的还是稳定性。
美团点评智能支付核心交易系统的可用性实践(下) 每个系统都有它最核心的指标。比如在收单领域:进件系统第一重要的是保证入件准确,第二重要的是保证上单效率。清结算系统第一重要的是保证准确打款,第二重要的是保证及时打款。我们负责的系统是美团点评智能支付的核心链路,承担着智能支付100%的流量,内部习惯称为核心交易。因为涉及美团点评所有线下交易商家、用户之间的资金流转,对于核心交易来说:第一重要的是稳定性,第二重要的还是稳定性。
境外银行数据治理——思路分享 前言:在数据治理领域的文章大多都是从业务角度进行切入,本文从纯技术角度切入,介绍了数据血缘关系的图分析思路、领域建模层面构建中间层的思路。以及纯粹从词法分析和语法分析的统计学层面维度。
交易撮合引擎:原理与实现 交易撮合引擎(Matching/Trading Engine),顾名思义是用来撮合交易的软件,广泛地应用在金融、证券、加密货币交易等领域。交易引擎负责管理加密资产市场中所有的开口订单(Open Orders),并在发现匹配的订单对(Trading Pair)时自动执行交易。
netfocus 对DDD领域驱动设计感兴趣,在.NET/JAVA平台都有多年工作经验。架构方面专注于CQRS/Event Souring/EDA架构的研究和框架开发。热衷于开源,拥有两个个人开源项目:ENode,EQueue
相关文章
- 性能优化与测试:性能分析的思路
- Python经典算法-猴子吃桃-思路分析
- java实现遍历树形菜单方法——设计思路【含源代码】
- Java实现 LeetCode 672 灯泡开关 Ⅱ(数学思路问题)
- Infos - 信息分析思路概要
- php之购物车类思路及代码
- MySQL:错误代码1215 无法添加外键约束的解决思路
- 前端动态菜单权限、按钮权限实现思路
- SAP 产品一脉相承的 UI 增强思路,在 SAP Commerce Cloud(电商云) UI 增强实现中的体现
- SAP 产品一脉相承的 UI 增强思路,在 SAP Commerce Cloud(电商云) UI 增强实现中的体现
- ML之XGBoost:XGBoost算法模型(相关配图)的简介(XGBoost并行处理)、关键思路、代码实现(目标函数/评价函数)、安装、使用方法、案例应用之详细攻略
- ML之分类预测:机器学习中多分类预测数据集可视化(不同类别赋予不同颜色)设计思路及代码实现
- ML之GB:GB算法相关论文、相关思路、关键步骤、代码实现、配图集合、案例应用之详细攻略
- 动手实操丨RC522射频卡模块与IC卡完成充值消费查询的技术实现思路
- Fp关联规则算法计算置信度及MapReduce实现思路
- 政府採购电子办公系统总结-思路篇
- 游戏中每日刷新实现思路浅析
- lucene中facet实现统计分析的思路——本质上和word count计数无异,像splunk这种层层聚合(先filed1统计,再field2统计,最后field3统计)lucene是排序实现
- 【2022年江西省研究生数学建模】冰壶运动 思路分析及代码实现
- 程序员的算法趣题Q56: 鬼脚图中的横线(思路1)
- ZYNQ之路--制作AXI4 接口IP的思路