为什么DDD很难?

DDD为什么很难实施 领域驱动设计(Domain Driven Design)的概念已经被发明了十多年,而且也不乏相关著作,但是业界宣称自己应用了DDD原则的项目,软件却鲜有耳闻。随着微服务架构的流行,DDD在边界识别,服务划分等方面不断被提及,作为一种应对复杂软件的方法论,似乎又被重视起来了。 那么,为什么这个听起来很靠谱的方法论实际上很难实施呢?我们以DDD创始人Eric Evans的经典书籍《领域驱动设计:软件核心复杂性应对之道》为例,分析一下,可能的原因是这样的: 新概念数量比较多 战术/战略都有所涉及 不同层次的概念混杂(除了设计模式之外,还有架构风格的讨论)在一起 繁复而混杂的概念 领域,子域,核心子域,通用子域,实体,值对象,领域服务,应用服务,领域事件,统一语言,衔接上下文,遵循者等等。DDD中有着大量的新概念,而且这些概念有些是技术相关的,有些是问题相关的,交织在一起之后,很难理清头绪。 DDD中的模式又分为战术的和战略的两大部分,有很多团队应用了战术相关的,比如实体,值对象,领域服务,仓库等模式,代码看似DDD,实则与DDD强调的以领域为中心相去甚远,陷入了开发者太过于关注技术本身的老路上,这种现象还有个专门的名词,叫DDD-Lite。 不同的思维方式要求 DDD要求读者既有具体Coding的技能,有需要跳出圈外,以架构师的角度来审视整个系统。DDD需要开发者以一个全新的视角去认识软件开发,强调对业务流程的熟悉,强调与领域专家一起协作,强调软件的表达能力和进化能力。而这些思维方式的转变,都有很大阻力的(想想从面向对象世界切换到函数式编程,再到响应式函数编程的切换)。 It should be noted that no ethically-trained software engineer would ever consent to write a DestroyBaghdad procedure. Basic professional ethics would instead require him to write a DestroyCity procedure, to which Baghdad could be given as a parameter. – Nathaniel Borenstein 我自己在工作的前4年,非常反感业务,认为case by case的业务流程会严重影响我的代码的通用性。然而事实是,纯粹通用的代码是不存在的。毕竟,诸如IoC容器,Web等基础设施已经相当完善,完全无需我们重复发明轮子。而作为应用开发人员,更重要的是在充分理解业务的前提下,写出易于维护,易于扩展,可以更快速响应业务变化的代码。 正因为如此,DDD在一定程度上会让程序员感到不适,它太强调领域了,而领域又是独一无二的,每个公司的每个系统都有其独立性,这就要求你的代码可能没法做到纯粹。 正确姿势 要解决上面提到的这些问题,正确的实施DDD来指导实际的开发工作,需要至少做到这样几个事情。首先,明确分清问题和方案(这是初学DDD者最容易犯的错误);其次,转变思维方式,将软件开发的重心放在梳理并明确业务需求上,而不是代码逻辑上;最后,需要应用一些合理的工程实践来促成DDD-Lite的落地。 分清问题和解决方案 我们说领域(子域,通用子域,支撑子域等)的时候,是在讨论问题域,即定义我们要解决的问题是什么。而说到限界上下文,聚合,实体,仓库则是在讨论解决方案部分,人们很容易将两者搞混。 在电商的领域中,一些典型的问题是: 如何获得更多的客户 如何让客户更快速的找到自己想要的商品 如何准确的推荐相关产品给客户 用户如何付费 要解决这些问题,人们可能会开发出来一个软件系统,也可能会用手工的流程,也可能是混合模式。同样,一些具体的解决方案的例子是:...

January 11, 2017 2 min

软件开发为什么很难

问题的分类 最初在1999年被Dave Snowden开发出来的**Cynefin框架**尝试把世界上的问题划分到了5个域中(大类): 简单(Simple)问题,该域中的因果关系非常明显,解决这些问题的方法是 感知-分类-响应(Sense-Categorise-Respond),有对应的最佳实践 复合(Complicated)问题,该域中的因果关系需要分析,或者需要一些其他形式的调查和/或专业知识的应用,解决这些问题的方法是感知-分析-响应(Sense-Analyze-Respond),有对应的好的实践 复杂(Complex)问题,该域中的因果关系仅能够从回顾中发现,解决这些问题的方法是探索-感知-响应(Probe-Sense-Respond),我们能够感知涌现实践(emergent practice) 混乱(Chaotic)问题,该域中没有系统级别的因果关系,方法是行动-感知-响应(Act-Sense-Respond),我们能够发现新颖实践(novel practice) 失序(Disorder)问题,该域中没有因果关系,不可感知,其中的问题也无法被解决 显然,软件开发过程更多地是一个复杂(Complex)问题。在一个产品被开发出来之前,不确定性非常高,团队(包括业务人员和技术人员)对产品的知识也是最少的,而且需要大量的学习和尝试才可以明确下一步可能的方向。不幸的是,很多时候我们需要在一开始(不确定性最高的时候)就为项目做计划。这种从传统行业中非常适合的方法在软件开发领域不再适用,这也是敏捷开发、精益等方法论在软件开发中更加适合的原因。 来源:http://alistair.cockburn.us/Disciplined+Learning 正因为软件开发事实上是一个学习的过程,我们学习到的新知识反过来会帮助我们对问题的定义,从而带来变化。这里的变化可能来自两个方向: 功能性 非功能性 功能性的变化指随着对业务的深入理解、或者已有业务规则为了匹配市场而产生的变化。比如支付方式由传统的货到付款变成了网银付款,又变成了微信支付、支付宝扫码等等。一个原始的电商平台仅仅提供基本的购物服务,但是后来可以根据已有数据产生推荐商品,从来带来更大的流量。这些变化需要体现在已有的代码中,而对代码的修改往往是牵一发而动全身。 非功能性的变化是指随着业务的发展,用户规模的增加,数据量的变化,安全认知的变化等产生的新的需求。比如100个用户的时候无需考虑性能问题,但是100万用户的时候,性能就变成了必须重视的问题。天气预报应用的数据安全性和网络银行的数据安全性要求也大不相同。 而在业务提出一个需求的时候,往往只是一个简化过的版本。 非功能性复杂性 来源:https://d13yacurqjgara.cloudfront.net/users/749341/screenshots/2228676/uielements_day021_dribbbleinvites.jpg 这是一个经过设计师精确设计的界面,在它被设计出来之前,用户事实上无法准确的描述出它。设计过程中经历了很多的诸如: 线框图 颜色的确定 交互的动画 信息层次 往复多次之后,界面确定了。在没有仔细思考使用场景的时候,开发会误以为这个功能非常简单。但是如果你是一个有经验的开发者,很快会想到的一些问题是: 在宽屏下如何展示 在平板上如何展示 在手机上如何展示 即使仅仅支持桌面版,跨浏览器要考虑吗?支持哪些版本? 有些UI效果在低版本的浏览器上不工作,需要Shim技术 除此之外,依然有大量的其他细节需要考虑: 性能要求是什么样的? 安全性要考虑吗? 在网络环境不好的时候,要不要fallback到基础视图? 既然涉及发送邀请函,送达率如何保证 与外部邮件服务提供商集成时的工作量 等等。这些隐含的信息需要被充分挖掘出来,然后开发者才能做一个合理的评估,而且这还只是开始。一旦进入开发阶段,很多之前没有考虑到的细节开始涌现:字体的选用,字号,字体颜色,元素间的间距等等,如何测试邮件是否发送成功,多个角色之间的conversation又会消耗很多时间。 需求的变化方向 作为程序员,有一天你被要求写一段代码,这段代码需要完成一件很简单的事: 打印"Hello, world"5次 很容易嘛,你想,然后顺手就写下了下面这几行代码: print("Hello, world") print("Hello, world") print("Hello, world") print("Hello, world") print("Hello, world") 不过,拷贝粘贴看起来有点低端,你做了一个微小的改动: for(var i = 0; i < 5; i++) { print("Hello, world") } 看起来还不错,老板的需求又变成了打印"Goodbye, world"5次。既然是打印不同的消息,那何不把消息作为参数呢?...

January 6, 2017 1 min

你需要的编程练习

高效幻象 通过对自己的行为观察,我发现在很多时候,我以为我掌握了的知识和技能其实并不牢靠。我引以为豪的高效其实犹如一个彩色的肥皂泡,轻轻一碰就会破碎,散落一地。 你可能只是精通搜索 我们现在所处的时代,信息爆炸,每个人每天都会接触,阅读很多的信息,快速消费,快速遗忘。那种每天早上起来如同皇帝批阅奏折的、虚假的误以为掌握知识的错觉,驱动我们进入一个恶性循环。 即使在我们真的打算解决问题,进行主动学习时,更多的也只是在熟练使用搜索引擎而已(在一个领域待久了,你所使用的关键字准确度自然要比新人高一些,仅此而已)。精通了高效率搜索之后,你会产生一种你精通搜索到的知识本身的错觉。 如何写一个Shell脚本 在写博客的时候,我通常会在文章中配图。图片一般会放在一个有固定格式的目录中,比如现在是2016年5月,我本地就会有一个名为$BLOG_HOME/images/2016/05的目录。由于使用的是markdown,在插入图片时我就不得不输入完整的图片路径,如:/images/2016/05/stack-overflow.png。但是我又不太记得路径中的images是单数(image)还是复数(images),而且图片格式又可能是jpg,jpeg,gif或者png,我也经常会搞错,这会导致图片无法正确显示。另外,放入该目录的原始文件尺寸有可能比较大,我通常需要将其缩放成800像素宽(长度无所谓,因为文章总是要从上往下阅读)。 为了自动化这个步骤,我写了一个小的Shell脚本。当你输入一个文件名如:stack-overflow.png后,它会缩放这个文件到800像素宽,结果是一个新的图片文件,命名为stack-overflow-resized.png,另外它将符合markdown语法的文件路径拷贝到剪贴板里:/images/2016/05/stack-overflow-resized.png,这样我在文章正文中只需要用Command+V粘贴就可以了。 有了思路,写起来就很容易了。缩放图片的命令我是知道的: $ convert -resize 800 stack-overflow.png stack-overflow-resized.png 但是要在文件明上加入-resized,需要分割文件名和文件扩展名,在Bash里如何做到这一点呢?Google一下: FULLFILE=$1 FILENAME=$(basename "$FULLFILE") EXTENSION="${FILENAME##*.}" FILENAME="${FILENAME%.*}" convert -resize 800 $FULLFILE $FILENAME-resized.EXTENSION 难看是有点难看,不过还是可以工作的。接下来是按照当前日期生成完整路径,date命令我是知道的,而且我知道它的format格式很复杂,而且跟JavaScript里Date对象的format又不太一样(事实上,世界上有多少种日期工具,基本上就有多少种格式)。再Google一下: $ date +"/images/%Y/%m/" 最后一步将路径拷贝到剪贴板也容易,Mac下的pbcopy我也会用:echo一下字符串变量,再管道到pbcopy即可: PREFIX=`date +"/images/%Y/%m/"` echo "$PREFIX$FILENAME-resized.EXTENSION" | pbcopy 但是将内容粘贴到markdown里之后,我发现这个脚本多了一个换行。我想这个应该是echo自己的行为吧,会给字符串自动加上一个换行符。Google一下,发现加上-n参数就可以解决这个问题。 好了,完整的脚本写好了: #!/bin/bash FULLFILE=$1 FILENAME=$(basename "$FULLFILE") EXTENSION="${FILENAME##*.}" FILENAME="${FILENAME%.*}" convert -resize 800 $FULLFILE $FILENAME-resized.EXTENSION PREFIX=`date +"/images/%Y/%m/"` echo -n "$PREFIX$FILENAME-resized.EXTENSION" | pbcopy 嗯,还不错,整个过程中就用了我十几分钟时间而已,以后我在写博客时插入图片就方便多了! 不过等等,好像有点不对劲儿,我回过头来看了看这段脚本:7行代码只有1行是我独立写的!没有Google的话,查看man date和man echo也可以解决其中一部分问题,不过文件扩展名部分估计又得花较长时间。 仔细分析一下,之前的成就感荡然无存。 更多的例子 我相信,过几周我再来写这样一个简单的脚本时,上面那一幕还是会出现。开发者的IDE的外延已经将Google和Stack Overflow集成了。很难想象没有这两个IDE的插件我要怎样工作。 其实除此之外,日常工作中这样的事情每时每刻都在发生: Ansible里如何创建一个给用户robot读写权限的目录? Python 3中启动简单HTTPServer的命令是? Spring Boot的Gradle String是? Mongodb中如何为用户robot授权? Gulp里一个Task依赖另一个Task怎么写? 等等等等,这个列表可以根据你的技术栈,偏向前端/后端的不同而不同,但是相同的是在Google和Stack Overflow上搜索,阅读会浪费很多时间,而这些本来都是可以避免的。...

May 26, 2016 1 min