
Rafy 框架作者。(https://github.com/zgynhqf/rafy) 专注于:领域驱动设计、面向服务架构、插件化架构、模型驱动架构、产品线工程、快速界面生成。
本文为转载 心理导读:今天为大家分享几个实用的工具,来源网络。 SWOT分析法 Strengths:优势 Weaknesses:劣势 Opportunities:机会 Threats:威胁 意义:帮您清晰地把握全局,分析自己在资源方面的优势与劣势,把握环境提供的机会,防范可能存在的风险与威胁,对我们的成功有非常重要的意义。 PDCA循环规则 Plan:制定目标与计划; Do:任务展开,组织实施; Check:对过程中的关键点和最终结果进行检查; Action:纠正偏差,对成果进行标准化,并确定新的目标,制定下一轮计划。 意义:每一项工作,都是一个pdca循环,都需要计划、实施、检查结果,并进一步进行改进,同时进入下一个循环,只有在日积月累的渐进改善中,才可能会有质的飞跃,才可能取得完善每一项工作,完善自己的人生。 6W2H法 What:工作的内容和达成的目标; Why:做这项工作的原因; Who:参加这项工作的具体人员,以及负责人; When:在什么时间、什么时间段进行工作; Where:工作发生的地点 ; Which:哪一种方法或途径; How:用什么方法进行; How much:需要多少成本? 意义:做任何工作都应该从6W2H来思考,这有助于我们的思路的条理化,杜绝盲目性。我们的汇报也应该用6W2H,能节约写报告及看报告的时间。 SMART原则 Specific 具体的; Measurable 可测量的; Attainable 可达到的; Relevant 相关的; Time based 时间的; 意义:人们在制定工作目标或者任务目标时,考虑一下目标与计划是不是SMART化的。只有具备SMART化的计划才是具有良好可实施性的,也才能指导保证计划得以实现。 特别注明: 有的又如此解释此原则: S代表具体(Specific),指绩效考核要切中特定的工作指标,不能笼统; M代表可度量(Measurable),指绩效指标是数量化或者行为化的,验证这些绩效指标的数据或者信息是可以获得的; A代表可实现(Attainable),指绩效指标在付出努力的情况下可以实现,避免设立过高或过低的目标; R代表现实性(realistic),指绩效指标是实实在在的,可以证明和观察; T代表有时限(time bound),注重完成绩效指标的特定期限。 时间管理-重要与紧急 A、重要且紧急 紧急状况 迫切的问题 限期完成的工作 你不做其他人也不能做 B、重要不紧急 准备工作 预防措施 价值观的澄清 计划 人际关系的建立 真正的再创造 增进自己的能力 C、紧急不重要 造成干扰的事、电话、 信件、报告 会议 许多迫在眉捷的急事 符合别人期望的事 D、不重要不紧急 忙碌琐碎的事 广告函件 电话 逃避性活动 等待时间 优先顺序=重要性*紧迫性在进行时间安排时,应权衡各种事情的优先顺序,要学会“弹钢琴”。 对工作要有前瞻能力,防患于未然,如果总是在忙于救火,那将使我们的工作永远处理被动之中。 任务分解法[WBS] 即Work Breakdown Structure,如何进行WBS分解:目标→任务→工作→活动 WBS分解的原则: 将主体目标逐步细化分解,最底层的任务活动可直接分派到个人去完成;每个任务原则上要求分解到不能再细分为止。 WBS分解的方法: 至上而下与至下而上的充分沟通; 一对一个别交流; 小组讨论。 WBS分解的标准: 分解后的活动结构清晰; 逻辑上形成一个大的活动; 集成了所有的关键因素包含临时的里程碑和监控点; 所有活动全部定义清楚。 意义:学会分解任务,只有将任务分解得足够细,您才能心里有数,您才能有条不紊地工作,您才能统筹安排您的时间表。 二八原则 巴列特定律:“总结果的80%是由总消耗时间中的20%所形成的。” 按事情的“重要程度”编排事务优先次序的准则是建立在“重要的少数与琐碎的多数”的原理的基础上。 举例说明: 80%的销售额是源自20%的顾客; 80%的电话是来自20%的朋友; 80%的总产量来自20%的产品; 80%的财富集中在20%的人手中; 这启示我们在工作中要善于抓主要矛盾,善于从纷繁复杂的工作中理出头绪,把资源用在最重要、最紧迫的事情上。
时过6年,再看起这个视频 《一个22岁黑社会青年说的话》,发现依然如此令人动容。 视频道理虽然简单,但是并不是所有人都真正的理解。视频道理虽然简单,但是并不是所有人都真正的理解。我身边就遇到好多的小孩、好多的年轻男女,走的其实就是主角的老路。经历固然不会完全一样,但是换汤不换药:不听长者言,执迷不悟、自以为是…… 所以今天把这个视频分享出来,希望能帮助到这些需要帮助的人。 何为“命”:人一叩(叩首)。改变命运:人一认错,命运就改变了!!! 有空的还可以学习一下弟子规。我百度了一下:弟子规:古人留下的传统文化,老少皆宜。 读《弟子规》不是为了比别人,就是为了自我提升和宁静: 1、弟子规告诉你人生获得幸福的基本修为是什么。 2、通过反复读诵弟子规,可以进行自我反省。 3、通过反复读诵弟子规,让自己静心。 几年后的完整版在这里:《http://v.youku.com/v_show/id_XMjgwODAzMzQ4OA==.html》。这几年,这兄弟一直在弘扬弟子规!虽然过了这么多年,讲到真情流露处时,他依然还是会哽咽…… 最后,理了一下视频中几个引用的内容: 己有能,勿自私。 事非宜,勿轻诺,若轻诺,进退错。 君子乐得做君子,小人枉自做小人。 两个石头的故事!
最近一个月,我一直在一个项目上“救火”。这个项目已经做了一年多了,一直没有交付,最近几个月更是不断地出现问题,导致客户经常给老板打电话……无奈之下,老板安排我放下部门的其它工作,亲自奔赴一线,带领研发团队完成交付。 到现场已经一个月了,这段时间整个团队基本上都是 200% 的工作强度,没有周六日,每晚最早 12 点下班,经常搞到两三点,早上则必须 9 点按时上班。虽然目前并没有真正地交付,不过经常这段时间的努力和团队的不断调整,能够感觉到这个项目正在逐步地好转。上周五,给客户演示的核心开发内容,也第一次没有被客户“批”回来。团队也逐渐地有了信心,但是真正的第一次完整交付,还需要等到 11 月 15 日,到时候才能知道我们的努力会不会有成果。 今天,趁着飞机上有一些时间,我决定整理一下对于“火坑项目”的救火方案。这样,形成经验与模式,方便后续的重用。 救火步骤 经过本项目,我整理出救火大概分为以下几个步骤: 救火准备与计划 紧急问题处理 系统问题处理 救火总结 救火准备与计划 真正抵达火坑项目一线之前,应该做一些简要的规划。我建议使用一个脑图,简要整理出这个项目的以下内容: 项目背景、救火任务目标、现场调研方向与实际问题、结果物梳理、具体工作任务安排。 这样,有的放矢,比到了项目上一头乱麻很太多了。 紧急问题处理 到了救火现场,其实第一要务应当是解决当下最紧急的问题。这些问题往往是客户想要“骂娘”的紧急问题。 这一点是我在这个项目上遗漏的地方。我到了现场之后,重心放在处理整体、全局问题上,而忽略了客户当下紧要却不重要的问题。导致客户又打了几次电话给老板…… 系统问题处理 紧急问题处理完善之后。我们更需要系统性地解决整个项目组的问题。这才是问题的关键。 这个环节分几步: 系统、全面、客观地了解项目问题 有针对性地制定解决方案 全员宣贯 执行与监控 要真正的系统解决问题,往往需要全面、客观、细微地了解整个项目的所有问题。我认为这需要至少持续一周以上的沟通、一线工作。只是通过一天两天,和几个人简单地快速沟通,是不能看到很多真实的问题的。 其次,可以使用脑图的方式,制定出有优先级、有重要度、可落地的执行方案。并向全体团队成员宣贯、执行。 下面是我在这个项目上的问题梳理与方案建设,由于涉及到具体项目及具体人员,一些信息不便展开,大概看一下结构就可以了。 救火总结 对于出了比较多问题的火坑项目,救火完成后,我们应该对这个项目的问题、方案、后续计划进行总结。并需要总结出后续项目需要注意的关键关注点。 先总结到这里,希望这个项目能如期按质按量上线。
上周末参加了道宝《公众演讲》的学习。下面对主要内容进行总结。 明确演讲的目的 首先要想清楚,本次演讲的目的是什么…… 处理演讲前的紧张 腹式呼吸法 用腹部呼吸,而不是使用胸部。 内在感知模式 感受自身,感受内在,而不要被外部的环境影响。 演讲演练 我坐在椅子上,感受身体的平衡。 我感受到双手的重量 我慢慢地把手放在身体的两侧,感受到双手的重量 我轻轻地站起来,向前一小步,感受到双手的重量 我慢慢地走向讲台,感受到双手的重量 我慢慢转身,面向观众,感受到双手的重量 啪啪啪(停顿手拍三下的时间) …… 啪啪啪(停顿手拍三下的时间) 我慢慢走向座位,感受到双手的重量 放松的小技巧和建议 提前到达演讲场地,不要赶在最后一分钟匆匆到场。迟到会使你很难集中精力。 在重要的面试或演讲前一天晚上,就把要穿的衣服准备好。衣服应该熨平,然后干净、平民地挂在衣柜里。 不要在演讲前几个小时内喝咖啡、糖。这些会另你加速产生肾上腺素。 重要的演讲需要演练。计时演练。 把自己的演练录下来,看看是否有“嗯……”“啊……”。这些词过多,演讲会失去吸引力。如果有此问题,平时稍微注意一下就好,这个习惯慢慢就会消失了。 问答环节,可以重复一下问题,可以为自己争取时间。不要看着问问题的人,而是望着全场。 演讲参考框架 开场白 可以是问题、趣闻、逸事、统计数据、对比介绍。也可以忽略此章节。 主题句 本次演讲的主题。 模板:今天(停顿)我想和大家谈谈(停顿)…… 信息句 信息句表明了演讲的观点,是演讲最核心的部分、主心骨。需要简短、清晰并且慢速地讲出来。 背景信息 个人背景信息模板: 我开始对……产生兴趣/进行关注大约是在……年前。当时我……就在那个时候,我开始意识到(停顿)……比如说 (停顿)……所以(停顿)我…… 组织背景信息模板: 围绕自己或自己的公司展开故事。 ……(日期)……第一次引起了我们的注意(停顿)。当时我们的公司/部门/处/分公司正在……比如说(停顿)……它还要(停顿)……结果(停顿)……今天(停顿)……(大停顿) 历史背景信息模板: 用一个故事给您的主题提供上下文。 ……的历史开始于……当时(停顿)……逐渐地(停顿)……比如说(停顿)……结果(停顿)……今天(停顿)……(大停顿) 要点清单 第一步:头脑风暴,列出相关的子标题。 第二步:筛选。 第三步:排列顺序。 在谈论(主题)……的过程中,我将讨论5从此问题它们是: (大停顿)…… (大停顿)…… (大停顿)…… (大停顿)…… (大停顿)最后是:…… 图像法练习 可以将每个要点想像为一幅图片来帮助记忆。 要点 参考模板: 模板1 在任何有关……的讨论当中,值得注意的是……比如(停顿)……相关研究/经验告诉我们……而且(停顿)……这让我们相信(停顿)……有些评论会说……而我们对此的回应是(停顿)……(大停顿) 模板2 我相信(重复信息句)……因为(停顿)……对我们来说,理解(此要点)……是很重要的。因此……比如说(停顿)……通常情况下……而且……结果(停顿)……所以,我觉得……(大停顿) 模板3 ……的意义/重要必近来变得越来越明显,因为……结果……比如说(停顿)……我们的立场/目标/建议/提议是……通常来说(停顿)……比如(停顿)……这就导致……所以我、我们觉得……(大停顿) 模板4 很信想像……但是……比如说(停顿)……仍然存在希望。而这个希望来自于……事实上……其他的因素……而且(停顿)……很可能是真的。有关专家质疑……事实是……最后……(大停顿) 模板5 (说明时间)……我有机会……。当时(停顿)……非常有趣的是……比如说(停顿)……而且(停顿)……我在对些正在考虑……的人的建议是……从这次经历中,我意识到……以前(停顿)……因此(停顿)……(大停顿) 模板6 试图……可以说是最……的之一。我第一次意识到这一点是在(具体时间)……当我还是……。以前我从来没想到……。比如说(停顿)……所以我不得不……而且(停顿)……我意识到……最后(停顿),我……这导致……今天(停顿),我很高兴/难过/激动地说……(大停顿) 高潮 最后一个要点。 最好能有趣、有戏剧性。 修改到满意为止。 问答 在总结之前,进行问答环节。 参考模板: “在我总结之前,大家还有什么问题吗?” “谢谢大家的提问!”然后再做总结。 如果预计会有一个挑战性大的问答环节,那么把你不想被问题牟5个难以回答的问题写下来。 然后再对这5个问题,想一些完善的答案。可以寻求别人的帮助。 准备好以后,就变得轻而易举了。 结论 只总结前面的内容,不添加新的内容。 参考模板: 最后(停顿)我想提醒/要求/建议/推荐/请求大家……因为(停顿)……我相信,你们将会非常快乐/惊讶/高兴/充实/满足地发现……(大停顿) 在讨论(重复演讲的主题)……的过程中,我向大家介绍了: (大停顿)…… (大停顿)…… (大停顿)…… (大停顿)…… (大停顿)最后是:…… (大停顿) 综上所述,想象一下…… (大停顿)
上个月公司组织了每年一次的年中团建。本次团建的形式是:军训! 过程中训练强度较大。回家以后,发现一身是伤,全身疼痛! 除了身体上的锻炼,思考上也有一些斩获。下面,将本次军训过程中一些思考的内容进行一个整理、反思。 (有些内容可能不太适合在博客写出来。但是我一直认为,反思、总结、博客,主要是写给自己的,写给未来的自己的。每一次总结,都把自己往前推了一步!所以,只要对自己有帮助,有总结的意义,就应该写出来。) 领导力 我们四连在做翻牌游戏的时候,连续三次尝试都没有成功翻出所有的 13 张牌。总体感觉上发现,现场有点乱。过程中,连长、政委、我在都在努力维持局势,而且还有成员会不断的插嘴。没有人去组织成员出发的顺序和翻牌的顺序?整体上,其实表现为没有领导专注于游戏的控场。在多个游戏的过程中,团队都表现出了缺失一些领导力、凝聚力。在玩虎口脱险的时候,连长非常的犹豫。不断地询问大家,谁先过,谁后过。当团队成员你一言我一语的提出建议时,他就会更加犹豫。 我作为部门领导,在这次团建中,我给大家明确提出不做连长、不做政委。因为我的想法很简单,还是要让员工有学习的机会、成长的机会、担当的机会、表现的机会。让他们能够换位思考,站在领导立场去体会领导他人时的心态。 后来我发现,这样使得我们团队的战斗力大打折扣。在其他很多团队中,其实有很多连队的连长本身就是公司内部的领导。这样的连队体现出来的战斗力都是不错的!特别是一连,在老梁的带领下,展示出了很强的战斗力、高效的协作能力、坚定的执行力。老梁本身,就是一个执行力、纪律性、规范性都非常强的一位橙色领导。(橙色的概念,见:《 4D卓越团队-两天培训总结 》) 我们四连在连续做了几个游戏之后,感觉成绩都不是特别好。所以在最后一个游戏的时候,连长、政委都推荐我来做军师。其实,后来我这个军师基本上已经篡权了,俺就成了连长啊,哈哈。我在做最后的扫雷游戏的时候,就完全按照强势连长的风格,领导了团队: 所有人向我汇报,不再过多讨论。 任何人不能随意打断,全部向我汇报。我让你说,你就说,我不让你说,你就站好别动。 由我来听取所有人建议后,直接决策。 团队成员,我安排做什么,你就做什么,不要过问太多。 我把整个的团队分成几个小组:扫雷组、成功路径记录组、失败方格记录组、间谍组、后勤小组,以及现场维稳人员。每个小组有自己的职责,干好自己的事。 我将来承担结果的主要责任。 经过组织架构的调整后,发现在游戏的过程中,相对原来的团队要好了很多,前期沟通也少了很多废话、节省了时间,协作也更高效。结果也不错,我们连是第一个一次性就通过游戏的团队。 不过,也有需要反思的地方。我在整体任务的执行过程中,对最终的目标没有明确得非常清楚。我们就想成功通过游戏,但是更高的目标是需要在更短的时间内完成任务。我们的整个执行策略都没有太多考虑时间的因素,导致时间维度上表现一般。其次,各小组分工明确,在执行过程中,也额外的消耗了一些时间来沟通;这相对于其它连队的“敢死队、不断安排人员往前冲”策略,耗时更多。 翻牌游戏的设计上,其实运气的成分很高。(有些团队翻到最后一张牌,才翻出来“1”。)所以我们要做的就是想尽一切办法,提高组织的运转效率。其它的就交给老天吧…… 小结:组织在管理过程中,领导的作用是非常大的。一个好的领导需要有足够的经验与自信,要有一定的权威、领导力、影响力,并对任务整体进行规划与控制:对任务整体进行整体宏观分析(清晰任务规则与目标、明确任务策略、制定行动方针);对团队进行分工,设计组织架构;对分歧进行决策;对任务结果承担责任。而团队成员,则需要对领导的命令坚决执行到底! 短板效应 其中,有一个换杆前行的游戏(具体名字不记得了~):8个人围成一圈,每人一个杆竖立在身前,需要同时放开自己的杆并拿到前一个人的杆,连续8次即算获胜。 这个游戏中,我们也表现得不好。我现在反思起来,实际上体现了一个短板效应。 在游戏中的任何人都必须成功的往前移动,并抓住前一位队友的杆子,任何人出现失误没有抓到,则整个游戏重来。所以,其中只要一个人出错,整个团队就算失败,不论之前成功了几次。这是明显的短板效应,而我作为智囊,也没有分辨出来。 我们团队中的一个人,不断地出错。但是,我们只是在他一直出现问题的时候,快速的告诉他应该怎么做,也没有过多的责备他,选择相信队友。但是,他依然在不停的出错。过程中他确实有所改善。但是直到比赛的时候,我才发现了这个问题很严重。 一个人的短板,直接形成了整个团队的短板。 这与平时的一些工作,也是有些类似的。例如一个产品出现了 BUG,虽然这个 BUG 是一个能力较差的实习生编写的代码造成的,但是整个团队的产品,在别人的眼中就是错误百出!改进方法:合团队之力量,来克服短板、战胜短板。如果游戏再来一次的话,我们应该先拿出时间,好好就这个人对他进行一次问题的检查和修正,这样的话我相信他个人的能力会成长得很快。 九头牛的故事 教练还给我们讲了一个“九头牛的故事”。找了一下,故事原版见:《九头牛的故事》。 感悟: 我常常以一些很高的标准去要求我的团队、或者某些人的时候,往往他们不太会理解。为什么要使自己变得这么累?这个细节也没有人注意到,为什么我要做这么好? 你以高标准要求自己,逐渐的,你就会成为高标准的这样一个人;你以什么标准要求自己,你就是一个什么标准的人! 故事在演讲中的作用 最后一天下午,专业的教练给我们进行了一个小时左右的演讲,过程中,我还发现我平时在说话时的一个缺点。 我往往在给讲一些道理时、或者我开会时,都喜欢讲一些过于抽象、严谨的东西。这跟我个人的习惯相关,我比较注重体系、理论、原理。当别人给我讲一个故事的时候,往往我也不太记得住故事本身,而只是记住了故事所想要表达的思想、原理。这种抽象思维,在用于学习、理解时,是有用的,但是用于表达却非常不利。当我给别人讲的时候,道理讲得多,故事讲得少,引经据曲就更少了。 以后的讲话过程中,可以多用一些故事来引题,这是因为: 一个由浅入深的故事往往会把听众带入到这个环节当中,然后再由听众自己慢慢地挖掘它内在的哲理,最后,当演讲者来揭示其中的哲理之时,会与听众形成一种强烈的共鸣,从而为听众留下很深的印象! 孝道 教练其实今天说了很多经典的话。其中一句关于孝道,在这里记一下: 何为孝道? 首孝:孝父母之志。也就是说,你的事业、成就,要让父母引以为傲。 次孝:孝父母之心。要让父母感到放心、安心、顺心。 三孝:孝父母之身。孝养父母。 我个人反思一下:做到了前两孝,但是第三孝却没有! 上台讲话 我在最后的一个环节中,由于教练的演讲太过煽情,不禁让我想起了很多很多的往事,我和妻子的互相信任、我和家里的关系,以及我对父母未尽之孝道。我感触良多,就上台去做了一个简短的讲话。大概讲了两件事情: 未尽之孝道 小时候父母视我为掌上明珠。特别是我的妈妈,把一切能给我的都给我,我就是她的一切、她的希望、她的未来、她的骄傲。父母,对你是绝对无私的奉献,真心的爱! 我在十五岁的时候,就离乡背井,为的是学业、成长。 我的家乡在云南,离我学习、工作的城市都非常的远。早些年回家的时候,下了火车,还得坐十个小时的汽车,才能到家。所以自那以后,十五、六年的时间里,我都很少能回家。特别是在工作以后,我每年只能回家一次!而且一次也就只能呆几天时间。 我比一些同学相对要好的地方是,我回到家以后,在仅有的那几天,每天都会和父母一起坐在一起促膝长谈,直到深夜…… 但是……时间还是太短了!!! 看着父母渐渐变得苍老,我深深地觉得自己有很多未尽之孝道! 我很努力的工作、成长,第一个目标,为的是将来有一天,我可以拥有绝对的自由,我和我爱的人能够朝夕相处、共享美好人生! 我正在逐渐靠近这个目标…… 团队之教诲 前一个话题,我讲的有些动情,在台上已经有一点抽泣,流了一些眼泪。后来,我收拾了一下情绪,开始说我对部门的教诲、对人员的要求。 “只有父母会对你无私的奉献”,这句话是错的!教练说到,每个人的周围,总会有一些家人、朋友、同事,会不断地给你提出好的建议、指正你的缺点,他们不求回报,只是希望你变得更好! 作为领导,我对研究院的各位同学,也算是用尽了心血!我不断地教导所有人,要学习、要成长、要反思、要总结。花时间和老何一起商量各种让大家成长的方式:学习积分、晚校、分享、反思。但是,只有很少的人真正听进去了。我花了很多的时间去准备,给大家讲了一些文化、倡导的价值观,“合作、责任、主动、求知”,也只有很少的几个人能体会,能身体力行。 我告诉我自己,我有责任去指导后辈,去教导大家。但是成长的最大受益者,其实是你自己。在我眼中,很多同事就小孩一样,不论是专业能力、还是软技能,都有很多东西需要学习,有很长的路要走。但是他们却把大把大把最宝贵的时间,浪费在了玩乐之上。 人,是很难被改变的! 研究院一开始只有几个人,最多的时候有六七十个人。很多同事,已经进到了公司其它部门、其它团队。来来往往的人中,大部分的,其实我感觉并没有太大的变化,没有高标准要求自己、养成学习的习惯、倡导的文化(可能是我没有看到)。 我多少会觉得有些可惜…… 接下来,我会更加严厉,继续以“九头牛”的高标准来要求所有人!哪怕只有那很少的几个人有所变化,哪怕,没有一个人有变化! 照片 补一些照片。要不,感觉好枯燥啊…… 总结了不少。 补一下,这次团队的口号是:“留血、留汗、不留泪;掉皮、掉肉、不掉队!”。 每次这种虐心的拓展训练,总能或多或少地带来一些思考、成长。不虚此行! 不过,这次团建的缺点也很明显,感觉少了一个很重要的元素:开心与快乐。
转载一张开源许可的说明图,涉及了几个常见的开源协议的选择,非常易理解。
你有个任务,需要用到某个开源项目;或者老大交代你一个事情,让你去了解某个东西。怎么下手呢?如何开始呢?我的习惯是这样: 1.首先,查找和阅读该项目的博客和资料,通过google你能找到某个项目大体介绍的博客,快速阅读一下就能对项目的目的、功能、基本使用有个大概的了解。 2.阅读项目的文档,重点关注类似Getting started、Example之类的文档,从中学习如何下载、安装、甚至基本使用该项目所需要的知识。 3.如果该项目有提供现成的example工程,首先尝试按照开始文档的介绍运行example,如果运行顺利,那么恭喜你顺利开了个好头;如果遇到问题,首先尝试在项目的FAQ等文档里查找答案,再次,可以将问题(例如异常信息)当成关键词去搜索,查找相关的解决办法,你遇到了,别人一般也会遇到,热心的朋友会记录下解决的过程;最后,可以将问题提交到项目的邮件列表,请大家帮你看看。在没有成功运行example之前,不要尝试修改example。 4.运行了第一个example之后,尝试根据你的理解和需要修改example,测试高级功能等。 5.在了解基本使用后,需要开始深入的了解该项目。例如项目的配置管理、高级功能以及最佳实践。通常一个运作良好的项目会提供一份从浅到深的用户指南,你并不需要从头到尾阅读这份指南,根据时间和兴趣,特别是你自己任务的需要,重点阅读部分章节并做笔记(推荐evernote)。 6.如果时间允许,尝试从源码构建该项目。通常开源项目都会提供一份构建指南,指导你如何搭建一个用于开发、调试和构建的环境。尝试构建一个版本。 7.如果时间允许并且有兴趣,可以尝试阅读源码: (1)阅读源码之前,查看该项目是否提供架构和设计文档,阅读这些文档可以了解该项目的大体设计和结构,读源码的时候不会无从下手。 (2)阅读源码之前,一定要能构建并运行该项目,有个直观感受。 (3)阅读源码的第一步是抓主干,尝试理清一次正常运行的代码调用路径,这可以通过debug来观察运行时的变量和行为。修改源码加入日志和打印可以帮助你更好的理解源码。 (4)适当画图来帮助你理解源码,在理清主干后,可以将整个流程画成一张流程图或者标准的UML图,帮助记忆和下一步的阅读。 (5)挑选感兴趣的“枝干”代码来阅读,比如你对网络通讯感兴趣,就阅读网络层的代码,深入到实现细节,如它用了什么库,采用了什么设计模式,为什么这样做等。如果可以,debug细节代码。 (6)阅读源码的时候,重视单元测试,尝试去运行单元测试,基本上一个好的单元测试会将该代码的功能和边界描述清楚。 (7)在熟悉源码后,发现有可以改进的地方,有精力、有意愿可以向该项目的开发者提出改进的意见或者issue,甚至帮他修复和实现,参与该项目的发展。 8.通常在阅读文档和源码之后,你能对该项目有比较深入的了解了,但是该项目所在领域,你可能还想搜索相关的项目和资料,看看有没有其他的更好的项目或者解决方案。在广度和深度之间权衡。 本文系转载,转载自:《如何熟悉一个开源项目?》
上周六参加了《一言以蔽之 十年架构之路 架构大会》,简单写一篇随笔,记录一些要点的 PPT。 会议日程如下: 第一场:如何应对架构的高复杂度 结构分解是永久的主旋律 微服务架构,将系统的复杂度从应用中移动到底层通用平台中。 康威定律:软件系统的结构应该匹配组织架构。 风险驱动模型 & 质量属性。 约束驱动。 第二场:微服务架构实战-京东开放式平台架构演变 从 1.0 到 3.0 的架构演变: 三个案例: 关键语句:想到抗量,就要想到redis! 第三场:58 同城的转转 IM 架构 添加移动端之后的架构。 第四场:SOA 服务治理的经验 第五场:深度学习的集群方案 人工智能中的神经网络算法,涉及到过多的分支。所以一般都使用多核(几千个核心)的 GPU 进行运算,而不是 CPU。 后来因为有事,提前离开了会场……
大好的周末,决定写一篇读书笔记。:) 最近读了一些股票估值以及价值投资相关的文章和书籍。今天将其中的一本做一些笔记以及简单的总结。 该书名为《Complete Guide to Value Investing》,是 Doctor Wealth 公司出品的。 书籍目录结构 下面,我将重点章节进行简单总结。 概念及历史 本书前5章讲了价值投资的概念以及相关历史。 历史 下图,说明了价值投资主要理论的发展,以及相关几个大牛: 全是投资界大名鼎鼎的人物! 巴菲特的建议 其中,也说明了 Warren Buffet 所使用的两类投资策略,Cigar Butt Strategy & Value Investing。 值得注意的是,巴菲特在早期没有太多资本时,使用的正是雪茄烟蒂投资法。这种投资方法能够在资本量较少时,更好地获利。但是缺点也是很明显的,当资本越来越多时,该方法就不太易用了。 My cigar-butt strategy worked very well while I was managing small sums. Indeed, the many dozens of free puffs I obtained in the 1950s made that decade by far the best of my life for both relative and absolute investment performance. But a major weakness in this approach gradually became apparent: Cigar-butt investing was scalable only to a point. With large sums, it would never work well. 详情可见:《https://www.drwealth.com/2015/04/02/should-you-invest-in-cigar-butt-stocks/》。巴菲特给出建议,作为中小投资者,应该如何投资股票: If he were a small investor, he would pick Graham type stocks If he were a small investor, he would have more advantage (too many opportunities:more small companies he could buy and make money.) If he were a small investor, he would diversify across many stocks 巴菲特认为,如果他是小额度投资者,使用 Cigar Butt 投资法,他一年至少可以做到 50% 的收益率。 There are three points to glean from what he said, He has made higher percentage returns on his capital buying the cigar butt stocks. It is advantageous to have smaller capital. He has to stop investing in cigar butt stocks because his capital has grown too big. 关于 Cigar Butt,见:《雪茄烟蒂 》及《如何理解巴菲特的“烟蒂”论》。 “步步高”创始人段永平就是“雪茄烟蒂”理论的受益者。他于2001年底斥资200万美元(其中包括一些融资),以0.8美元-1美元的价格,在纳斯达克市场买进约200多万股网易股票。这只股票后来给段永平带来的回报差不多有1亿美元。 “网易的股票每股含2美元多的现金,才卖不到1美元”,这就是段永平买入的理由。这是不折不扣的“雪茄烟蒂式投资”。 但在中国A股市场寻找“雪茄烟蒂式投资”是困难的,严格来说A股最近十几年来也没有出现过像网易那样的投资案例。 通用术语 Intrinsic Value:固有价值。 Margin of Safety:安全边际。 Undervalued or Overvalued:低估或高估。 Alpha:回报相对于市场平均值的相对比。https://zhidao.baidu.com/question/2075895179181355508.html Beta:风险相对于市场平均值的相对比。https://zhidao.baidu.com/question/2075895179181355508.html。 EBIT:税前收益(Earnings Before Interest and Taxes)。 EBITDA:未计利息、税项、折旧及摊销前的利润(Earings Before Interest, Taxes, Depreciation and Amortization)。 CAPEX:资本支出(Capital Expenditures)。 财务术语 Revenue for Sales:销售收入。 Expenses:费用。 Profits:收益。 Current assets:流动资产。 Current liabilities:流动负债、经常性贷款。 Equity:book value Or Net Asset Value(NAV)、净资产。 Paid in capital:实收资本。 Retained earnings:留存收益。 Cash flow from operations:经营产生的现金流量。 Cash flow from Investments:投资现金流。 Cash flow from financing activities:筹资活动产生的现金流量。 Free Cash Flow:自由现金流。 = CFO - Capital Expenditures. 价值投资者必备的8个财务指标 下面的一些概念比较简单,在这里不作过多解释。想了解细节的同学,可以直接百度~ PE(Price Earnings) 市盈率。 越低越好。 始终在变。 只体现过去,不体现未来。 P/FCF(Price to Free Cash Flow) 收入好造假,现金流却不易造假。 PEG(Price Earnings Growth Rate) PE / Annual Earnings Per Share (EPS) Growth Rate. 同时关注过去的投资价值,以及未来的成长价值。 PEG 小于 1,表示股票被低估。 难点,在于 EPS 的增幅只能使用估算。越是了解企业内部信息,越是知道企业未来的成长率。 PB(Price to Book or Price to Net Asset Value) 市净率。 DA/DE(Debt to Asset or Debt to Equity) 资产负债率。 Current Ratio、Quick Ratio 流动比率、速动比率 Current Ratio = Current Assets / Current Liabilities. Quick Ratio = (Current Assets - Inventory) / Current Liabilities. Payout Ratio 股息支付率、派息率、分红比率。 Management Ownership Percentage 管理层股权比例。 对小公司适用。 价值投资的特征 Irrational Market 非理性市场,由非理性的投资人组成。这样,才会有高估和低估。 Intrinsic Value 每个股票都有其内在的价值。 Margin of Safety 类似于:Risk involved。 与风险承受度相关。 不论分析得有多深入,都不能保证股票会按你预想的方向前进。特别是在一个非理性的市场中。所以,安全边际越大,风险承受能力就越大。 Benjamin Graham 只投资价格是价值的 2/3 的股票。 Time and Effort 估值,需要时间。 找好的股票,需要时间。 股票的价值被市场认可,需要时间。这一项,有时甚至需要好几年。也是投资者认为最难的一项。 Contrarian 反向操作。 不跟随市场情绪。 估值策略 “低买高卖”,是一件最简单的事,每个人都知道要这么做,但是也是一件最难的事,因为很多人都做不到。要真正做到低买高卖的第一个前提,就是找出高估、低估所相对的一个值,也就是计算出股票真正的内在价值。这,也就是估值技术。 书中提出了下面的几种估值技术: Net Net Strategy (Benjamin Graham's Investing Strategy) 股价 < 2/3 * (流动资产 - 总负债) / 总股本. 适用此策略的公司的一些特征: 不受欢迎:这样大多数投资者不关注的股票,有被低估的趋势。 低流动性:因为投资者较少,所以卖家少,买家难买到股票,所以买家也不多。 小公司:投资者往往认为小公司的风险较高。但是,其实很多小公司的经济能力要比大公司强。 常常带有问题:此类公司面临一些短期的问题,导致股份下跌。一旦问题解决,股份必然上涨。 Net Asset Value (NAV) Valuation 股价 < (总资产 - 总负债) / 总股本. 相对上一策略 Net Net Strategy,更加激进一些。 其实就是股份应该要小于每股净资产。 Discounted Cash Flow (DCF) Valuation 折算现金流估值法:将未来所有的现金流折现后的股价。 因为要估计未来每一年的现金流,以及每一年的折现率。所以此法难以计算。好像巴菲特也算不出来。所以此法一般不推荐。 适用场景:现金流非常稳定的公司。 类似的策略还有 Discounted Earings Per Share(EPS)。 Price/Earnings to Growth (PEG) Ratio (Peter Lynch's Investing Strategy) 在上面讲到 PEG 概念时,其实已经把此估值策略简单描述了。 PEG(Price Earnings Growth Rate) = PE / Annual Earnings Per Share (EPS) Growth Rate. 此估值法适用于增长型公司。“市盈率应该与该公司的增长率相等”。也就是年增长8%的公司,合适的市盈率就是 8。 PEG < 1 说明股票被低估,PEG > 1 则表示被高估。 Conservative Net Asset Value (CNAV) (Dr Wealth's Investing Strategy) CNAV 策略,是 Dr Wealth 公司使用的估值策略。该方法包含两个度量指标,以及三步量化分析: 指标1:CNAV CNAV 是更加保守的每股净资产算法,这样就添加了一些理加的安全边际。 计算方式:全额计入现金(cash)、所有权(property),以及下列资产的一半将会被计入:设备、应收款、投资、库存、无形资产(income generating intangles such as operating rights and customer relationships. Goodwill and other non-income generating intangibles are exxcluded)。 指标2:POF 分值 此分值包含三个方面: Portability:在考虑净资产的同时,也需要适当考虑盈利。 Operating Efficiency:现金流不但证明企业的盈利能力,间接证明产品被社会认可的价值。同时,负的现金流也会导致企业的净资产不断降低。同时,企业很可能需要增加更多的债务,这会影响更多投资者。 Financial Position:企业不样不能有过多的负债。否则,一旦利率升高,又会较大的影响现金流。 3步进行量化估算。 第一步:在年报之后,检查企业公告及企业的行为。 年报公布的时间,往往在年报日期之后的三四个月。这正好给投资者提供了审查年报的机会。 我们需要检查这段时间内,企业是否有大的变化。 一些主要影响 CNAV 的关键事件是: 股本的变化。(附权发行、可转债等会稀释股东权益) 大量的分红。(大量现金派发,导致 CNAV、NAV 降低) 大量的收购或资产剥离。 大量举债,例如企业债券。 第二步:分清你正在购买的主要资产是什么 正如:指标1:CNAV 中所给出的计算公式。需要分清该公司的主要资产是什么。如果公司的主要资产在某一项之上,则应该考虑是否合理。 例如,如果应收款过高,就应该考虑公司是否能够让其客户真正付款? 同样,如何库存过高,那这些产品不应该是易“腐坏”的,也要考虑企业能够多久消化这些库存? 第三步:确定管理的确定性 由于我们的投资决策都是基于企业管理层发布的数据来进行计算的。那么公告的真实性,完全依赖于管理层的诚信。 如果小企业的管理层本身能够持有较大的股权,那么我们认为他们的公告和行为应该是一致的。例如,持有 50% 以上的股权。 常见问题 投资者最常见的问题是,对同一支股票使用不同的估值方法在不同的时间进行估值。 例如,买入的时候使用 NAS,卖出的时候使用DCF…… 这是估值的第一规则:买入、卖出时,对股票使用同一个估值策略。 其次: 学习一个较为完整的估值技术,并不断地完善该方法。 实战 作者在 How is Value Investing Like 一节中,使用了一个较为完整的例子,来说明 Dr Wealth 公司是如何使用 CNAV 策略来进行真实的企业投资的。这里就不再赘述了,有兴趣的同学直接看原书吧。 书籍下载地址 本书的下载地址是:https://www.drwealth.com/value-investing-guide/ 其次,Dr Wealth 公司也给出了一些其它的好书:https://www.drwealth.com/start-here/。 后话 其实,我也不知道 Dr Wealth 公司有多大名气,但是看上去是一个投资咨询公司。我是在偶然的机会下发现他们的网站和书籍的。觉得很不错,就下载了几本来学习。 没想到这本书总结了这么久。本来觉得已经看过一遍了,只需要把重点总结一下,应该要不了多久时间的。结果差不多花了整整一天…… 总结一遍,严重地发现其实只看一遍不加总结的话,其实跟没有看是差不多的。学习到的东西非常之少!其实今天总结的很多概念,当时觉得自己看明白了,转过头来其实就已经忘记了!写完本文总结之后,发现整个体系就清晰很多了,而且所有的概念几乎都已经固化在脑子里面了。 后续,我还会接着总结几篇最后看过的关于估值的文章。并尽早建立自己的估值策略,并将其系统化、固化、程序化,形成最终我自己的 估值自动化系统。:)
最近 Rafy 开源中心 启动刚一个月,在初始的讨论会上,成员们对面向对象设计、领域驱动设计等概念展开了大量的讨论。 下面我转载一篇文章,这篇文章的详细内容我都还没看完。不过,文章的结构正是我想要的!其结构非常清晰,很好地说明了领域驱动设计相关的起源、重点、模式、经典架构,以及一些后人扩展的新概念。 转载自《DDD领域驱动设计基本理论知识总结》,并稍微调整了一下内容顺序。 为什么面向对象比面向过程更能适应业务变化 对象将需求用类一个个隔开,就像用储物箱把东西一个个封装起来一样,需求变了,分几种情况,最严重的是大变,那么每个储物箱都要打开改,这种方法就不见得有好处;但是这种情况发生概率比较小,大部分需求变化都是局限在一两个储物箱中,那么我们只要打开这两个储物箱修改就可以,不会影响其他储物柜了。 而面向过程是把所有东西都放在一个大储物箱中,修改某个部分以后,会引起其他部分不稳定,一个BUG修复,引发新的无数BUG,最后程序员陷入焦头烂额,如日本东京电力公司员工处理核危机一样,心力交瘁啊。 所以,我们不能粗粒度看需求变,认为需求变了,就是大范围变,万事万物都有边界,老子说,无欲观其缴,什么事物都要观察其边界,虽然需求可以用“需求”这个名词表达,谈到需求变了,不都意味着最大边界范围的变化,这样看问题容易走极端。 其实就是就地画圈圈——边界。我们小时候写作文分老三段也是同样道理,各自职责明确,划分边界明确,通过过渡句实现承上启下——接口。为什么组织需要分不同部门,同样是边界思维。画圈圈容易,但如何画才难,所以OO中思维非常重要。 需求变化所引起的变化是有边界,若果变化的边界等于整个领域,那么已经是完全不同的项目了。要掌握边界,是需要大量的领域知识的。否则,走进银行连业务职责都分不清的,如何画圈圈呢? 面向过程是无边界一词的(就算有也只是最大的边界),它没有要求各自独立,它可以横跨边界进行调用,这就是容易引起BUG的原因,引起BUG不一定是技术错误,更多的是逻辑错误。分别封装就是画圈圈了,所有边界都以接口实现。不用改或者小改接口,都不会牵一发动全身。若果面向过程中考虑边界,那么也就已经上升到OO思维,即使用的不是对象语言,但对象已经隐含其中。说白了,面向对象与面向过程最大区别就是:分解。边界的分解。从需求到最后实现都贯穿。 面向对象的实质就是边界划分,封装,不但对需求变化能够量化,缩小影响面;因为边界划分也会限制出错的影响范围,所以OO对软件后期BUG等出错也有好处。 软件世界永远都有BUG,BUG是清除不干净的,就像人类世界永远都存在不完美和阴暗面,问题关键是:上帝用空间和时间的边界把人类世界痛苦灾难等不完美局限在一个范围内;而软件世界如果你不采取OO等方法进行边界划分的话,一旦出错,追查起来情况会有多糟呢? 软件世界其实类似人类现实世界,有时出问题了,探究原因一看,原来是两个看上去毫无联系的因素导致的,古人只好经常求神拜佛,我们程序员在自己的软件上线运行时,大概心里也在求神拜佛别出大纰漏,如果我们的软件采取OO封装,我们就会坦然些,肯定会出错,但是我们已经预先划定好边界,所以,不会产生严重后果,甚至也不会出现难以追查的魔鬼BUG。 领域驱动设计之领域模型 加一个导航,关于如何设计聚合的详细思考,见这篇文章。 2004年Eric Evans 发表Domain-Driven Design –Tackling Complexity in the Heart of Software (领域驱动设计),简称Evans DDD。领域驱动设计分为两个阶段: 以一种领域专家、设计人员、开发人员都能理解的通用语言作为相互交流的工具,在交流的过程中发现领域概念,然后将这些概念设计成一个领域模型; 由领域模型驱动软件设计,用代码来实现该领域模型; 由此可见,领域驱动设计的核心是建立正确的领域模型。 为什么建立一个领域模型是重要的 领域驱动设计告诉我们,在通过软件实现一个业务系统时,建立一个领域模型是非常重要和必要的,因为领域模型具有以下特点: 领域模型是对具有某个边界的领域的一个抽象,反映了领域内用户业务需求的本质;领域模型是有边界的,只反应了我们在领域内所关注的部分; 领域模型只反映业务,和任何技术实现无关;领域模型不仅能反映领域中的一些实体概念,如货物,书本,应聘记录,地址,等;还能反映领域中的一些过程概念,如资金转账,等; 领域模型确保了我们的软件的业务逻辑都在一个模型中,都在一个地方;这样对提高软件的可维护性,业务可理解性以及可重用性方面都有很好的帮助; 领域模型能够帮助开发人员相对平滑地将领域知识转化为软件构造; 领域模型贯穿软件分析、设计,以及开发的整个过程;领域专家、设计人员、开发人员通过领域模型进行交流,彼此共享知识与信息;因为大家面向的都是同一个模型,所以可以防止需求走样,可以让软件设计开发人员做出来的软件真正满足需求; 要建立正确的领域模型并不简单,需要领域专家、设计、开发人员积极沟通共同努力,然后才能使大家对领域的认识不断深入,从而不断细化和完善领域模型; 为了让领域模型看的见,我们需要用一些方法来表示它;图是表达领域模型最常用的方式,但不是唯一的表达方式,代码或文字描述也能表达领域模型; 领域模型是整个软件的核心,是软件中最有价值和最具竞争力的部分;设计足够精良且符合业务需求的领域模型能够更快速的响应需求变化; 领域通用语言(UBIQUITOUS LANGUAGE) 我们认识到由软件专家和领域专家通力合作开发出一个领域的模型是绝对需要的,但是,那种方法通常会由于一些基础交流的障碍而存在难点。开发人员满脑子都是类、方法、算法、模式、架构,等等,总是想将实际生活中的概念和程序工件进行对应。他们希望看到要建立哪些对象类,要如何对对象类之间的关系建模。他们会习惯按照封装、继承、多态等面向对象编程中的概念去思考,会随时随地这样交谈,这对他们来说这太正常不过了,开发人员就是开发人员。但是领域专家通常对这一无所知,他们对软件类库、框架、持久化甚至数据库没有什么概念。他们只了解他们特有的领域专业技能。比如,在空中交通监控样例中,领域专家知道飞机、路线、海拔、经度、纬度,知道飞机偏离了正常路线,知道飞机的发射。他们用他们自己的术语讨论这些事情,有时这对于外行来说很难直接理解。如果一个人说了什么事情,其他的人不能理解,或者更糟的是错误理解成其他事情,又有什么机会来保证项目成功呢? 在交流的过程中,需要做翻译才能让其他的人理解这些概念。开发人员可能会努力使用外行人的语言来解析一些设计模式,但这并一定都能成功奏效。领域专家也可能会创建一种新的行话以努力表达他们的这些想法。在这个痛苦的交流过程中,这种类型的翻译并不能对知识的构建过程产生帮助。 领域驱动设计的一个核心的原则是使用一种基于模型的语言。因为模型是软件满足领域的共同点,它很适合作为这种通用语言的构造基础。使用模型作为语言的核心骨架,要求团队在进行所有的交流是都使用一致的语言,在代码中也是这样。在共享知识和推敲模型时,团队会使用演讲、文字和图形。这儿需要确保团队使用的语言在所有的交流形式中看上去都是一致的,这种语言被称为“通用语言(Ubiquitous Language)”。通用语言应该在建模过程中广泛尝试以推动软件专家和领域专家之间的沟通,从而发现要在模型中使用的主要的领域概念。 将领域模型转换为代码实现的最佳实践 拥有一个看上去正确的模型不代表模型能被直接转换成代码,也或者它的实现可能会违背某些我们所不建议的软件设计原则。我们该如何实现从模型到代码的转换,并让代码具有可扩展性、可维护性,高性能等指标呢?另外,如实反映领域的模型可能会导致对象持久化的一系列问题,或者导致不可接受的性能问题。那么我们应该怎么做呢? 我们应该紧密关联领域建模和设计,紧密将领域模型和软件编码实现捆绑在一起,模型在构建时就考虑到软件和设计。开发人员会被加入到建模的过程中来。主要的想法是选择一个能够恰当在软件中表现的模型,这样设计过程会很顺畅并且基于模型。代码和其下的模型紧密关联会让代码更有意义并与模型更相关。有了开发人员的参与就会有反馈。它能保证模型被实现成软件。如果其中某处有错误,会在早期就被标识出来,问题也会容易修正。写代码的人会很好地了解模型,会感觉自己有责任保持它的完整性。他们会意识到对代码的一个变更其实就隐含着对模型的变更,另外,如果哪里的代码不能表现原始模型的话,他们会重构代码。如果分析人员从实现过程中分离出去,他会不再关心开发过程中引入的局限性。最终结果是模型不再实用。任何技术人员想对模型做出贡献必须花费一些时间来接触代码,无论他在项目中担负的是什么主要角色。任何一个负责修改代码的人都必须学会用代码表现模型。每位开发人员都必须参与到一定级别的领域讨论中并和领域专家联络。 领域建模时思考问题的角度 “用户需求”不能等同于“用户”,捕捉“用户心中的模型”也不能等同于“以用户为核心设计领域模型”。 《老子》书中有个观点:有之以为利,无之以为用。在这里,有之利,即建立领域模型;无之用,即包容用户需求。举些例子,一个杯子要装满一杯水,我们在制作杯子时,制作的是空杯子,即要把水倒出来,之后才能装下水;再比如,一座房子要住人,我们在建造房子时,建造的房子是空的,唯有空的才能容纳人的居住。因此,建立领域模型时也要将用户置于模型之外,这样才能包容用户的需求。 所以,我的理解是: 我们设计领域模型时不能以用户为中心作为出发点去思考问题,不能老是想着用户会对系统做什么;而应该从一个客观的角度,根据用户需求挖掘出领域内的相关事物,思考这些事物的本质关联及其变化规律作为出发点去思考问题。 领域模型是排除了人之外的客观世界模型,但是领域模型包含人所扮演的参与者角色,但是一般情况下不要让参与者角色在领域模型中占据主要位置,如果以人所扮演的参与者角色在领域模型中占据主要位置,那么各个系统的领域模型将变得没有差别,因为软件系统就是一个人机交互的系统,都是以人为主的活动记录或跟踪;比如:论坛中如果以人为主导,那么领域模型就是:人发帖,人回帖,人结贴,等等;DDD的例子中,如果是以人为中心的话,就变成了:托运人托运货物,收货人收货物,付款人付款,等等;因此,当我们谈及领域模型时,已经默认把人的因素排除开了,因为领域只有对人来说才有意义,人是在领域范围之外的,如果人也划入领域,领域模型将很难保持客观性。领域模型是与谁用和怎样用是无关的客观模型。归纳起来说就是,领域建模是建立虚拟模型让我们现实的人使用,而不是建立虚拟空间,去模仿现实。 以Eric Evans(DDD之父)在他的书中的一个货物运输系统为例子简单说明一下。在经过一些用户需求讨论之后,在用户需求相对明朗之后,Eric这样描述领域模型: 一个Cargo(货物)涉及多个Customer(客户,如托运人、收货人、付款人),每个Customer承担不同的角色; Cargo的运送目标已指定,即Cargo有一个运送目标; 由一系列满足Specification(规格)的Carrier Movement(运输动作)来完成运输目标; 从上面的描述我们可以看出,他完全没有从用户的角度去描述领域模型,而是以领域内的相关事物为出发点,考虑这些事物的本质关联及其变化规律的。上述这段描述完全以货物为中心,把客户看成是货物在某个场景中可能会涉及到的关联角色,如货物会涉及到托运人、收货人、付款人;货物有一个确定的目标,货物会经过一系列列的运输动作到达目的地;其实,我觉得以用户为中心来思考领域模型的思维只是停留在需求的表面,而没有挖掘出真正的需求的本质;我们在做领域建模时需要努力挖掘用户需求的本质,这样才能真正实现用户需求; 关于用户、参与者这两个概念的区分,可以看一下下面的例子: 试想两个人共同玩足球游戏,操作者(用户)是驱动者,它驱使足球比赛领域中,各个“人”(参与者)的活动。这里立下一个假设,假设操作者A操作某一队员a,而队员a拥有着某人B的信息,那么有以下说法,a是B的镜像,a是领域参与者,A是驱动者。 设计领域模型的一般步骤 根据需求建立一个初步的领域模型,识别出一些明显的领域概念以及它们的关联,关联可以暂时没有方向但需要有(1:1,1:N,M:N)这些关系;可以用文字精确的没有歧义的描述出每个领域概念的涵义以及包含的主要信息; 分析主要的软件应用程序功能,识别出主要的应用层的类;这样有助于及早发现哪些是应用层的职责,哪些是领域层的职责; 进一步分析领域模型,识别出哪些是实体,哪些是值对象,哪些是领域服务; 分析关联,通过对业务的更深入分析以及各种软件设计原则及性能方面的权衡,明确关联的方向或者去掉一些不需要的关联; 找出聚合边界及聚合根,这是一件很有难度的事情;因为你在分析的过程中往往会碰到很多模棱两可的难以清晰判断的选择问题,所以,需要我们平时一些分析经验的积累才能找出正确的聚合根; 为聚合根配备仓储,一般情况下是为一个聚合分配一个仓储,此时只要设计好仓储的接口即可; 走查场景,确定我们设计的领域模型能够有效地解决业务需求; 考虑如何创建领域实体或值对象,是通过工厂还是直接通过构造函数; 停下来重构模型。寻找模型中觉得有些疑问或者是蹩脚的地方,比如思考一些对象应该通过关联导航得到还是应该从仓储获取?聚合设计的是否正确?考虑模型的性能怎样,等等; 领域建模是一个不断重构,持续完善模型的过程,大家会在讨论中将变化的部分反映到模型中,从而是模型不断细化并朝正确的方向走。领域建模是领域专家、设计人员、开发人员之间沟通交流的过程,是大家工作和思考问题的基础。 领域驱动设计的经典分层架构 用户界面/展现层 负责向用户展现信息以及解释用户命令。更细的方面来讲就是: 请求应用层以获取用户所需要展现的数据; 发送命令给应用层要求其执行某个用户命令; 应用层 很薄的一层,定义软件要完成的所有任务。对外为展现层提供各种应用功能(包括查询或命令),对内调用领域层(领域对象或领域服务)完成各种业务逻辑,应用层不包含业务逻辑。 领域层 负责表达业务概念,业务状态信息以及业务规则,领域模型处于这一层,是业务软件的核心。 基础设施层 本层为其他层提供通用的技术能力;提供了层间的通信;为领域层实现持久化机制;总之,基础设施层可以通过架构和框架来支持其他层的技术需求; 领域驱动设计过程中使用的模式 所有模式的总揽图 关联的设计 关联本身不是一个模式,但它在领域建模的过程中非常重要,所以需要在探讨各种模式之前,先讨论一下对象之间的关联该如何设计。我觉得对象的关联的设计可以遵循如下的一些原则: 关联尽量少,对象之间的复杂的关联容易形成对象的关系网,这样对于我们理解和维护单个对象很不利,同时也很难划分对象与对象之间的边界;另外,同时减少关联有助于简化对象之间的遍历; 对多的关联也许在业务上是很自然的,通常我们会用一个集合来表示1对多的关系。但我们往往也需要考虑到性能问题,尤其是当集合内元素非常多的时候,此时往往需要通过单独查询来获取关联的集合信息; 关联尽量保持单向的关联; 在建立关联时,我们需要深入去挖掘是否存在关联的限制条件,如果存在,那么最好把这个限制条件加到这个关联上;往往这样的限制条件能将关联化繁为简,即可以将多对多简化为1对多,或将1对多简化为1对1; 实体(Entity) 实体就是领域中需要唯一标识的领域概念。因为我们有时需要区分是哪个实体。有两个实体,如果唯一标识不一样,那么即便实体的其他所有属性都一样,我们也认为他们两个不同的实体;因为实体有生命周期,实体从被创建后可能会被持久化到数据库,然后某个时候又会被取出来。所以,如果我们不为实体定义一种可以唯一区分的标识,那我们就无法区分到底是这个实体还是哪个实体。另外,不应该给实体定义太多的属性或行为,而应该寻找关联,发现其他一些实体或值对象,将属性或行为转移到其他关联的实体或值对象上。比如Customer实体,他有一些地址信息,由于地址信息是一个完整的有业务含义的概念,所以,我们可以定义一个Address对象,然后把Customer的地址相关的信息转移到Address对象上。如果没有Address对象,而把这些地址信息直接放在Customer对象上,并且如果对于一些其他的类似Address的信息也都直接放在Customer上,会导致Customer对象很混乱,结构不清晰,最终导致它难以维护和理解; 值对象(Value Object) 在领域中,并不是没一个事物都必须有一个唯一标识,也就是说我们不关心对象是哪个,而只关心对象是什么。就以上面的地址对象Address为例,如果有两个Customer的地址信息是一样的,我们就会认为这两个Customer的地址是同一个。也就是说只要地址信息一样,我们就认为是同一个地址。用程序的方式来表达就是,如果两个对象的所有的属性的值都相同我们会认为它们是同一个对象的话,那么我们就可以把这种对象设计为值对象。因此,值对象没有唯一标识,这是它和实体的最大不同。另外值对象在判断是否是同一个对象时是通过它们的所有属性是否相同,如果相同则认为是同一个值对象;而我们在区分是否是同一个实体时,只看实体的唯一标识是否相同,而不管实体的属性是否相同;值对象另外一个明显的特征是不可变,即所有属性都是只读的。因为属性是只读的,所以可以被安全的共享;当共享值对象时,一般有复制和共享两种做法,具体采用哪种做法还要根据实际情况而定;另外,我们应该给值对象设计的尽量简单,不要让它引用很多其他的对象,因为他只是一个值,就像int a = 3;那么”3”就是一个我们传统意义上所说的值,而值对象其实也可以和这里的”3”一样理解,也是一个值,只不过是用对象来表示。所以,当我们在C#语言中比较两个值对象是否相等时,会重写GetHashCode和Equals这两个方法,目的就是为了比较对象的值;值对象虽然是只读的,但是可以被整个替换掉。就像你把a的值修改为”4”(a = 4;)一样,直接把”3”这个值替换为”4”了。值对象也是一样,当你要修改Customer的Address对象引用时,不是通过Customer.Address.Street这样的方式来实现,因为值对象是只读的,它是一个完整的不可分割的整体。我们可以这样做:Customer.Address = new Address(…); 领域服务(Domain Service) 领域中的一些概念不太适合建模为对象,即归类到实体对象或值对象,因为它们本质上就是一些操作,一些动作,而不是事物。这些操作或动作往往会涉及到多个领域对象,并且需要协调这些领域对象共同完成这个操作或动作。如果强行将这些操作职责分配给任何一个对象,则被分配的对象就是承担一些不该承担的职责,从而会导致对象的职责不明确很混乱。但是基于类的面向对象语言规定任何属性或行为都必须放在对象里面。所以我们需要寻找一种新的模式来表示这种跨多个对象的操作,DDD认为服务是一个很自然的范式用来对应这种跨多个对象的操作,所以就有了领域服务这个模式。和领域对象不同,领域服务是以动词开头来命名的,比如资金转帐服务可以命名为MoneyTransferService。当然,你也可以把服务理解为一个对象,但这和一般意义上的对象有些区别。因为一般的领域对象都是有状态和行为的,而领域服务没有状态只有行为。需要强调的是领域服务是无状态的,它存在的意义就是协调领域对象共完成某个操作,所有的状态还是都保存在相应的领域对象中。我觉得模型(实体)与服务(场景)是对领域的一种划分,模型关注领域的个体行为,场景关注领域的群体行为,模型关注领域的静态结构,场景关注领域的动态功能。这也符合了现实中出现的各种现象,有动有静,有独立有协作。 领域服务还有一个很重要的功能就是可以避免领域逻辑泄露到应用层。因为如果没有领域服务,那么应用层会直接调用领域对象完成本该是属于领域服务该做的操作,这样一来,领域层可能会把一部分领域知识泄露到应用层。因为应用层需要了解每个领域对象的业务功能,具有哪些信息,以及它可能会与哪些其他领域对象交互,怎么交互等一系列领域知识。因此,引入领域服务可以有效的防治领域层的逻辑泄露到应用层。对于应用层来说,从可理解的角度来讲,通过调用领域服务提供的简单易懂但意义明确的接口肯定也要比直接操纵领域对象容易的多。这里似乎也看到了领域服务具有Façade的功能,呵呵。 说到领域服务,还需要提一下软件中一般有三种服务:应用层服务、领域服务、基础服务。 应用层服务 获取输入(如一个XML请求); 发送消息给领域层服务,要求其实现转帐的业务逻辑; 领域层服务处理成功,则调用基础层服务发送Email通知; 领域层服务 获取源帐号和目标帐号,分别通知源帐号和目标帐号进行扣除金额和增加金额的操作; 提供返回结果给应用层; 基础层服务 按照应用层的请求,发送Email通知; 所以,从上面的例子中可以清晰的看出,每种服务的职责; 聚合及聚合根(Aggregate,Aggregate Root) 聚合,它通过定义对象之间清晰的所属关系和边界来实现领域模型的内聚,并避免了错综复杂的难以维护的对象关系网的形成。聚合定义了一组具有内聚关系的相关对象的集合,我们把聚合看作是一个修改数据的单元。 聚合有以下一些特点: 每个聚合有一个根和一个边界,边界定义了一个聚合内部有哪些实体或值对象,根是聚合内的某个实体; 聚合内部的对象之间可以相互引用,但是聚合外部如果要访问聚合内部的对象时,必须通过聚合根开始导航,绝对不能绕过聚合根直接访问聚合内的对象,也就是说聚合根是外部可以保持 对它的引用的唯一元素; 聚合内除根以外的其他实体的唯一标识都是本地标识,也就是只要在聚合内部保持唯一即可,因为它们总是从属于这个聚合的; 聚合根负责与外部其他对象打交道并维护自己内部的业务规则; 基于聚合的以上概念,我们可以推论出从数据库查询时的单元也是以聚合为一个单元,也就是说我们不能直接查询聚合内部的某个非根的对象; 聚合内部的对象可以保持对其他聚合根的引用; 删除一个聚合根时必须同时删除该聚合内的所有相关对象,因为他们都同属于一个聚合,是一个完整的概念; 关于如何识别聚合以及聚合根的问题: 我觉得我们可以先从业务的角度深入思考,然后慢慢分析出有哪些对象是: 有独立存在的意义,即它是不依赖于其他对象的存在它才有意义的; 可以被独立访问的,还是必须通过某个其他对象导航得到的; 如何识别聚合? 我觉得这个需要从业务的角度深入分析哪些对象它们的关系是内聚的,即我们会把他们看成是一个整体来考虑的;然后这些对象我们就可以把它们放在一个聚合内。所谓关系是内聚的,是指这些对象之间必须保持一个固定规则,固定规则是指在数据变化时必须保持不变的一致性规则。当我们在修改一个聚合时,我们必须在事务级别确保整个聚合内的所有对象满足这个固定规则。作为一条建议,聚合尽量不要太大,否则即便能够做到在事务级别保持聚合的业务规则完整性,也可能会带来一定的性能问题。有分析报告显示,通常在大部分领域模型中,有70%的聚合通常只有一个实体,即聚合根,该实体内部没有包含其他实体,只包含一些值对象;另外30%的聚合中,基本上也只包含两到三个实体。这意味着大部分的聚合都只是一个实体,该实体同时也是聚合根。 如何识别聚合根? 如果一个聚合只有一个实体,那么这个实体就是聚合根;如果有多个实体,那么我们可以思考聚合内哪个对象有独立存在的意义并且可以和外部直接进行交互。 工厂(Factory) DDD中的工厂也是一种体现封装思想的模式。DDD中引入工厂模式的原因是:有时创建一个领域对象是一件比较复杂的事情,不仅仅是简单的new操作。正如对象封装了内部实现一样(我们无需知道对象的内部实现就可以使用对象的行为),工厂则是用来封装创建一个复杂对象尤其是聚合时所需的知识,工厂的作用是将创建对象的细节隐藏起来。客户传递给工厂一些简单的参数,然后工厂可以在内部创建出一个复杂的领域对象然后返回给客户。领域模型中其他元素都不适合做这个事情,所以需要引入这个新的模式,工厂。工厂在创建一个复杂的领域对象时,通常会知道该满足什么业务规则(它知道先怎样实例化一个对象,然后在对这个对象做哪些初始化操作,这些知识就是创建对象的细节),如果传递进来的参数符合创建对象的业务规则,则可以顺利创建相应的对象;但是如果由于参数无效等原因不能创建出期望的对象时,应该抛出一个异常,以确保不会创建出一个错误的对象。当然我们也并不总是需要通过工厂来创建对象,事实上大部分情况下领域对象的创建都不会太复杂,所以我们只需要简单的使用构造函数创建对象就可以了。隐藏创建对象的好处是显而易见的,这样可以不会让领域层的业务逻辑泄露到应用层,同时也减轻了应用层的负担,它只需要简单的调用领域工厂创建出期望的对象即可。 仓储(Repository) 仓储被设计出来的目的是基于这个原因:领域模型中的对象自从被创建出来后不会一直留在内存中活动的,当它不活动时会被持久化到数据库中,然后当需要的时候我们会重建该对象;重建对象就是根据数据库中已存储的对象的状态重新创建对象的过程;所以,可见重建对象是一个和数据库打交道的过程。从更广义的角度来理解,我们经常会像集合一样从某个类似集合的地方根据某个条件获取一个或一些对象,往集合中添加对象或移除对象。也就是说,我们需要提供一种机制,可以提供类似集合的接口来帮助我们管理对象。仓储就是基于这样的思想被设计出来的; 仓储里面存放的对象一定是聚合,原因是之前提到的领域模型中是以聚合的概念去划分边界的;聚合是我们更新对象的一个边界,事实上我们把整个聚合看成是一个整体概念,要么一起被取出来,要么一起被删除。我们永远不会单独对某个聚合内的子对象进行单独查询或做更新操作。因此,我们只对聚合设计仓储。 仓储还有一个重要的特征就是分为仓储定义部分和仓储实现部分,在领域模型中我们定义仓储的接口,而在基础设施层实现具体的仓储。这样做的原因是:由于仓储背后的实现都是在和数据库打交道,但是我们又不希望客户(如应用层)把重点放在如何从数据库获取数据的问题上,因为这样做会导致客户(应用层)代码很混乱,很可能会因此而忽略了领域模型的存在。所以我们需要提供一个简单明了的接口,供客户使用,确保客户能以最简单的方式获取领域对象,从而可以让它专心的不会被什么数据访问代码打扰的情况下协调领域对象完成业务逻辑。这种通过接口来隔离封装变化的做法其实很常见。由于客户面对的是抽象的接口并不是具体的实现,所以我们可以随时替换仓储的真实实现,这很有助于我们做单元测试。 尽管仓储可以像集合一样在内存中管理对象,但是仓储一般不负责事务处理。一般事务处理会交给一个叫“工作单元(Unit Of Work)”的东西。关于工作单元的详细信息我在下面的讨论中会讲到。 另外,仓储在设计查询接口时,可能还会用到规格模式(Specification Pattern),我见过的最厉害的规格模式应该就是LINQ以及DLINQ查询了。一般我们会根据项目中查询的灵活度要求来选择适合的仓储查询接口设计。通常情况下只需要定义简单明了的具有固定查询参数的查询接口就可以了。只有是在查询条件是动态指定的情况下才可能需要用到Specification等模式。 在分层架构中其他层如何与领域层交互 从经典的领域驱动设计分层架构中可以看出,领域层的上层是应用层,下层是基础设施层。那么领域层是如何与其它层交互的呢? 对于会影响领域层中领域对象状态的应用层功能 一般应用层会先启动一个工作单元,然后: 对于修改领域对象的情况,通过仓储获取领域对象,调用领域对象的相关业务方法以完成业务逻辑处理; 对于新增领域对象的情况,通过构造函数或工厂创建出领域对象,如果需要还可以继续对该新创建的领域对象做一些操作,然后把该新创建的领域对象添加到仓储中; 对于删除领域对象的情况,可以先把领域对象从仓储中取出来,然后将其从仓储中删除,也可以直接传递一个要删除的领域对象的唯一标识给仓储通知其移除该唯一标识对应领域对象; 如果一个业务逻辑涉及到多个领域对象,则调用领域层中的相关领域服务完成操作; 注意,以上所说的所有领域对象都是只聚合根,另外在应用层需要获取仓储接口以及领域服务接口时,都可以通过IOC容器获取。最后通知工作单元提交事务从而将所有相关的领域对象的状态以事务的方式持久化到数据库; 关于Unit of Work(工作单元)的几种实现方法 基于快照的实现,即领域对象被取出来后,会先保存一个备份的对象,然后当在做持久化操作时,将最新的对象的状态和备份的对象的状态进行比较,如果不相同,则认为有做过修改,然后进行持久化;这种设计的好处是对象不用告诉工作单元自己的状态修改了,而缺点也是显而易见的,那就是性能可能会低,备份对象以及比较对象的状态是否有修改的过程在当对象本身很复杂的时候,往往是一个比较耗时的步骤,而且要真正实现对象的深拷贝以及判断属性是否修改还是比较困难的; 不基于快照,而是仓储的相关更新或新增或删除接口被调用时,仓储通知工作单元某个对象被新增了或更新了或删除了。这样工作单元在做数据持久化时也同样可以知道需要持久化哪些对象了;这种方法理论上不需要ORM框架的支持,对领域模型也没有任何倾入性,同时也很好的支持了工作单元的模式。对于不想用高级ORM框架的朋友来说,这种方法挺好; 不基于快照,也不用仓储告诉工作单元数据更改了。而是采用AOP的思想,采用透明代理的方式进行一个拦截。在NHibernate中,我们的属性通常要被声明为virtual的,一个原因就是NHibernate会生成一个透明代理,用于拦截对象的属性被修改时,自动通知工作单元对象的状态被更新了。这样工作单元也同样知道需要持久化哪些对象了。这种方法对领域模型的倾入性不大,并且能很好的支持工作单元模式,如果用NHibernate作为ORM,这种方法用的比较多; 一般是微软用的方法,那就是让领域对象实现.NET框架中的INotifiyPropertyChanged接口,然后在每个属性的set方法的最后一行调用OnPropertyChanged的方法从而显示地通知别人自己的状态修改了。这种方法相对来说对领域模型的倾入性最强。 对于不会影响领域层中领域对象状态的查询功能 可以直接通过仓储查询出所需要的数据。但一般领域层中的仓储提供的查询功能也许不能满足界面显示的需要,则可能需要多次调用不同的仓储才能获取所需要显示的数据;其实针对这种查询的情况,我在后面会讲到可以直接通过CQRS的架构来实现。即对于查询,我们可以在应用层不调用领域层的任何东西,而是直接通过某个其他的用另外的技术架构实现的查询引擎来完成查询,比如直接通过构造参数化SQL的方式从数据库一个表或多个表中查询出任何想要显示的数据。这样不仅性能高,也可以减轻领域层的负担。领域模型不太适合为应用层提供各种查询服务,因为往往界面上要显示的数据是很多对象的组合信息,是一种非对象概念的信息,就像报表; 领域驱动设计的其他一些主题 上面只是涉及到DDD中最基本的内容,DDD中还有很多其他重要的内容在上面没有提到,如: 模型上下文、上下文映射、上下文共享; 如何将分析模式和设计模式运用到DDD中; 一些关于柔性设计的技巧; 如果保持模型完整性,以及持续集成方面的知识; 如何精炼模型,识别核心模型以及通用子领域; 这些主题都很重要,因为篇幅有限以及我目前掌握的知识也有限,并且为了突出这篇文章的重点,所以不对他们做详细介绍了,大家有兴趣的可以自己阅读一下。 一些相关的扩展阅读 CQRS架构 核心思想是将应用程序的查询部分和命令部分完全分离,这两部分可以用完全不同的模型和技术去实现。比如命令部分可以通过领域驱动设计来实现;查询部分可以直接用最快的非面向对象的方式去实现,比如用SQL。这样的思想有很多好处: 实现命令部分的领域模型不用经常为了领域对象可能会被如何查询而做一些折中处理; 由于命令和查询是完全分离的,所以这两部分可以用不同的技术架构实现,包括数据库设计都可以分开设计,每一部分可以充分发挥其长处; 高性能,命令端因为没有返回值,可以像消息队列一样接受命令,放在队列中,慢慢处理;处理完后,可以通过异步的方式通知查询端,这样查询端可以做数据同步的处理; Event Sourcing(事件溯源) 对于DDD中的聚合,不保存聚合的当前状态,而是保存对象上所发生的每个事件。当要重建一个聚合对象时,可以通过回溯这些事件(即让这些事件重新发生)来让对象恢复到某个特定的状态;因为有时一个聚合可能会发生很多事件,所以如果每次要在重建对象时都从头回溯事件,会导致性能低下,所以我们会在一定时候为聚合创建一个快照。这样,我们就可以基于某个快照开始创建聚合对象了。 DCI架构 DCI架构强调,软件应该真实的模拟现实生活中对象的交互方式,代码应该准确朴实的反映用户的心智模型。在DCI中有:数据模型、角色模型、以及上下文这三个概念。数据模型表示程序的结构,目前我们所理解的DDD中的领域模型可以很好的表示数据模型;角色模型表示数据如何交互,一个角色定义了某个“身份”所具有的交互行为;上下文对应业务场景,用于实现业务用例,注意是业务用例而不是系统用例,业务用例只与业务相关;软件运行时,根据用户的操作,系统创建相应的场景,并把相关的数据对象作为场景参与者传递给场景,然后场景知道该为每个对象赋予什么角色,当对象被赋予某个角色后就真正成为有交互能力的对象,然后与其他对象进行交互;这个过程与现实生活中我们所理解的对象是一致的; DCI的这种思想与DDD中的领域服务所做的事情是一样的,但实现的角度有些不同。DDD中的领域服务被创建的出发点是当一些职责不太适合放在任何一个领域对象上时,这个职责往往对应领域中的某个活动或转换过程,此时我们应该考虑将其放在一个服务中。比如资金转帐的例子,我们应该提供一个资金转帐的服务,用来对应领域中的资金转帐这个领域概念。但是领域服务内部做的事情是协调多个领域对象完成一件事情。因此,在DDD中的领域服务在协调领域对象做事情时,领域对象往往是处于一个被动的地位,领域服务通知每个对象要求其做自己能做的事情,这样就行了。这个过程中我们似乎看不到对象之间交互的意思,因为整个过程都是由领域服务以面向过程的思维去实现了。而DCI则通用引入角色,赋予角色以交互能力,然后让角色之间进行交互,从而可以让我们看到对象与对象之间交互的过程。但前提是,对象之间确实是在交互。因为现实生活中并不是所有的对象在做交互,比如有A、B、C三个对象,A通知B做事情,A通知C做事情,此时可以认为A和B,A和C之间是在交互,但是B和C之间没有交互。所以我们需要分清这种情况。资金转帐的例子,A相当于转帐服务,B相当于帐号1,C相当于帐号2。因此,资金转帐这个业务场景,用领域服务比较自然。有人认为DCI可以替换DDD中的领域服务,我持怀疑态度。 四色原型分析模式 时刻-时间段原型(Moment-Interval Archetype) 表示在某个时刻或某一段时间内发生的某个活动。使用粉红色表示,简写为MI。 参与方-地点-物品原型(Part-Place-Thing Archetype) 表示参与某个活动的人或物,地点则是活动的发生地。使用绿色表示。简写为PPT。 描述原型(Description Archetype) 表示对PPT的本质描述。它不是PPT的分类!Description是从PPT抽象出来的不变的共性的属性的集合。使用蓝色表示,简写为DESC。 举个例子,有一个人叫张三,如果某个外星人问你张三是什么?你会怎么说?可能会说,张三是个人,但是外星人不知道“人”是什么。然后你会怎么办?你就会说:张三是个由一个头、两只手、两只脚,以及一个身体组成的客观存在。虽然这时外星人仍然不知道人是什么,但我已经可以借用这个例子向大家说明什么是“Description”了。在这个例子中,张三就是一个PPT,而“由一个头、两只手、两只脚,以及一个身体组成的客观存在”就是对张三的Description,头、手、脚、身体则是人的本质的不变的共性的属性的集合。但我们人类比较聪明,很会抽象总结和命名,已经把这个Description用一个字来代替了,那就是“人”。所以就有所谓的张三是人的说法。 角色原型(Role Archetype) 角色就是我们平时所理解的“身份”。使用黄色表示,简写为Role。为什么会有角色这个概念?因为有些活动,只允许具有特定角色(身份)的PPT(参与者)才能参与该活动。比如一个人只有具有教师的角色才能上课(一种活动);一个人只有是一个合法公民才能参与选举和被选举;但是有些活动也是不需要角色的,比如一个人不需要具备任何角色就可以睡觉(一种活动)。当然,其实说人不需要角色就能睡觉也是错误的,错在哪里?因为我们可以这样理解:一个客观存在只要具有“人”的角色就能睡觉,其实这时候,我们已经把DESC当作角色来看待了。所以,其实角色这个概念是非常广的,不能用我们平时所理解的狭义的“身份”来理解,因为“教师”、“合法公民”、“人”都可以被作为角色来看待。因此,应该这样说:任何一个活动,都需要具有一定角色的参与者才能参与。 用一句话来概括四色原型就是:一个什么什么样的人或组织或物品以某种角色在某个时刻或某段时间内参与某个活动。 其中“什么什么样的”就是DESC,“人或组织或物品”就是PPT,“角色”就是Role,而”某个时刻或某段时间内的某个活动"就是MI。 以上这些东西如果在学习了DDD之后再去学习会对DDD有更深入的了解,但我觉得DDD相对比较基础,如果我们在已经了解了DDD的基础之上再去学习这些东西会更加有效和容易掌握。 希望本文对大家有所帮助。
背景 最近两年,工作中虽然大量使用了 Rafy 框架作为各个产品、项目的开发框架。我是 2015 年的年中加入现在这家公司的,由于我个人工作太忙的缘故,一直没怎么编码,Rafy 框架底层的核心成长也比较慢。这两年只是在必须更新时,安排了一些开发者做了很少的一些代码更新。 这几年,Rafy 框架 2.0 版本没怎么大力推广。目标客户不精确、产品的设计有些问题、框架本身的价值也没有被大众认可,这些都需要对框架本身不断地进行更新。由于最近两年编码较少,我也停下来在更高的维度思考了框架 3.0 版本应该如何发展。这些,都使得我发现 Rafy 框架需要做的工作还很多。同时,意识到自己未来一段时间之内,都不能再象以前一样大量编码,也不能是主要靠个人的力量来更新框架。所以,我和几个同事讨论了一下,成立了一个民间组织:“Rafy 开源贡献中心”,把对 Rafy 框架有兴趣的人、愿意为框架做出一定贡献的人,聚集在一起。集众人之力量,一起来把 Rafy 框架做好。一个开源框架要有持续的发展动力,不能只靠一己之力,而是应该依靠开源社区的力量,依靠开源团队的力量。 组织运行方式 每周六一天线下聚会 团队动作的主要形式为周末一天活动制:周六早上十点到统一的地点集合,展开一天的活动。活动地点在北京昌平龙泽地铁附近。非北京的人员,则以线上参会的形式参与。活动内容主要是围绕 Rafy 框架开源,偶尔也会展开一些其它话题进行讨论。活动的形式不限,有:Rafy框架学习、各类框架研究、针对主题的开放式讨论、任务驱动式的自组织开发小组。 这种形式能保证这个团队是以兴趣来驱动的。参加到组织中的同学,都是愿意将一天的周末时间投入到技术研究中的技术分子。 当然,除了周六一天外。个人还可以在其它时间通过线上的形式与开发组展开讨论。 团队文化 开放、贡献、积极、平等。 任务驱动方式 组织的目的,是推动 Rafy 框架的开发、成长、开源、推广。相关的任务,都会以任务导向的形式成立自组织的任务小组,该任务后续安排都由具体的小组来推动,最终的成果物由开源中心统一审核。小组会由一个资深的老手带队,其他人则自发报名参与。 参与组织的回报 参与的开发者,可以获得以下回报: 自我技术的精进 在这个组织中,我们邀请了一些技术老鸟,通过开放式的讨论、任务小组的推进,大部分的人都可以在这个过程中获得自己技术的长进。这些技术方面的长进,是千篇一律的日常工作无法给予的。 成为框架的主人翁(参与者、开发者、主导者) 刚开始加入组织的开发者,只是简单地学习框架,参与讨论。随着越来越深入,可以参与到代码的开发、文档的编写中。这时,在 GitHub 的贡献列表中会列出贡献者的名字与贡献度。 同时,当一些开发者贡献越来越大,将会沉淀下来,成为框架未来的主导者。这些,都无疑是个人简历中的亮点! 成就感 不论是框架开发的参与者、推广者,或是成为框架的主导者,都将会在框架大量成功应用后,获得巨大的成就感。实现人生不一样的价值。 Write the Code. Change the World! 兴趣 因为喜欢,所以参与。 其实,对技术有兴趣的同学,对 Rafy 框架、开源框架有贡献意愿的同学,并不会太在意回报。 最近几周进展与效果 Rafy 开源贡献中心是在本月初成立并试运行的。 目前,本中心拥有成员 23 位,80% 还是以公司内部成员居多。当然,未来人员流动在所难免,所以也希望更多的同学,特别是北京的同学,能够参与进来。 目前已经开展了三次周六聚会了。每次聚会参与人员在 10 人左右,线上也有几个人参与。 前两周主要是完成了组织本身的讨论(目的、意义、制度等)、以及辅导一些新人了解 Rafy 框架。同时,还启动了第一个任务驱动的小组:Rafy 跨平台任务研究小组。 第三周则展开了第一次讨论会议,主要讨论了大家提出的 Rafy 框架的问题与建议,并展开讨论了相关的许多知识点(OOD、DDD、ABP 等);同时,还规划了后期 Rafy 框架的开发、推广方向(用户定位、价值点等)。 本周六将进入第四周的聚会。将成立更多的任务驱动的小组,逐步开展更多任务。 QQ 群即时交流 我们目前使用 QQ 群来实现线上的成员管理、即时沟通、活动安排。 期望加入此开发小组的同学,可以添加 QQ 群:638407102。 附 QQ 群群规: Rafy 开源贡献中心-群规 目的 Rafy 开源贡献中心旨在构建一个对 Rafy 开源框架作出杰出贡献的核心开发团队,以推动 Rafy 框架的开发、开源、推广等相关事宜。(不参与贡献者,请加入另外一个 Rafy 框架关注群:175227630。) 本QQ群则用于团队成员的学习、交流、技术探讨、思维碰撞。 团队文化 我们是一群有激情的人,为了为开源做出自己的贡献,成立了民间的开源组织,并取名为 Rafy 开源贡献中心。 非常欢迎新人的加入,我们以开放、贡献、积极、平等的文化氛围,拥抱每一个加入组织的成员。 QQ 群规 新人:新人需要学习并熟悉 Rafy 框架的开发,并在 Rafy GitHub Issue 中提交自己对于框架的问题与建议。提交任意建议后,即可加入开发者群(入群时,需要提交 Issue 号)。 实名制: 进群人员设置自己的备注,格式为:地点-真实姓名,如:北京-胡庆访。 报到时间:线下活动每周六早上十点到团队工作地集合。迟到者给大家发红包。 参加次数:每个月最少参加两次活动。常期不参加活动者、没有贡献者,将被管理员移出本群,敬请谅解。
上个月,我写了一篇《架构设计师能力模型》,为开发者指出一些发展的方向、架构师的能力要求,以及需要学习的相关知识。 本月,我为公司的人力部门编制了更加量化的《2017年研发人员岗位能力模型 V1.4》。用于说明不同岗位(实习工程师、初级工程师、中级工程师、高级工程师、资深工程师、架构师、高级架构师)的具体的不同要求。该模型以工作年限、技术能力、工作态度&目标导向&文化匹配度、业务熟练度、学习能力、项目管理、影响力、运维能力等不同的维度,来给开发者进行加权评分。我们可以通过最终的分数,来说明某位开发者与该岗位的匹配程度。 同时,这个模型公布后,不同级别的开发者,也可以根据模型中的后续级别的具体要求来学习,不断地要求自己、提升自己。 源文件下载:http://pan.baidu.com/s/1o8LkgUA 截图如下:
本月最重要的一件事情,是给部门的所有成员召开 2017 年的启动会议。这个会议中,讲了五个部分:2016成果总结、2017工作计划、组织架构、部门文化、部门制度。由于部门的大部分人员是在 2016 年招聘进来的新员工,每个人都来自不同的公司,其思想、价值观、文化不尽相同,所以导致在行动上出现了很多疑惑以及问题。所以文化及制度的宣贯,无疑是本次会议中最重要的部分,也是我在部门内第一次向所有人正式地说明了部门的文化,并逐一对我们倡导的价值观进行了讲解。 讲这些东西,有什么意义?其实,文化中我们倡导的价值观,远远不止在一个部门中有用。商业社会有其特殊的游戏规则!商场如战场!这些规则是在职场生存的重要规则。狼行千里吃肉!作为部门负责人、技术负责人,需要承担起教育后来者的责任,我有义务要把我认为正确的道理教给大家。当然,讲完这些“虚”的东西,不一定每个人都会深刻理解、甚至会有所改变,但是只要有一部分的人因为我的宣贯而成长或改变,我觉得就非常值得! 另外,在会议上我也明确了一些红线、以及一些具体的制度。其中的许多现象及问题,都是实实在在地发生过的现象。统一说明这些内容,是所有人后期一致行动的必要条件。 下面是本次会议中我编制的 PPT 中与文化相关的片子。由于内容比较简单,都是一些潜显的道理,所以在此不做过多的解释。 研究院部门文化 PS: 本文没有做过多解释,主要目的是为了记录一下本次部门文化宣贯的主要内容,以备后查。 (另外,事后有几个人私自给了反馈,都觉得非常有用。有人甚至找到我,觉得”受用终生“。我个人非常欣慰,努力没有白费,非常值得!)
2016年,区块链技术火了!各大金融公司、互联网巨头都竞相参加到区块链技术的研究中。我们公司的业务是税务的信息化领域,也希望通过区块链技术的应用,来提升为财税领域的服务。 区块链技术优缺点总结 下图是对区块链技术的一些特点的总结: 痛点及应用场景 对税务领域进行了一些思考,我整理出以下几类痛点,以及区块链对应的可能的应用模式: 1.发票电子化 纸质发票电子化在 2017 年起的未来几年中,将会掀起一波不可阻挡的浪潮。而目前电子票的存储,还依然分散在不同的电子票供应商中,构成了分散的数据孤岛。这些数据之间的集成、验证、追踪,较为复杂,也造成了大量成本的浪费。 其实,电子票交易是区块链技术极为天然的应用场景。使用分布式账本,可记录跨地域、跨企业的电子票信息。对于电子票据商业背景的追溯、背书连续性、交易主体身份真实性以及电子票在中小规模业务中的普及都有重要意义。使用区块链技术,通过其互联互通的优势,建立相应的联盟链或公有链,可以使这些信息孤岛中的数据真正的整合起来。同时,还为链中的所有数据提供了透明、安全的分布式存储方案。而且,这些集成后的数据,拥有可信度高、不或篡改、可验证性强等特点。 2.发票虚开、错开 一些企业常常由于利益的驱使,虚开大额发票,甚至为不存在的虚假交易开出发票。 通过区块链技术,我们可以将发票数据存储在区块链上。结合交易数据的区块链技术,就可以使得交易数据与发票能够拥有公开、透明以及可跟踪性,使交易数据与发票数据能够保持一定的匹配关系,进而快速鉴别虚开发票的现象。 另外,发票开具系统其实也能自动使用区块链中的交易数据来开具发票,减少了因为人工疏忽,而导致错开发票的问题。 3.发票真伪鉴别 发票造假,企业对于发票的验证手段单一,而且目前验证有一定的滞后性。这使得企业蒙受信息不对称产生的损失,降低了员工与企业间、企业与企业间的信任。 如果使用区块链技术来管理发票数据,这将会使得这些发票数据可以快速地在所有节点中被记录,所有安装了客户端的企业都可以及时地查询到这些发票数据。同时,由于区块链技术拥有透明、去信任化的特点,使得只要是能在区块链中查询到的发票数据,都是真实的发票!一并解决了假票难查、慢查的问题。 4.发票全流程管理 在当下的环境中,不同的信息化供应商提供了不同的税务管理系统。而这些系统与订单系统、支付系统、财务系统的集成需要分别进行定制化接口对接。 当区块链技术在上述领域得到深入的应用后,解决了不同系统间的不同数据的孤岛问题。我们可以在区块链中获得高质量、高精确度、较高实时性、真实的数据。这些不同系统、不同类型的数据,都可以在对应的区块链中获得。而且我们可以追踪到每一条数据的产生时间、历史来源、以及后续变化。 使用区块链技术,可以通过时间戳、哈希算法等对发票进行真伪确认,证明其存在性、真实性和唯一性。一旦在区块链上被确定,票剧的后续操作都会被实时记录,其全生命周期可追溯、可追踪,这为财税全业务流程管理,提供了一种强大的技术保障和完整的数据支撑。区块链技术的大规模应用,必将优化财税领域的业务流程、降低运营成本、提升协同效率,进而为票剧电子化升级提供系统化的支撑。 技术架构 上图引用自《中国区块链技术和应用发展白皮书》,具体介绍请见书中内容。 我们的应用及应用架构方向 基于上述场景,我们将会应用区块链技术。我们选用的区块链应用架构模式将会先在企业内部应用“私有链+API”的模式,如下图: 其中,在区块链中的每一个节点,都可能是一个数据集群。每一个节点上,都部署统一的区块链节点软件,拥有完整的区块链数据;这样,这些节点还可以作为单独的服务器,向企业中的其它应用提供数据服务。 随着该系统的逐渐稳定,我们需要制定技术标准,发布标准的区块链节点软件。这样,就可以开展第二个阶段:引入电子票信息化供应商,同时加入这个区块链,进而形成“联盟链+API”的模式,如下图: 联盟链中的所有企业,都将拥有联盟中所有企业的发票数据。方便为其客户提供围绕所有发票数据的相关服务。 展望 中国正处于税务互联网化、业务创新发展的孕育期。在税务总局‘互联网+税务’的实践方针指引下,我们利用‘税务+区块链’的重要技术战略来创新甚至引领税务行业,快速实现税务领域的电子化、互联网化! 参考 《中国区块链技术和应用发展白皮书》 《当互联网金融遇到区块链……》 《比特币崩盘之后,是时候聊聊2016的区块链市场了》 《分布式账本技术在支付、清算与结算领域的应用:特征、机遇与挑战》 《区块链在腾讯的可能性》
2016年我主要是在公司带一个以研发为主要工作的部门,我们部门起的名字也比较高大上,叫“软件研究院”。年末到了,从几个方面对部门的工作进行一个简单的总结。 人员变化 严格来讲,研究院是在2016年2月时才成立的,最开始时组织架构上有十来位,但是真正参与到研究院工作的人员其实只有3位,主要工作为 ACME 产品的研发。随之时间推移、产品逐渐增多,到10月份时这个部门的人员已经增至65位,同时研发的产品达到了七、八个。11月全公司组织架构变更、采用矩阵式管理之后,研究院的人员收缩到 20 位,其中包含四个团队:ACME团队12人;发票查验团队2人;VICA 团队3人;公共技术组3个。 在人员招聘的过程中,研究院建立了一整套的技术人员培训体系,以培训所有刚入职的新员工。这些新鲜血液经过培训,有的进入了事业群,有的进入ACME 团队,有的进入了不同的产品研发组。事实证明,该培训体系能够很好地保证了新人在入职时,快速、高效地学习、掌握我们的基础业务知识和技术体系,并快速进入各团队展开工作。但是随着时间的推移、技术的升级,目前的培训也显示出了一些缺点:没有实时更新、没人去讲、没人去一对一带新人。这些问题,会在后面的培训中进行改善。 团队建设 研究院在 2016 年构建之初,制定了整个部门的愿景、职责及文化: 这些文化起到了很好的团队粘性作用。但是有一些部分并没有很好的落地,需要反思,并在 2017 年更好地执行。 2016 年,研究院招聘、培训并组建了许多团队:公共技术组;ACME 研发测试团队;SaaS 研发、测试团队;VICA 团队;数据提取团队;前期的简程团队。 特别是公共技术组、ACME团队,引入并建立了团队的敏捷流程、两阶段发版流程,与事业群的协作机制等,并制定了整个团队的其它相关工作制度。团队战斗力也不错,当重要命令下达时,能积极主动执行到位。 项目辅助 2016 年研究院的 ACME 团队,在研发产品的同时,辅助了许多的项目开发。从北京银行,几乎全部.NET人员都写JAVA 代码;到证通项目、太平洋证券、温州银行,再到后来的农信、哈行等,ACME 团队都会安排人到现场解决问题,尽全力去帮助所有项目组的实施成功。 公共技术组中的团队成员,能力级别基本都是高级研发。公司各业务线上的问题,也都会去找到他们来解决一些棘手的问题。 研究院当下以及未来都会做到:只要人员出马,问题必须解决! 产品产出 2016年研究院最大的工作就是在组建团队的同时,开展各产品的研发。截止到现在,已经产出的产品有下面一些: ACME 管理系统:从最开始的 MVP 版本,到现在ACME保持着一月两版的发展速度。截止目前,已经开发到 4.2.3 版本。这个版本中包含了58个功能模块,也解决了稳定性问题、安全性问题、性能问题。由于过程中一直被市场推动着走,整个ACME 的研发历程比较艰难。但是随着团队配合度越来越高,流程制度逐步完善,未来 ACME 产品的研发会更加顺畅。 ACME 流程引擎:已经伴随 ACME 同步发布,目前已经内置 50 个左右的通用技术及业务功能组件,可使用拖拽的方式,通过该引擎来完善不同格式的数据的对接。 简税:简税是公司 2016 年的一个重点产品,是公司战略转移后的重心。历经半年,目前已经发布了2.3版。从无到有,大贲拥有了自己的 SaaS 产品平台。目前的产品已经在多家酒店实施。虽然还有一些稳定性问题,年底将会完成一个稳定的版本。 简程(已暂停):配合简税发布了 1.0 版本,含Andriod、IOS 两大平台版本。在开发 2.0 版本时,因为转做发票管家而暂停了此版本的开发。 浏览器开票插件(将逐步废弃):是 VICA 客户端的前生,该插件集成了百望、航信的所有开票接口,给简税、ACME 的开发者提供了统一、易用的开票接口。 VICA(客户端助手):继浏览器开票插件后,研究院开发出了一个轻量级客户端。其本质是一个客户端的插件平台,各产品线可以在这个技术产品的基础上研发自己的客户端业务组件。未来大贲的所有产品线的客户端都将使用 VICA。目前该产品已经在简税上全面使用。接下来将在 ACME 上使用。 客户端开票软件(已废弃):年初时公司决定开发一个简单的客户端开票软件,以应对最简单的开票需求。该产品研发到 MVP 版本。后来由于重心转移到简税,且简税、ACME 统一使用浏览器开票后,此产品不再维护与升级。 数据提取:目前实现了 U 盘版本,以及 2.0 MVP 版本。但是只实现了航信的数据提取。百望的数据提取方法还在努力尝试中。 发票查验:发票的查验功能,在所有产品线中起到非常重要的作用。这个产品为开发者提供统一的查验功能性接口。后端则封装了大量的数据源(国税总局、各地区税局、各互联网电商平台、电子票平台等)。该产品目前已经在一些客户中进行试用,在未来几乎会在所有的其它产品线中被直接使用到。 回首整年,2016 年研究院的主要职责就是新产品的研发。接下来,这些产品线研发小组会陆续成为独立的业务组。 就目前而言,虽然有些文档并不齐备,技术有些方面考虑还是不全,但是已经建立起整个公司的整体技术方案。虽然一些新产品线还象嗷嗷待哺的婴儿,但是他们已经可以独立行走,终会走向成熟! 2017年,我们更想看到的是,这些大贲的孩子,能够更健康、稳定地成长。所以 2017 年研究院的工作将会专注这些内容。 个人小结 今年对于我个人的工作来说,总结起来就是:忙,但是有意义! 太多沟通协调的事,占据了大量的时间。导致几乎没有时间去写代码。对于一个技术出身的我来说,第一次忙到连续几个月都没有写过一行代码,确实是第一次,不时有些忐忑和不适应。但是回头看看,做的事情是很有意义的。对于公司来说,现阶段需要一个这样的人来带领研发,以及完成各产品初期的技术规划。对于个人来说,这也算是一种成长吧,让自己的手能够脱离底层代码这么久,站在更高的层面来思考公司的战略、方向、研发的管理等更有意义的话题。 2017 年,公司发生了许多变化,研究院的职责也发生了一些变化。我将努力在新的一年中,不断适应并成长,为大贲的成长与壮大,贡献自己的一份力量!同时,我认为2017年是大贲腾飞的一年,我也让自己在 2017 年多做一些技术方面的工作,为大贲的腾飞建立起起飞的跑道,在技术上为各业务线铺平道路、保驾护航!
在应用开发过程中,有 80% 的场景下,开发者所需要的实体查询,查询条件中其实都是一些简单的属性匹配,又或是一些属性匹配的简单组合。Rafy 为这样的场景提供了更为方便使用的 API:CommonQueryCriteria。 属性匹配 在查询时,当需要使用一个或几个属性的限定匹配来进行查询时,我们可以通过 CommonQueryCriteria 来使用以下方法进行快速查询。例如,以下查询实现了通过用户的编码的精确匹配来查询唯一指定的用户: C# public User GetByCode(string code) { return this.GetFirstBy(new CommonQueryCriteria { new PropertyMatch(User.CodeProperty, PropertyOperator.Equal, code) }); } 例如,以下查询实现同时通过用户名称的模糊匹配、年龄的精确匹配来查询一组用户(由于 Age 未指定 PropertyOperator,所以使用的是 Equal): C# public UserList GetByNameAge(string name, int age) { return this.GetBy(new CommonQueryCriteria { new PropertyMatch(User.NameProperty, PropertyOperator.Contains, name), new PropertyMatch(User.AgeProperty, age) }); } 上述查询默认使用 And 进行多条件的连接。如果需要修改,可以通过 CommonQueryCriteria 的构造器传入或属性进行设置。 PropertyOperator 表示属性匹配的方式,可用的操作有: Equal NotEqual Greater GreaterEqual Less LessEqual Like NotLike Contains NotContains StartsWith NotStartsWith EndsWith NotEndsWith In NotIn Note 为了方便开发者使用 CommonQueryCriteria,RafySDK 提供了代码段 RafyQuery_Common 来生成上述代码。 使用多个属性匹配组进行查询 上面是比较简单的查询,只是对单个属性或使用 And、Or 连接的几个条件进行匹配。我们还可以通过属性匹配组来实现相对复杂的查询。 一个 CommonQueryCriteria 中可以通过 And、Or 连接多个属性匹配组,而每一个属性匹配组也可以通过 And、Or 连接多个具体的属性匹配条件。 下面的代码演示了如何使用('Name contains name' And 'Age equal age' Or 'Code equal code')的条件进行查询: C# this.GetBy(new CommonQueryCriteria(BinaryOperator.Or) { new PropertyMatchGroup { new PropertyMatch(TestUser.NameProperty, PropertyOperator.Contains, name) new PropertyMatch(TestUser.AgeProperty, age) }, new PropertyMatchGroup { new PropertyMatch(TestUser.CodeProperty, code) } }); 相对于 Linq 查询的优势 使用 CommonQueryCriteria 进行查询时,相对于 Linq 查询 而言,有以下的优势: 更加方便、简单 仓库类型上已经提供了参数是 CommonQueryCriteria 的公有查询方法,开发可以直接使用这些方法进行查询,没有必要再封装一个相应的公有方法。 例如,上面的示例中,也可以不封装 GetByCode 方法,而是由仓库的调用者直接使用 GetBy(CommonQueryCriteria) 方法。 性能更好 使用 Linq 查询时,编译器会使用反射生成表达式树,然后 Rafy 框架才会解析这棵树,生成最终的 Sql 树。但是使用 CommonQueryCriteria 通用查询时,Rafy 框架会直接将 CommonQueryCriteria 中的条件生成对应的 Sql 树,这就节省了表达式树的生成和解析的环节,提升了性能。 PS:该文已经纳入《Rafy 用户手册》中。
《上一篇》说到了仓位管理的重要性。这一篇则说明我对仓位控制算法的设计,以及最终使用的算法。由于内容较多,本文中我尽量只说重点。 概念 算法:就是将一定可变范围内的一组输入条件,轮换到确定的输出时,所使用到的逻辑换算关系。 仓位控制算法:其输入就是投资标的的相关因素,输出则是当前应该使用的仓位占比。不同的算法,所使用的输入条件不同,但是需要想办法进行量化,如:估值、价格、宏观环境、行业、量能、时间……等。得出仓位占比,乘以你对该标的的总投资金,就得出它的持仓资金,剩下的则是空仓资金,以备必要时加仓。 模拟运行表格 用于算法设计的表格如下: 初始的资金设置为 10000 元。通过24个月的数据模拟,来观察按照不同算法不断进行调仓后,每个月的资金的变化。每一个页面中的表格都是这个统一的格式,不同的地方在于测试的曲线不同,曲线的介绍详见后文。 各列的含义: 月份:为期两年的数据模拟测试。 浮动:当前月份的价格对于估值的绝对浮动百分比。 估值:当前月份人为对标的(大盘)的估值。 当前点位:该月份的价格(大盘的点位)。 涨跌:本月相对上月的价格的涨跌幅度。 指导仓位:通过仓位控制算法得出的本月底应该拥有的仓位占比。 月初现金:月初时的空仓现金量。 月末投金:月末时仓位中的资金量。=上月投金*(1+本月涨跌) 月末总资金:月末时空仓现金与持仓资金的总和。也就是月底时的总资产量。 仓位总资金:用于计算最终仓位资金的总资金。仓位资金=仓位总资金*指导仓位。 调整后股金:月末时按照指导仓位进行资金调整后的持仓资金。=仓位总资金/月末总资金/固定资本金*指导仓位。(用于计算仓位资金的总资金的选择会有变化,后面会有介绍) 调整后现金:调整后的空仓资金。 当月盈利:当月的总资产相对于上月的增幅。 最后一个月的月末总资金,就是经过两年投资后的资产终值。 用于测试的曲线 对于选定的仓位控制算法,我们需要使用不同的涨跌幅曲线对其进行模拟运行。下面我设计了8个测试曲线,并追加了2015年的真实涨跌幅曲线。 曲线说明: X轴:月份。 Y轴:涨跌幅。 测试曲线由零点开始,经历不同的波动后,最终都再次回复到零点。 考虑到股市总体是向上的,所以有些曲线并不是X轴对称的。 仓位控制算法的设计与演进 下面我将从最简单的算法开始设计,并根据其缺点,进行逐步演讲: 方法一:仓位控制模拟-1(根据每月资本金计算仓位) 指导仓位=50%+50%*(估值-点数)/(估值*IF(点位>估值,1,0.4))。也就是50%的基础值,50%的变化值,变化值随着点数变高而降低。当点位上浮100%时,仓位0%;下浮40%时,仓位100%。 调整后仓内资金=月末总资金*指导仓位。也就是月末的持仓资金通过每个月底的总资产来计算。 经测试,这种算法在大涨大跌曲线时,最终造成了较多的亏损。如下图: 原因分析:当跌幅较大时,这时仓位虽然很高,但是本金变得很少,股金随之变少,再也无法赚回原来亏损的资金。 方法二:仓位控制模拟-2(根据固定资本金计算仓位) 指导仓位算法同方法一。 调整后仓内资金=10000*指导仓位。也就是永远按照初始的资金来计算仓位。 经测试,这种算法在大跌大涨时,出现了较多的借款(杠杆资金)。更重要的缺点是:当盈利后,还按照原始资金计算仓位,导致现金与股金失调,仓位控制算法逐步失去意义。 方法三:仓位控制模拟-3(根据递增资本金计算仓位) 指导仓位算法同方法一。 调整后仓内资金=仓位总资金*指导仓位。仓位总资金是不断只递增不减的月末总资金。这样设计是为了解决方法一中遇到点位大跌时,总资金变少后期无法回本的问题。 但是,也有缺点: 1、杠杆太高。在第19个月时,达到了需要借款-2.6倍。不过,指数在估值的基础上跌到这种份上的机率从历史来看,从未发生过。如果发生这种情况,说明整个国家也出现了问题。 2、资金利用率不高。在现实场景下,大部分都是在估值旁边的小范围内变化。而方法一到三的指导仓位算法,导致大部分时间,空仓的闲置资金较多,利用率不高。 大涨大跌时: 方法四:仓位控制模拟-3-1(根据递增资本金计算仓位 & 基础仓位70%) 从这里开始,开始只调整指导仓位的计算方法。这里我们将方法中指导仓位中的基础仓位由50%调整为70%,提高初期的仓位占比。 指导仓位=70%+30%*(估值-点数)/(估值*IF(点位>估值,1,0.4))。也就是70%的基础值,30%的变化值,变化值随着点数变高而降低。当点位上浮100%时,仓位40%;下浮40%时,仓位100%。 大涨大跌时: 方法四、五、六比较相近,最后一起分析。 方法五:仓位控制模拟-3-2(根据递增资本金计算仓位 & 基础仓位80%) 指导仓位=80%+20%*(估值-点数)/(估值*IF(点位>估值,1,0.4))。 大涨大跌时: 方法六:仓位控制模拟-3-3(根据递增资本金计算仓位 & 永远满仓) 指导仓位=100%+0%*(估值-点数)/(估值*IF(点位>估值,1,0.4))。 大涨大跌时: 方法四、五、六中,分别调整了不同的基础仓位百分比,得出的结论类似:虽然最终的盈利都比较高,但是同样造成杠杆过高的问题,方法六中最多时达到了 -400% 的杠杆资金。 方法七:仓位控制模拟-3-4(根据递增资本金计算仓位 & 基础仓位70% & 变化仓位50%) 四、五、六中,变化的百分比较少,只有30%、20%、0%,这样会导致仓位控制算法的作用变得比较少。所以在方法七是在方法四的基础上,调高了变化值。 指导仓位=70%+50%*(估值-点数)/(估值*IF(点位>估值,1,0.4))。 这种方法下,杠杆也不少…… 收益率对比 上面对比了几种不同方案下,可能出现的问题,以及风险所在、杠杆资金需求。下面从收益的角度来对这几个方案进行综合的对比: 3-4 的方案,在过山车行情中,赚得最多,达到了1000%的涨幅! 而平衡波动的三种曲线下,收益率几乎一致。(原因是每个月的涨跌幅其实都是10%)。 再来看一下这几个方案所需要的杠杆资金: 结论 能从上述对比的表格中看出:3-4 相对于 3-2 来说,运用的资金和杠杆基本一致,但是收益却近乎两倍。另外,3-3 满仓方案下,杠杆要求最大,但是收益却并不高。这正体现出,在同样的资金和承担同样的风险的情况下,选择合适的仓位控制算法,能带来更好的收益率。不同的仓位控制算法,收益率大相径庭。这与《前一篇》中描述的理论一致。 其实没有最好的方案,只有最适合的方案。在投资时,需要根据合适的收益率预期、风险预期、以及预期可动用的杠杆资金,来确定需要采用的方案。 根据我个人的情况,我最终选择的是最后一个方案:3-4。
看到文章标题中的“仓位管理”,读者可能会认为它只适用于股市投资。其实不然。只要是投资都涉及到风险、回报率、投资额度,都会涉及到仓位管理。再者,人生本身就带着无数的抉择、风险和回报,人生中的很多事情,其实都是投资的一种。(关于本人的基金投资方法以及仓位管理的原因,见《股票、基金投资方案总结》。) 两三个月前,看了好几篇文章都提到了“凯利公式”,所以自己也去特地去搜索了一些文章并进行学习。看完之后,比较震憾。第一次认识到了仓位管理的重要性竟然如此之高。这也正是原来在赌博机上没有继续赢下去的原因(见:《让赌博成为一种投资行为》)。 目前在股市中,我其实算是一个长线的量化投资者,使用自己编写的模型与程序来分析大量的数据以做出投资决策(去年的成绩还算不错,20150731-20160731,上证指数跌了19%,创业板指跌16.5%,我的投资方案涨了3%。)。在看到这个理论之前,我并不太重视仓位管理的重要性。当时我也有自己的仓位管理公式,但只是一个线性的简单公式。所以学习了这个理论之后,虽然我很难套用凯利公式来更新我的仓位控制算法,但是我却知道了仓位的重要性。所以我决定对我的仓位算法进行重要设计并测试,以得到更优的仓位控制算法。该仓位算法的具体内容,我将在下一篇博客中写出。 凯利公式的原理及介绍,网上比较多,我就不在这里总结了。下面是我学习的几篇文章,给大家推荐: 《凯利公式——仓位控制的利器》、《如何利用凯利公式控制股票仓位》、《凯利公式的神奇》。 下面是摘抄的一些细节: “同样一个赌局,使用经过公式的计算后的仓位20%进行现金管理,与任意下资的相比,有着天壤之别!“这就是知识的力量!”。由于从小就经常在赌场玩,所以每次看到这里,我都感到莫名的震撼。赌场中又有几个人是真的懂得仓位管理的呢? ”“索普利用工作之余,通过数个月的艰苦演算,写了一篇题为《“二十一点”优选策略》的数学论文。他利用自己的知识,一夜之间“奇袭”了内华达雷诺市所有的赌场,并成功的从二十一点赌桌上赢得了上万美元。他还是美国华尔街量化交易对冲基金的鼻祖,70年代首创第一个量化交易对冲基金。1962年出版了他的专著《打败庄家》,成为金融学的经典著作之一。” “如何利用凯利公式在现实生活中赚钱? 那就是要去创造满足凯利公式运用条件的“赌局”。在我看来,这个“赌局”一定是来自金融市场。 对于一个优秀的交易系统来说什么是最重要的?一个期望收益为正的买卖规则占到重要性的10%,而一个好的资金控制方法占到了重要性的40%,剩下的50%是操控人的心理控制力。”
下面这篇文章总结了 asp.net MVC 框架程序的生命周期。觉得写得不错,故转载一下。 转载自:http://www.cnblogs.com/yplong/p/5582576.html 首先我们知道http是一种无状态的请求,他的生命周期就是从客户端浏览器发出请求开始,到得到响应结束。那么MVC应用程序从发出请求到获得响应,都做了些什么呢? 本文我们会详细讨论MVC应用程序一个请求的生命周期,从一个控件到另一个控件是怎样被处理的。我们还会详细介绍一下整个请求的生命周期中,用到的相关组件。因为在平常的开发过程中,我们可能知道怎样去使用MVC框架来处理相关的请求,大部分的时候我们只是在controller和action方法之间做相关的处理,对于真正内在的运行机制可能不是很了解。其实当我们对内在机制有了一定的了解以后,会发现微软的MVC框架的扩展性很强,到处都留有扩展接口,让我们通过扩展能够自己定义自己所需要的处理机制,这也正是为什么MVC框架如此出名的原因。 当我最开始学习使用mvc的时候,困扰我的一个问题就是,一个请求的流程控制是怎样的呢?从view到controller再到action之间经历了什么?那个时候我还不清楚HTTP module和HTTP handler在处理一个请求中扮演什么样的角色,起什么样的作用呢。毕竟MVC是一个web开发框架,在整个请求处理过程中,肯定包含了http module和http handler。其实还有很多相关的组件包含在一个完整的mvc应用程序请求生命周期里,在整个请求过程中他们都扮演者非常重要的角色。尽管大部分时候我们都使用的是框架提供的默认的函数,但是如果我们了解了每个控件所扮演的角色,我们就可以轻松的扩展和使用我们自己实现的方法,就目前来说MVC是扩展性比较强的框架。下面是本章节的主要内容: HttpApplication HttpModule HttpHandler ASP.NET MVC运行机制 UrlRoutingModule RouteHandler MvcHandler ControllerFactory Controller ActionInvoker ActionResult ViewEngine HttpApplication 我们都知道,在ASP.NET MVC框架出现之前,我们大部分开发所使用的框架都是ASP.NET WebForm.其实不管是MVC还是WebForm,在请求处理机制上,大部分是相同的。这涉及到IIS对请求的处理,涉及的知识较多,我们就不做介绍了,下次有机会我写一篇专文。我们从HttpApplication说起。先看看微软官方是怎么定义HttpApplication的: 定义 ASP.NET 应用程序中的所有应用程序对象共有的方法、属性和事件。此类是用户在 Global.asax 文件中所定义的应用程序的基类。 可能我翻译不是很准确,原文连接在这里:https://msdn.microsoft.com/en-us/library/system.web.httpapplication(v=vs.110).aspx 微软官方文档中Remark里有这么一段话:HttpApplication 类的实例是在 ASP.NET 基础结构中创建的,而不是由用户直接创建的。使用HttpApplication 类的一个实例来处理其生存期中收到的众多请求。但是,它每次只能处理一个请求。这样,成员变量才可用于存储针对每个请求的数据。 意思就是说ASP.NET应用程序,不管是MVC还是WebForm,最终都会到达一个HttpApplication类的实例。HttpApplication是整个ASP.NET基础架构的核心,负责处理分发给他的请求。HttpApplication处理请求的周期是一个复杂的过程,在整个过程中,不同阶段会触发相映的事件。我们可以注册相应的事件,将处理逻辑注入到HttpApplication处理请求的某个阶段。在HttpApplication这个类中定义了19个事件来处理到达HttpApplication实例的请求。就是说不管MVC还是WebForm,最终都要经过这19个事件的处理,那么除了刚才说的MVC和WebFrom在请求处理机制上大部分都是相同的,不同之处在哪呢?他们是从哪里开始分道扬镳的呢?我们猜想肯定就在这19个方法中。我们继续往下看。 我们来看看这19个事件: 应用程序按照以下顺序执行由 global.asax 文件中定义的模块或用户代码处理的事件: 事件名称: 简单描述: BeginRequest 在 ASP.NET 响应请求时作为 HTTP 执行管线链中的第一个事件发生 AuthenticateRequest 当安全模块已建立用户标识时发生。注:AuthenticateRequest事件发出信号表示配置的身份验证机制已对当前请求进行了身份验证。预订 AuthenticateRequest 事件可确保在处理附加的模块或事件处理程序之前对请求进行身份验证 PostAuthenticateRequest 当安全模块已建立用户标识时发生。PostAuthenticateRequest事件在 AuthenticateRequest 事件发生之后引发。预订PostAuthenticateRequest 事件的功能可以访问由PostAuthenticateRequest 处理的任何数据 AuthorizeRequest 当安全模块已验证用户授权时发生。AuthorizeRequest 事件发出信号表示 ASP.NET 已对当前请求进行了授权。预订AuthorizeRequest 事件可确保在处理附加的模块或事件处理程序之前对请求进行身份验证和授权 PostAuthorizeRequest 在当前请求的用户已获授权时发生。PostAuthorizeRequest 事件发出信号表示 ASP.NET 已对当前请求进行了授权。预订PostAuthorizeRequest 事件可确保在处理附加的模块或处理程序之前对请求进行身份验证和授权 ResolveRequestCache 当 ASP.NET 完成授权事件以使缓存模块从缓存中为请求提供服务时发生,从而跳过事件处理程序(例如某个页或 XML Web services)的执行 PostResolveRequestCache 在 ASP.NET 跳过当前事件处理程序的执行并允许缓存模块满足来自缓存的请求时发生。)在 PostResolveRequestCache 事件之后、PostMapRequestHandler 事件之前创建一个事件处理程序(对应于请求 URL 的页 PostMapRequestHandler 在 ASP.NET 已将当前请求映射到相应的事件处理程序时发生。 AcquireRequestState 当 ASP.NET 获取与当前请求关联的当前状态(如会话状态)时发生。 PostAcquireRequestState 在已获得与当前请求关联的请求状态(例如会话状态)时发生。 PreRequestHandlerExecute 恰好在 ASP.NET 开始执行事件处理程序(例如,某页或某个 XML Web services)前发生。 PostRequestHandlerExecute 在 ASP.NET 事件处理程序(例如,某页或某个 XML Web service)执行完毕时发生。 ReleaseRequestState 在 ASP.NET 执行完所有请求事件处理程序后发生。该事件将使状态模块保存当前状态数据。 PostReleaseRequestState 在 ASP.NET 已完成所有请求事件处理程序的执行并且请求状态数据已存储时发生。 UpdateRequestCache 当 ASP.NET 执行完事件处理程序以使缓存模块存储将用于从缓存为后续请求提供服务的响应时发生。 PostUpdateRequestCache 在 ASP.NET 完成缓存模块的更新并存储了用于从缓存中为后续请求提供服务的响应后,发生此事件。 LogRequest 在 ASP.NET 完成缓存模块的更新并存储了用于从缓存中为后续请求提供服务的响应后,发生此事件。 仅在 IIS 7.0 处于集成模式并且 .NET Framework 至少为 3.0 版本的情况下才支持此事件 PostLogRequest 在 ASP.NET 处理完 LogRequest 事件的所有事件处理程序后发生。 仅在 IIS 7.0 处于集成模式并且 .NET Framework 至少为 3.0 版本的情况下才支持此事件。 EndRequest 在 ASP.NET 响应请求时作为 HTTP 执行管线链中的最后一个事件发生。 在调用 CompleteRequest 方法时始终引发 EndRequest 事件。 对于一个ASP.NET应用程序来说,HttpApplication派生与Global.aspx(可以看看我们创建的应用程序都有一个Global.aspx文件),我们可以在Global.aspx文件中对HttpApplication的请求进行定制即注入这19个事件中的某个事件进行逻辑处理操作。在Global.aspx中我们按照"Application_{Event Name}"这样的方法命名进行事件注册。 Event Name就是上面19个事件的名称。比如Application_EndRequest就用于处理Application的EndRequest事件。 HttpModule ASP.NET拥有一个高度可扩展的引擎,并且能够处理对于不同资源类型的请求。这就是HttpModule。当一个请求转入ASP.net管道时,最终负责处理请求的是与资源相匹配的HttpHandler对象,但是在HttpHandler进行处理之前,ASP.NET先会加载并初始化所有配置的HttpModule对象。HttpModule初始化的时候,会将一些回调事件注入到HttpApplication相应的事件中。所有的HttpModule都实现了IHttpModule接口,该接口有一个有一个Init方法。 public interface IHttpModule { // Methods void Dispose(); void Init(HttpApplication context); } 看到Init方法呢接受一个HttpApplication对象,有了这个对象就很容易注册HttpApplication中19个事件中的某个事件了。这样当HttpApplication对象执行到某个事件的时候自然就会出发。 HttpHandler 对于不同的资源类型的请求,ASP.NET会加载不同的HttpHandler来处理。所有的HttpHandler都实现了IhttpHandler接口。 public interface IHttpHandler { // Methods void ProcessRequest(HttpContext context); // Properties bool IsReusable { get; } } 我们看到该接口有一个方法ProcessRequest,顾名思义这个方法就是主要用来处理请求的。所以说每一个请求最终分发到自己相应的HttpHandler来处理该请求。 ASP.NET MVC 运行机制 好了,上面说了那么多,其实都是给这里做铺垫呢。终于到正题了。先看看下面这张图,描述了MVC的主要经历的管道事件: 上图就是一个完整的mvc应用程序的一个http请求到响应的整个儿所经历的流程。从UrlRoutingModule拦截请求到最终ActionResult执行ExecuteResult方法生成响应。 下面我们就来详细讲解一下这些过程都做了些什么。 UrlRoutingModule MVC应用程序的入口UrlRoutingModule 首先发起一个请求,我们前面讲到ASP.NET 会加载一个HttpModule对象的初始化事件Init,而所有的HttpModule对象都实现了IHttpModule接口。我们看看UrlRoutingModule的实现: 从上图中我们看到UrlRoutingModule实现了接口IHttpModule,当一个请求转入ASP.NET管道时,就会加载 UrlRoutingModule对象的Init()方法。 那么为什么偏偏是UrlRoutingModule被加载初始化了呢?为什么不是别的HttpModule对象呢?带着这个疑问我们继续。 在ASP.NET MVC中,最核心的当属“路由系统”,而路由系统的核心则源于一个强大的System.Web.Routing.dll组件。System.Web.Routing.dll 不是MVC所特有的,但是MVC框架和它是密不可分的。 首先,我们要了解一下UrlRoutingModule是如何起作用的。 (1)IIS网站的配置可以分为两个块:全局 Web.config 和本站 Web.config。Asp.Net Routing属于全局性的,所以它配置在全局Web.Config 中,我们可以在如下路径中找到:“C\Windows\Microsoft.NET\Framework\版本号\Config\Web.config“,我提取部分重要配置大家看一下: <httpModules> <add name="OutputCache" type="System.Web.Caching.OutputCacheModule" /> <add name="Session" type="System.Web.SessionState.SessionStateModule" /> <add name="WindowsAuthentication" type="System.Web.Security.WindowsAuthenticationModule" /> <add name="FormsAuthentication" type="System.Web.Security.FormsAuthenticationModule" /> <add name="PassportAuthentication" type="System.Web.Security.PassportAuthenticationModule" /> <add name="RoleManager" type="System.Web.Security.RoleManagerModule" /> <add name="UrlAuthorization" type="System.Web.Security.UrlAuthorizationModule" /> <add name="FileAuthorization" type="System.Web.Security.FileAuthorizationModule" /> <add name="AnonymousIdentification" type="System.Web.Security.AnonymousIdentificationModule" /> <add name="Profile" type="System.Web.Profile.ProfileModule" /> <add name="ErrorHandlerModule" type="System.Web.Mobile.ErrorHandlerModule, System.Web.Mobile, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" /> <add name="ServiceModel" type="System.ServiceModel.Activation.HttpModule, System.ServiceModel.Activation, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> <add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule" /> <add name="ScriptModule-4.0" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/> </httpModules> 大家看到没有,我上面标红的那一行:<add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule" /> UrlRoutingModule并不是MVC特有的,这是一个全局配置,就是说所有的ASP.NET请求都会到达这里,所以该Module还不能最终决定是MVC还是WebForm请求。但是也是至关重要的地方。 (2)通过在全局Web.Config中注册 System.Web.Routing.UrlRoutingModule,IIS请求处理管道接到请求后,就会加载 UrlRoutingModule类型的Init()方法。其源码入下: [TypeForwardedFrom("System.Web.Routing, Version=3.5.0.0, Culture=Neutral, PublicKeyToken=31bf3856ad364e35")] public class UrlRoutingModule : IHttpModule { // Fields private static readonly object _contextKey = new object(); private static readonly object _requestDataKey = new object(); private RouteCollection _routeCollection; // Methods protected virtual void Dispose() { } protected virtual void Init(HttpApplication application) { if (application.Context.Items[_contextKey] == null) { application.Context.Items[_contextKey] = _contextKey; application.PostResolveRequestCache += new EventHandler(this.OnApplicationPostResolveRequestCache); } } private void OnApplicationPostResolveRequestCache(object sender, EventArgs e) { HttpApplication application = (HttpApplication) sender; HttpContextBase context = new HttpContextWrapper(application.Context); this.PostResolveRequestCache(context); } [Obsolete("This method is obsolete. Override the Init method to use the PostMapRequestHandler event.")] public virtual void PostMapRequestHandler(HttpContextBase context) { } public virtual void PostResolveRequestCache(HttpContextBase context) { RouteData routeData = this.RouteCollection.GetRouteData(context); if (routeData != null) { IRouteHandler routeHandler = routeData.RouteHandler; if (routeHandler == null) { throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, SR.GetString("UrlRoutingModule_NoRouteHandler"), new object[0])); } if (!(routeHandler is StopRoutingHandler)) { RequestContext requestContext = new RequestContext(context, routeData); context.Request.RequestContext = requestContext; IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext); if (httpHandler == null) { throw new InvalidOperationException(string.Format(CultureInfo.CurrentUICulture, SR.GetString("UrlRoutingModule_NoHttpHandler"), new object[] { routeHandler.GetType() })); } if (httpHandler is UrlAuthFailureHandler) { if (!FormsAuthenticationModule.FormsAuthRequired) { throw new HttpException(0x191, SR.GetString("Assess_Denied_Description3")); } UrlAuthorizationModule.ReportUrlAuthorizationFailure(HttpContext.Current, this); } else { context.RemapHandler(httpHandler); } } } } [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")] void IHttpModule.Dispose() { this.Dispose(); } [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")] void IHttpModule.Init(HttpApplication application) { this.Init(application); } // Properties public RouteCollection RouteCollection { get { if (this._routeCollection == null) { this._routeCollection = RouteTable.Routes; } return this._routeCollection; } [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")] set { this._routeCollection = value; } } } 看看上面的UrlRoutingModule源码里面是怎么实现Init方法的,Init()方法里面我标注红色的地方: application.PostResolveRequestCache += new EventHandler(this.OnApplicationPostResolveRequestCache); 这一步至关重要哈,看到没有,就是对我们在HttpApplication那19个事件中的PostResolveRequestCache事件的注册。注册的方法是OnApplicationPostResolveRequestCache事件。也就是说HttpApplication对象在执行到PostResolveRequestCache这个事件的时候,就会执行OnApplicationPostResolveRequestCache事件。决定是MVC机制处理请求的关键所在就是OnApplicationPostResolveRequestCache事件。 从源码中我们看出,OnApplicationPostResolveRequestCache事件执行的时候,最终执行了PostResolveRequestCache这个方法。最关键的地方呢就在这里了。 当请求到达UrlRoutingModule的时候,UrlRoutingModule取出请求中的Controller、Action等RouteData信息,与路由表中的所有规则进行匹配,若匹配,把请求交给IRouteHandler,即MVCRouteHandler。我们可以看下UrlRoutingModule的源码来看看,以下是几句核心的代码: 我们再分析一下这个方法的源码: 1 public virtual void PostResolveRequestCache(HttpContextBase context) 2 { 3 // 通过RouteCollection的静态方法GetRouteData获取到封装路由信息的RouteData实例 4 RouteData routeData = this.RouteCollection.GetRouteData(context); 5 if (routeData != null) 6 { 7 // 再从RouteData中获取MVCRouteHandler 8 IRouteHandler routeHandler = routeData.RouteHandler; 9 ...... 10 if (!(routeHandler is StopRoutingHandler)) 11 { 12 ...... 13 // 调用 IRouteHandler.GetHttpHandler(),获取的IHttpHandler 类型实例,它是由 IRouteHandler.GetHttpHandler获取的,这个得去MVC的源码里看 14 IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext); 15 ...... 16 // 合适条件下,把之前将获取的IHttpHandler 类型实例 映射到IIS HTTP处理管道中 17 context.RemapHandler(httpHandler); 18 } 19 } 20 } 看到了吧,通过路由规则,返回的不为空,说明匹配正确,关于路由规则的匹配,说起来也不短,这里就不大幅介绍,有时间下次再开篇详解路由机制。匹配成功后,返回一个RouteData类型的对象,RouteData对象都有些什么属性呢?看看这行源码: IRouteHandler routeHandler = routeData.RouteHandler;或者看源码我们知道,RouteDate有一个RouteHandler属性。 那么UrlRouting Module是如何选择匹配规则的呢? 我们看看我们新建的MVC应用程序,在App_Start文件夹下面有一个RouteConfig.cs类,这个类的内容如下: 1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Web; 5 using System.Web.Mvc; 6 using System.Web.Routing; 7 8 namespace ApiDemo 9 { 10 public class RouteConfig 11 { 12 public static void RegisterRoutes(RouteCollection routes) 13 { 14 routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 15 16 routes.MapRoute( 17 name: "Default", 18 url: "{controller}/{action}/{id}", 19 defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } 20 ); 21 } 22 } 23 } 我们在这个类里面,主要是给路由表添加路由规则。在看看上面的UrlRoutingModule类,里面有一个RoutCollection属性,所以UrlRoutingModule能够获取路由表中的所有规则,这里值得注意的是,路由规则的匹配是有顺序的,如果有多个规则都能够匹配,UrlRoutingModule至选择第一个匹配的规则就返回,不再继续往下匹配了。相反的如果一个请求,没有匹配到任何路由,那么该请求就不会被处理。 这里返回的RouteData里的RouteHandler就是MVCRouteHandler。为什么呢?那我们继续往下看RouteHandler。 RouteHandler 生成MvcHander 在上面路由匹配的过程中,与匹配路由相关联的MvcRouteHandler ,MvcRouteHandler 实现了IRouteHandler 接口。MvcRouteHandler 主要是用来获取对MvcHandler的引用。MvcHandler实现了IhttpHandler接口。 MVCRouteHandler的作用是用来生成实现IHttpHandler接口的MvcHandler。而我们前面说过最终处理请求的都是相对应的HttpHandler。那么处理MVC请求的自然就是这个MvcHandler。所以这里返回MvcRouteHandler至关重要: 那么,MvcRouteHandler从何而来呢?众所周知,ASP.NET MVC项目启动是从Global中的Application_Start()方法开始的,那就去看看它: public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); } } public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); System.Web.Mvc.RouteCollectionExtensions routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); } } 看看我上面标红的代码:这是路由注册,玄机就在这里。那我们去看看MapRoute源码就知道咯: 1 public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces) 2 { 3 ...... 4 5 Route route = new Route(url, new MvcRouteHandler()) { 6 Defaults = new RouteValueDictionary(defaults), 7 Constraints = new RouteValueDictionary(constraints), 8 DataTokens = new RouteValueDictionary() 9 }; 10 ...... 11 return route; 12 } 看看我们5-8行代码,在MVC应用程序里,在路由注册的时候,我们就已经给他一个默认的HttpRouteHandler对象,就是 New MvcRouteHandler().现在我们反推回去,我们MVC程序在路由注册的时候就已经确定了HttpRouteHandler为MvcRouteHandler,那么当我们在前面PostResolveRequestCache方法里,当我们的请求与路由匹配成功后,自然会返回的是MvcRouteHandler。 好啦,MvcRouteHandler生成了。那么MvcRouteHandler能做什么呢?又做了什么呢? 再回头看看 PostResolveRequestCache方法,在成功获取到IHttpRouteHandler对象即MvcRouteHandler之后,又做了下面这一个操作: IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext); 我们看看这个IHttpHandler 的源码: namespace System.Web.Routing { public interface IRouteHandler { IHttpHandler GetHttpHandler(RequestContext requestContext); } } 有一个GetHttpHandler的方法,恰好就调用了这个方法。那我们看看MvcRouteHandler是怎么实现这个GetHttpHandler的呢: 1 public class MvcRouteHandler : IRouteHandler 2 { 3 // Fields 4 private IControllerFactory _controllerFactory; 5 6 // Methods 7 public MvcRouteHandler() 8 { 9 } 10 11 public MvcRouteHandler(IControllerFactory controllerFactory) 12 { 13 this._controllerFactory = controllerFactory; 14 } 15 16 protected virtual IHttpHandler GetHttpHandler(RequestContext requestContext) 17 { 18 requestContext.HttpContext.SetSessionStateBehavior(this.GetSessionStateBehavior(requestContext)); 19 return new MvcHandler(requestContext); 20 } 21 22 protected virtual SessionStateBehavior GetSessionStateBehavior(RequestContext requestContext) 23 { 24 string str = (string) requestContext.RouteData.Values["controller"]; 25 if (string.IsNullOrWhiteSpace(str)) 26 { 27 throw new InvalidOperationException(MvcResources.MvcRouteHandler_RouteValuesHasNoController); 28 } 29 IControllerFactory factory = this._controllerFactory ?? ControllerBuilder.Current.GetControllerFactory(); 30 return factory.GetControllerSessionBehavior(requestContext, str); 31 } 32 33 IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext) 34 { 35 return this.GetHttpHandler(requestContext); 36 } 37 } 38 39 看第16-20行代码,这时候应该明白了吧。顺理成章的返回了MvcHandler对象。记得我们前面说过,请求最终是被相对应的HttpHander对象处理的。MvcHandler就是那个用来处理Mvc请求的HttpHandler。MvcRouteHandler把请求交给了MvcHandler去做请求处理管道中后续事件的处理操作了。 下面我们就看看MvcHandler做了些什么: MvcHandler MvcHandler就是最终对request进行处理。 MvcHandler的定义如下: 我们可以看到MvcHandler就是一个普通的Http Handler.我们知道一个http handler需要实现一个ProcessRequest()的方法,这个方法就是处理request的核心。所以MvcHandler实现了ProcessRequest()方法。 ProcessRequest主要功能: (1)在ASP.NET MVC中,会调用MvcHandler的ProcessRequest()方法,此方法会激活具体请求的Controller类对象,触发Action方法,返回ActionResult实例。 (2)如果ActionResult是非ViewResult,比如JsonResult, ContentResult,这些内容将直接被输送到Response响应流中,显示给客户端;如果是ViewResult,就会进入下一个渲染视图环节。 (3)在渲染视图环节,ViewEngine找到需要被渲染的视图,View被加载成WebViewPage<TModel>类型,并渲染生成Html,最终返回Html。 ProcessRequest()定义如下: 1 // Copyright (c) Microsoft Open Technologies, Inc.<pre>// All rights reserved. See License.txt in the project root for license information. 2 void IHttpHandler.ProcessRequest(HttpContext httpContext) 3 { 4 ProcessRequest(httpContext); 5 } 6 protected virtual void ProcessRequest(HttpContext httpContext) 7 { 8 HttpContextBase iHttpContext = new HttpContextWrapper(httpContext); 9 ProcessRequest(iHttpContext); 10 } 11 protected internal virtual void ProcessRequest(HttpContextBase httpContext) { 12 SecurityUtil.ProcessInApplicationTrust(() => { 13 IController controller; 14 IControllerFactory factory; 15 ProcessRequestInit(httpContext, out controller, out factory); 16 try 17 { 18 controller.Execute(RequestContext); 19 } 20 finally 21 { 22 factory.ReleaseController(controller); 23 } 24 }); 25 } 从上面的代码可以看出调用了一个ProcessRequestInit()方法,定义如下: 1 private void ProcessRequestInit(HttpContextBase httpContext, 2 out IController controller, out IControllerFactory factory) { 3 // If request validation has already been enabled, make it lazy. 4 // This allows attributes like [HttpPost] (which looks 5 // at Request.Form) to work correctly without triggering full validation. 6 bool? isRequestValidationEnabled = 7 ValidationUtility.IsValidationEnabled(HttpContext.Current); 8 if (isRequestValidationEnabled == true) { 9 ValidationUtility.EnableDynamicValidation(HttpContext.Current); 10 } 11 AddVersionHeader(httpContext); 12 RemoveOptionalRoutingParameters(); 13 // Get the controller type 14 string controllerName = RequestContext.RouteData.GetRequiredString("controller"); 15 // Instantiate the controller and call Execute 16 factory = ControllerBuilder.GetControllerFactory(); 17 controller = factory.CreateController(RequestContext, controllerName); 18 if (controller == null) { 19 throw new InvalidOperationException( 20 String.Format( 21 CultureInfo.CurrentCulture, 22 MvcResources.ControllerBuilder_FactoryReturnedNull, 23 factory.GetType(), 24 controllerName)); 25 } 26 } 在ProcessRequestInit()方法中首先创建了ControllerFactory()的对象 factory.然后ControllerFactory创建了相关Controller的实例.最终调用了Controller的Excute()方法。 好我们再来看看ControllerFactory: ControllerFactory 主要是用来生成Controller对象 ControllerFactory实现了接口IControllerFactory. Controller 到这里我们大概就知道了,MvcHandler通过ProcessRequest()方法最终创建了Controller对象,这里我们都应该知道,Controller里面包含很多的Action方法,每一次请求至少一个Action方法会被调用。为了明确的实现IController接口,框架里面有一个ControllerBase的类已经实现了IController接口,其实我们自己的Controller也可以不继承ControllerBase,只要实现IController接口即可。 1 public abstract class ControllerBase : IController 2 { 3 protected virtual void Execute(RequestContext requestContext) 4 { 5 if (requestContext == null) 6 { 7 throw new ArgumentNullException("requestContext"); 8 } 9 if (requestContext.HttpContext == null) 10 { 11 throw new ArgumentException( 12 MvcResources.ControllerBase_CannotExecuteWithNullHttpContext, 13 "requestContext"); 14 } 15 VerifyExecuteCalledOnce(); 16 Initialize(requestContext); 17 using (ScopeStorage.CreateTransientScope()) 18 { 19 ExecuteCore(); 20 } 21 } 22 protected abstract void ExecuteCore(); 23 // ....... controller对象实际上使用ActionInvoker来调用Action方法的,当Controller对象被创建后,会执行Controller对象的基类ControllerBase类里面的Excute方法。Excute方法又调用了ExcuteCore()方法。Controller类里面实现了ExcuteCore()方法。ExcuteCore调用了ActionInvoker的InvokerAction方法来调用Action方法。 ActionInvoker ActionInvoker方法有很重要的责任来查找Controller中的Action方法并且调用。 ActionInvoker是一个实现了IActionInvoker接口的对象: bool InvokeAction( ControllerContext controllerContext, string actionName ) Controller类里面暴露了一个ActionInvoker 属性,会返回一个ControllerActionInvoker 。ActionInvoker通过CreateActionInvoker()方法来创建ControllerActionInvoker对象。 public IActionInvoker ActionInvoker { get { if (_actionInvoker == null) { _actionInvoker = CreateActionInvoker(); } return _actionInvoker; } set { _actionInvoker = value; } } protected virtual IActionInvoker CreateActionInvoker() { return new ControllerActionInvoker(); } 我们看到CreateActionInvoker()是一个Virtual方法,我们可以实现自己的ActionInvoker. ActionInvoker类需要匹配Controller中详细的Action来执行,而这些详细的信息是由ControllerDescriptor 提供的。ControllerDescriptor 和ActionDescriptor在ActionInvoker中扮演重要的角色。这两个分别是对Controler和Action的详细描述。ControllerDescriptor 描述了Controller的相关信息比如name,action,type等。 ActionDescriptor 描述了Action相关的详情,比如name,controller,parameters,attributes和fiflters等。 ActionDescriptor 中一个中要的方法就是FindAction(),这个方法返回一个ActionDescriptor 对象,所以ActionInvoker知道该调用哪个Action。 ActionResult 到目前为止,我们看到了Action方法被ActionInvoker调用。所有的Action方法有一个特性,就是返回一个ActionResult类型的数据。 public abstract class ActionResult { public abstract void ExecuteResult(ControllerContext context); } ExecuteResult()是一个抽象方法,所以不同的子类可以提供不同的ExecuteResult()实现。 ActionResult执行后响应输出到客户端。 ViewEngine ViewResult几乎是大部分应用程序的返回类型,主要通过ViewEngine引擎来展示view的。ViewEngine可能主要就是生成Html元素的引擎。Framwork提供了2种引擎,Razor View Engine 和Web Form View Engine.如果你想自定义引擎,你可以创建一个引擎只要实现IViewEngine接口即可。 IViewEngine 有下面几个方法: 1、FindPartialView :当controller需要返回一个PartialView的时候,FindPartialView方法 就会被调用。 2、FindView 3、ReleaseView :主要用来有ViewEngine释放资源 ViewResultBase 和ViewResult是比较重要的两个类。ViewResultBase 包含下面的实现代码: if (View == null) { result = FindView(context); //calls the ViewResult's FindView() method View = result.View; } ViewContext viewContext = new ViewContext(context, View, ViewData, TempData); View.Render(viewContext, context.HttpContext.Response.Output); protected abstract ViewEngineResult FindView(ControllerContext context); //this is implemented by //the ViewResult protected override ViewEngineResult FindView(ControllerContext context) { ViewEngineResult result = ViewEngineCollection.FindView(context, ViewName, MasterName); if (result.View != null) { return result; } //rest of the code omitted } 当ViewResult的方法ExecuteResult被调用后,ViewResultBase 的ExecuteResult 方法被调用,然后ViewResultBase 调用ViewResult的FindView 。紧接着ViewResult 返回ViewEngineResult,之后ViewEngineResult调用Render()方法来绘制html输出响应。 总结:如果我们理解了整个过程中发生了什么,哪些类和哪些方法被调用,我们就可以在需要扩展的地方轻松的进行扩展。
之前转载过一篇对 Martin Fowler 大师写的微服务架构的说明文章:《微服务(Microservices)》。今天再转载一篇对于这个架构的优缺点进行总结的文章。 转载自:《微服务,让开发过程更简单还是更复杂?》、《有关微服务架构的争论:更简单还是更复杂?》。 随着DevOps、持续交付等理念的深入人心,微服务架构开始走进我们的视野。 那么微服务是业界期待已久的解决方案么?或者说微服务要比整体解决方案更加简单? 让我们先对微服务下个定义: 微服务是用一组小服务的方式来构建一个应用,服务独立运行在不同的进程中,服务之间通过轻量的通讯机制(如RESTful接口)来交互,并且服务可以通过自动化部署方式独立部署。正因为微服务架构中,服务之间是相互独立的,所以不同的服务可以使用不同的语言来开发,或者根据业务的需求使用不同类型的数据库。 来自ThoughtWorks的James Lewis和Martin Fowler分享了他们对微服务架构的理解以及看法。文章中作者详细介绍了微服务的特点以及相对于传统架构的微服务架构的优势。 微服务的一些优势是显而易见的: 服务简单,只关注一个业务功能 在James看来,传统的整体风格的架构在构建部署和扩展伸缩方面有很大的局限性,其服务端应用就像是一块铁板,笨重且不可拆分,系统中任何程序的改变都需要整个应用重新构建和部署新版本。在进行水平扩展时也只能整个系统扩展,而不能针对某一个功能模块进行扩展。 而微服务架构将系统以组件化的方式分解为多个服务,服务之间相对独立且松耦合,单一功能的改变只需要重新构建部署相应的服务即可。 每个微服务可由不同团队开发 传统的开发模式在分工时往往以技术为单位,比如UI团队、服务端团队和数据库团队,这样的分工可能会导致任何功能上的改变都需要跨团队沟通和协调。而微服务则倡导围绕服务来分工,不同的服务可以采用不同的技术来实现,一个团队中应该包含开发所需的所有技能,比如用户体验、数据库、项目管理。 微服务是松散耦合的 微服务架构抛弃了ESB复杂的业务规则编排、消息路由等功能,微服务架构中服务是高内聚的,每个服务都会处理相应的业务,所有的业务逻辑应该尽量在服务内部处理,且服务间的通信尽可能的轻量化,比如使用Restful的方式。 可用不同的编程语言与工具开发 传统的软件开发中经常会使用同一个技术平台来解决所有的问题,而经验表明使用合适的工具做合适的事情会让开发变得事半功倍。微服务架构天生就具有这样的特性,我们可以使用Node.js来开发一个简单的报表页面,使用C++来编写一个实时聊天组件。 微服务架构的引入为多样化持久保存数据提供可能,持久层可以使用传统关系数据库和NoSQL。不同于传统的应用,微服务架构中,我们可以为每个服务选择一个新的适合业务逻辑的数据库系统,比如MongoDB、PostgreSQL。这样做的好处是显而易见的,首先我们可以根据业务类型(读多还是写多等)来决定使用哪种类型的数据库,其次这样可以减小单个数据库的负载。 James的文章在社区引起了广泛的讨论,Contino公司的CTO Benjamin Wootton撰文表示微服务并没有想象中的那么好,并建议开发者在选用此架构时一定要慎重。Benjamin认为微服务架构时可能会面临下面一些挑战: 运维开销 更多的服务也就意味着更多的运维,产品团队需要保证所有的相关服务都有完善的监控等基础设施,传统的架构开发者只需要保证一个应用正常运行,而现在却需要保证几十甚至上百道工序高效运转,这是一个艰巨的任务。 DevOps要求 使用微服务架构后,开发团队需要保证一个Tomcat集群可用,保证一个数据库可用,这就意味着团队需要高品质的DevOps和自动化技术。而现在,这样的全栈式人才很少。 隐式接口 服务和服务之间通过接口来“联系”,当某一个服务更改接口格式时,可能涉及到此接口的所有服务都需要做调整。 重复劳动 在很多服务中可能都会使用到同一个功能,而这一功能点没有足够大到提供一个服务的程度,这个时候可能不同的服务团队都会单独开发这一功能,重复的业务逻辑,这违背了良好的软件工程中的很多原则。 分布式系统的复杂性 微服务通过REST API或消息来将不同的服务联系起来,这在之前可能只是一个简单的远程过程调用。分布式系统也就意味着开发者需要考虑网络延迟、容错、消息序列化、不可靠的网络、异步、版本控制、负载等,而面对如此多的微服务都需要分布式时,整个产品需要有一整套完整的机制来保证各个服务可以正常运转。 事务、异步、测试面临挑战 跨进程之间的事务、大量的异步处理、多个微服务之间的整体测试都需要有一整套的解决方案,而现在看起来,这些技术并没有成熟。 总而言之,微服务架构有很多吸引人的地方,不过在拥抱微服务之前要认清它所带来的挑战。而每一种架构都有其优缺点,我们需要根据项目业务和团队情况来选择合适的架构。
下面是一位同事对当前的产品开发框架提出的一些建议,以及我的回复。我觉得一些问题提得有一定的代表性,在征得本人同意后,将本邮件发布在博客中。 同时,也非常希望对框架、产品有好的建议的小伙伴,都可以给我发邮件:9474649 @ qq.com。 发件人: 胡庆访 发送时间: 2016-07-16 15:36 收件人: 杨盛元 主题: Re: 渣打银行项目,关于rafy框架,一点个人意见。 盛元: 由于最近工作时间的问题,一直没有回复你的技术建议,深感抱歉。 首先,非常感谢你提出的技术上的建议。我们非常期望项目组上的开发人员都能象你这样深入的思考,和产品组的研发人员一起探讨,提出更多的技术建议,一起让产品不断更新升级。 对于你邮件中提出的问题,我整理了一下。下面对它们进行一一的回复。 1、领域建模的意义。 答:“领域模型是辅助技术人员更容易理解业务的设计图。”这句是没有问题的。其实我们产品上的新模块开发也是这样去做的。其实在培训的视频中,已经说明了,虽然 Rafy 使用的是 CodeFirst 的开发模式 ,但是开发者的第一步应该是使用 UML 来进行领域建模,而非直接使用 Rafy 框架进行编码。先画图而不是先编码的方式的目的也是为了帮助开发人员去理解业务。使用 UML 的原因,也是因为这一步是与任何框架无关的。同时,这样也更进一步地确定出领域模型未来在程序中的表现形式。 2、三层变为两层的问题。 答:几层并不重要,重要的是关注点分离就好了。另外,Rafy 框架中的 Repository 代码是可以分开来放在单独的项目中的。 3、框架能否写 SQL 的问题。 答:Rafy 框架体系中包含一个 ORM 框架:Rafy 领域实体框架。任何 ORM 框架为了保证开发的灵活性,都会允许开发者去编写 SQL。但是 ORM 框架也不会建议开发者去编写原生 SQL。 4、“领域模型,只是辅助技术更好的理解业务的一种UML图,并不是用来引导项目技术架构的图。(简单点就是:技术什么都不用动,理解业务就好。)” 答:其实业务和技术之间存在着很大的鸿沟,Eric Evans 在提出 DDD 时,就是为了解决这个问题。而领域建模只是解决这个鸿沟的第一个步骤。有了领域模型图之后,如何使用技术来实现就是第二步。 开发者当然可以按照自己的理解,使用常用的架构模式来搭建自己的技术架构。 Rafy 领域实体框架的意义:在这种开发方法的基础上,把一个常用的领域驱动架构模式以框架的形式固化下来,辅助开发者快速实现领域模型图中的所有领域模型,并在这个架构上解决一系列技术问题。 其实,这就是框架带来的进步。例如 ASP.NET MVC 框架就是把 MVC 架构在 Web 开发场景中的架构模式固化下来的 Web 开发技术框架。那为什么我们不用 MVC 框架,而非要基于 ASP.NET WebForm 去实现自己的 MVC 框架呢?只有以下原因会促使你去这样做:老的框架无法解决你的问题、或者开发者认为自己可以开发出更优秀的框架。 5、TypeScript 学习成本较高的问题。 答:采用新生事物,都是要付出代价的。我们需要考虑的是:这个代价值得吗?对于 TS,我们得出的结论是:性价比很高。 6、EF 6 还是 Rafy 的问题。 答:Rafy 不单只是一个 ORM 框架。它是一个产品线的开发平台,为我们提供了除 ORM 外的许多框架:DDD、Plugin、SOA、MDA、PL,详见下文:《Rafy 框架设计理论》。 如果单纯从 ORM 的角度来对比 EF6 和 Rafy 的话,也有诸多优势,详见下文:《Rafy 框架发布》。 7、你建议使用代码生成器。 答:对于你使用的一站式代码生成的模式,是比较老的开发模式了。虽然是一个比较易用、方便的开发模式,但是这在代码的可重用性、可维护性、架构的传承性等方面都较差(见:《对代码生成器的简单思考》)。 代码生成器和框架作用在开发过程中的不同层次、不同阶段。二者需要配合使用。不需要开发者修改的代码,应该编写在框架内部;而开发者需要修改或者调整的代码,应该使用代码生成器生成在外部,方便二次修改。另外,为了减轻开发者的负担,应该把尽可能多地封装在框架中。就拿 EF 作为例子,实体的代码在框架之外,ORM 的逻辑在框架之内;这与更老的 TypedDataSet 的代码生成模式相比,优势太多。 Rafy 框架的开发模式的设计中,我们是非常重视代码生成器的,所以也是在提供框架的同时结合代码生成器在使用。例如:属性的生成是使用 VS 自带的代码段(轻量代码生成);实体文件的生成是使用项模板。这些代码生成功能都是结合在 VisualStudio 中的,给开发者的体验是连贯一致的,要比外部的代码生成功能好很多。 8、纯model表字段部份单独放在一个cs文件里标记上partial。 答:产品的开发模式上,吸取了你所提及的许多代码生成器开发模式的经验。Entity 与 Repository 的代码是可以分开放的。目前为了方便使用,放在了一起。二者的代码都分为开发者编写的代码和自动生成的代码。例如,实体的代码是分在两个文件中的,以 User 实体为例,一个文件是 User.cs,另一个则是 User.g.cs 文件中(自动代码生成)。 9、最好能再加一个viewModel文件夹,放视图数据模型。(项目里边有很多都是直接DataTable处理的) 答:建议很好。如果 ViewModel 较多的话,应该放在一个单独的文件夹中。不建议项目组使用 DataTable 作为 ViewModel。 10、后端返回统一一个模型 Result。 答:现在就是这样做的。 下面是同事建议的邮件内容: 发件人: 杨盛元 发送时间: 2016-06-13 15:05 收件人: 胡庆访 主题: rafy框架问题,主要针对领域模型(ddd) 在tp(vim)渣打银行这个项目中看框架,技术和业务实现部份感觉有很多缺陷。 我对领域模型的理解:首先对领域模型DDD,是领域驱动设计,它的起源由来是早期业务设计是由流程图+用例图,横纵向表达业务流程和项目中各业务需要的模块。 之后加入领域模型,是为了在业务和技术实现之间起到一个桥梁作用,让技术更方便理解业务。领域图就包含了 (数据字段,数据关系,业务主要方法)。 领域模型是辅助技术人员更容易理解业务的设计图。 在RAFY里,框架想要实现用技术代码直接实现DDD。这样得出的结果就是,业务数据模型+数据逻辑+业务逻辑。三种代码全部混合堆在一起。三层+Model的结构被做成了两层,应用层和混合层(业务逻辑,数据逻辑,数据模型)。像框架介绍里说处理聚合组合关系。这一类属于业务逻辑关系,并不像一对多,多对多这样硬性的数据结构关系。和数据层混合在一起也不合理。 在这个框架下还出现了在控制器下能够直接写SQL语句的情况。在开发人员水平参差不齐的情况下,这框架不够规范。 等一些别的问题。。。巴拉巴拉。 网上现在很多人把领域模型理解成“要求技术按照领域图,实现技术架构”。 而我觉得领域模型,只是辅助技术更好的理解业务的一种UML图,并不是用来引导项目技术架构的图。(简单点就是:技术什么都不用动,理解业务就好。该三层还是三层,或是别的更灵活的技术架构。) 如果有一天有个名词叫流程驱动设计,那么所有代码都要堆在一起了。 所以领域驱动设计DDD,只是业务设计上照顾技术。对设计的要求更高了,而不是针对技术架构。 我希望能用通用技术来开发项目,三层结构+Model,Orm改用entity(ef)。这样对开发人员的学习成本就底了,项目开发效率和质量应该更好。 同样可以做公司项目中的各种业务封装(工作流,固定字段,角色/权限等)。 另外:可以的话最好前端也能用通用常用的技术JS+JQUERY之类。TypeScript不是很成熟,对大众开发者有过多的学习成本和更少的可参考资料。 觉得我理解得不对,可以进一步交流。 发件人: 杨盛元 发送时间: 2016-07-06 14:10 收件人: 胡庆访 主题: 渣打银行项目,关于rafy框架,一点个人意见。 钉钉不方便。我说直接点。 ORM部份,其实我更希望用ef6,不过rafy已经跟项目和产品绑在一起了,就不提这个。 我长期积累的做项目的方案步奏是: 1:由数据库表/视图,自动生成Model,连带备注和所有字段。(将纯字段模型标记成partial,让此部份永远自动生成,人工不干涉,可靠性更高,而且不用再写数据字典)。 2:做模板。由模板+数据Model,直接生成页面。可以是.net,js甚至是android的。原理简单就是替换Txt中标签内容。 (各种增删改查,树形数据维护,报表等都可以做成代码模板。软件大部份页面都可以设计成一样的,只要数据结构一样) 这里指的是用model一次生成整个.cs和csview和js全部文件。不是代码片段。 (这招我已经用几年了,很方便,一个页面一秒钟完成。生成文件后,删除多余的自动生成代码,补上业务逻辑就行了。) 以上是整个的方案。rafy把Model的关联关系和字段映射写在一起。这招就行不通了。感觉很不方便。 能不能做这样的小调整,把纯model表字段部份单独放在一个cs文件里标记上partial。 最好能再加一个viewModel文件夹,放视图数据模型。在rafy里加个方法.ToModel<T>()数据转成对应视图模型的方法。现在项目里边有很多都是直接DataTable处理的。虽然要做ddd领域架构,但视图数据模型开发过程中是必然会有的。有个这种方法处理起来规范些。 前后端交互: 1: 在后端所有的Action返回统一一个模型如{success:data:,msg:,code:}; 2: 这样在前端接收数据部份,就可以做一个封装统一处理。(如:code是异常的话,就弹出框。展示Msg内信息,正确就返回data数据跑前端业务逻辑) 以上这个框架里有一部份,我觉得还可以封装得更全一点。让后端统一返回模型,前端就能统一处理了。 我觉得这样可以在对框架不作大改动的情况下,项目开发会更方便些。 你参考下。
老是记不住各 Windows 版本中的 .NET 版本号,下面汇总一下: .NET Framework各版本汇总以及之间的关系 Mailbag: What version of the .NET Framework is included in what version of the OS? 这些系统自带的.net framework版本是多少? 版本之间的区别:.NET Framework 各版本区别 各 .net 版本的文件大小: v2.0 22.4M v2.0 SP2 24M v3.0 50.28M v3.5 197M v4.0 48.1M v4.5 48.02M
看了《AutoMapper and the Static Class Debate》,记录一下自己的看法。 在进行API设计时,静态类的使用有时会为设计者带来一些烦恼。应该将某个函数暴露为静态函数还是实例方法,这一点常常会造成人们的争论。 大部分人学完设计模式后,都会尽量使用单例模式。但是,静态函数的主要优点在于其简便性。调用者可以在代码中的任意位置使用静态函数,而无需为实例的创建、管理以及依赖注入等问题而烦恼。同时,由于没有创建新的实例,因而也不存在垃圾回收的问题,从而使性能也得以提高。 当然,有时静态API也需要维护一些状态,这时设计者必须保证静态函数的线程安全,而这往往牵涉到开销较大的加锁与同步等技术。而且即便独立的调用是线程安全的,但调用者也往往需要将一系列调用过程封装为一个原子性的事务。 无状态的尽量设计为静态。 我在面试应试者时,往往会问的一个问题是:使用单例模式相对于使用静态方法,有什么绝对性的优势?(或者说,有什么是单例能做到的,但是使用静态 API 的设计却无法办到的?) 读者,你知道吗?
影片探讨了2008年金融危机产生的原因。 美国忽略1933年的旧法律,立新法,以放松金融监管。 投资银行被允许更高的杠杆率,33:1,也就是说,投资物跌价3%就会导致破产。 投资银行放贷,但是转手将贷款包装后(CDO)卖给其他投资者。银行本身不再关心贷款的质量与风险。甚至保险公司也来为这种理财品进行担保(AIG)。有人买房时的贷款,甚至占到了房价的99.3%。 借款容易,导致物价房价飞涨。社会整体出现泡沫。这个过程中,监管层、银行家都赚取了大量收益。而由最终购买了理财品的一般投资人来承担这些风险。这些一般投资者,很多是被3A的评级给欺骗了(评级机构同样赚得盆满钵满)。这是一个全球性的庞氏骗局! 风险逐步累积,泡沫越来越大,并最终破裂。高盛、雷曼等公司破产。这些银行的投资者遍布全球,全球股价大跌。美国人收缩消费,也影响全世界。中国下岗千万人。 It's a WallStreet government. (影片前言中的冰岛,就是华盛顿的缩影。金融掌权导致最终的崩塌!) 看完这个,能看到资本主义国家的缺点。相反,就能体现出非资本主义国家的优势,例如中国:央妈监管,执行力强,非金融掌权,党说了算,而不是钱说了算。
本章说明如何使用额外的插件(如客户化插件)对另一插件(如产品插件)进行扩展。 使用场景 在 产品线工程 中,项目的研发分为领域工程和应用工程。这个过程中会需要对领域工程中的内容进行大量的扩展。 分层与扩展点 下图中显示了一个产品插件的逻辑分层,以及各层对应的扩展点。 可扩展的内容 实体属性扩展 可以为产品插件的实体添加新的实体属性,也可以修改现有属性的一些元数据。 实体配置扩展 可以随意修改产品插件中的实体配置,如实体的数据库映射。 实体查询的扩展 可以添加新的实体查询。可以修改、替换产品插件中现有的查询的实现。 实体保存的扩展 可以扩展产品插件实体在保存时的行为。 领域逻辑的扩展 可以添加新的领域逻辑,也可以修改、替换产品插件中现有的领域逻辑。 PS:该文已经纳入《 Rafy 用户手册》中。
本系列演示如何使用 Rafy 领域实体框架快速转换一个传统的三层应用程序,并展示转换完成后,Rafy 带来的新功能。 《福利到!Rafy(原OEA)领域实体框架 2.22.2067 发布!》 《Rafy 领域实体框架示例(1) - 转换传统三层应用程序》 《Rafy 领域实体框架演示(2) - 新功能展示》 以 Rafy 开发的应用程序,其实体、仓库、服务代码不需要做任何修改,即可同时支持单机部署、C/S 分布式部署。本文将说明如果快速使用 C/S 分布式部署。 前言 截止到上一篇,我们开发的应用程序都是采用直接连接数据库的模式: 接下来,将通过一些简单的调整,使得这个应用程序支持以 C/S 架构部署。整个过程只需要少量的代码: 包含以下步骤: 添加服务端控制台应用程序项目 修改客户端应用程序连接方式 配置客户端应用程序 运行示例 代码下载 添加服务端控制台应用程序项目 在整个解决方案中添加一个新的控制台应用程序,取名为 ServerConsole: 为项目添加所有 Rafy 程序集、CS 实体程序集以及 System.ServiceModel 程序集的引用: 在 Main 函数中添加以下代码,启动服务端领域项目,并开始监听 WCF 端口: 1: using System; 2: using System.Collections.Generic; 3: using System.Linq; 4: using System.ServiceModel; 5: using System.Text; 6: using CS; 7: using Rafy; 8: using Rafy.Domain; 9: 10: namespace ServerConsole 11: { 12: class Program 13: { 14: static void Main(string[] args) 15: { 16: PluginTable.DomainLibraries.AddPlugin<CSPlugin>(); 17: new DomainApp().Startup(); 18: 19: using (ServiceHost serviceHost = new ServiceHost(typeof(Rafy.DataPortal.WCF.ServerPortal))) 20: { 21: serviceHost.Open(); 22: Console.WriteLine("Press <enter> to terminate service"); 23: Console.ReadLine(); 24: serviceHost.Close(); 25: } 26: } 27: } 28: } 然后,为本项目添加应用程序配置文件 App.config,代码如下: 1: <?xml version="1.0" encoding="utf-8" ?> 2: <configuration> 3: <appSettings> 4: <add key="SQL_TRACE_FILE" value="D:\SQLTraceLog.txt"/> 5: </appSettings> 6: <connectionStrings> 7: <add name="CS" connectionString="server=.\SQLExpress;database=ClothesSys;uid=sa;pwd=GIX4" providerName="System.Data.SqlClient"/> 8: </connectionStrings> 9: <system.serviceModel> 10: <services> 11: <service name="Rafy.DataPortal.WCF.ServerPortal" behaviorConfiguration="includesException"> 12: <endpoint address="/Text" binding="basicHttpBinding" contract="Rafy.DataPortal.WCF.IWcfPortal"/> 13: <host> 14: <baseAddresses> 15: <add baseAddress="http://localhost:8000/RafyServer" /> 16: </baseAddresses> 17: </host> 18: </service> 19: </services> 20: <behaviors> 21: <serviceBehaviors> 22: <behavior name="includesException"> 23: <serviceMetadata httpGetEnabled="true" /> 24: <serviceDebug includeExceptionDetailInFaults="true" /> 25: </behavior> 26: </serviceBehaviors> 27: </behaviors> 28: </system.serviceModel> 29: </configuration> 修改客户端应用程序连接方式 接下来需要把界面程序变更为连接服务端。打开 ClothesSys 项目中的 Program.cs 文件,修改为以下代码: 1: static class Program 2: { 3: /// <summary> 4: /// 应用程序的主入口点。 5: /// </summary> 6: [STAThread] 7: static void Main() 8: { 9: PluginTable.DomainLibraries.AddPlugin<CSPlugin>(); 10: new ClientDomainApp().Startup(); 11: 12: Application.EnableVisualStyles(); 13: Application.SetCompatibleTextRenderingDefault(false); 14: Application.Run(new formLogin()); 15: } 16: } 17: 18: /// <summary> 19: /// 客户端使用的应用程序类型。 20: /// </summary> 21: public class ClientDomainApp : DomainApp 22: { 23: protected override void InitEnvironment() 24: { 25: RafyEnvironment.Location.DataPortalMode = DataPortalMode.ThroughService; 26: 27: base.InitEnvironment(); 28: } 29: } RafyEnvironment.Location.DataPortalMode 表示连接数据的模式,默认值是DataPortalMode.ConnectDirectly(直接连接数据库),ClientDomainApp 类把该值变更为 DataPortalMode. ThroughService(通过服务连接数据)。 配置客户端应用程序 在客户端配置文件中,删除数据库连接配置,并添加 WCF 连接配置,如下: 1: <?xml version="1.0"?> 2: <configuration> 3: <configSections> 4: <section name="rafy" type="Rafy.Configuration.RafyConfigurationSection, Rafy" /> 5: </configSections> 6: <rafy 7: dataPortalProxy="Rafy.DataPortal.WCF.ClientProxy, Rafy.Domain" 8: collectDevLanguages="IsDebugging"> 9: </rafy> 10: <system.serviceModel> 11: <client> 12: <endpoint name="ClientProxyEndPoint" address="http://localhost:8000/RafyServer/Text" 13: binding="basicHttpBinding" bindingConfiguration="basicHttpBindingConfig" 14: contract="Rafy.DataPortal.WCF.IWcfPortal" /> 15: </client> 16: <bindings> 17: <basicHttpBinding> 18: <binding name="basicHttpBindingConfig" receiveTimeout="00:20:00" sendTimeout="02:00:00" maxReceivedMessageSize="1000000000"> 19: <readerQuotas maxDepth="64" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="4096" maxNameTableCharCount="16384"/> 20: </binding> 21: </basicHttpBinding> 22: </bindings> 23: </system.serviceModel> 24: </configuration> 运行程序 先运行 ServerConsole,成功运行界面: 再运行 ClothesSys,点击登录。登录成功,即说明已经成功使用 C/S 进行部署。 代码下载 下载地址:http://pan.baidu.com/s/1AB9TL 本文的代码在“3.使用 CS 部署程序”文件夹中。 欢迎试用 Rafy 领域实体框架,框架发布地址:http://www.cnblogs.com/zgynhqf/p/3356692.html。
本系列演示如何使用 Rafy 领域实体框架快速转换一个传统的三层应用程序,并展示转换完成后,Rafy 带来的新功能。 《福利到!Rafy(原OEA)领域实体框架 2.22.2067 发布!》 《Rafy 领域实体框架示例(1) - 转换传统三层应用程序》 《Rafy 领域实体框架演示(2) - 新功能展示》 《Rafy 领域实体框架演示(3) - 快速使用 C/S 架构部署》 前言 支持一款与 Access 类似的文件型数据库,对于一些绿色安装的应用程序来说是非常必须的。使用 Rafy 领域实体框架开发的应用程序,可以在不变更一行代码的情况下,直接由大型数据库管理系统,移植到使用简单的 SqlCE 4 文件型数据库。(关于选择使用 SQLCE 4 作为文件型数据库的原因,详见:《OEA 2.11 支持单机版数据库 - SQLite与SQLCE对比》。) 本文说明如何快速配置 Rafy 应用程序,使得不需要修改任何代码的同时,让原本支持分布式部署、连接 SqlServer 的应用程序,转换为使用 SQLCE 数据库,以支持绿色部署。 拷贝 SQLCE 4 相关程序集 首先,需要把 SQLCE 对应的文件都拷贝到执行文件对应的项目中: 引用 System.Data.SqlServerCe: 把刚拷贝进项目中的 amd64 及 x86 文件夹中所有文件的”Copy to Output Directory” 属性设置为”Copy if newer”,这样,编译后的文件夹中就会自动拷贝这两个文件夹中的所有文件了: 修改配置文件 然后,需要修改配置文件中的数据库连接相关的配置节。修改后的配置文件内容如下: 1: <?xml version="1.0"?> 2: <configuration> 3: <connectionStrings> 4: <add name="CS" connectionString="Data Source=Data\ClothesSys.sdf;Case Sensitive=True;" providerName="System.Data.SqlServerCe" /> 5: </connectionStrings> 6: <system.data> 7: <DbProviderFactories> 8: <remove invariant="System.Data.SqlServerCe"/> 9: <add name="SqlServerCe Data Provider" invariant="System.Data.SqlServerCe" description="SqlServerCe Data Provider" 10: type="System.Data.SqlServerCe.SqlCeProviderFactory, System.Data.SqlServerCe, Version=4.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91"/> 11: 12: </DbProviderFactories> 13: </system.data> 14: </configuration> 运行程序 这样,就已经完成了所有的步骤。此时运行程序,会发现首次运行时程序启动较慢,这是因为Rafy正在创建指定的 ClothesSys.sdf 数据库及其中的初始数据: 数据库创建完成后,程序正常启动,并可以直接登录。 代码下载 下载地址:http://pan.baidu.com/s/1AB9TL 本文对应的示例代码在“4.使用 SQLCE4 文件型数据库”文件夹中。 欢迎试用 Rafy 领域实体框架,框架发布地址:http://www.cnblogs.com/zgynhqf/p/3356692.html。
前言 Rafy 领域实体框架作为一个使用领域驱动设计作为指导思想的开发框架,必然要处理领域实体到数据库表之间的映射,即包含了 ORM 的功能。由于在 09 年最初设计时,ORM 部分的设计并不是最重要的部分,那里 Rafy 的核心是产品线工程、模型驱动开发、界面生成等。所以当时,我们简单地采用了一个开源的小型 ORM 框架:《Lite ORM Library》。这个 ORM 框架可以生成比较简单的 Sql 语句,以处理一般性的情况。 随着不断使用,我们也不断对 ORM 的源码做了不少改动,让它在支持简单语句生成的同时,也支持让开发人员直接使用手动编写的 Sql 语句来查询领域实体。但是过程中,一直没有修改最核心的 Sql 语句生成模块。随着应用的不断深入,遇到的场景越来越多,需要生成复杂 Sql 语句的场景也越来越多。而这些场景如果还让开发人员自己去编写复杂 Sql 语句,不但框架的易用性下降,而且由于写了过多的 Sql 语句,还会让开发人员面向领域实体来开发的思想减弱。 这两周,我们对 Sql 语句生成模块实施了重构。与其说是重构,不如说重写,因为 90% Lite ORM 的类库都已经不再使用。但是又不得不面对对历史代码中接口的兼容性问题。接下来,将说明本次重构中的关键技术点。 旧代码讲解 最初采用的 Lite ORM 是一个轻量级的 ORM 框架,采用在实体对象上标记特性(Attribute)来声明实体的元数据,并使用链式接口来作为查询接口以方便开发人员使用。这是一个简单、易移植的 ORM 框架,对初次使用、设计 ORM 的同学来说,可以起到一个很好的借鉴作用。相关的设计,可以参考 Lite ORM 的原文章:《Lite ORM Library V2 》。 由于这几年我们已经对该框架做了大量的修改,所以很多接口已经与原框架不一致了。IQuery 作为描述查询的核心类型,被重命名为 IPropertyQuery,所有方法的参数也都直接面向 Rafy 实体的《托管属性》。但是在整体结构上,还是与原框架保持一致。例如,它还只是一个一维的结构: 1: /// <summary> 2: /// 使用托管属性进行查询的条件封装。 3: /// </summary> 4: public interface IPropertyQuery : IDirectlyConstrain 5: { 6: /// <summary> 7: /// 是否还没有任何语句 8: /// </summary> 9: bool IsEmpty { get; } 10: 11: /// <summary> 12: /// 当前的查询是一个分页查询,并使用这个对象来描述分页的信息。 13: /// </summary> 14: PagingInfo PagingInfo { get; } 15: 16: /// <summary> 17: /// 用于查询的 Where 条件。 18: /// </summary> 19: IConstraintGroup Where { get; set; } 20: 21: /// <summary> 22: /// 对引用属性指定的表使用关联查询 23: /// 24: /// 调用此语句会生成相应的 INNER JOIN 语句,并把所有关联的数据在 SELECT 中加上。 25: /// 26: /// 注意!!! 27: /// 目前不支持同时 Join 两个不同的引用属性,它们都引用同一个实体/表。 28: /// </summary> 29: /// <param name="property"></param> 30: /// <param name="type">是否同时查询出相关的实体数据。</param> 31: /// <param name="propertyOwner"> 32: /// 显式指定该引用属性对应的拥有类型。 33: /// 一般使用在以下情况中:当引用属性定义在基类中,而当前正在对子类进行查询时。 34: /// </param> 35: /// <returns></returns> 36: IPropertyQuery JoinRef(IRefProperty property, JoinRefType type = JoinRefType.JoinOnly, Type propertyOwner = null); 37: 38: /// <summary> 39: /// 按照某个属性排序。 40: /// 41: /// 可以调用此方法多次来指定排序的优先级。 42: /// </summary> 43: /// <param name="property">按照此属性排序</param> 44: /// <param name="direction">排序方向。</param> 45: /// <returns></returns> 46: IPropertyQuery OrderBy(IManagedProperty property, OrderDirection direction); 47: 48: //其它部分省略... 49: } 可以看到,该类型以一维的形式来描述了一个 Sql 查询的相关元素:Join 数据源、Where 条件、OrderBy 规则、分页信息。 只有其中的 Where 条件被设计为树型结构来处理相对复杂的 And、Or 连接的条件。 可以看到,虽然有 SqlWhereConstraint 来添加任意的 Sql 语句作为 Where 约束条件,但是这样的结构还是比较简单,不足以描述所有的 Sql。 重构方案 我们的目标是实现复杂 Sql 的生成,理论上需要支持所有能想到的 Sql 语句的生成。 初期方案其实很简单,就是使用解释器模式与访问器模式配合来重构底层代码。根据 Sql 的语法规定,构造 Sql 语法树节点中的相关类型,这样就可以用一棵树来解释任意的 Sql 语句;同时使用访问器模式来遍历某个具体 Sql 语法树。过程中还需要特别注意,尽量不要构造不必要的树节点,以增加垃圾回收器的压力。 在此初步方案上,还需要考虑:分层架构、组件间依赖、以及旧代码的兼容性设计。 以下是整个方案的分层设计: SqlTree:核心的、可重用的 Sql 语法树层。定义了通用的 Sql 语法结构,并解决从语法树到 Sql 语句的转换、生成,以及屏蔽不同数据库间不同子句的生成规则。 EntityQuery:把 SqlTree 作为类库引用,同时整合领域实体、实体属性的设计。 Query Interface:以 IQuery 接口的方式提供给应用层。 Linq Query:为了给开发人员提供更易用的接口,需要提供 Linq 语法的支持。本层用于解析 Linq 表达式树,并生成最终的实体查询的对象。 Property Query:为了兼容旧的接口,该部分在提供旧接口的前提下,换为使用新的 IQuery 来实现。 Application:开发人员的应用层代码。可以使用最易用的 Linq、旧的 PropertyQuery,同时也可以直接使用 IQuery 接口来完成复杂查询。 组件详细设计 Sql 语法树 使用解释器模式设计,用于描述 Sql 查询语句。 所有树节点都从 SqlNode 继承,并拥有自己的属性来描述不同的节点位置。例如 SqlSelect 类型,代码如下: 1: /// <summary> 2: /// 表示一个 Sql 查询语句。 3: /// </summary> 4: class SqlSelect : SqlNode 5: { 6: private IList _orderBy; 7: 8: public override SqlNodeType NodeType 9: { 10: get { return SqlNodeType.SqlSelect; } 11: } 12: 13: /// <summary> 14: /// 是否只查询数据的条数。 15: /// 16: /// 如果这个属性为真,那么不再需要使用 Selection。 17: /// </summary> 18: public bool IsCounting { get; set; } 19: 20: /// <summary> 21: /// 是否需要查询不同的结果。 22: /// </summary> 23: public bool IsDistinct { get; set; } 24: 25: /// <summary> 26: /// 如果指定此属性,表示需要查询的条数。 27: /// </summary> 28: public int? Top { get; set; } 29: 30: /// <summary> 31: /// 要查询的内容。 32: /// 如果本属性为空,表示要查询所有列。 33: /// </summary> 34: public SqlNode Selection { get; set; } 35: 36: /// <summary> 37: /// 要查询的数据源。 38: /// </summary> 39: public SqlSource From { get; set; } 40: 41: /// <summary> 42: /// 查询的过滤条件。 43: /// </summary> 44: public SqlConstraint Where { get; set; } 45: 46: /// <summary> 47: /// 查询的排序规则。 48: /// 可以指定多个排序条件,其中每一项都必须是一个 SqlOrderBy 对象。 49: /// </summary> 50: public IList OrderBy 51: { 52: get 53: { 54: if (_orderBy == null) 55: { 56: _orderBy = new ArrayList(); 57: } 58: return _orderBy; 59: } 60: internal set { _orderBy = value; } 61: } 62: 63: //... 64: } Sql 生成器 使用访问器模式设计,用于遍历整个 Sql 语法树。以下是 SqlNodeVisitor 的代码: 1: /// <summary> 2: /// SqlNode 语法树的访问器 3: /// </summary> 4: abstract class SqlNodeVisitor 5: { 6: protected SqlNode Visit(SqlNode node) 7: { 8: switch (node.NodeType) 9: { 10: case SqlNodeType.SqlLiteral: 11: return this.VisitSqlLiteral(node as SqlLiteral); 12: case SqlNodeType.SqlSelect: 13: return this.VisitSqlSelect(node as SqlSelect); 14: case SqlNodeType.SqlColumn: 15: return this.VisitSqlColumn(node as SqlColumn); 16: case SqlNodeType.SqlTable: 17: return this.VisitSqlTable(node as SqlTable); 18: case SqlNodeType.SqlColumnConstraint: 19: return this.VisitSqlColumnConstraint(node as SqlColumnConstraint); 20: case SqlNodeType.SqlBinaryConstraint: 21: return this.VisitSqlBinaryConstraint(node as SqlBinaryConstraint); 22: case SqlNodeType.SqlJoin: 23: return this.VisitSqlJoin(node as SqlJoin); 24: case SqlNodeType.SqlArray: 25: return this.VisitSqlArray(node as SqlArray); 26: case SqlNodeType.SqlSelectAll: 27: return this.VisitSqlSelectAll(node as SqlSelectAll); 28: case SqlNodeType.SqlColumnsComparisonConstraint: 29: return this.VisitSqlColumnsComparisonConstraint(node as SqlColumnsComparisonConstraint); 30: case SqlNodeType.SqlExistsConstraint: 31: return this.VisitSqlExistsConstraint(node as SqlExistsConstraint); 32: case SqlNodeType.SqlNotConstraint: 33: return this.VisitSqlNotConstraint(node as SqlNotConstraint); 34: case SqlNodeType.SqlSubSelect: 35: return this.VisitSqlSubSelect(node as SqlSubSelect); 36: default: 37: break; 38: } 39: throw new NotImplementedException(); 40: } 41: 42: protected virtual SqlJoin VisitSqlJoin(SqlJoin sqlJoin) 43: { 44: this.Visit(sqlJoin.Left); 45: this.Visit(sqlJoin.Right); 46: this.Visit(sqlJoin.Condition); 47: return sqlJoin; 48: } 49: 50: protected virtual SqlBinaryConstraint VisitSqlBinaryConstraint(SqlBinaryConstraint node) 51: { 52: this.Visit(node.Left); 53: this.Visit(node.Right); 54: return node; 55: } 56: 57: //... 58: } 基于实体的查询 1. IQuery 相关接口用于描述整个基于实体的查询。 例如,IColumnNode 表示一个列节点,其实是由一个实体属性来指定的: 1: namespace Rafy.Domain.ORM.Query 2: { 3: /// <summary> 4: /// 一个列节点 5: /// </summary> 6: public interface IColumnNode : IQueryNode 7: { 8: /// <summary> 9: /// 本列属于指定的数据源 10: /// </summary> 11: INamedSource Owner { get; set; } 12: 13: /// <summary> 14: /// 本属性对应一个实体的托管属性 15: /// </summary> 16: IManagedProperty Property { get; set; } 17: 18: /// <summary> 19: /// 本属性在查询结果中使用的别名。 20: /// </summary> 21: string Alias { get; set; } 22: } 23: } 2. EntityQuery 层中的类型实现了 IQuery 中对应的接口,并使用领域实体的相关 API 来实现从实体到表、实体属性到列的转换。同时,为了减少对象的数量,这些类型与 Sql 语法树的关系都使用继承,而不是关联。也就是说,它们直接从 SqlTree 对应的类型上继承下来,这样,在构造 EntityQuery 的同时,也构造好了底层的 Sql 语法树。 3. QueryFactory 封装了大量易用的 API 来构造 IQuery 接口。 使用示例 下面,就以几个典型的单元测试的相关代码来说明新的查询框架的使用方法: 使用 Linq 的数据层查询 1: public int LinqCountByBookName(string name) 2: { 3: return this.FetchCount(r => r.DA_LinqCountByBookName(name)); 4: } 5: private EntityList DA_LinqCountByBookName(string name) 6: { 7: var q = this.CreateLinqQuery(); 8: q = q.Where(c => c.Book.Name == name); 9: return this.QueryList(q); 10: } 使用 IQuery 的数据层查询 1: public int CountByBookName2(string name) 2: { 3: return this.FetchCount(r => r.DA_CountByBookName2(name)); 4: } 5: private EntityList DA_CountByBookName2(string name) 6: { 7: var source = f.Table(this); 8: var bookSource = f.Table<BookRepository>(); 9: var q = f.Query( 10: from: f.Join(source, bookSource) 11: ); 12: q.AddConstraintIf(Book.NameProperty, PropertyOperator.Equal, name); 13: return this.QueryList(q); 14: } 可以看到,使用 IQuery 接口来查询,虽然灵活性最大、性能更好,但是相对于 Linq 来说会更加复杂。 使用 IQuery 来生成 Sql 1: [TestMethod] 2: public void ORM_TableQuery_InSubSelect() 3: { 4: var f = QueryFactory.Instance; 5: var articleSource = f.Table(RF.Concrete<ArticleRepository>()); 6: var userSource = f.Table(RF.Concrete<BlogUserRepository>()); 7: var query = f.Query( 8: from: userSource, 9: where: f.Constraint( 10: column: userSource.Column(BlogUser.IdProperty), 11: op: PropertyOperator.In, 12: value: f.Query( 13: selection: articleSource.Column(Article.UserIdProperty), 14: from: articleSource, 15: where: f.Constraint(articleSource.Column(Article.CreateDateProperty), DateTime.Today) 16: ) 17: ) 18: ); 19: 20: var generator = new SqlServerSqlGenerator { AutoQuota = false }; 21: f.Generate(generator, query); 22: var sql = generator.Sql; 23: 24: Assert.IsTrue(sql.ToString() == 25: @"SELECT * 26: FROM BlogUser 27: WHERE BlogUser.Id IN ( 28: SELECT Article.UserId 29: FROM Article 30: WHERE Article.CreateDate = {0} 31: )"); 32: Assert.IsTrue(sql.Parameters.Count == 1); 33: Assert.IsTrue(sql.Parameters[0].Equals(DateTime.Today)); 34: } 使用 SqlTree 来生成 Sql 1: [TestMethod] 2: public void ORM_SqlTree_Select_InSubSelect() 3: { 4: var select = new SqlSelect(); 5: var articleTable = new SqlTable { TableName = "Article" }; 6: var subSelect = new SqlSelect 7: { 8: Selection = new SqlColumn { Table = articleTable, ColumnName = "UserId" }, 9: From = articleTable, 10: Where = new SqlColumnConstraint 11: { 12: Column = new SqlColumn { Table = articleTable, ColumnName = "CreateDate" }, 13: Operator = SqlColumnConstraintOperator.Equal, 14: Value = DateTime.Today 15: } 16: }; 17: 18: var userTable = new SqlTable { TableName = "User" }; 19: select.Selection = new SqlSelectAll(); 20: select.From = userTable; 21: select.Where = new SqlColumnConstraint 22: { 23: Column = new SqlColumn { Table = userTable, ColumnName = "Id" }, 24: Operator = SqlColumnConstraintOperator.In, 25: Value = subSelect 26: }; 27: 28: var generator = new SqlServerSqlGenerator { AutoQuota = false }; 29: generator.Generate(select); 30: var sql = generator.Sql; 31: Assert.IsTrue(sql.ToString() == @"SELECT * 32: FROM User 33: WHERE User.Id IN ( 34: SELECT Article.UserId 35: FROM Article 36: WHERE Article.CreateDate = {0} 37: )"); 38: Assert.IsTrue(sql.Parameters.Count == 1); 39: Assert.IsTrue(sql.Parameters[0].Equals(DateTime.Today)); 40: } 框架下载 框架使用测试驱动的方法开发,在开发时是先编写相关的测试用例,再实现内部代码。重构的同时,我们为能想到的场景都编写了测试用例: 目前,框架版本也升级到了 2.23.2155。 有兴趣的同学,了解、下载最新的框架,请参考:《Rafy 领域实体框架发布!》。(框架目前不开源,但可免费使用。)
去年4月,我们为 Rafy 框架添加了领域模型设计器组件。时隔一年,谨以本文,简要说明该领域模型设计器的设计思想。 设计目标 Rafy 实体框架中以领域驱动设计作为指导思想。所以在开发时,以领域建模为首要任务。为此,我们为它开发了领域模型设计器。开发人员可以在设计器中,设计相应的领域模型,查看现有代码对应的领域模型。 我们为这个设计器制定了以下功能: 外部简单设计器:也就是设计器可以部署为一个可以独立运行的软件。该软件可以打开领域模型的设计图,方便团队中的非开发人员角色查看。同样,这个软件最好也能支持对模型进行设计。 Rafy 运行时设计器:Rafy 是一个插件化架构的框架。所以我们也期望这个设计器可以以插件的形式,直接运行在 Rafy 框架构建的应用程序中。这样,在应用程序运行时,就能看到当前软件对应的领域模型。 VS 内部设计器:这是最重要的一个功能,我们希望设计器能与 Visual Studio 深度整合。首先,这样使得可以在 Visual Studio 中就能直接进行领域建模,并能根据模型来生成实体代码;其次,在使用 CodeFirst 的场景下,也能在 Visual Studio 中直接根据当前的实体类代码来生成相应的领域模型设计图。 设计方案 根据当前 Rafy 框架的架构,结合要实现的设计器的功能。规划整个设计器的所需要的组件: 并画出组件间的依赖图: 简要说明各组件的用途: DDD Object Model:位于最底层的 DDD 对象模型,用于描述领域建模中的实体及实体间的关系。这里的对象模型结构,决定了上层可支持的 DDD 建模类型。 Model Xml Document:用于支持对象模型的 XML 序列化。以 XML 文档的形式定义了模型的显示、实体类型、实体间的关系结构。 WPF Controls:WPF 中可用于显示模型的控件集。例如:实体块、连接线等。 WPF Model Viewer:基于 XML 文档模型,操作 WPF 控件集,实现模型的显示。 WPF Model Designer:同样基于 XML 文档模型,操作 WPF 控件集,实现模型的编辑。(由于编辑相对显示来说更复杂,所以设计器和查看器分为两个单独的包来实现。第一期只实现了查看器。) VS Package:Visual Studio 插件包。实现模型设计器集成到 VS 中;调用同步组件,实现代码与模型间的同步。 Code Synchronizer:代码同步组件,实现代码与模型间的同步(Model-First 或者 Code-First)。 Rafy Entity Meta:当下 Rafy 框架中的实体运行时元数据包。 Rafy Plugin:一个可运行在 Rafy 应用程序中的插件。这个插件用于查看运行时实体的领域模型关系图。 Standalone Designer(exe):独立运行的设计器应用程序。 重点组件 下面,是重点组件的关系图。 DDD Object Model: Model WPF Controls: 文档控件结构 实现细节 设计完成后,规划整个实现的顺序: 共花了半个月的时间来完成,以下是完成后的项目结构,其中选中的5个项目即是设计器相关的项目: Rafy.EntityObjectModel: 对应设计中的 DDD Object Model。 DesingerEngine: 对应 WPF Controls。该项目被设计为一个可重用的,与 DDD 无关,用于图形设计的程序集。 Rafy.DomainModeling: 包含了设计时的 RafyDomainDocument、ModelViewer、XML Document 等组件。 ModelingEnv: 一个简单的独立运行的 WPF 程序。 VSPackage: VS 插件。 初步成果 20130328 开始实现,至20130415 完成第一版本,可在 VS 中查看实体的领域模型关系图。 下面是模型查看器的效果: 以及使用独立应用程序查看的效果: PS: 由于一直都使用 CodeFirst 开发模式,所以过了一年了,到现在也只是实现了模型查看,还没有实现模型设计的功能 。 最近半年没怎么长进,所以没东西可写,今天把这个翻出来,给大家做个分享。
SOA SOA 是通过功能组件化、服务化,来实现系统集成、解决信息孤岛,这是其主要目标。而更进一步则是实现更快响应业务的变化、更快推出新的应用系统。与此同时,SOA 还实现了整合资源,资源复用。 SOA 服务的设计标准是粗粒度、高重用、灵活、标准。性能则并非首要考虑因素。 SOA 的两大功能是集成、服务编排(BPEL、BPM)。WF 在 SOA 架构中,实现服务编排的功能。 参考架构: 相关资源: SOA 的基本概念及设计原则浅议 SOA 有哪些基本原则 SOA 设计十大原则 SOA 服务设计原则 再谈SOA集成平台建设必要性 谈基于SOA的应用系统设计和开发 谈基于SOA的消费发布订阅 再谈服务设计 携程旅行网在SOA架构方面的探索 支付宝的SOA实践(程立) ESB ESB 是 SOA 的重要实现手段。ESB 实现 SOA 时,它作为中心、媒介,集成的系统将只与它进行交互。而 ESB 实现与各种系统间的协议转换、数据转换、透明的动态路由功能(基于内容)。 在设计 ESB 时,集中的分发模块会影响性能、可伸缩性、容错能力,所以 ESB 要有良好的可伸缩性,支持集群。 IBM 总结了 ESB 的功能,较完整的功能如下: 通信 服务交互 路由 寻址 通信技术、协议和标准(例如 IBM® WebSphere® MQ、HTTP 和 HTTPS) 发布/订阅 响应/请求 Fire-and-Forget,事件 同步和异步消息传递 服务接口定义(例如,Web 服务描述语言(Web Services Description Language,WSDL)) 支持替代服务实现 通信和集成所需的服务消息传递模型(例如 SOAP 或企业应用程序集成 (EAI) 中间件模型) 服务目录和发现 集成 服务质量 数据库 服务聚合 遗留系统和应用程序适配器 EAI 中间件的连接性 服务映射 协议转换 应用程序服务器环境(例如 J2EE 和 .NET) 服务调用的语言接口(例如 Java 和 C/C++/C#) 事务(原子事务、补偿、Web 服务事务(WS-Transaction)) 各种确定的传递范例(例如 Web 服务可靠消息传递(WS-ReliableMessaging)或对 EAI 中间件的支持) 安全性 服务级别 身份验证 授权 不可抵赖性 机密性 安全标准(例如 Kerberos 和 Web 服务安全性(WS-Security)) 性能 吞吐量 可用性 其他可以构成契约或协定的持久评估方法 消息处理 管理和自治 编码的逻辑 基于内容的逻辑 消息和数据转换 有效性 中介 对象标识映射 数据压缩 服务预置和注册 记录、测量和监控 发现 系统管理和管理工具的集成 自监控和自管理 建模 基础架构智能 对象建模 通用业务对象建模 数据格式库 B2B 集成的公共与私有模型 开发和部署工具 业务规则 策略驱动的行为,特别是对于服务级别、服务功能的安全和质量(例如 Web 服务策略(WS-Policy)) 模式识别 而最低要求的 ESB 需要具有的功能: 通信 集成 提供位置透明性的路由和寻址服务 控制服务寻址和命名的管理功能 至少一种形式的消息传递范型(例如,请求/响应、发布/订阅等等) 支持至少一种可以广泛使用的传输协议 支持服务提供的多种集成方式,比如 Java 2 连接器、Web 服务、异步通信、适配器等等 服务交互 一个开放且与实现无关的服务消息传递与接口模型,它应该将应用程序代码从路由服务和传输协议中分离出来,并允许替代服务的实现。 相关资源: 面向服务架构(SOA)和企业服务总线(ESB) C#ESB设计说明书 几种 ESB ESB企业服务总线 ESB项目需求分析和方案设计浅谈 ESB同步,异步选择,从项目实际出发(电信) ESB 优缺点 ESB 架构笔记 ESB 简介 - 百度知道 ESB 项目需求分析和方案设计浅谈 NServiceBus NServiceBus 是 .NET 平台上最受欢迎的一个开源 ESB 框架。有较完善的文档及示例代码。 目前,.NET 平台上开源的 ESB 框架,大多基于消息队列来实现。NServiceBus 同样也使用消息队列机制来实现消息的传递,例如可以使用 MSMQ。由于消息队列天生就是异步传输的,所以 NSB 也同样只支持异步消息,是一种‘发送即忘却’的模式。(As a general purpose communications technology, WCF does not enforce the queued messaging paradigm. NServiceBus does, and the architectural implications are profound.)。 NServiceBus 相对于 WCF 的优势在于:事件驱动的架构(发布、订阅)、更好地支持长时间运行的工作流。 缺点一:只支持异步的消息机制的问题是,无法进行传统的的数据查询。(To allow clients to perform queries, it is best not to use NServiceBus. Messaging is designed for non-blocking operations, and queries are (for the most part) operations for which the user wishes to wait.) 如果一定要使用 NSB 来实现数据查询,那么只能通过 CQRS 来进行系统的设计: 缺点二:NSB 的服务可以轻易集成到 WCF 中使用 MSMQ 实现,但是反之则不行。也就是说,已经使用 WCF 开发的服务,是无法使用 NSB 来完成简单的迁移的。(原因也主要是因为 NSB 的异步机制。) 相关资源: infoq 官方采访介绍:NServiceBus——让创建企业级.NET系统更加容易 NServiceBus---最流行的开源企业服务总线 for .Net NServiceBus 开源通讯框架(几种通信模式) NServiceBus 安装与调试 NServiceBus Overview NServiceBus And WCF 简单DEMO 三篇笔记:1、2 错误处理、3 云计算,及与 SOA 的关系 云计算是一种部署体系结构,而 SOA 则是企业 IT 的体系结构。 SOA与云整合既带来应用和业务流程灵活的虚拟化和节省的费用(云),又带来原有应用的集成应用及业务流程的敏捷重构(SOA)。 上层基于 SOA 进行应用服务的开发,底层基于云计算进行资源整合,包括存储,网络,数据库,服务器等。 目前业界比较多的观点赞同:SOA 与云计算将整合发展。 它们的关系: 从产生的背景和原因看,SOA产生的原因是为解决企业存在的信息孤岛和遗留系统这两大问题。云计算产生的原因是企业的信息系统数据量的高速增长与数据处理能力的相对不足,还有计算资源的利用率处于不平衡的状态。 从关键的技术和属性看,通过产生背景和原因的分析,SOA和云计算是不同的概念,但是它们却互相联系,又有一定的相似性。从服务角度来看,SOA实现了可以从多个服务提供商得到多个服务(一个服务便是一个功能模块),并通过不同的组合机制形成自己所需的一个服务;云计算实现了所有的资源都是服务,可以从云计算提供商购买硬件服务、平台服务、软件服务等,把购买的资源作为云计算提供商提供的一种服务。 从关键技术来看,SOA需要实现业务组件的可重用性、敏捷性、适应改变、松耦合、基于标准;云计算则需要虚拟化技术、按需动态扩展、资源即服务的支撑。 从应用场景来看,当企业的业务需求经常改变的时候可以考虑使用SOA;当企业对IT设施的需求经常改变或者无法提前预知的时候可以考虑使用云计算,当有大量的批处理计算的时候也可以考虑使用云计算。 从应用的侧重点来看,SOA侧重于采用服务的架构进行系统的设计,关注如何处理服务;云计算侧重于服务的提供和使用,关注如何提供服务。 从商业模式来看,SOA可能会降低软件的开发及维护的成本,商业模式是间接的,需要落地;云计算根据使用的时间(硬件)或流量(带宽)进行收费,具有明确的商业模式。 下面列出最近看的与本文相关的一些 pdf 书籍,东西太多,不上传了,列下书名: 《中国SOA最佳应用及云计算融合实践》、《SOA in the Real World》、《SOA应用案例分析及设计》、《A Developer’s Guide to the Microsoft .NET Service Bus》、《IBM ESB概要设计说明书@CBOD》、《Mule+ESB+Studio+v3.3安装使用手册》、《软通动力 兰州ESB平台项目详细设计说明书》、《SOA实践者指南》、《基于.NET+Framework+WCF的面向服务SOA中间件设计》、《基于WCF的SOA框架设计》、《IBM-ESB 在 SOA 内的工作角色》、《WSSF(服务工厂)架构剖析》、《开源SOA快速入门指南》、《Composite Software Construction》、《Enterprise Integration Patterns - Designing Building and Deploying Messaging Solutions》、《Enterprise SOA Adoption Strategies》、《Prentice.Hall.SOA.with.NET.and.Windows.Azure.May.2010》。 其它: Shuttle ESB
最近为公司完成了一个 ESB 的设计。下面简要说明一下具体的设计方案。 企业 SOA 整体方案 在前一篇《SOA、ESB、NServiceBus、云计算 总结》中说到,SOA 是面向服务的架构,其核心思想是把业务进行组件化,而业务组件的能力服务化。 我们的整个 SOA 的设计分为两个层面:一个是系统间的 SOA 设计,另一个则是单个系统内的 SOA 设计。系统间的 SOA 设计,主要是设计一个 ESB 系统来实现各业务系统间的交互。而系统内部的 SOA 设计,则是建立一个组件化的技术平台,使得系统的开发能以一个个业务组件的形式完成,并通过技术平台来实现各业务组件的组合与互连。 一般说的 SOA 设计,都是在讲如何进行系统间的互连,例如如何进行 ESB 的设计。但是,不论是系统间互连,还是系统内部的组件化,其实都是 SOA 思想在不同层面上的体现。而我认为,应用系统内部的 SOA 设计,会更重要。因为它不但是一个低耦合、高复用的产品设计,而且也为系统间的 SOA 提供了更好的支持。 本文,主要说明如何实现 ESB 的设计。而更重要的应用系统内部的组件化产品开发平台,则留到下一篇。 ESB 目标功能 在前一篇中,列出了一个较完整 ESB 应有的功能。SOA 不但包括简单的系统间互边的功能,也应该包含更高级的 BPM 业务流程编排的功能。 下面,简单列出了我们对于我们的 ESB 的功能树: 图中,功能按优先级进行了排序。第一个阶段,只会实现其中红色的部分。而服务编排,则放到了最后。红色部分,是一个 ESB 应该具有的最小功能集。在交互模式部分,我选择了实现‘响应/请求’模式,这种交互方式在系统间互连时场景相对较少,但是不需要引用 MSMQ 等功能,所以实现起来会更简单。 ESB 主体设计 对于 ESB 的主体设计,是参考了网上另一个 ESB 的设计,下面是它的设计图: ESB 详细设计 首先,规划出 ESB 整个系统内部的所有组件。 Web Portal:ESB 对外以网站的形式公布。同时,服务调用者、提供者,都是直接使用网站提供的功能。 Adapter:协议的适配器组件。 Service Invoker:服务的同步调用器。 Async Invoker:异步方式的同步调用器。 Service Mocker:这个组件用于实体 ESB 的服务可以以 WS 等方式暴露。 ESB Message:ESB 内部的消息结构体。 Service Registry:服务的注册库。 Service Router:服务的路由器组件。 Service Router Cache Notification:路由缓存通知组件。 Logger:日志组件。 Exception Handler:异常处理组件。 Performance Counter:服务调用过程中的一些性能统计工具。 以下是一些详细的调用设计。 ESB 网站: 模拟服务: 服务的调用: 服务调用过程中的管道模块设计: 路由表及路由更新: 适配器: 最后,是最重要的持久化的领域实体: 细节不说了,有兴趣的朋友可以参考初步的设计,并欢迎与我交流。:)
平台整体结构 在产品开发过程中,为了达到业务级别的较大粒度重用,我们需要把纵向把业务进行拆分,以业务组件的形式进行开发,并最终把多个开发完成的业务组件进行组合,形成最终的软件产品。 按照组件化开发的产品,是基于一个公共的产品开发平台来建立的。由平台来提供所有的底层设施。平台包括技术平台和业务平台两个层面。在技术层面上,平台提供了一系列的类库、框架、组件、工具,以及为业务组件化提供相应的技术支撑。在业务层面上,业务平台中积累了大量的封装完善的业务组件,以及一些常用的业务控件,以供开发新产品时进行选配。同时,平台还为整个软件过程提供一系列的其它支持,例如工具、设计器、管理界面等。 下图,是平台的整体结构图: 图中罗列了大部分的关键组成部分,细节本篇不述。 组件集成平台 对于一个独立的业务,我们可以将其封装为一个独立的业务组件,并最终放到组件库中。业务组件之间,则以服务、事件两种形式进行交互。要支持这种模式的交互,技术平台还需要提供几个技术框架:插件平台、服务容器、事件总线。 下图是组件集成架构: 技术平台提供事件总线、轻量级服务总线。 组件内部以领域驱动的模式开发,以领域实体框架作为基础框架。组件内、组件间,也都是面向领域实体来进行交互。 组件向外部的其它组件提供组件事件、组件服务。外部组件也只能直接调用组件提供的服务,或者监听组件的事件。 组件还提供了一些可重用的 UI、一些可直接使用的分布式服务。 整个应用系统在组合多个业务组件后,再开发一些特定的功能、UI 就可以完成一个完整的系统了。 产品构成 下图是一个完整产品的组件构成图: 由于我们的产品开发平台必须要支持 721 客户化定制,所以同一个业务组件还对应不同的业务通用级别进行划分:Organization Common 表示组织架构组件最通用的部分,Org Part1 表示组织架构组件的可选包。而 Customiztion 则可以对引用的业务组件做深入的定制和扩展,而不需修改引用组件的代码。 可以看到,对于整个产品来说,在引用了业务组件库中的一些业务组件后,就可以组成了产品的基础功能。Customer App Component 中是应用系统在组件的功能基础上需要再做的工作:完成产品的额外功能,并通过平台接口为一些组件做相关定制。 组件内部架构 对于单个的业务组件,其内部的架构依然采用领域驱动的分层架构: 图虽大,但并不复杂,就是领域驱动的经典分层:Distribute(DTO 接口层)、Application(应用层/领域逻辑层)、Repository(仓库)、Domain(领域实体)。 重点在于 Domain 包,它不但包括领域实体,还包括了组件事件、组件服务接口,这些都是领域的核心。 位于底层的技术平台,提供一系列支持:IOC/AOP、属性扩展框架、领域实体框架、721定制化框架、数据库生成框架等…… 结尾 其实,组件化架构设计中,最为复杂是分析出一个封装完好的组件,所要面向的使用者是哪些,这些使用者分别对组件有哪些需求,而这个架构如何满足这一系列需求。例如,我们在设计过程中,对这些方面进行了分析:组件自身的发展需求、组件中各组成部分的可扩展性、组件间的交互需求、系统集成需求、项目组定制化需求、系统外交互需求、易用性。 欢迎感兴趣的朋友交流。
在 Rafy 领域实体框架中,对自关联的实体结构做了特殊的处理,下面对这一功能进行讲解。 场景 在开发数据库应用程序时,往往会遇到自关联表的场景。例如,分类信息、组织架构中的部门、文件夹信息等,都是不限制层级的。如下图中操作系统的文件夹: 在开发这类程序时,往往是设计一张表,表中的一个可空的外键直接引用这张表本身。对应的实体如下图: 而针对这样的场景,许多ORM框架都不做默认的处理,开发者往往每次都要做重复的工作:建立类似结构的表,编写关系处理代码,编写查询代码……而这种场景经常会出现,所以 Rafy 实体框架中,默认就支持了树型实体的一系列功能,来降低重复劳动。 功能及使用说明 在 Rafy 中的树型实体功能,只需开发者使用一行代码为指定的实体打开这个功能,框架会自动完成以下工作: 自动添加实体的自引用关系。自动生成数据库自关联表。 自动维护树节点的 TreeIndex 索引。 自带多个查询,用于查询树节点。 查询结果自动转变为树的结构。 支持树节点的按需加载。 下面,将逐一进行讲解。 打开树型实体功能 开发者只需使用一行代码即可让指定的实体转变为树型实体。在指定实体的配置代码中,添加下面这行代码即可: 自动添加实体的自引用关系 实体基类上已经默认带有以下几个属性,来表达树节点之间的关系。 当某个实体类型被配置为树型实体时,这几个属性才会有意义。 SupportTree:指示该实体是否为树型实体。 TreeIndex:树节点的编码、索引。此属性会映射为数据库中的字段。 TreePId:该树节点的父节点的 Id。此属性会映射为数据库中的字段。 TreeParent:该树节点的父节点实体。 TreeChildren:该树节点的所有子节点集合。 自动生成数据库自关联表 运行程序后,该实体对应的表将会自动添加两个字段:TreeIndex、TreePId,如下图: 自动维护树节点的 TreeIndex 索引 TreeIndex 是树结点的系统编号,由框架自动维护。下图显示了一个正在使用的树的 TreeIndex 的格式: 这个属性不但可以用于显示,更重要的是它是树型实体大量功能的结构基础。例如,当查询某个节点下的所有节点时,就是通过 TreeIndex 来进行模糊匹配的。所以这个属性的值非常重要,只能由框架来自行维护,而不能由开发者来设置。 开发者可以通过 TreeParent、TreeChildren、TreePId 等属性来变更节点与节点之间的父子关系,这时,对应的节点的 TreeIndex 则会同时自动变更。 树结构的表示 树的结构非常重要,我手画了张草图来表示: 主要由三个类型构成整个树:EntityList、Entity、EntityTreeChildren。这个结构可以表示完整的一棵树,也可以表示部分树。其中,EntityList 用于存储树的根节点(如果是部分树,则表示最上层节点);Entity 表示树中的每一个节点;EntityTreeChildren 集合则表示某个节点下的子节点。 另外,EntityTreeChildren 集合可以按需加载。当它还没有进行加载时,遍历整个树只能遍历到当前已经在内存中的树节点。例如,上图中,Root3的子节点没有被加载,1.2.2 的子节点也没有被加载。 那么,如何加载还没有加载到内存中的节点呢?这需要使用到 ITreeComponent 接口中的 LoadAllNodes 方法。EntityList、Entity、EntityTreeChildren 这三个类型都实现了 ITreeComponent 接口,下面是这个接口的定义: 另外,可以使用其中的 EachNode 方法来以深度优先的算法遍历整棵树。 自带多个查询,用于查询树节点 实体仓库中带有许多查询方法,其中一些是专门为树型实体设计的: GetTreeRoots:查询所有的根节点。 GetByTreePId:查找指定树节点的直接子节点。 GetByTreeParentIndex:递归查找指定父索引号的节点下的所有子节点。 LoadAllTreeParents:递归加载某个节点的所有父节点。使用此方法后,指定节点的父节点将被赋值到它的 TreeParent 属性上。 GetAllTreeParents:获取指定索引对应的树节点的所有父节点。 查询出的父节点同样以一个部分树的形式返回。 另外,一些非树实体的查询方法,对于树型实体也是可用的。如 GetAll、GetByParentId 等。但是也会有所区别,例如 GetAll 方法在查询非树实体时,查询出的实体列表中包含所有的实体;但是在查询树型实体时,结果会按照树的结构来进行加载,即列表中只会有根节点,其它节点则分别在根节点的下级节点中。 同时,这些查询往往支持是否使用贪婪加载的参数。以 GetTreeRoots 方法举例,它的接口是这样的:public EntityList GetTreeRoots(EagerLoadOptions eagerLoad = null); 。它在默认情况下只返回根节点,而根节点中的子节点是没有被加载的。但是,我们可以通过参数中的 eagerLoad 来指定,在加载根节点的同时,把所有的子节点都加载上。 以上只是对一些接口做一些必要的解释,具体的使用方法及其它的接口,请参照注释及源码中的单元测试。 限制 说了上面这么多自带的功能,但是 Rafy 中树型实体的设计也有这的限制:一个树型实体类型对应的数据表中,只能存储一棵树。树中的所有节点的 TreeIndex 都必须是唯一的。 好了,鉴于篇幅,这篇文章只是简单地讲解了树型实体中的重点概念及功能,并没有深入说明。这是因为,在使用的过程中你会发现,一般情况下用起来非常容易,只需要打开树型实体功能,并调用想要的查询就可以了,用不到特别复杂的 API。如果确实需要深入了解,那么在理解了整个树的结构设计后,再结合帮助、注释以及源码中的单元测试,相信也会比较简单。
为了让开发者更方便地使用 Rafy 领域实体框架,本月,我们已经把最新版本的 Rafy 框架程序集发布到了 nuget.org 上,同时,还把 RafySDK 的最新版本发布到了 VisualStudio 插件仓库中。 以下说明如何下载、更新最新的 SDK 及程序集。 下载、更新最新的 RafySDK 在 VisualStudio 中打开扩展管理器(Tools -> Extensions and Updates),选择在线项目,并搜索 “Rafy” 安装即可。如下图: 同样,只需要在扩展管理器中,就可以方便地更新该 SDK。 使用 NuGet 安装、更新最新的 Rafy 框架程序集 在使用 NuGet 前,你必须为 VS 安装上 NuGet 包管理器。该工具同样可以在 VS 的插件管理器中进行安装: 在 NuGet 包管理器安装完成后。在解决方案管理器中的某个项目的引用节点上点击右键,就可以为这个项目添加 NuGet 程序集引用了,如下图: 在打开的管理器界面中,搜索 Rafy,如下: 搜索完成后,就会看到下面几个 NuGet 包: Rafy.Domain:Rafy 领域框架的核心程序集。引用该项即可完成对 Rafy 框架的引用。 Rafy_User_Guide:Rafy 框架的用户手册、帮助文档。引用该项目后,在解决方案根目录下的 package 文件夹中,可以看到对应的用户手册、工具、更新说明及 Demo 等。如下图: 另外,Rafy.ComopnentModel.UnityAdapter:不需要引用。该项是把 Rafy 中的 IOC 适配到 Unity 上的一个插件,是为特定项目公开的 NuGet 包。 一般情况下,引用 Rafy.Domain 就可以使用了。但是建议在正式使用前,先下载用户手册先进行必要的学习。 以后,我们会不定时更新 Nuget 包以及 SDK。开发者只需要直接更新就行了。
最近项目组开始使用 Git 来作为源码管理。我今天就顺便把 Rafy 的源码也迁移到了 github 上,方便大家使用。这是项目的地址:https://github.com/zgynhqf/rafy,Git Clone 地址为:https://github.com/zgynhqf/Rafy.git。 由于我个人也是第一次使用 Git 来做源码管理。本文主要是记录一下过程,及遇到的问题。 在 VS 中如何使用 Git 在 VisualStudio 2013 中使用 Git 有两种方法: 一种方法是安装并使用扩展“Git Source Control Provider”。试用了一下,发现按钮并不齐全,许多功能都是放在 TortoiseGit 下拉列表中,非常不易用: 另外一个方法是使用内置的 Microsoft Git Provider。这种方法的界面跟 TFS 是一样的,使用 Team Explorer 来操作,不符合我原来用 AknSVN 的习惯,不过功能非常齐全,Commit、Push 等都有。最终还是选择了这个: 在 Git 如何变换 Repository 的位置 当服务端 Repository 的地址变换时,在 SVN 中有 Relocate 命令可以直接使用。但是在 Git 中却没有这个命令。在 TortoiseGit 中,完成这个任务需要打开 Settings,修改以下配置: 将 SVN 仓库转换到 Git 仓库 原来用的源码管理都是 SVN,这次转换到 Git,希望能够直接把 SVN 进行升级,这样就不会丢失历史的提交记录。而我需要转换两种类型的 SVN 仓库到 Git: 转换发布到 SourceForge 的 SVN 仓库到 GitHub.com 这种转换比较简单,GitHub 中有功能可以进行直接转换,祥见:https://help.github.com/articles/importing-from-subversion/。 转换本地 SVN 仓库到本地 Git 仓库 这个转换比较复杂。过程中遇到的问题较多。 主要是因为git-svn只支持通过网络连接(svn:// 或 http://之类)的方式来转换,所以本地的仓库转换,是不能直接使用 file:/// 协议访问的,而是还得安装一个 SVN 服务端进行发布。这一步我是使用 VisualSVN 进行发布的。 发布完成后,使用 git svn clone 把所有内容复制下来。(这一步操作比较耗时) 复制完成后,需要修改 .git 文件夹中的 config 文件。把后面的两个 SVN 服务端配置节,修改为上图中的 Git 服务端配置节,然后设置正确的 Git 服务端地址。 最后,把整个本地记录全部 Push 到这个服务端中,就可以了。 完成后,这个 Git Repository 中就有了原来的所有历史记录了:
为了提高开发者的易用性,Rafy 领域实体框架在很早开始就已经支持使用 Linq 语法来查询实体了。但是只支持了一些简单的、常用的条件查询,支持的力度很有限。特别是遇到对聚合对象的查询时,就不能再使用 Linq,而只能通过构造底层查询树的接口来完成了。由于开发者的聚合查询的需求越来越多,所以本周我们将这部分进行了增强。 接下来,本文将说明 Rafy 框架原来支持的 Linq 语法,以及最新加入的聚合查询支持及用法。 使用 Linq 查询的代码示例 public WarehouseList GetByCode(string warehouseCode, string nameKeywords, PagingInfo pagingInfo) { return this.FetchList(r => r.DA_GetByCode(warehouseCode, nameKeywords, pagingInfo)); } private EntityList DA_GetByCode(string warehouseCode, string nameKeywords, PagingInfo pagingInfo) { var q = this.CreateLinqQuery(); //条件对比 q = q.Where(e => e.Code == warehouseCode); if (!string.IsNullOrEmpty(nameKeywords)) { q = q.Where(e => e.Name.Contains(nameKeywords)); } //排序 q = q.OrderByDescending(w => w.Name); return this.QueryList(q, pagingInfo);//以指定的分页信息 pagingInfo 分页 } 支持的一般查询 使用 CreateLinqQuery 方法创建出一个 IQueryable<Warehouse> 对象,针对该对象,我们可以以下的标准 Linq 方法:Where、OrderBy、OrderByDescending、ThenBy、ThenByDescending、Count。 对于其中最重要的 Where 方法,Rafy 也支持许多操作,包括: 属性的各种对比操作(=,!=,>,>=,<,<=,!,Contains,StartsWith,EndsWith等)。 支持两个属性条件间的连接条件:&&、||。 支持引用查询。即间接使用引用实体的属性来进行查询,在生成 Sql 语句时,将会生成 INNER JOIN 语句,连接上这些被使用的引用实体对应的表。例如:q = q.Where(warehouse => warehouse.Administrator.Name == "admin"); 这部分的内容,之前的版本已经支持了,各位可参见 Rafy 框架的用户手册。 聚合查询 聚合查询的功能是,开发者可以通过定义聚合子的属性的条件,来查询聚合父。这是本次升级的重点。 例如,书籍管理系统中,Book (书)为聚合根,它拥有 Chapter (章)作为它的聚合子实体,而 Chapter 下则还有 Section(节)。那么,我们可以通过这个功能,来查询类似以下需求的数据: 查询拥有某个章的名字的所有书籍。 要实现这种场景的查询,我们可以在仓库的数据层,使用下面的 Linq 语法: public BookList LinqGetIfChildrenExists(string chapterName) { return this.FetchList(r => r.DA_LinqGetIfChildrenExists(chapterName)); } private EntityList DA_LinqGetIfChildrenExists(string chapterName) { var q = this.CreateLinqQuery(); q = q.Where(book => book.ChapterList.Concrete().Any(c => c.Name == chapterName)); q = q.OrderBy(b => b.Name); return this.QueryList(q); } 其生成的 Sql 如下: SELECT [T0].[Id], [T0].[Author], [T0].[BookCategoryId], [T0].[BookLocId], [T0].[Code], [T0].[Name], [T0].[Price], [T0].[Publisher] FROM [Book] AS [T0] WHERE EXISTS ( SELECT 1 FROM [Chapter] AS [T1] WHERE [T1].[BookId] = [T0].[Id] AND [T1].[Name] = @p0 ) ORDER BY [T0].[Name] ASC 查询每个章的名字必须满足某条件的所有书籍。 我们可以在仓库的数据层,使用下面的 Linq 语法: public BookList LinqGetIfChildrenAll(string chapterName) { return this.FetchList(r => r.DA_LinqGetIfChildrenAll(chapterName)); } private EntityList DA_LinqGetIfChildrenAll(string chapterName) { var q = this.CreateLinqQuery(); q = q.Where(e => e.ChapterList.Cast<Chapter>().All(c => c.Name == chapterName)); q = q.OrderBy(e => e.Name); return this.QueryList(q); } 生成的 SQL 是: SELECT [T0].[Id], [T0].[Author], [T0].[BookCategoryId], [T0].[BookLocId], [T0].[Code], [T0].[Name], [T0].[Price], [T0].[Publisher] FROM [Book] AS [T0] WHERE NOT (EXISTS ( SELECT 1 FROM [Chapter] AS [T1] WHERE [T1].[BookId] = [T0].[Id] AND [T1].[Name] != @p0 )) ORDER BY [T0].[Name] ASC 查询某个章中所有节必须满足某条件的所有书籍。 我们可以在仓库的数据层,使用下面的 Linq 语法: public BookList LinqGetIfChildrenExistsSectionName(string sectionName) { return this.FetchList(r => r.DA_LinqGetIfChildrenExistsSectionName(sectionName)); } private EntityList DA_LinqGetIfChildrenExistsSectionName(string sectionName) { var q = this.CreateLinqQuery(); q = q.Where(book => book.ChapterList.Concrete().Any(c => c.SectionList.Cast<Section>().Any(s => s.Name.Contains(sectionName)))); q = q.OrderBy(b => b.Name); return this.QueryList(q); } 将会生成如下 SQL: SELECT [T0].[Id], [T0].[Author], [T0].[BookCategoryId], [T0].[BookLocId], [T0].[Code], [T0].[Name], [T0].[Price], [T0].[Publisher] FROM [Book] AS [T0] WHERE EXISTS ( SELECT 1 FROM [Chapter] AS [T1] WHERE [T1].[BookId] = [T0].[Id] AND EXISTS ( SELECT 1 FROM [Section] AS [T2] WHERE [T2].[ChapterId] = [T1].[Id] AND [T2].[Name] LIKE @p0 ) ) ORDER BY [T0].[Name] ASC 同时,这些查询也可以支持分页。例如,我们在上面的查询添加一个分页条件,代码如下: public BookList LinqGetIfChildrenExistsSectionName(string sectionName) { return this.FetchList(r => r.DA_LinqGetIfChildrenExistsSectionName(sectionName)); } private EntityList DA_LinqGetIfChildrenExistsSectionName(string sectionName) { var q = this.CreateLinqQuery(); q = q.Where(book => book.ChapterList.Concrete().Any(c => c.SectionList.Cast<Section>().Any(s => s.Name.Contains(sectionName)))); q = q.OrderBy(b => b.Name); return this.QueryList(q, new PagingInfo(2, 1));//分页 } 分成的 SQL 如下: SELECT TOP 1 [T0].[Id], [T0].[Author], [T0].[BookCategoryId], [T0].[BookLocId], [T0].[Code], [T0].[Name], [T0].[Price], [T0].[Publisher] FROM [Book] AS [T0] WHERE EXISTS ( SELECT 1 FROM [Chapter] AS [T1] WHERE [T1].[BookId] = [T0].[Id] AND EXISTS ( SELECT 1 FROM [Section] AS [T2] WHERE [T2].[ChapterId] = [T1].[Id] AND [T2].[Name] LIKE @p0 ) ) AND [T0].[Id] NOT IN ( SELECT TOP 1 [T0].[Id] FROM [Book] AS [T0] WHERE EXISTS ( SELECT 1 FROM [Chapter] AS [T1] WHERE [T1].[BookId] = [T0].[Id] AND EXISTS ( SELECT 1 FROM [Section] AS [T2] WHERE [T2].[ChapterId] = [T1].[Id] AND [T2].[Name] LIKE @p1 ) ) ORDER BY [T0].[Name] ASC ) ORDER BY [T0].[Name] ASC 头晕,越来越复杂……不过经过测试,上面都没有什么问题。 下面是一个单元测试生成的分页、复杂聚合查询的 SQL,贴上来观赏下: SELECT TOP 2 [T0].[Id], [T0].[Author], [T0].[BookCategoryId], [T0].[BookLocId], [T0].[Code], [T0].[Name], [T0].[Price], [T0].[Publisher] FROM [Book] AS [T0] LEFT OUTER JOIN [BookCategory] AS [T1] ON [T0].[BookCategoryId] = [T1].[Id] WHERE [T0].[Name] != @p0 AND [T1].[Name] = @p1 AND EXISTS ( SELECT 1 FROM [Chapter] AS [T2] WHERE [T2].[BookId] = [T0].[Id] AND [T2].[Name] = @p2 ) AND EXISTS ( SELECT 1 FROM [Chapter] AS [T3] WHERE [T3].[BookId] = [T0].[Id] AND [T3].[Name] = @p3 AND NOT (EXISTS ( SELECT 1 FROM [Section] AS [T4] LEFT OUTER JOIN [SectionOwner] AS [T5] ON [T4].[SectionOwnerId] = [T5].[Id] WHERE [T4].[ChapterId] = [T3].[Id] AND ([T4].[Name] NOT LIKE @p4 OR [T4].[SectionOwnerId] IS NULL OR [T5].[Name] != @p5) )) ) AND [T0].[Id] NOT IN ( SELECT TOP 4 [T0].[Id] FROM [Book] AS [T0] LEFT OUTER JOIN [BookCategory] AS [T1] ON [T0].[BookCategoryId] = [T1].[Id] WHERE [T0].[Name] != @p6 AND [T1].[Name] = @p7 AND EXISTS ( SELECT 1 FROM [Chapter] AS [T2] WHERE [T2].[BookId] = [T0].[Id] AND [T2].[Name] = @p8 ) AND EXISTS ( SELECT 1 FROM [Chapter] AS [T3] WHERE [T3].[BookId] = [T0].[Id] AND [T3].[Name] = @p9 AND NOT (EXISTS ( SELECT 1 FROM [Section] AS [T4] LEFT OUTER JOIN [SectionOwner] AS [T5] ON [T4].[SectionOwnerId] = [T5].[Id] WHERE [T4].[ChapterId] = [T3].[Id] AND ([T4].[Name] NOT LIKE @p10 OR [T4].[SectionOwnerId] IS NULL OR [T5].[Name] != @p11) )) ) ORDER BY [T0].[Name] ASC ) ORDER BY [T0].[Name] ASC 刚开始支持 Linq 查询的时候,就已经把聚合查询的单元测试给写了。鉴于比较复杂,所以一直没有实现。这周总算完成了这部分代码,心中一块石头落了地。
源码下载 源码我已经上传到 CSDN 了,无需资源分,下载地址:http://download.csdn.net/detail/zgynhqf/8565873。 源码使用 VS 2013 +TypeScript 1.4 进行开发。打开后,显示如下图: JsTankGame 1.0:老的使用 JS 编写的坦克游戏。 JsTankGame 2.0:新的使用 TS 直接翻译过来的游戏。 JsTankGame:在 2.0 的基础上,对类型进行了重构后的新游戏。 重构步骤 由于老的 JS 游戏是采用 MS Ajax Client Library 构建,并且采用了 OOD 的方式来进行设计,再加之 TypeScript 可以兼容 JS 的全部代码。所以使用 TypeScript 来移植的工作也比较简单,主要是替换类型设计的代码:类、继承、接口等。 完成以上工作后,也就得到了使用 TS 编写的 2.0 版本。过程中体会到了强类型语言的诸多好处,当然也有一些 TS 目前并不完善的地方(后面会说)。 得到了强类型的 2.0 版本后,并没有结束。为了体验强类型对于重构的好处,我决定在这个版本之上做代码结构上的重构。 有了强类型编写的代码,我可以很方便地分析出每一个类型、每一个方法,具体在哪些地方被使用。这样,我就能很快地知道类型之间的依赖关系。不看不知道,一看吓一跳。之前一点一点随心写的代码,本以为类型设计得还不错,之间耦合性应该不是很高。但是图画完之后,才发现与想象中差点很远,这就是没有画图直接编写代码的结果,见下面两张图: 可以看出各精灵类型之间的关系是比较乱的,双向依赖随处可见。(其实图中因为把 SpriteManager 画到了另外一张图,所以没有显示出更复杂、更乱的关系。) 据此,我绘制了新的关系图,然后按照此关系来重构了所有的代码。这样就得到了最新的 3.0 版本。 新版本的类型关系图如下: 分层: 精灵: 管理器: 代码层面,主要是把各精灵之间耦合的代码,都移植到了上层的管理器中。同时,为精灵定义事件来解除精灵与管理器的直接耦合。 TS 首次体验中感受的优缺点 优点: Lambda 非常好地解决了 this 指针的问题。 Chrome、IE 都能直接调试 TypeScript! 过程中还发现了弱类型无法发现的错误。因为重命名,还没有修改原来的代码。(SpriteManager.js 98 行) 缺点: 开发环境-还没有集成代码注释功能,只能手动拷贝。 开发环境-不支持代码区域的定义(Region,非常重要,便于分区域管理较多代码的类型。没有这个功能,同样导致代码无法写得更多,毕竟每个类的代码量也不少。)。 开发环境-目前还不支持 Code Snippets。 开发环境-不支持关键字代码生成:if、while、swith、括号匹配等。 语法-不支持事件的定义。 语法-暂时还不支持为类定义重载方法。见 SpriteBase.IsCollided 方法。 其它: 编译出的 JS 代码有一定的冗余。命名空间处显得特别明显。 重写基类方法,没有提示。 基类的属性获取器/设置器无法重写。 无法分辨哪些方法是虚方法。 接口中不能定义只读的属性。 还存在 BUG。(SpriteManager.ts line 93)。 简单定义一个数字类型的字段的话,默认值并不是 0,而是 NaN。 总结 总体说来,经过试用,我感觉 TS 到目前的 1.4 版本,已经可以用于正式的大型 JS 项目开发。但是还有很多地方需要改进! 附 Chrome、IE 调试 TS 截图:
项目组用到了 Node.js,发现下面这篇文章不错。转发一下。原文地址:《原文》。 ------------------------------------------- A chatroom for all! Part 1 - Introduction to Node.js Rami Sayar 4 Sep 2014 11:00 AM 7 This node.js tutorial series will help you build a node.js powered real-time chatroom web app fully deployed in the cloud. You are expected to know HTML5 and JavaScript. Throughout the series, you will learn how to setup node.js on your Windows machine, how to develop a web frontend with express, how to deploy a node express apps to Azure, how to use socketio to add a real-time layer and how to deploy it all together. Level: Beginner to Intermediate. Part 1 - Introduction to Node.js Part 2 - Welcome to Express with Node.js and Azure Part 3 - Building a Backend with Node, Mongo and Socket.IO Part 4 – Building a Chatroom UI with Bootstrap Part 5 - Connecting the Chatroom with WebSockets Part 6 – The Finale and Debugging Remote Node Apps Part 1 - Introduction to Node.js Welcome to Part 1 of the Node.js Tutorial Series: A chatroom for all! In this part, I will explain what node.js is, why you should pay attention to node.js and how to setup your machine. What is node? Why node? Node.js is a runtime environment and library for running JavaScript applications outside the browser. Node.js is mostly used to run real-time server applications and shines through its performance using non-blocking I/O and asynchronous events. A complete web ecosystem has been built around node.js with several web app frameworks and protocol implementations available for usage. It’s definitely one of the easiest and fastest way to develop real-time applications on the web today. Why use node? One word answer: JavaScript. JavaScript is an extremely popular language and is credited with being one of the driving forces that turned the web into the dynamic wonderland that it is today. What you can do in a browser nowadays, rivals all others! JavaScript arose on the frontend but - thanks to the V8 JavaScript engine and the work of Ryan Dahl - you can now run networked JavaScript applications outside of the browser precisely to build web apps. Node.js lets you unify the programming language used by your app - no longer do you need a different language for your backend, you can use JavaScript throughout. If your background is in building and design websites and web app frontends in HTML, CSS and JavaScript, you don’t need to pick up another language to develop complex data-driven back-ends for your apps. Node.js plays a critical role in the advancement of WebSockets as a method for real-time communication between the front and back ends. The use of WebSockets and the libraries building on that protocol such as Socket.IO have really pushed what is expected of web applications and lets us developers explore new ways to create the web. Setting up node.js on Windows 8 To get started, you will need a reasonably up to date machine, I will be showing you how to install Node.js on Windows 8.1. Firstly, you will need to download and install the node.js runtime. You can download the current version 0.10.30 (as of this writing) here:http://nodejs.org/download/. Choosing the Windows Installer is one of the easiest ways to get started. Alternatively, if you are a fan of Chocolatey, the package manager for Windows, you can install node by running: choco install nodejs.install You should double check that the node executable has been added to your PATH system environment variable. Watch this video, if you want to see how to change your environment variables on Windows 8 and Windows 8.1. You will want to make sure the following folder has been added to the PATH variable: C:\Program Files (x86)\nodejs\ If you go to your Command Prompt and type in node –h, you should see the help documentation for the node executable displayed. Along with Node.js, NPM, the system used to manage node packages, should be installed and available on the Command Prompt as well. Simply type in npm –h, you should see the help documentation for NPM displayed. If you encounter an error similar to this one: Error: ENOENT, stat 'C:\Users\someuser\AppData\Roaming\npm' The resolution is to create a folder at the path specified above, as shown in this StackOverflow question. This is only an issue in the latest node.js installer and should be resolved by next release. You can create the folder like so: mkdir -r C:\Users\someuser\AppData\Roaming\npm Why use Visual Studio 2013? Node Tools for Visual Studio! With Node.js installed, it’s time to select a development tool. Of course, you are free to use any editing tool you want but why use a glorified notepad when you can experience the full power of enterprise-grade integrated development environments like Visual Studio. You get it for free to boot! Now what’s cool about Node Tools for Visual Studio is that is adds Node.js support for editing, Intellisense, performance profiling, npm, TypeScript, Debugging locally and remotely (including on Windows/MacOS/Linux), as well Azure Web Sites and Cloud Service. Throughout these tutorials, I will be using Visual Studio 2013 to develop, debug and deploy the chat engine, you are welcome to use any development tool you wish. If you want to use Visual Studio, You can download any of the following editions of Visual Studio and then install the free Node Tools for Visual Studio. · [THIS ONE IS FREE] Visual Studio 2013 Express for Web (requires Update 2). Download here. · Visual Studio 2013 Pro or higher (requires Update 2) · Visual Studio 2012 Pro or higher (requires Update 4) Don’t forget to install the free Node Tools for Visual Studio. Starting a new node.js project in Visual Studio Note: Screenshots show Visual Studio 2013 Ultimate. Starting a new node.js project is fairly straight forward. 1. You want to boot Visual Studio and go to the File > New > Project menu item. 2. You will want to go to Installed > Templates > JavaScript menu item on the left and select Blank Node.js Web Application on the right. Choose a location and name for your project and press OK. 3. You will be presented with the following screen, feel free to explore Visual Studio at this point. You will want to open the generated server.js file in the Solution Explorer (on the right typically but may be located elsewhere on your screen.) 4. You can now debug your node web application in your preferred browser. Hello World in Node.js As is typical in other languages, the generated code example shows you how to output “Hello World” in the browser. Let me explain how the generated code in server.js works line by line. *Note: As stated in this tutorial series description, I am assuming you have a knowledge of JavaScript, HTML5 and how HTTP/the Internet work. Line 1 var http = require('http'); Node.js has a simple module and dependencies loading system. You simply call the function “require” with the path of the file or directory containing the module you would like to load at which point a variable is returned containing all the exported functions of that module. Line 2 var port = process.env.port || 1337; On this line, we want to determine on which port the HTTP server serving the HTML should run. If a port number is specified in the environment variables, we will use that one or we will simply use 1337. Line 3 http.createServer(function (req, res) { We want to create a server to handle HTTP requests. We will also pass the createServer function a function callback containing two parameters to a handle each individual request and return a response. Take a look at Michael Vollmer’sarticle if you’ve never encountered callback functions in JavaScript. The request received is passed in the req parameter and the response is expected to written to the res parameter. Line 4 res.writeHead(200, { 'Content-Type': 'text/plain' }); Any HTTP response requires a status-line and headers, to learn more about HTTP headers and how they work check out this article. In this case, we want to return 200 OK as the status response and to specify the content-type as plain text. We specify this by calling the writeHead function on the response object. Line 5 res.end('Hello World\n'); Once we are done writing the response we want to call the end function. We can also pass the final content through the end function, in this case we want to send the string “Hello World” in plain text. Line 6 }).listen(port); We close off the callback and call the function listen at the port we defined earlier, this will start the server and start accepting requests sent to the defined port. To see the result, you can start debugging by pressing on the button shown in the previous screenshot. You can see “Hello World” in the browser. Voila! You have now successfully run a node.js app on Windows 8.1 using Visual Studio 2013. Stay Tuned! Stay tuned for Part 2 – How to Deploy Your Hello World into the Cloud to be released in the next week. You can stay up to date by following my twitter account @ramisayar.
今天搞这两个关键字搞得有点晕,主要还是没有彻底理解其中的原理。 混淆了一个调用异步方法的概念: 在调用异步方法时,虽然方法返回一个 Task,但是其中的代码已经开始执行。该方法在调用时,即刻执行了一部分代码,直接最底层的 Async API 处才产生真正的异步操作,这时向上逐步返回,并最终使用一个 Task 来代表该异步任务。 当不使用 await 关键字时,该异步方法同样在异步执行。而使用 await 关键字后,只不过是对 Task(awaitable) 对象异步等待其执行结束,然后再同上下文中执行后续代码。 如果使用 await task.ConfigureAwait(false),表示该行后的代码,都不需要一定在同一上下文中执行。 也就是说,对于 Task Run() 的调用来说: 1. RunAsync():直接执行该异步方法,后续代码紧接着执行。 2. await RunAsync():执行异步方法,并在结束后再执行后续代码(在此行代码之前的代码在同一线程中执行)。 3. await RunAsync().ConfigureAwait(false):执行异步方法,并在结束后再执行后续代码(执行的线程不指定)。 Await 的使用 另外,由于 await 只针对 awaitable 对象,所以并不要求一定要在异步方法之前使用。可以在适当的时候再使用 await,例如: var task = RunAsync();//开始执行异步操作。 DoSth();//同时主线程执行其它操作。 await task;//此时等待异步执行完成。 DoOtherThing();//再执行其它操作。 参考: 下面,列出几篇 async await 相关的文章 dudu:实际案例:在现有代码中通过async/await实现并行 有关async/await的实现背后 在MVC中使用async和await的说明 async & await 的前世今生(Updated) C# 5.0 Async Tips and Tricks, Part 1 Async and Await
上个月写了一个团队中的 BaaS API 的设计规范,给大家分享下: 目录 1. 引言... 4 1.1. 概要... 4 1.2. 参考资料... 4 1.3. 阅读对象... 4 1.4. 术语解释... 4 2. API 设计规范... 5 2.1. 地址格式... 5 2.2. 输入与输出... 6 2.2.1. 通用输入数据... 6 2.2.2. 主体输入... 6 2.2.3. 通用输出数据... 6 2.2.4. 状态码... 7 2.2.5. 异常处理... 7 2.2.6. 其它... 8 2.3. API操作设计... 8 2.3.1. 资源型操作... 8 2.3.2. 业务型操作... 12 3. API 帮助文档规范... 12 3.1. 帮助文档内容规范... 12 3.2. 文档编写方法... 13 3.3. 帮助文档XML模板... 13 1. 引言 1.1. 概要 BAAS 平台上的所有 API,必须严格遵守本规范。 通过本文档规范 BAAS 平台所有向外提供 API,体现技术的统一性、规范性。并使得所有 API 尽量靠近业界规范的同时,提高API的易用性、可读性、兼容性等,并方便平台的使用者更快地发现、熟悉所有API以供开发。 主要包含两个方面的规范:API 本身的设计规范、API 帮助文档的编写规范。 1.2. 参考资料 《Representational State Transfer (REST)》 1.3. 阅读对象 · 需要把 API 发布到BAAS 平台中的所有开发者。 · 使用 BAAS API 的开发者。 1.4. 术语解释 Ø BAAS:后端即服务。参见:《BaaS服务的定义、发展以及未来》。 Ø REST:一种开放的基于互联网的软件架构模式。参见:《Representational State Transfer (REST)》。 2. API 设计规范 2.1. 地址格式 对于发布的所有 API,地址应该满足以下格式: · 格式一,直接访问资源型: /api/v(version)/area/resources/{id} · 格式二,资源查询型: /api/v(version)/area/resources/param1Name/param1Value/param2Name/param2Value/?optionalParameters · 格式三,资源操作型、跨资源业务型: /api/v(version)/area/resources or controller/action?parameters 示例: /api/v1.0/acs/users/ 表示访问所有的用户资源。 /api/v1.0/acs/users/1 表示访问 Id 为 1 的用户。 /api/v1.0/acs/users/group/iws-tech/minAge/30 表示访问 iws-tech 组中最小年龄30岁的用户。 /api/v1.0/acs/accounts/UpdateAllUserFlags 表示更新所有用户的某个标识。 其它说明: Version 表示版本号,只有两级的版本号。 不同的版本号之间,原则上可以不保证 API 的兼容。 某个版本一旦发布,在同一个版本号之内的 api 升级,必须保证兼容原来发布的 API。不能兼容时,需要使用新的 API 地址,同时必须保留旧的 API。 Area 表示某个业务模块,如 ACS、Org、OneDoc、OnePlus 等。 2.2. 输入与输出 2.2.1. 通用输入数据 对于整个BAAS中每一个 API 的调用都需要提交的数据,使用 Http Header 来进行传输。例如:App 授权码、用户标识 等信息。 某个 Area 中大量 API都需要提交的数据,也应该使用 Http Header 来进行提交。 2.2.2. 主体输入 考虑到接口的扩展性,所有API的输入只能接受一般的 JSON 对象作为输入参数,同时也只能输出一个 JSON 对象。 当输入输出的值是单一值、数组时,需要使用一个对象对其进行封装。 所有 JSON 对象的属性名,全部使用首字母小写的驼峰式语法。 2.2.3. 通用输出数据 对于 CDU 以及修改数据为主的操作型API的响应,都必须返回一个统一的数据格式 Result,该结构定义如下: { success: boolean, statusCode: int, message: string, data: object } 其中: success:表示该操作是否成功。 statusCode:该操作如果有多种返回的状态,使用statusCode进行区分。一般情况下,statusCode 返回1或0表示成功或失败。该属性用于给开发者进行程序分支的逻辑判断使用。 message:总是返回一个可用于客户端显示的字符串。该属性用于显示给软件使用者查看。 data是可选属性。即如果没有额外的数据,可以没有data属性,也可以data 返回 null。 2.2.4. 状态码 状态码分为两类,一个是 Http 状态码;一个是 Result 数据结构中的 StatusCode 状态码。HTTP 状态码表示该 HTTP 请求的处理状态。一个请求是否成功是由 HTTP 状态码标明的. 一个 2XX 的状态码表示成功, 而一个 4XX 表示请求失败. 一般情况下,如果能使用 HTTP 状态码表示的状态,应该优先使用 HTTP 状态码。其次,BAAS 内部的各种业务逻辑状态,则应该由 StatusCode 来标明。 1. 对于 HTTP 状态码而言,所有API暂时只使用以下状态码: · 200:操作成功返回。 · 201:表示创建成功,POST 添加数据成功后必须返回此状态码。 · 400:请求格式不对。 · 401:未授权。(App、User) · 404:请求的地址未找到。如 users/1 未找到该资源。 · 500:内部程序错误。 其中,201、404这两个状态码,是需要API开发者在每一个API中,根据业务逻辑的执行结果来主动返回的。其它的状态码由框架统一进行返回。 2. StatusCode StatusCode将统一使用6位编码,代表所有不同的业务逻辑分支。6位编码中的前两位代表不同的Area (模块),由BAAS平台统一规范。后四位由模块开发者自行定义。如:01表示ACS,那么010001可能表示ACS模块中的登录API的用户名错误、010002表示ACS中的登录API的用户密码错误。 2.2.5. 异常处理 请求失败返回 4XX 后,响应的主体依然是 Result 数据格式。其中 message 表示错误的信息。方便进行调试。如: HttpStatusCode:404 Response Body:{success:false, statusCode:100003, message:'不存在该用户。'} 2.2.6. 其它 时间的格式:API返回 值中的时间,都统一采用UTC格式 时间。 API的返回值中,如果需要包含调试相关信息(如调用时间、调用次数等),由BAAS平台框架统一处理,不单独在各API中处理。 2.3. API操作设计 每个具体的 API地址,都是一个操作。操作分为两种类型:资源型操作、业务型操作。 2.3.1. 资源型操作 资源型操作是满足REST规范化设计的。在设计API 时,应尽量首选这种模式。即:如果 API 能抽象为资源的CRUD操作的,应该尽量先抽象为对资源的操作。 2.3.1.1. 添加 地址:资源列表地址。如 /users/。 使用 POST动作提交实体对应的JSON格式数据。 2.3.1.2. 更新 地址:具体某个资源的地址。如 /users/1,表示id为1的用户。 动作:使用 PUT 动作提交。 数据格式:实体的 JSON格式数据。其中,JSON 数据中不需要列全所有的属性,只需要列出需要更新的属性即可。 如:PUT /users/1 {username:'hqf'}。 对应的响应是: HttpStatusCode:200 ResponseBody: { success: true, statusCode: 1, message: '更新成功!'} (另:如果使用 ASP.NET WebApi 框架搭建API,则这里需要提供统一的框架处理此类型的反序列化。) 2.3.1.3. 删除 地址:具体某个资源的地址。如 /users/1。 动作:使用DELETE动作提供请求。 如:DELETE /users/1 对应的响应是: HttpStatusCode:200 ResponseBody: { success: true, statusCode: 1, message: '删除成功!'} 2.3.1.4. 批量保存 设计建议:尽量不要为每一个资源提供批量保存的操作。只有在对资源的操作的性能要求较高时,才选择性提供。 地址:资源列表地址。如 /users/。 动作:使用 POST 动作提供数据。 数据格式:使用一个 JSON 对象提交数据,该对象中包含一个属性名为 list,属性类型为数组的属性。该数组中的每一个对象都是要更新的实体对象。 对于每一个实体对象:可以为每一个子实体对象添加 persistenceStatus 属性,值为 Deleted、Modified、New 来表示该实体的状态:删除、更新、添加。如果不提供该属性,那么如果实体有 Id 属性,则表示更新,否则表示添加。 例如: {list:[ {name:'c1', persistenceStatus:'New'}, {id:1, name:'c2', persistenceStatus:'Modified'}, {id:2, persistenceStatus:'Deleted'} ]} 也可省略为: {list:[ {name:'c1'},//添加 {id:1, name:'c2'},//更新 {id:2, persistenceStatus:'Deleted'} ]} 2.3.1.5. 保存聚合子 设计建议:在需要更新聚合子实体时,如果公布了聚合子资源 API,那么应该首选这个资源来实现保存。否则,才可以在更新聚合父实体时,同时更新它的聚合子实体。 地址与动作:保存聚合子使用聚合父资源相同的地址和动作,见:更新。 数据格式:聚合父对象中有聚合子对应的属性,该属性使用批量更新中定义的数据格式来定义需要更新的聚合子实体集合。见:批量保存。如: {name:'parent', children:[ {name:'c1', persistenceStatus:'New'}, {id:1, name:'c2', persistenceStatus:'Modified'}, {id:2, persistenceStatus:'Deleted'} ]} 也可省略为: {name:'parent', children:[ {name:'c1'}, {id:1, name:'c2'}, {id:2, persistenceStatus:'Deleted'} ]} 2.3.1.6. 查询 · 查询所有资源 地址:资源列表地址。如:/users/。 动作:使用 GET 来进行请求。 · 查询指定id的资源 地址:资源地址+Id。如:/users/1。 动作:使用 GET 来进行请求。 · 其它查询 每一个特殊查询,都需要提供相应的特殊查询地址。必须参数以URI Part 的形式给出,可选参数则以查询字符串的形式给出。例如,使用以下格式: /users/username/hqf/minAge/30/?optionalParam1=1 如果两个 API 使用了相同的参数,则需要在资源后追加一个查询的名称,用以区分。如: /users/find2/username/hqf/minAge/30/?optionalParam1=1 · OData 查询 设计建议:尽量不要提供OData查询。 如果要提供OData查询API,必须考虑查询的权限的限制,同时不要公布排序接口,否则性能可能会很差。 · 查询资源的合集 有时,查询不是直接针对某个单一的资源,而是联合查询一系列资源的合集,返回值的格式也与单一资源格式不同。这时,需要为这个资源合集声明一个新的资源地址。例如,查询用户与角色的合集,可以使用新的资源地址:/userRoles/。 2.3.2. 业务型操作 业务型操作表示可能跨越多个资源的逻辑操作。服务器端直接提供的服务。 · 一般只使用 POST 动作,偶尔使用 GET 动作。不能使用 PUT、DELETE 动作。 · Action 不要使用简单的、通用的名称。如不要使用与资源操作冲突的 Get、Add、Update、Delete、Save 等名称。而使用具体的逻辑名称,如 transfer、refreshTag 等…… · 推荐放到单独的服务地址(控制器)中。如:POST /{transaction}/{transfer} {from:'a',to:'b',money:10}。 · 如果只是对某个资源单独的操作,那么也可以放在该资源地址下。如:POST /users/refreshLogoutTime。; 3. API 帮助文档规范 BAAS 平台中的 API 帮助文档将采用统一的格式编写,并以 HTML 页面的形式发布。 帮助文档使用以下地址:GET /api/v1.1/ 返回 1.1 版本 API 的帮助文档首页。 3.1. 帮助文档内容规范 向外公布的每个API的帮助说明,必须至少包含以下几项: · API 简介 · 请求 o 说明请求的方法、地址。 o URI 参数:如果 URI 中某部分是动态的,请使用大括号说明:api/values/{id}。 o URI 查询参数:如果 URI 地址有参数,描述各项参数与说明。每个参数是否可选。 o 请求标头:如果有特殊的请求标头,需要特别逐一说明。 · 响应 o 说明响应的状态码、内容格式。 o 响应标头:如果有特殊的请求标头,需要特别逐一说明。 o 响应正文:特殊字段、重点必须说明含义。尽量说明响应正文的所有字段意义。 · 可选:授权、备注 · 示例请求与响应 参考示例: · MS Azure 文档示例 3.2. 文档编写方法 API开发者需要为其公布的每一个 API建立一个XML文档用于详细描述上述的帮助内容。该文档建议以与API对应的方法名起名,方便查找。文档的内容由统一的模板确定。 框架组提供统一的转换工具来生成相应的 API 网页。最终会集成在整个 API 网站中。 3.3. 帮助文档XML模板 该模板以附件形式给出。 文件下载地址:http://pan.baidu.com/s/1pJsswQJ
前段时间把 Rafy 的用户手册由 CHM 格式转换为了网页格式,而且发布到了 github.io 上,即方便文档的实时更新,也方便大家查看。 Rafy 用户手册网页版地址: http://zgynhqf.github.io/Rafy。 --------------------------------------------------------------------------- 附: 如何使用 github.io 来发布网页版帮助文档: https://help.github.com/categories/github-pages-basics/ 发布这个走了一些弯路。其实,就是在 git 上建立一个名为 gh-pages 的分支,里面就是专门用来发布帮助文档的分支。 这样,本地还可以直接以提交文件到 github 的形式来更新帮助文档网站内容,非常方便:
当开发者使用 CodeFirst 开发模式,编写了大量的实体类,在代码中编写了完整的类型注释和属性注释,并自动生成数据库后,往往需要把实体类型和实体属性上的注释同时生成到对应的数据库表及字段上。这样,即方便在查看数据库时能清晰地看到每一个表及字段的含义,也方便使用一些第三方的工具(如 PowerDesigner 等)为数据库生成较为全面的文档。 使用方法 在为数据库生成注释之前,需要保证数据库已经全部生成成功(即和实体保持一致)。否则更新字段的注释时,可能因为字段不存在而导致执行失败。 需要在编译领域实体所在的程序集时,同时生成对应的 XML 注释文件。否则,程序会找不到需要编写的注释。 打开方法:在领域实体项目上点击右键->属性->编译->输出->在“XML document file”前打勾。 打开后,重新编译整个解决方案。 编写以下代码来生成数据库的注释。(只需要执行一次即可) using (var context = new RafyDbMigrationContext(JXCEntityRepositoryDataProvider.DbSettingName)) { context.RefreshComments(); } 注意 目前为数据库生成注释的功能,只支持 Oracle 和 SqlServer 两个数据库。 PS:该文已经纳入《 Rafy 用户手册》中。
有时候,开发者不想通过实体来操作数据库,而是希望通过 SQL 语句或存储过程来直接访问数据库。Rafy 也提供了一组 API 来方便实现这类需求。 IDbAccesser 接口 为了尽量屏蔽各数据库中 SQL 语句参数的不同标识,同时也为了使开发者更简单地实现参数化的查询。Rafy 中提供了 IDbAccesser 接口来方便开发者使用。接口定义如下: /// <summary> /// A db accesser which can use formatted sql to communicate with data base. /// </summary> public interface IDbAccesser : IDisposable { /// <summary> /// The underlying db connection /// </summary> IDbConnection Connection { get; } /// <summary> /// 数据连接结构 /// </summary> DbConnectionSchema ConnectionSchema { get; } /// <summary> /// Gets a raw accesser which is oriented to raw sql and <c>IDbDataParameter</c>。 /// </summary> IRawDbAccesser RawAccesser { get; } /// <summary> /// Execute a sql which is not a database procudure, return rows effected. /// </summary> /// <param name="formattedSql">a formatted sql which format looks like the parameter of String.Format</param> /// <param name="parameters">If this sql has some parameters, these are its parameters.</param> /// <returns>The number of rows effected.</returns> int ExecuteText(string formattedSql, params object[] parameters); /// <summary> /// Execute the sql, and return the element of first row and first column, ignore the other values. /// </summary> /// <param name="formattedSql">a formatted sql which format looks like the parameter of String.Format</param> /// <param name="parameters">If this sql has some parameters, these are its parameters.</param> /// <returns>DBNull or value object.</returns> object QueryValue(string formattedSql, params object[] parameters); /// <summary> /// Query out some data from database. /// </summary> /// <param name="formattedSql">a formatted sql which format looks like the parameter of String.Format</param> /// <param name="parameters">If this sql has some parameters, these are its parameters.</param> /// <returns></returns> IDataReader QueryDataReader(string formattedSql, params object[] parameters); /// <summary> /// Query out some data from database. /// </summary> /// <param name="formattedSql">a formatted sql which format looks like the parameter of String.Format</param> /// <param name="closeConnection">Indicates whether to close the corresponding connection when the reader is closed?</param> /// <param name="parameters">If this sql has some parameters, these are its parameters.</param> /// <returns></returns> IDataReader QueryDataReader(string formattedSql, bool closeConnection, params object[] parameters); /// <summary> /// Query out a row from database. /// If there is not any records, return null. /// </summary> /// <param name="formattedSql">a formatted sql which format looks like the parameter of String.Format</param> /// <param name="parameters">If this sql has some parameters, these are its parameters.</param> /// <returns></returns> DataRow QueryDataRow(string formattedSql, params object[] parameters); /// <summary> /// Query out a DataTable object from database by the specific sql. /// </summary> /// <param name="formattedSql">a formatted sql which format looks like the parameter of String.Format</param> /// <param name="parameters">If this sql has some parameters, these are its parameters.</param> /// <returns></returns> DataTable QueryDataTable(string formattedSql, params object[] parameters); /// <summary> /// Query out a row from database. /// If there is not any records, return null. /// </summary> /// <param name="formattedSql">a formatted sql which format looks like the parameter of String.Format</param> /// <param name="parameters">If this sql has some parameters, these are its parameters.</param> /// <returns></returns> LiteDataRow QueryLiteDataRow(string formattedSql, params object[] parameters); /// <summary> /// Query out a DataTable object from database by the specific sql. /// </summary> /// <param name="formattedSql">a formatted sql which format looks like the parameter of String.Format</param> /// <param name="parameters">If this sql has some parameters, these are its parameters.</param> /// <returns></returns> LiteDataTable QueryLiteDataTable(string formattedSql, params object[] parameters); } 该接口使用类似于 String.Format 中的字符串格式来表达 SQL 中的参数。并在连接不同的数据库时,生成相应数据库对应的参数格式。 具体使用方法如下: 执行查询代码示例: var bookRepo = RF.Concrete<BookRepository>(); using (var dba = DbAccesserFactory.Create(bookRepo)) { DataTable table = dba.QueryDataTable("SELECT * FROM Books WHERE id > {0}", 0); } 执行非查询代码示例: var bookRepo = RF.Concrete<BookRepository>(); using (var dba = DbAccesserFactory.Create(bookRepo)) { int linesAffected = dba.ExecuteText("DELETE FROM Books WHERE id > {0}", 0); } 另外,DbAccesserFactory 中也提供了不通过仓库对象,而直接使用‘数据库连接的配置名’进行构建的方法,例如: using (var dba = DbAccesserFactory.Create("JXC")) { int linesAffected = dba.ExecuteText("DELETE FROM Books WHERE id > {0}", 0); } 参数过多时,则依次按顺序传入即可: using (var dba = DbAccesserFactory.Create(bookRepo)) { for (int i = 0; i < 10; i++) { dba.ExecuteText( "INSERT INTO Book (Author,BookCategoryId,BookLocId,Code,Content,Name,Price,Publisher) VALUES ({0},{1},{2},{3},{4},{5},{6},{7})", string.Empty, null, null, string.Empty, string.Empty, i.ToString(), null, string.Empty ); } } IRawDbAccesser 接口 由于 IDbAccesser 接口封装了 SQL 语句中参数对应不同数据库中名称的变化,同时也更方便开发者使用,所以一般情况下,都推荐使用该接口。但是,IDbAccesser 接口并不支持存储过程的调用。另外,有时开发者希望自己来构建原生的 SQL 语句和参数,这时,就需要用到 IRawDbAccesser 接口了。(接口定义过长,这里不再贴出。) 该接口的使用方法与 IDbAccesser 类似,不同的地方在于 SQL 中需要传入特定数据库的参数名,并且参数需要自行构造,例如: using (var dba = DbAccesserFactory.Create(bookRepo)) { for (int i = 0; i < 10; i++) { dba.RawAccesser.ExecuteText( "INSERT INTO Book (Author,BookCategoryId,BookLocId,Code,Content,Name,Price,Publisher,Id) VALUES ('', NULL, NULL, '', '', :p0, NULL, '', :p1)", dba.RawAccesser.ParameterFactory.CreateParameter("p0", i), dba.RawAccesser.ParameterFactory.CreateParameter("p1", i) ); } } 另外,IRawDbAccesser 接口也可以使用存储过程了,例如: using (var dba = DbAccesserFactory.Create(bookRepo)) { for (int i = 0; i < 10; i++) { dba.RawAccesser.ExecuteProcedure( "InsertBookProcedure", dba.RawAccesser.ParameterFactory.CreateParameter("p0", i), dba.RawAccesser.ParameterFactory.CreateParameter("p1", i) ); } } PS:该文已经纳入《 Rafy 用户手册》中。
某些场景下,开发者希望能够大批量地把实体的数据导入到数据库中。虽然使用实体仓库保存实体列表非常方便,但是其内部实现机制是一条一条的保存到数据库,当实体的个数较多时,效率就会很低。所以 Rafy 设计了批量导入插件程序,其内部使用 ADO.NET 及 ODP.NET 中的批量导入机制来把大量数据一次性导入到数据库中。 使用方法 步骤 由于批量导入功能是一个额外的程序集,所以在使用该功能时,需要先使用 NuGet 引用最新版本的 Rafy.Domain.ORM.BatchSubmit 程序集。 如果准备导入 ORACLE 数据库,则也需要引用 Oracle.ManagedDataAccess(12.1.022 以上版本) 程序集。 修改需要保存大量实体的代码,例如,原代码如下: var books = new BookList(); for (int i = 0; i < 1000000; i++) { var book = new Book { ChapterList = { new Chapter(), new Chapter(), } }; books.Add(book); } //直接使用实体仓库进行保存。 repo.Save(books); 需要把最后一行使用仓库保存实体列表,修改为创建导入器来保存实体列表: //创建一个批量导入器进行保存。 repo.CreateImporter().Save(books); 注意 从上面的代码可以看出,批量导入程序是面向整个聚合的。也就是说,批量导入父实体时,同时也会批量导入父实体下的所有子实体。 批量导入不但支持添加新实体,同时也支持批量更新、批量删除。使用方法与使用仓库保持一致。 对于大批量的数据,使用批量导入,比直接使用仓库来保存实体,速度要快两个数据级左右。 目前批量导入实体的功能,只支持 Oracle 和 SqlServer 两个数据库。 在使用 Oracle 数据库时,还需要在数据库生成完成后,特别地调用以下代码以启用某个聚合实体的批量导入功能,否则导入过程中会抛出异常(原因请见后面的实现原理章节)。代码如下: Rafy.Domain.ORM.BatchSubmit.Oracle.OracleBatchImporter.EnableBatchSequence( RF.Concrete<OriginalDataRepository>() ); 实现原理 下面简要介绍批量导入的原理。 Sql Server 对于 Sql Server 数据库的批量保存: 批量新增数据,是使用 System.Data.SqlClient.SqlBulkCopy 来实现的。 批量更新数据,是使用 System.Data.SqlClient.SqlDataAdapter 来实现的。 批量删除数据,则是直接拼接 SQL 语句,把需要删除的实体的 Id 放到 In 语句中进行删除。例如: DELETE FROM Books WHERE Id IN (1,3,5,7......); Oracle 对于 Oracle 数据库的批量保存: 新增数据、更新数据都是使用 ODP.NET 中原生的批量导入功能。 参见:Oracle.ManagedDataAccess.Client.OracleCommand.ArrayBindCount 属性。 而删除数据的实现则和 SQLServer 的实现一致,均是拼接 DELETE 语句。 新增大量实体时,实体的 Id 生成 一般情况下,使用仓库保存一个新增的实体时,仓库会使用数据库本身的机制来为实体生成 Id,在 SQLServer 中是使用 IDENTITY 列,在 ORACLE 中则是使用每个表对应的 SEQUENCE 来生成。但是,批量导入大量新实体时,为了性能上的考虑,则需要一次性为需要保存的所有新实体统一生成 Id。 在 SQLServer 中,可以方便地使用 SQL 语句调整表中 IDENTITY 下一次的值,所以实现比较简单。只需要设置 IDENTITY 下一次的值 + 100000,并使用中间跳过的这些值来作为实体的 Id 即可。 但是在 ORACLE 中,如果去调整 SEQUENCE 的值,则属于 DDL 语句,会隐式自动提交事务,会造成数据的错误。所以我们最终决定:如果在 ORACLE 中要使用批量导入功能,数据表对应的 SEQUENCE 必须以较大的数字为步距(如 ALTER SEQUENCE "SEQ_TABLE_ID" INCREMENT BY 100000 NOCACHE)。这样,在批量导入时,就不再需要增修改 SEQUENCE 的步距,而直接使用中间跳过的这些值作为实体的 Id。这样做也比较方便,但是负面效果则是使用仓库保存单一实体时,两次保存不同实体生成的 Id 会相差 100000,不再是连续的。 PS:该文已经纳入《 Rafy 用户手册》中。
最近在设计框架时,需要设计一类扩展点,发现不能简单地继承或使用事件来给使用者提供 API。最终使用拦截器模式解决了 API 的设计。 扩展点使用场景 该扩展点的使用场景如下: 不能使用继承;需要在类型的继承体系外(非被扩展类型的子类)对类型进行扩展。 需要能在基本逻辑的执行前、后扩展新的逻辑,甚至可以使用新的逻辑替换基础逻辑。 对于性能敏感。由于该基础逻辑是比较核心的代码,需要尽量地减少扩展点带来的额外性能消耗、并尽量少地产生额外的对象。 扩展点设计方案选型 在框架设计时,扩展点设计主要通过几种类型的 API 来提供:虚方法、事件、接口。(关于扩展点的设计,详细的内容,参见:《Framework Design Guidelines 2nd Edition》第六章,扩展性设计。)而最常用、最方便使用者使用的扩展点则是前两个:虚方法和事件。 前两种扩展点设计方案的主要区别在于: 场景:继承中的虚方法主要是为类型的子类型进行扩展而提供的,而事件则主要是为继承体系外的类型来扩展继承体系内的类型的行为而提供的; 控制度:子类对虚方法进行重写时,可以在基类的基本方法前、后编写自己的扩展代码,同时还可以控制是否需要调用基类的方法;而事件要实现这些功能,需要提供逻辑前事件(Invoking)、逻辑后事件(Invoked),并通过类似 CancelEventArgs.IsCancel 属性等方式来控制是否需要执行基本的逻辑。 性能:虚方法的调用是非常高效的,也不会产生额外的对象。而事件的机制本质是委托列表,会遍历该列表进行调用,同时需要产生额外的委托对象;其次,由于 .NET 事件的设计中往往要求提供一个从 EventArgs 类型上继承的事件参数对象,在每次调用都构造并传递该对象,这也会产品额外的对象压力。 可以看出,如果是想设计一类提供给继承体系外类型进行扩展的扩展点, 虚方法、事件两类 API 都不合适。那我们只能在第三种方式上想办法:接口。接口的设计则非常灵活,而其实我上面描述的场景会经常遇到,所以应该提取出一类设计模式。经过一番思考,我发现其实拦截器模式比较适合该场景。拦截器模式本身注重对消息、方法的拦截处理,是一种继承体系外的扩展方法,并被大量用于 AOP 的实现。在这里采用该模式,我们更加关注其在真正核心方法调用前后的扩展机制、以及核心方法的阻断机制,以及最终扩展 API 提供的形式。 实现 该模式放到 Rafy 中实现提交时的扩展点后,类图如下: 扩展点使用方法也较简单,使用者继承拦截器,编写相应的扩展逻辑即可: 有一个细节需要注意:上图中能看到,方法的第一个参数也是一个自定义的参数类型 SubmitArgs。但是由于拦截器是一种链式调用,所以这个类型可以采用值类型;在此方法被大量调用时,相对于事件的扩展机制,没有大量的冗余对象。 在启动时,加入以下代码就可以把该拦截器添加到保存的拦截器列表中: 总结 拦截器模式实现起来比较简单,该模式的结构非常类似于 GOF 中的职责链模式,只是关注点不同。在使用它作为扩展点时,对于使用者来说也比较易用,而且性能相对于事件机制来说更好,所以可以直接作为一种常用的扩展点设计方案。
Rafy 框架又添新成员:幽灵插件。本文将解释该插件的场景、使用方法、原理。 场景 在开发各类数据库应用系统时,往往需要在删除数据时不是真正地删除数据,而只是把数据标识为‘已删除’状态。这些数据在业务逻辑上是已经完全删除、不可用的数据,但是不能在数据库中真正的把它们删除,而是需要永久保留这些历史数据。即开发人员常说的‘假删除’功能。 这种需求往往是系统级的。往往不是针对某一张表,而很可能是针对系统中的所有表都需要实现‘假删除’功能。 使用方法 由于这种需求比较常见,所以我们决定专门为该功能写一个独立的 Rafy 插件。这样,开发人员需要实现假删除功能时,只需要引用该插件后,系统中所有删除的实体都自动变为‘幽灵’,同时这些幽灵数据在仓库的所有查询中都将被自动过滤。 使用步骤: 通过 Nuget Package Manager 搜索并安装 Rafy.Domain.EntityPhantom 插件。 在 DomainApp 中添加该插件: class JXCApp : DomainApp { protected override void InitEnvironment() { //添加幽灵插件到 Rafy 应用程序集中。 RafyEnvironment.DomainPlugins.Add(new Rafy.Domain.EntityPhantom.EntityPhantomPlugin()); RafyEnvironment.DomainPlugins.Add(new JXCPlugin()); base.InitEnvironment(); } } 为需要幽灵功能的实体打开该功能,需要在实体元数据配置中进行配置: internal class UserConfig : JXCEntityConfig<User> { protected override void ConfigMeta() { Meta.MapTable().MapAllProperties(); //在实体配置中加入此行代码,为实体启用幽灵功能。 Meta.EnablePhantoms(); } } 效果 所有继承自 Entity 的实体都会统一的添加一个 IsPhantom 的属性。这个属性表示这个实体是否为‘幽灵’,即已经删除的数据。 开发者可以使用 Meta.EnablePhantoms() 来为某个指定的实体类型开启‘幽灵’功能。 开启该功能的实体的 IsPhantom 属性会自动映射到数据库中。 在保存实体时,如果要删除一个聚合实体,则这个聚合中的所有实体都将会被标记为‘幽灵’状态。 在查询实体时,所有的查询,都将会自动过滤掉所有‘幽灵’状态的数据。(手写 SQL 查询的场景不在考虑范围内。) 使用批量导入数据插件进行数据的批量导入时,批量删除的实体同样都会被标记为‘幽灵’状态。 运行程序后,数据库中的字段,已经自动添加上 IsPhantom 字段了: 在使用 GetAll 查询所有实体时,框架自动加上一 IsPhantom = false 的过滤条件: SELECT * FROM [User] WHERE [User].[IsPhantom] = @p0 ORDER BY [User].[Id] ASC Parameters:False 数据的删除,变为更新表中对应行的 IsPhantom 字段为 True: UPDATE [User] SET [Name] = @p0,[IsPhantom] = @p1 WHERE [Id] = @p2 Parameters:"Name",True,3 原理 幽灵插件的原理比较简单。在 Rafy 框架的基础上,以插件的形式对 Rafy 框架中实体的数据层进行了扩展。在启用实体的幽灵功能后,该实体的 DataProvider 类型的 Deleting、Querying 事件都会被监听并扩展: /// <summary> /// 数据的删除、查询的拦截器。 /// </summary> internal static class PhantomDataInterceptor { internal static void Intercept() { RepositoryDataProvider.Deleting += RepositoryDataProvider_Deleting; RepositoryDataProvider.Querying += RepositoryDataProvider_Querying; } } 在查询时,框架自动分析出当前查询的 SQL 树,并在主查询上加上 IsPhantom = false 的过滤条件。 有兴趣的同学,可以查看 Rafy 框架源码。 PS:该文已经纳入《 Rafy 用户手册》中。
一直想在 Linux 上使用 MONO 试试运行 Rafy,最近因为业务需要,总算是真正地试验了一次。下面是本次部署记录的一些要点。 Linux 这次部署,我是和两位同事一起来试验的。由于我们对 Linux 都不太熟悉(多年前曾经用过很少一段时间的 RedHat,那些命令现在也早已忘记了,哈。),所以我们三个分别测试了三个不同的镜像,最终选定了 OpenSUSE 的一个镜像。(其实,我下载了一个Ubuntu,才 800M,安装后老是有问题,不得不放弃,悲剧……) 相对于 Windows 来说,Linux 更轻量,安装很简单,安装速度也非常快。 ASP.NET vNext vs MONO 这两个是目前可行的 .NET 跨平台方案。我们需要快速理解两个方案,分析哪个方案的移植成本最低。 ASP.NET vNext 微软本身已经逐步支持开源了,所以我们的想法自然是尽量先用微软官方发布的跨平台方案。ASP.NET vNext(5) 目前已经发布了 RC1。但是经测试,发现整个代码构建于新的 API 之上,如果要把我们的程序移植到 vNext 之上,需要修改许多代码。所以暂时还是没有选择使用 vNext,但是长期看来,必然还是需要选择 VNext。 下面是 vNext 官网和其文档: http://www.asp.net/vnext、https://docs.asp.net/en/latest/index.html 下面是一些 vNext 相关的系列教程: 《解读ASP.NET 5 & MVC6系列》、《ASP.NET 5系列教程》。 MONO 其实,目前来说,.NET 跨平台,大家用的比较多的还是 MONO。官网:http://www.mono-project.com/ MONO 可简单理解为跨平台的 .NET 平台,包括运行时、框架、工具。MONO 框架是 .NET 框架的重写版本,其 API 尽量保持与 .NET 框架一致,也支持 CLI 规范,所以上层的应用程序不需要重新编译,也可以直接运行在 MONO 上。所以直接使用 MONO 应该是成本最低的跨平台方案。 但是,MONO 并不支持完整的 .NET,例如 WPF、WWF 就没有在 MONO 上实现(详见:《兼容性对比》)。所以,我们可以使用一个工具来检测应用程序是否会有兼容性问题:“Mono Migration Analyzer”。 MONO 安装完成后,对其进行了测试,可正确运行即可: Web Server 在 Linux 在运行 MONO 可以使用 Apache、Nginx 等作为 Web 服务器,见:《 MONO ASP.NET》、《mod_mono》。另外,国人编写的 Jexus 服务器也是比较流行的,对 .NET 支持非常好,用起来很简单。所以我们选择了最简单的 Jexus 服务器,降低学习的难度。 这里遇到了一个问题,Jexus 服务器使用的是 IIS 经典模式,导致网站 Web.config 中 <System.WebServer> 配置节不可用(该配置节用于 IIS 集成模式)。这里,需要把该配置节中的内容都修改到 <System.Web> 中对应的配置节即可。 最终运行环境 Linux(OpenSUSE)+MONO+Jexus+MVC5+Rafy+Oracle。 在上述环境中,程序总算可以运行了。 不过还是发现了很多的兼容性问题。比较多的情况是由于 Linux 是大小写敏感的,而 Windows 并不敏感,所以程序中大量的文件在 Linux 上‘找不到’。另外,Windows 中的路径分隔符是’\’,而 Linux 中是 ‘/’,也导致了一些问题。关于程序移植时考虑的内容,详见:《Application Portability》。
最近两周完成了对公司某一产品的前端重构,本文记录重构的主要思路及相关的设计内容。 公司期望把某一管理类信息系统从项目代码中抽取、重构为一个可复用的产品。该系统的前端是基于 ExtJs 5 进行构造的,后端是基于 Asp.net MVC 提供的 REST 数据接口。同时,希望通过这次重构,不但能将其本身重构至可用于快速二次开发的产品,同时还要求该前端代码要保证相对的独立,使得同时可以接入 .NET 和 JAVA 两个不同的后端平台所提供的数据接口。 旧代码的问题 老系统的前端代码如下图所示: 在构造之初,并没有考虑太多的产品化工作,而主要还是为了快速实现项目中的需求。也并没有对前端代码进行一个较好的架构设计。这导致了一些问题: 可维护性差:开发者为了快速开发出相应的界面,随意地把整个界面的代码罗列在一起,形成了大量意大利面式的代码。这其中包括了各种不同类型的代码:界面结构声明、界面样式代码、动态界面代码、事件监听代码、事件逻辑控制代码、JS实体声明代码、数据源声明代码、数据获取代码……大量不同类型的逻辑与视图的代码混合在一起,导致了一个模块的代码文件越来越大,有的甚至达到了几千行。 大量重复的代码:由于在初期,并没有搭建一个统一的框架,把一些通用的代码提取出来,而且项目组的开发人员也很随意地拷贝代码,导致大量页面都有些重复的逻辑。而当前开发的模块本身的特性代码,则混杂在其中。 无法统一处理许多问题:这也是大量重复代码引发的另一个问题,项目组想要对统一的页脚、页面的自适应、Ajax 请求等进行统一处理,都必须逐一页面进行修改。 可扩展性差:由于没有前期设计,可扩展性较差。二次开发也只能是拷贝代码并在该代码基础上进行修改。 易错、难写:这是 JavaScript 这种弱类型、解释型脚本语言的通性,再加上 EXTJS 框架本身大量使用 JSON 对象来表达参数,开发环境无法提供智能提示,开发者只能靠不断地查询 Api 文档才能编程,一不小心就会弄错。 重构目标 独立的前端:对数据接口层需要进行适当的封装。使其同时可对接 .NET、JAVA 两个版本的后端。 强类型化:使用强类型脚本语言 TypeScript 来编写整个应用程序的代码。 结构化:基于 MVC 模式来搭建,使视图代码、逻辑代码分离。 产品化-模块化:重构后的产品前端应该与后端遵循一致的业务模块划分,并在技术上提供插件化框架。 产品化-支持二次开发:不能以修改产品源码的形式来进行二次开发,而是以扩展的形式完成。 产品化-提高可重用性:为二次开发提供方便易用的框架、基础业务逻辑、基础界面。 产品化-提高可扩展性:基于框架开发的界面,需要为二次开发提供易用、有粗有细的扩展点,方便二次开发团队在产品的基础上快速搭建新的界面。这些扩展点包含:模块级别的扩展或替换、模块中的指定界面扩展或替换、控制器中的业务逻辑的扩展或替换,甚至任意逻辑的扩展或替换。 设计难点 类型系统冲突 由于EXTJS 中的 MVC 模式要求 Controller 从 Ext.app.Controller 类继承,视图则从 Ext.Component 类继承。这种继承需要使用的是 EXTJS 本身的面向对象类型系统框架带来的继承方案,即使用 Ext.define 来定义继承的子类。但是我们又需要使用 TypeScript 来编写整个应用程序,而 TypeScript 在语言层面提供了新的面向对象系统,使用后者将导致我们不能使用 EXTJS 5 本身自带的 MVC 模式。由于我们更倾向于使用语言层面的面向对象系统,所以只有放弃 EXTJS 中的面向对象框架和 MVC 框架。 TypeScript-MVC 框架的设计 首先,与原系统一致,界面框架主要还是采用 EXTJS 5。不同的是,这里的 MVC 需要自行重新设计,Controller、View 都需要重新建立新的基类。由于视图控件还是采用 EXTJS 中的控件,所以这个 MVC 框架中的 View 其实是图中的 ViewBuilder,其职责为创建 EXTJS 中的控件。所有构造界面相关的代码,都将编写在 ViewBuilder 中。 其次,Controller 与 ViewBuilder 之间独立开之后,还需要建立哪些关联? Controller 要能获取到 View 中的指定 Id 的界面元素(如按钮、表格、文本框等)。这样,Controller 不但能监听任意界面元素的事件;还可以把这些界面元素缓存下来,在 Controller 中的其它逻辑代码处,来使用这些界面元素。(Controller 需要提供非常方便的 Api,来让使用者快速建立上述关联,这样可以强化 Controller 和 ViewBuilder 之间的配对关系。) 添加 ViewModel,实现 View 的逻辑数据抽象,并由其完成自 Controller 到 View 的数据传递。 实现 目前已经实现了第一个版本。 过程中其实还解决了之前项目中老是出现的 Ext 控件 Id 重复的问题:通过定义新的 cId 来替换 Id,并提供相应的通过 cId 查询对应控件的方法。这样,就算有重复的 cId 的控件,也不会有什么问题了。 另外,完成后的框架,虽然带来了诸多好处,但是开发者的第一感觉还是复杂了许多。之前全都堆在一个文件中的代码,现在要分为控制器、视图,而且还需要基于统一的底层框架来实现,框架中的 Api 还需要慢慢熟悉,学习门槛高了不少。 PS----------------------------------------- 附上基于该 MVC 框架的某模块的最终部分 TS 代码: HolidayViewBuilder.ts: module DBI.modules.holiday { /** * 假日页面的视图。 */ export class HolidayViewBuilder extends ViewBuilder { buildView(): View { return this.buildGrid({ cId: 'grid', region: 'center', store: this.buildStore(), tbar: this.buildToolbar({ items: [ DBI.Workflow.createStatusComboBox({ model: this.modelName }), { cId: 'btnSearch', text: "查询", operationName: 'Search' }, { cId: 'btnAdd', text: '添加', operationName: 'Add' }, { cId: 'btnEdit', text: '修改', operationName: 'Edit' }, { cId: 'btnDelete', text: '删除', operationName: 'Delete' }, { cId: 'btnSubmitWF', text: '提交审批', operationName: 'SubmitWF' } ] }), columns: [ { text: "ID", width: 60, dataIndex: 'Id', hidden: true, align: "center" }, { xtype: "rownumberer", text: "序号", width: 50, align: "center" }, { text: "开始时间", width: 150, dataIndex: 'StartDate', sortable: true, align: 'center', renderer: function (value) { return Ext.util.Format.date(value, 'Y-m-d'); } }, { text: "结束时间", width: 150, dataIndex: 'EndDate', sortable: true, align: 'center', renderer: function (value) { return Ext.util.Format.date(value, 'Y-m-d'); } }, { text: "节假日名称", width: 150, dataIndex: 'HolidayName', sortable: true, align: 'center' }, { text: "状态", width: 150, dataIndex: 'WF_ApprovalStatus', sortable: true, align: 'center' }, { text: "审核原因", width: 180, dataIndex: 'WF_ApprovalReason', sortable: true, align: 'center' }, //{ text: "生效时间", width: 135, dataIndex: 'WF_EffectiveTime', sortable: true, align: 'center' }, { text: "最后更新时间", width: 150, dataIndex: 'UpdatedTime', sortable: true, align: 'center', renderer: function (value) { return Ext.util.Format.date(value, 'Y-m-d H:i:s'); } }, { text: "生效时间", width: 150, dataIndex: 'WF_EffectiveTime', sortable: true, align: 'center', renderer: function (value) { return Ext.util.Format.date(value, 'Y-m-d'); } } ] }); } } } HolidayController.ts module DBI.modules.holiday { /** * 假日模块的控制器 */ export class HolidayController extends ViewController { viewBuilder = new HolidayViewBuilder(); modelName = "DBI.Holiday"; moduleTitle = "节假日管理"; store: Ext.data.IStore; grid: Ext.grid.IGridPanel; formWindow: Ext.IWindow; formPanel: Ext.IFormPanel; form: Ext.form.IBasic; init() { super.init(); this.grid = this.view; this.store = this.grid.store; this.control(this.view, { btnSearch: { click: this.onBtnSearchClick }, btnAdd: { click: this.onBtnAddClick }, btnEdit: { click: this.onBtnEditClick }, btnDelete: { click: this.onBtnDeleteClick }, btnSubmitWF: { click: this.onBtnSubmitWFClick } }); this.reloadData(); } onBtnAddClick() { this.showFormWindow(); this.formWindow.setTitle("添加节假日"); this.form.url = urls.Holiday.InsertHoliday; } /** * 打开提交申请的窗体 */ onBtnSubmitWFClick() { if (DBI.Workflow.canSubmitApply({ grid: this.grid })) { var applyController = new wf.CommonApplyWinController(); applyController.modelName = this.modelName; applyController.viewModel = { flowCode: "WF_HOLIDAY", windowTitle: "假日审批流程", columns: HolidayApporvalViewBuilder.buildApprovingGridColumns(), dataSource: new wf.ApplyWinDataSource(this.grid) }; applyController.init(); applyController.showWindow(); } } showFormWindow() { this.formWindow = this.viewBuilder.buildFormWindow(); this.formPanel = this.formWindow.getChild("form"); this.form = this.formPanel.getForm(); this.control(this.formWindow, { btnSubmit: { click: this.submitForm }, btnClose: { click: () => { this.formWindow.close(); } } }); this.formWindow.show(); } submitForm() { var form = this.form; if (!form.isValid()) return; var startDate = form.findField('StartDate').getValue(); var endDate = form.findField('EndDate').getValue(); if (startDate > endDate) { Ext.MessageBox.alert('提示', "开始时间不能大于结束时间"); return; } //提交数据到服务端。 form.submit({ success: () => { Ext.MessageBox.alert('提示', "提交成功!"); this.formWindow.close(); this.store.reload(); }, failure: () => { Ext.MessageBox.alert('提示', "提交失败!"); this.formWindow.close(); this.store.reload(); } }); } reloadData() { var filter = DBI.Workflow.createStatusFilter(); this.store.proxy.url = DBI.OData.createUrl({ model: this.modelName, filter: filter }); this.store.load(); } } }
Rafy 框架又添新成员:流水号插件。本文将解释 Rafy 框架中的流水插件的场景、使用方法。 场景 在开发各类数据库应用系统时,往往需要生成从一开始的流水号,有时还需要按月或者按日进行独立生成,如下面的格式:2016031800000001、2016031800000002……。 设计本插件用于生成上述相应格式的编号。 使用方法 添加插件 1.通过 Nuget Package Manager 搜索并安装 Rafy.SerialNumber 插件。 2.在 DomainApp 中添加该插件;同时,设置该插件所对应的数据库配置名: class JXCApp : DomainApp { protected override void InitEnvironment() { //配置插件所对应的数据库配置名。 Rafy.SerialNumber.SerialNumberPlugin.DbSettingName = "TestDb"; //添加流水号插件到 Rafy 应用程序集中。 RafyEnvironment.DomainPlugins.Add(new Rafy.SerialNumber.SerialNumberPlugin()); base.InitEnvironment(); } } 使用插件 1.生成数据库。 该插件中自带两个实体:SerialNumberInfo 、SerialNumberValue ,所以 Rafy 会为其在数据库中添加相应的两张表。 2.添加流水号生成规则。 SerialNumberInfo 表示定义的流水号生成规则信息。而 SerialNumberValue 则表示生成的流水号的具体值。所以要生成流水号,必须先为其定义生成规则。可以使用 SerialNumberController 进行简单的每日规则创建,示例如下: var controller = DomainControllerFactory.Create<SerialNumberController>(); var sni = controller.CreateDailySerialNumberInfo("流水号规则-1"); CreateDailySerialNumberInfo 方法内部其实非常简单,开发者可以参考以下代码创建新的生成规则,如下: /// <summary> /// 创建一个以日期进行分组生成编号的规则,存储到仓库中,并返回。 /// 性能-仓库访问次数:1。 /// </summary> /// <param name="name"></param> /// <returns></returns> public SerialNumberInfo CreateDailySerialNumberInfo(string name, string format = "yyyyMMdd********") { var sni = new SerialNumberInfo { Name = name, TimeGroupFormat = "yyyyMMdd", Format = format, RollValueStart = 1, RollValueStep = 1, }; var infoRepo = RF.Concrete<SerialNumberInfoRepository>(); infoRepo.Save(sni); return sni; }); 3.生成流水号。 使用以下代码生成流水号即可: var next = controller.GenerateNext("流水号规则-1"); Assert.AreEqual("2016031800000001", next); next = controller.GenerateNext("流水号规则-1"); Assert.AreEqual("2016031800000002", next); next = controller.GenerateNext(sni); Assert.AreEqual("2016031800000002", next); PS:该文已经纳入《 Rafy 用户手册》中。
本文转载自:http://www.cnblogs.com/liuning8023/p/4493156.html ---------------------------------------------------------------------------- 原文是 Martin Flower 于 2014 年 3 月 25 日写的《Microservices》。 本文内容 微服务 微服务风格的特性 组件化(Componentization )与服务(Services) 围绕业务功能的组织 产品不是项目 强化终端及弱化通道 分散治理 分散数据管理 基础设施自动化 容错性设计 设计改进 微服务是未来吗 其它 微服务系统多大 微服务与SOA 多语言多选择 实践标准和强制标准 让做对事更容易 断路器circuit breaker和产品中现有的代码 同步是有害的 参考资料 微服务 “微服务架构(Microservice Architecture)”一词在过去几年里广泛的传播,它用于描述一种设计应用程序的特别方式,作为一套独立可部署的服务。目前,这种架构方式还没有准确的定义,但是在围绕业务能力的组织、自动部署(automated deployment)、端智能(intelligence in the endpoints)、语言和数据的分散控制,却有着某种共同的特征。 “微服务(Microservices)”——只不过在满大街充斥的软件架构中的一新名词而已。尽管我们非常鄙视这样的东西,但是这玩意所描述的软件风格,越来越引起我们的注意。在过去几年里,我们发现越来越多的项目开始使用这种风格,以至于我们身边的同事在构建企业级应用时,把它理所当然的认为这是一种默认开发形式。然而,很不幸,微服务风格是什么,应该怎么开发,关于这样的理论描述却很难找到。 简而言之,微服务架构风格,就像是把一个单独的应用程序开发为一套小服务,每个小服务运行在自己的进程中,并使用轻量级机制通信,通常是 HTTP API。这些服务围绕业务能力来构建,并通过完全自动化部署机制来独立部署。这些服务使用不同的编程语言书写,以及不同数据存储技术,并保持最低限度的集中式管理。 在开始介绍微服务风格(microservice style)前,比较一下整体风格(monolithic style)是很有帮助的:一个完整应用程序(monolithic application)构建成一个单独的单元。企业级应用通常被构建成三个主要部分:客户端用户界面(由运行在客户机器上的浏览器的 HTML 页面、Javascript 组成)、数据库(由许多的表构成一个通用的、相互关联的数据管理系统)、服务端应用。服务端应用处理 HTTP 请求,执行领域逻辑(domain logic),检索并更新数据库中的数据,使用适当的 HTML 视图发送给浏览器。服务端应用是完整的 ,是一个单独的的逻辑执行。任何对系统的改变都涉及到重新构建和部署一个新版本的服务端应用程序。 这样的整体服务(monolithic server)是一种构建系统很自然的方式。虽然你可以利用开发语基础特性把应用程序划分成类、函数、命名空间,但所有你处理请求的逻辑都运行在一个单独的进程中。在某些场景中,开发者可以在的笔计本上开发、测试应用,然后利用部署通道来保证经过正常测试的变更,发布到产品中。你也可以使用横向扩展,通过负载均衡将多个应用部署到多台服务器上。 整体应用程序(Monolithic applications)相当成功,但是越来越多的人感觉到有点不妥,特别是在云中部署时。变更发布周期被绑定了——只是变更应用程序的一小部分,却要求整个重新构建和部署。随着时间的推移,很难再保持一个好的模块化结构,使得一个模块的变更很难不影响到其它模块。扩展就需要整个应用程序的扩展,而不能进行部分扩展。 图 1 整理架构与微服务架构 这导致了微服务架构风格(microservice architectural style)的出现:把应用程序构建为一套服务。事实是,服务可以独立部署和扩展,每个服务提供了一个坚实的模块边界,甚至不同的服务可以用不同的编程语言编写。它们可以被不同的团队管理。 我们必须说,微服务风格不是什么新东西,它至少可以追溯到 Unix 的设计原则。但是并没有太多人考虑微服务架构,如果他们用了,那么很多软件都会更好。 微服务风格的特性 微服务风格并没有一个正式的定义,但我们可以尝试描述一下微服务风格所具有的共同特点。并不是所有的微服务风格都要具有所有的特性,但我们期望常见的微服务都应该有这些特性。我们的意图是尝试描述我们工作中或者在其它我们了解的组件中所理解的微服务。特别是,我们不依赖于那些已经明确过的定义。 组件化(Componentization )与服务(Services) 自从我们开始软件行业以来,一直希望由组件构建系统,就像我们在物理世界所看到的一样。在过去的几十年里,我们已经看到了公共库的大量简编取得了相当的进步,这些库是大部分语言平台的一部分。 当我们谈论组件时,可能会陷入一个困境——什么是组件。我们的定义是,组件(component)是一个可独立替换和升级的软件单元。 微服务架构(Microservice architectures)会使用库(libraries),但组件化软件的主要方式是把它拆分成服务。我们把库(libraries)定义为组件,这些组件被链接到程序,并通过内存中函数调用(in-memory function calls)来调用,而服务(services )是进程外组件(out-of-process components),他们利用某个机制通信,比如 WebService 请求,或远程过程调用(remote procedure call)。组件和服务在很多面向对象编程中是不同的概念。 把服务当成组件(而不是组件库)的一个主要原因是,服务可以独立部署。如果你的应用程序是由一个单独进程中的很多库组成,那么对任何一个组件的改变都将导致必须重新部署整个应用程序。但是如果你把应用程序拆分成很多服务,那你只需要重新部署那个改变的服务。当然,这也不是绝对的,有些服务会改变导致协调的服务接口,但是一个好的微服务架构的目标就是通过在服务契约(service contracts)中解耦服务的边界和进化机制来避免这些。 另一个考虑是,把服务当组件将拥有更清晰的组件接口。大多数开发语言都没有一个良好的机制来定义一个发布的接口(Published Interface)。发布的接口是指一个类向外公开的成员,比如 Java 中的声明为 Public 的成员,C# 中声明为非 Internal 的成员。通常只有在文档和规范中会说明,这是为了让避免客户端破坏组件的封装性,阻止组件间紧耦合。服务通过使用公开远程调用机制可以很容易避免这些。 像这样使用服务也有不足之处。远程调用比进制内调用更消耗资源,因此远程 API 需要粗粒度(coarser-grained),但这会比较难使用。如果你需要调整组件间的职责分配,当跨越进程边界时,这样做将会很难。 一个可能是,我们看到,服务可以映射到运行时进程(runtime processes)上,但也只是一个可能。服务可以由多个进程组成,它们会同时开发和部署,例如一个应用程序进程和一个只能由这个服务使用的数据库。 围绕业务功能的组织 当寻找把一个大的应用程序拆分成小的部分时,通常管理都会集中在技术层面,UI团队、服务端业务逻辑团队和数据库团队。当使用这种标准对团队进行划分时,甚至小小的更变都将导致跨团队项目协作,从而消耗时间和预算审批。一个高效的团队会针对这种情况进行改善,两权相害取其轻。业务逻辑无处不在。实践中,这就是 Conway's Law 的一个例子。 设计一个系统的任何组织(广义上)都会产生这样一种设计,其结构是组织交流结构的复制。 ——Melvyn Conway, 1967 Melvyn Conway 的意识是,像下图所展示的,设计一个系统时,将人员划分为 UI 团队,中间件团队,DBA 团队,那么相应地,软件系统也就会自然地被划分为 UI 界面,中间件系统,数据库。 图 2 实践中的 Conway's Law 微服务(microservice )的划分方法不同,它倾向围绕业务功能的组织来分割服务。这些服务实现商业领域的软件,包括用户界面,持久化存储,任何的外部协作。因此,团队是跨职能的(cross-functional),包含开发过程所要求的所有技能:用户体验(user-experience)、数据库(database)和项目管理(project management)。 图 3 通过团队边界强调服务边界 www.comparethemarket.com就是采用这种组织形式。跨职能的团队同时负责构建和运营每个产品,每个产品被分割成许多单个的服务,这些服务通过消息总线(Message Bus)通信。 大型的整体应用程序(monolithic applications)也可以按照业务功能进行模块化(modularized),尽管这样情况不常见。当然,我们可以敦促一个构建整体应用程序(monolithic application )的大型团队,按业务线来分割自己。我们已经看到的主要问题是,这种组件形式会导致很多的依赖。如果整体应用程序(monolithic applications)跨越很多模块边界(modular boundaries ),那么对于团队的每个成员短期内修复它们是很困难的。此外,我们发现,模块化需要大量的强制规范。服务组件所要求的必需的更明确的分离使得保持团队边界清晰更加容易。 产品不是项目 大部分的软件开发者都使用这样的项目模式:至力于提供一些被认为是完整的软件。交付一个他们认为完成的软件。软件移交给运维组织,然后,解散构建软件的团队。 微服务(Microservice )的支持者认为这种做法是不可取的,并提议团队应该负责产品的整个生命周期。Amazon 理念是“你构建,你运维(you build, you run it)”,要求开发团队对软件产品的整个生命周期负责。这要求开发者每天都关注他们的软件运行如何,增加更用户的联系,同时承担一些售后支持。 产品的理念,跟业务能力联系起来。不是着眼于完成一套功能的软件,而是有一个持续的关系,是如何能够帮助软件及其用户提升业务能力。 为什么相同的方法不能用在整体应用程序(monolithic applications),但更小的服务粒度能够使创建服务的开发者与使用者之间的个人联系更容易。 强化终端及弱化通道 当构建不同的进程间通信机制的时候,我们发现有许多的产品和方法能够把更加有效方法强加入的通信机制中。比如企业服务总线(ESB),这样的产品提供更有效的方式改进通信过程中的路由、编码、传输、以及业务处理规则。 微服务倾向于做如下的选择:强化终端及弱化通道。微服务的应用致力松耦合和高内聚:采用单独的业务逻辑,表现的更像经典Unix意义上的过滤器一样,接受请求、处理业务逻辑、返回响应。它们更喜欢简单的REST风格,而不是复杂的协议,如WS或者BPEL或者集中式框架。 有两种协议最经常被使用到:包含资源API的HTTP的请求-响应和轻量级消息通信协议。最为重要的建议为: 善于利用网络,而不是限制(Be of the web, not behind the web)。 ——Ian Robinson 微服务团队采用这样的原则和规范:基于互联网(广义上,包含Unix系统)构建系统。这样经常使用的资源几乎不用什么的代价就可以被开发者或者运行商缓存。 第二种做法是通过轻量级消息总线来发布消息。这种的通信协议非常的单一(单一到只负责消息路由),像RabbitMQ或者ZeroMQ这样的简单的实现甚至像可靠的异步机制都没提供,以至于需要依赖产生或者消费消息的终端或者服务来处理这类问题。 在整体工风格中,组件在进程内执行,进程间的消息通信通常通过调用方法或者回调函数。从整体式风格到微服务框架最大的问题在于通信方式的变更。从内存内部原始的调用变成远程调用,产生的大量的不可靠通信。因此,你需要把粗粒度的方法成更加细粒度的通信。 分散治理 集中治理的一种好处是在单一平台上进行标准化。经验表明这种趋势的好处在缩小,因为并不是所有的问题都相同,而且解决方案并不是万能的。我们更加倾向于采用适当的工具解决适当的问题,整体式的应用在一定程度上比多语言环境更有优势,但也适合所有的情况。 把整体式框架中的组件,拆分成不同的服务,我们在构建它们时有更多的选择。你想用Node.js去开发报表页面吗?做吧。用C++来构建时时性要求高的组件?很好。你想以在不同类型的数据库中切换,来提高组件的读取性能?我们现在有技术手段来实现它了。 当然,你是可以做更多的选择,但也不意味的你就可以这样做,因为你的系统使用这种方式进行侵害意味着你已经有的决定。 采用微服务的团队更喜欢不同的标准。他们不会把这些标准写在纸上,而是喜欢这样的思想:开发有用的工具来解决开发者遇到的相似的问题。这些工具通常从实现中成长起来,并进行的广泛范围内分享,当然,它们有时,并不一定,会采用开源模式。现在开源的做法也变得越来越普遍,git或者github成为了它们事实上的版本控制系统。 Netfix就是这样的一个组织,它是非常好的一个例子。分享有用的、尤其是经过实践的代码库激励着其它的开发着也使用相似的方式来解决相似的问题,当然,也保留着根据需要使用不同的方法的权力。共享库更关注于数据存储、进程内通信以及我们接下来做讨论到的自动化等这些问题上。 微服务社区中,开销问题特别引人注意。这并不是说,社区不认为服务交互的价值。相反,正是因为发现到它的价值。这使得他们在寻找各种方法来解决它们。如Tolearant Reader和Consumer-Driven Contracts这样的设计模式就经常被微服务使用。这些模式解决了独立服务在交互过程中的消耗问题。使用Consumer-Driven Contracts增加了你的信心,并实现了快速的反馈机制。事实上,我们知道澳大利亚的一个团队致力使用Consumer-Drvien Contracts开发新的服务。他们使用简单的工程,帮助他们定义服务的接口。使得在新服务的代码开始编写之前,这些接口就成为自动化构建的一个部分。构建出来的服务,只需要指出这些接口适用的范围,一个优雅的方法避免了新软件中的'YAGNI '困境。这些技术和工具在使用过程中完善,通过减少服务间的耦合,限制了集中式管理的需求。 也许分散治理普及于亚马逊“编译它,运维它”的理念。团队为他们开发的软件负全部责任,也包含7*24小时的运行。全责任的方式并不常见,但是我们确实发现越来越多的公司在他们的团队中所推广。Netfix是另外一个接受这种理念的组件。每天凌晨3点被闹钟吵醒,因为你非常的关注写的代码质量。这在传统的集中式治理中这是一样多么不思议的事情呀。 分散数据管理 对数据的分散管理有多种不同的表现形式。最为抽象层次,它意味着不同系统中的通用概念是不同的。这带来的觉问题是大型的跨系统整合时,用户使用不同的售后支持将得到不同的促销信息。这种情况叫做并没有给用户显示所有的促销手段。不同的语法确实存在相同的词义或者(更差)相同的词义。 应用之间这个问题很普遍,但应用内部这个问题也存在,特别是当应用拆分成不同的组件时。对待这个问题非常有用的方式为Bounded Context的领域驱动设计。DDD把复杂的领域拆分成不同上下文边界以及它们之间的关系。这样的过程对于整体架构和微服务框架都很有用,但是服务间存在着明显的关系,帮助我们对上下文边界进行区分,同时也像我们在业务功能中谈到的,强行拆分。 当对概念模式下决心进行分散管理时,微服务也决定着分散数据管理。当整体式的应用使用单一逻辑数据库对数据持久化时,企业通常选择在应用的范围内使用一个数据库,这些决定也受厂商的商业权限模式驱动。微服务让每个服务管理自己的数据库:无论是相同数据库的不同实例,或者是不同的数据库系统。这种方法叫Polyglot Persistence。你可以把这种方法用在整体架构中,但是它更常见于微服务架构中。 图 4 Polyglot Persistence 微服务音分散数据现任意味着管理数据更新。处理数据更新的常用方法是使用事务来保证不同的资源修改数据库的一致性。这种方法通常在整体架构中使用。 使用事务是因为它能够帮助处理一至性问题,但对时间的消耗是严重的,这给跨服务操作带来难题。分布式事务非常难以实施,因此微服务架构强调服务间事务的协调,并清楚的认识一致性只能是最终一致性以及通过补偿运算处理问题。 选择处理不一致问题对于开发团队来说是新的挑战,但是也是一个常见的业务实践模式。通常业务上允许一定的不一致以满足快速响应的需求,但同时也采用一些恢复的进程来处理这种错误。当业务上处理强一致性消耗比处理错误的消耗少时,这种付出是值的的。 基础设施自动化 基础设施自动化技术在过去几年中得到了长足的发展:云计算,特别是AWS的发展,减少了构建、发布、运维微服务的复杂性。 许多使用微服务架构的产品或者系统,它们的团队拥有丰富的持集部署以及它的前任持续集成的经验。团队使用这种方式构建软件致使更广泛的依赖基础设施自动化技术。下图说明这种构建的流程: 图 5 基本的构建流程 尽管这不是介绍自动部署的文章,但我们也打算介绍一下它的主要特征。我们希望我们的软件应该这样方便的工作,因此我们需要更多的自动化测试。流程中工作的软件改进意味着我们能自动的部署到各种新的环境中。 整体风格的应用相当开心的在各种环境中构建、测试、发布。事实证明,一旦你打算投资一条整体架构应用自动化的的生产线,那么你会发现发布更多的应用似乎非不那么的可怕。记住,CD(持续部署)的一个目标在于让发布变得无趣,因此无论是一个还是三个应用,它都一样的无趣。 另一个方面,我们发现使用微服务的团队更加依赖于基础设施的自动化。相比之下,在整体架构也微服务架构中,尽管发布的场景不同,但发布工作的无趣并没有多大的区别。 图 6 模块化部署的区别 容错性设计 使用服务作为组件的一个结果在于应用需要有能容忍服务的故障的设计。任务服务可能因为供应商的不可靠而故障,客户端需要尽可能的优化这种场景的响应。跟整体构架相比,这是一个缺点,因为它带来的额外的复杂性。这将让微服务团队时刻的想到服务故障的情况下用户的体验。Netflix 的Simian Army可以为每个应用的服务及数据中心提供日常故障检测和恢复。 这种产品中的自动化测试可以让大部分的运维团队正常的上下班。这并不意味着整体构架的应用没有这么精巧的监控配置,只是在我们的经验中它并不常见。 由于服务可以随时故障,快速故障检测,乃至,自动恢复变更非常重要。微服务应用把实时的监控放在应用的各个阶段中,检测构架元素(每秒数据库的接收的请求数)和业务相关的指标(把分钟接收的定单数)。监控系统可以提供一种早期故障告警系统,让开发团队跟进并调查。 对于微服务框架来说,这相当重要,因为微服务相互的通信可能导致紧急意外行为。许多专家车称赞这种紧急事件的价值,但事实是这种紧急行为有时是灾难。监控是至关重要的,它能快速发现这种紧急不良行为,让我们迅速修复它。 整体架构,跟微服务一样,在构建时是通明的,实情上,它们就是这样子的。它们不同之处在于,你需要清楚的认识到不同进程间运行的服务是不相关的。库对于同一进程是透明的,也因此不那么重要了。 微服务团队期望清楚的监控和记录每个服务的配置,比如使用仪表盘显示上/下线状态、各种运维和业务相关的指标。对断路器(circuit breaker)状态、目前的吞吐量和时延细节,我们也会经常遇到。 设计改进 微服务实践者,通常有不断改进设计的背景,他们把服务分解成进一步的工具。这些工具可以让应用开发者在不改变速度情况下,控制都他们的应用的需求变更。变更控制不意味首减少变更,而是使用适当的方式和工具,让它更加频繁,至少,很好让它变得可控。 不论如何,当你试图软件系统拆分成组件时,你将面临着如何拆分的问题。那么我们的决定拆分我们应用的原则是什么呢?首要的因素,组件可以被独立替换和更新的,这意味着我们寻找的关键在于,我们要想象着重写一个组件而不影响它们之前的协作关系。事实上,许多的微服务小组给它进一步的预期:服务应该能够报废的,而不是要长久的发展的。 Guardian网站就是这方面的一个优秀的例子,它初期被设计和构建成一个整体架构,但它已经向微服务的发展了。整体构架仍然是它网站的核心,但是他们使用微服务来增加那些使用整体架构API的新特性。这种方法增加这些临时的特性非常方便,比如运动新闻的特稿。这样站点的一个部分可以使用快速的开发语言迅速整合起来,当它过时后可以一次性移除。我们发现一家金融机构用相似的方法增加新的市场营销活动,数周或者几个月后把它撤销。 可代替是模块化开发中的一个特例,它是用模块来应对需要变更的。你希望让变更是相同模块,相同周期中进行变化而已。系统的某些很小做变更部分,也应该放在不同的服务中,这样它们更容易让它们消亡。如果你发现两个服务一直重复的变更时,这就是一个要合并它们的信号了。 把组件改成服务,增加了细化发布计划的一个机会。整体构架的任务变更需要整个应用的完整的构建和发布。然而,使用微服务,你只需要发布你要修改的服务就可以了。这将简化和加速你的发布周期。缺点是你需要为一个变更服务发布可能中断用户的体验而担心。传统的集成方法是使用版本来处理这些问题,但是微服务版本仅是最后的通告手段。我们需要在设计服务时尽可能的容忍供应商的变更,以避免提供多个版本。 微服务是未来吗? 我们写这篇文章的主要目的在于解释微服务的主要思想和原则。但是发时间做这事的时候,我们清醒的认识到微服务构架风格是一个非常重要的想法:一个值得企业应用中认真考虑的东西。我们最近使用这种风格构建了几个系统,认识那些也使用和喜欢这种方法的爱好者。 我们认识的使用这种方式的先行者,包含亚马逊、Netflix、The Guardian、The UK Government Digital Service、realestate.com.au、Forward和comparethemarket.com。2013看的巡回会议充满了向正在想成为微服务一分子的公司,包含Travis CI。此外,大量的组件正在从事我们认为是微服务的事,只是没有使用微服务的名字而已。(通常,它们被打上SOA的标签,尽管,我们认为SOA有许多不同的地方。) 尽管有这些积极的经验,然后,我们也不急于确认微服务是未来软件架构方向。至今为止,我们的经验与整体风格的应该中相比出来的是有优势的,但是我们意识知这样的事实,我们并没有足够的时间来证明我们的论证。 你所使用的架构通常是你开发出来后,使用的几年的实际成果。我们看到这些工程是在一个优秀的团队,带着对模块化的强烈追求,使用在过去几年中已经衰退的整体架构构建出来的。许多人相信,这种衰退不太可能与微服务有关,因为服务边界是清晰的并且很难再完善的。然而,当我们还没看到足够多的系统运行足够长时间时,我们不能肯定微服务构架是成熟的。 当然,还有原因就是,有人期望微服务构架不够成熟。在组件化方面的任何努力,其成功都依赖于软件如何拆分成适合的组件。指出组件化的准确边界应该在那,这是非常困难的。改良设计要承认边界的权益困境和因此带来的易于重构的重要性。但是当你的组件是被远程通信的服务时,重构比进程内的库又要困难的多。服务边界上的代码迁移是困难的,任务接口的变更需要参与者的共同协作,向后兼容的层次需要被增加,测试也变更更加复杂。 另一个问题在于,如果组件并没有清晰的划分,你的工作的复杂性将从组件内部转向组件间的关系。做这事不仅要围绕着复杂,它也要面对着不清晰和更难控制的地方。很容易想到,当你在一个小的、简单的组件内找东西,总比在没有关系的混乱的服务间要容易。 最后,团队技能也是重要的因素。新的技术倾向于被掌握更多的技能的团队使用。但是掌握多技能的团队中使用的技巧在较少技能的团队中并不是必需的。我们发现大量的少技能的团队构建混乱的整合构架,但是它要发时间去证明使用微服务在这种情况下会发生什么。一个糟糕的团队通常开发糟糕的系统:很难说,微服务在这种情况下是否能帮助它们,还是破坏它们。 一个理性的争议在于,我们听说,你不应该从微服务构架开始做。最好从整体构架开发,做模块化开发,然后当整体构架出现问题是再把模块化拆分成服务。(尽管这种建议不是好主意,因为一个好的进程内接口并不是一个好的服务接口。) 因此我们持这种谨慎的乐观。到目前为止,我们还没有足够认识,关于微构架能否被大范围的推广。我们不能肯定的说,我们要终结什么,但是软件开发的挑战在于你只能在不完整的信息中决定你目前要处理的问题。 其它 微服务系统多大? 尽管“微服务”一词在架构风格中越来越流行,它的名字很不辛让人关注它的服务大小,以及对“微”这个组成的争议。在我们与微服务实践者的谈话中,我们发现了服务的大小范围。被报道的最大团队遵循亚马逊Tow Pizaa团队理念(比如,一个团队吃两个比萨就可以了。),这意味着不超过20号(一打)人。我们发现最小配置是半打的团队支撑起一打的服务。 这也引发这样的考虑:规模为一个服务一打人到一个服务一个人的团队打上微服务的标签。此刻我们认为,它们是一样的,但是随着对这种风格的深入研究,也存在我们改变我们的想法的可能。 微服务与SOA 当前我们谈到微服务时,通常会问,这是不是我们20年前讨论的面向服务架构(SOA)。这是一个很好的观点,因为微服务风格也SOA所提倡的一些优势非常相似。尽管如此,问题在于SOA意味的太多不同的东西了,因此通常时候我们谈的所谓“SOA”时,它与我们谈论的风格不一至,因为它通常是指在整体风格应用中的ESB。 此外,我们发现面向服务的风格是这么的拙劣:从试图使用ESB隐藏复杂性, 到使用多年才认识到发费数百美元却没产生任务价值这样的失败,到集中治理模式抑制变更。而且这些问题往往很难发现。 可以肯定的时,微服务社区中使用的许多的技术都开发者是从大型机构的整合服务经验中发展来的。Tolerant Reader模式就是这样的一个例子。由于互联网的发展,利用简单的协议这种方法,让它从这些经验传达的出来。这是从已经很复杂的集中式标准中的一种反模式,坦白的说,真让人惊叹。(无论何时,当你需要用一个服务来管理你的所有的服务,你就知道这很麻烦。) SOA的这种常见行为让微服务的提倡者拒绝打上SOA的标签,尽管有人认为微服务是从SOA中发展而来的,或许面向服务是对的。无论如何,事实上SOA表达这么多的含义,它给一个团队清醒的认识到这种构架风格就已经值的了。 多语言,多选择 JVM做为一个平台,它的增长就是一个平台中运行多语言的最大的例子。过去二十年中,它通常做为更高层次语言的壳,以达到更高层次的抽象。比如,研究它的内部结构,、使用低级的语言写更高效的代码。尽管如此,许多整体风格并不需要这种层次的性能优化或者在语法及高层次上的抽象,这很常见(让我们很失望)。此外整体构架通常意味着使用单一的语言,这也限制着使用技术的数量。 实践标准和强制标准 它有点尴尬,微服务团队倾向于避免这种通常由企业架构队伍定制的僵硬的强制标准,但是它们却非常乐于甚至推广这些开放的标准,如HTTP、ATOM、其它微规范。 关键的不同在这些标准是怎么开发出来的,以及它们是怎么被推广的。标准被一些组件管理,如IETF认证标准,仅当它们在互联网上有几个在用的实现,通常源自于开源工程的成功应用。 这些标准单独分离出来,与那种在企业中通常有没有什么编码经验的或者没有什么影响力的厂商标准进行区别。 让做对事更容易 一方面,我们发现在持续发布、部署越来越多的使用自动化,是很多有用的工具开发出来帮助开发者和运营商的努力结果。为打包、代码管理、支撑服务的工具,或者增加标准监控的记录的工具,现在都非常常见了。网络中最好的,可能就是Netflix's的开源工具,但是包含Dropwizard在内的其它工具也被广泛的使用着。 断路器(circuit breaker)和产品中现有的代码 断路器(circuit breaker)出现在《Realease It!》一书中,与Bulkhead和Timeout这样的模式放在一起。实施起来,这些模式用于构建通信应用时相当的重要。Netflix的博客在解释它们的应用时,做了大量的工作。 同步是有害的 任务时候,你在服务间的调用使用同步的方法,都会遇到宕机时间的乘积效应。简单的说,你的系统宕机时间是你系统的单独组件的宕机时间的乘积。你面临的选择使用异步或者管理宕机时间。在www.guardian.co.uk中,它们在新平台中使用一种简单的规则来实现它:在Netflix中每次用户请求的同步调用,他们重新设计的平台API都会把它构建成异步的API来执行。