关于软件设计:一文探究系统分析与设计的逻辑性

一、系统分析与设计的逻辑性框架在日常的工作中,「软件剖析」与「软件设计」这样的词眼常常听到,然而要真正了解「软件剖析」和「软件设计」的实质是比拟难的,它依赖极强的工作教训,又加上软件剖析与设计没有规范的程式化步骤,导致不同的人有本人不同的办法,也就造成了很多人认为软件剖析与设计是十分「空洞」,还不如写具体的代码切实,而大部分的人写的是业务型代码,被嘲弄写CRUD的代码没成就感。 软件剖析与设计如其它行业一样,具备很强的逻辑性,没有逻辑性撑持,很难做好事。比方写作,有「谋篇布局」、「起承转合」、「遣词造句」等这些「准则」和「办法」,没有洞悉到这些底层逻辑,一个高手写的文章和一个普通人写的文章,成果是天差地别。因而,首先要讲清楚的是软件剖析与设计的「逻辑性」到底是什么,下图是软件剖析与设计的逻辑全景图。 1.1 办法1.1.1 分析阶段软件剖析与设计并没有那么神秘,实质来讲还是为了解决事实的问题,和「医生看病」、「工人修车」、「厨师做菜」一样的,都须要办法作为领导,否则没有任何脉络,只能抓瞎。办法是具备普适性,只是不同的行业有各自的个性,具体落地上有差别。 既然是要解决问题,那么总得晓得问题是什么吧,就好比医生看病,做各种查看、化验,都是为了全面地理解疾病,所以第一步是须要定义好问题,然而当下很多人都疏忽了这一步,间接上来想我要用哪种中间件、哪个框架,连问题都没有定义好,间接想实现无疑是轻重倒置。 问题是现实与实现差距的矛盾,事实是不满足现实的诉求,因而,首先须要理解用户的诉求是什么,想解决怎么的问题,这也即为是需要。需要剖析最大的挑战是什么是真正想要的,就好比一个病人说了一大堆的症状,他所说的症状体现与书本上的形容有时是有出入的,定义出真正的需要至关重要,接下来就要思考通过怎么的办法去梳理分明用户需要。 用例是表白用户应用零碎实现某些指标文本化的情节形容,它强调的是用户指标、观点。咱们应该从用户指标的角度登程思考,也有的人称之为利益相关者的关注点,情理很简略,咱们做进去的软件是为了服务于用户的,不是用户想要的,就算用了多高科技的的技术也是失败的。 用例只是一个概括的形容,因而还须要细化,一个用例包含一个或多个场景,场景是参与者与零碎之间的流动和交互,比方用户下单,有下单胜利,有下单失败两个场景。因而到这里还只是分析阶段,剖析的目标是理解现状和指标,以及零碎因素组成,它不关怀如何实现做、如何实现。剖析不仅限于软件行业,其它的行业也是如此,只是在剖析过程的程式化步骤、办法不一样而已。 1.1.2 设计阶段当咱们分明地晓得要做什么之后,接下来要思考如何去实现,实现的路径有很多种,如同一百个厨师做同样的菜,做进去的成果不一样。设计阶段有两点须要思考的:一是如何将性能细化实现;另一点是如何更好地实现。第一点不论用什么办法都能够实现,难的是第二点,更好地实现是须要遵循一些章法,也须要评判体系,要不然你怎么晓得好与不好呢,常见的定量评判指标有:老本、性能、可靠性、效率等,还有一类是定性的评判指标有:开放性、体验性等。 度量的指标绝对容易,就像医生看病,看疗效、看老本,软件设计也是一样,归根到底是「多快好省」。然而软件设计的章法就简单得多,它具备很强的艺术性,正所谓「文无第一,武无第二」,比武肯定能够比出个高下,谁打赢了就是胜利一方,然而比文就难了,不能说你写的就肯定比我写的好,只是不同人的爱好不一样而已,当然这里指的旗鼓相当地比照,不同档次的比照一眼还是能看得出来的。 在设计阶段最为要害的是定义出「要害的技术问题」,个别分类两类:一是站在用户视角的设计,它重点考量的是「便捷性」和「易了解」;另一个是站在零碎层面,重点考量的是「复用性」、「扩展性」和「稳定性」。贴近用户的设计,让用户用起码的了解就能应用它,用户毋庸感知底层简单的设计,外围是答复用户最奢侈的原始诉求。零碎层面的设计要灵便,多用组合正交设计晋升零碎的复用性和扩展性,在具体方案设计中要思考到稳定性因素,比方写日志会带来性能问题,这个在方案设计中就要思考到。洞察到关键技术问题,并非久而久之就能练就成,须要在工作中大量地实际,总结经验,放弃技术的敏感度才行,在第二节中通过理论的案例不便大家加深了解。 1.2 工具有了办法,接下来要有工具来帮咱们更好地做事,与办法对应的,咱们软件设计的工具是UML,接下来介绍UML中罕用的图。 1.2.1 流动图软件从实质上是在模仿事实业务运行的过程,是由一个个交互流动组成的,因而,在分析阶段须要梳理出业务的流动是怎么的,通过图形的形式记录下来。 流动图要体现出「参与者」、「流动终点」、「流动要害门路」、「流动起点」,比方用户下单,有「浏览」、「加购」、「领取」等流动。通过流动图能够看出业务的生命周期是怎么的,可能抓住业务的要害流程。 1.2.2 用例图用例图是强调用户的指标和观点,是文本化的情节形容,用例从实质上讲并不是图,它是文本,用图形是简化了表达形式,它外围有三点:「参与者」、「要做什么」、「后果是怎么的」。用例图是对流动图的细化,对其中的一个流动定义出要实现怎么的指标。场景又是对用例图的细化,你会发现,从指标到实现,一步一步地细化下来,细化是对扩充对意识的了解,不在意识范畴内,也就不会去做。 1.2.3 程序图将场景通过程序图表达出来,它外围强调的是零碎应该提供怎么的能力,留神程序图与时序图的区别,程序图是人与零碎的交互,它想表白的是零碎应该提供怎么的能力满足用户的诉求,而时序图零碎外部实现,强调的是如何实现这种能力。 其实到程序图这一步,基本上零碎要提供的能力就分明了,当然,这里是晓得零碎要做什么,至于要怎么实现它并不关怀。以上我认为它是分析阶段,把用户想实现的内容分明地定义进去,接下来就是思考怎么实现。 1.2.4 时序图时序图有两种作用:一是表白性能是如何实现的;另一个是看责任调配是否正当。第一点比拟好了解,一个性能实现是由多个不同的对象组合来实现,对象间有交互依赖。第二点是评判对象设计是否正当,如何两个对象频繁交互,是不是能够合并在一起,如何一对象中的操作过多,是不是能够拆解。 1.2.5 类图类图的作用也有两种:一是表白属性和职责;另一个是层次结构。类中的属性和职责是一个统一体,属性体现的是认知能力,职责体现的是行为能力,领有怎么的意识,就会产生怎么的行为。类不是一个孤零零的个体,它与其它的类之间有依赖、协作关系,因而,类图中体现继承、依赖、泛化、蕴含等关系。 1.3 准则篇软件设计准则车载斗量,简化下来就三点:「复用」、「变动」、「认知复杂度」,好的设计处处体现设计准则,把这些准则刻画到骨子里,而不是刻意体现,如同「没有规矩不成方圆」一样,重点是要了解为什么要这些准则,从实质上讲是为了软件可能「多快好省」地实现。 利润 = 支出 - 老本,从这个公式中,很显著咱们想要实现利润最大化,怎么办呢,有两个办法:一是支出变多,最好地形式是实现规模化;二是老本升高,不须要或者很少投入老本。从这两点中,引申出「复用」和「变动」两个准则,复用是不投入或者少投入实现性能,相比从头做是不是要节省成本呢,咱们的产品不可能变化无穷的,那么变动是在劫难逃的,如果能撑持灵便地扩大是不是也能节省成本呢。 1.3.1 复用从下面的剖析看,「复用性」的重要水平显而易见,比起烟囱式开发,复用的老本要低得多,所以产生出了xx平台、xx中台,它们的实质目标还是为了复用,缩小反复开发成本。实现复用的伎俩有很多,复用的水平也不一样,这个就要靠平时的积攒,就像医生积攒「药」和「药方」一样,这些都是咱们解决问题的「工具」。先要有复用的思维,否则只有工具也是无用的,不晓得要怎么用、在哪里用。 不同的场景,复用采纳的设计办法是不一样的,举几个例子不便大家了解。 齐全复用最简略的复用是100%的复用,比方加法计算操作,它必定是100%复用的,只用传输不同的数字进去,就能够计算出后果。个别齐全复用的是工具型的能力,它与具体的业务语义无关。 配置化复用这一类的复用水平靠近100%,只用配置一些与业务相干的具体的参数即可,比方对账场景,配置两个不同的数据源表,再配置对账规定即可实现对账。这一类场景实用于业务比拟固定,流程是通用的,并且差别变动是可枚举的。 局部复用然而,在实现世界中,没有太多像齐全复用的事件,如果变动还不能通过配置化来实现,能够应用「模板办法」或「策略模式」,将变动提早到子类中去实现,这种办法在大家日常工作很常见,也有的应用SPI扩大点实现。这一类复用场景是有明确的「主流程」,只有大量的变动随业务变动,变动也是可枚举的,那么就能够形象出扩大点。 还有一类变动是很难枚举的,不晓得会有怎么的变动,此时最好的办法是通过「事件」解耦,主流程实现之后,发一个事件音讯进去,谁关注就去实现该性能,实质上讲Spring中Aware机制也属于事件的解决办法。 齐全不能复用还有一类齐全不能复用,但要形象出规范的接口,比方罕用的数据库操作,Connection、Statement就是规范的接口,不同的数据库厂商实现具体的数据库操作。不要感觉这种没有意义,它从更高的层面定义了标准,使用者是面向形象应用,能够不关注具体的实现是怎么的。 1.3.2 变动复用和变动是一起进去的,软件惟一不变的是变动,怎么撑持将来更好地扩大是咱们要思考的,如果一个性能千年不变,怎么简略就怎么实现,而如果有变动的话,那就须要好好地设计,用起码的老本去撑持将来的变动。比拟难的是要洞察出什么在变动,这个还真不是那么好想到的,须要有行业教训积攒,看多了、实际多了,会发现外面的一些门道。 举一个应答变动的例子,税务在计税时,不同的业务计税规定不一样,有的金本位要计税,有的不须要计税,有的非金本位要计税,有的不须要。如果放在一个大的扩大点中实现,那么这就是典型的面向过程的设计思维,咱们形象出了「计税表达式」这个实体来应答变动,「计税项」要不要计税、计税口径是怎么的,新业务接入通过「配置化」来解决。 1.3.3 意识复杂度意识是分档次的,最高层越简略,最低层越简单,对于使用者来讲,他心愿看到的是简略的内容,如果太简单,很难上手,比方命令行式的操作系统和桌面式的操作系统,显著桌面式的操作系统更受公众的欢送,这也是微软在目前仍然在操作系统市场占用份额上还是大头的起因。软件分层的目标不仅是让关注点拆散,还有另外一个目标是升高意识复杂度,从简略到简单,这和咱们软件剖析一样的,从粗到细。 类的设计也是一样的,举一个例子,税务在开发票时,开票这个模型构造须要调用者感知吗,必定不须要,因为发票中有很多的畛域概念,如开票主体、发票行等,用户的目标就是开一张发票,他只用通知你他晓得的信息,不关怀你外部要怎么实现,基于这个思考,咱们形象出了「开票申请」这个实体进去,它实质是贴近业务场景的实体,所蕴含的信息也是无限的,极大地升高了认知复杂度。 二、系统分析与设计的2个案例2.1 日志框架2.1.1 日志框架剖析打印日志在咱们日常工作简直是人人都会接触到,日志的核心作用是记录要害无效的信息,帮忙咱们疾速地排查、定位问题,否则没有日志信息两眼一抓瞎。依据咱们的教训,心愿日志中蕴含工夫、类、办法、代码行、要害日志信息,用了这些信息就能不便咱们排查问题。 依据下面的剖析,咱们很快能够画出日志的概念模型,如下图所示。从实质上讲,咱们是将日志信息存储到指定的中央,如存储文件中,输入到管制台上,另外还有日志存储的格局能够有多种,比方一般的格局,还有XML、HTML的格局等。 从概念模型上看,设计一个日志框架并不简单,但在设计阶段中,还须要开掘更多的信息,咱们输出的信息更多,设计时思考的因素也就越全,更能满足用户的诉求。 2.1.1 日志框架设计2.1.1.1 贴近用户的设计 站在用户的视角,他关注的是信息以怎么的格局存储在哪里,因而,有两个概念用户是要关注的,一个是「存储目的地」,另一个是「存储款式」,这两个是能够依据用户的爱好配置的,将这两个概念形象下,「存储目的地」形象成「Appender」,「存储款式」形象成「LayoutPattern」。除了配置外,用户在应用时须要一个接口类,将其形象成「Logger」门面类,只用简略的调用日志打印接口即可。 2.1.1.1 零碎视角的设计 站在零碎视角上,用户在日志打印时,他关注的是日志信息,如工夫、类名、办法名等信息他不会显示去写,因而还须要形象一个概念进去表白日志信息的概念,形象成「LogRecord」,这样概念类图如下图所示。 依照这个设计,很快能够设计出一个繁难的日志框架,代码构造如下所示。 Appender类如下所示,它定义的是一个模板办法,先调用LayoutPattern获取格式化的日志数据,而后再输入到指标存储上,Appender和LayoutPattern是能够独立变动的,同时将写操作提早到子类中实现。 再细细想一下,日志自身是为了不便排查问题,但额定日志的存储是有性能开销的,这个在设计时就要着重思考了。如何缩小写日志带来的性能开销呢,从三个方面思考: 大文件变小文件:关上一个1G的文件和关上1M的文件必定是不一样的,简单问题的治理也是同样的思路,拆分成小问题解决,因而在写文件时能够依照「工夫」、「容量大小」切分成小的文件。内存映射写文件:传统的IO写数据,操作系统须要从用户态切换到内核态,性能开销会很大,能够应用内存映射的形式写文件,晋升IO性能。同步变异步写文件:同步写文件是须要等文件写好了后再往下执行业务代码,而异步就不一样,它将日志记录存储在一个队列中,开户另一个线程缓缓写到文件中,不阻塞业务逻辑执行。通过下面的剖析,一个简略的日志框架随着对它的了解加深,设计方案也在变动,外围是要能看到要害的技术问题有哪些,能提供哪些增量价值或差异化的价值。 ...

September 12, 2023 · 1 min · jiezi

关于软件设计:聊聊前端-UI-组件组件体系

本文是文章系列「聊聊前端 UI 组件」的第三篇。 在本系列的上篇文章《聊聊前端 UI 组件:组件特色》中,通过从关注点拆散的角度进行前端 UI 组件的形成剖析,并以较为形象的视角对 UI 组件分门别类,以及形容了让组件间能够体现复用的继承关系,从而建设出前端 UI 组件的特色模型。 本文将以上篇文章中所得出的特色模型为根底,探讨下如何设计并建设一个前端 UI 组件体系。 在做组件体系设计的时候,最重要的一点就是——要真真正正地想着把 UI 组件弄成可复用的,就像制造业生产时所用的物料一样——结构可替换的 UI 组件。 因为 UI 组件形成元素的易变性对组件体系的设计有着很大的影响,为了不便查看,将上篇文章中的易变性及其影响因素的表格搬过去: 形成 易变性影响因素构造视觉构造不易变内容构造、布局类款式 内容构造较易变生成 HTML 的 JS 库/框架的源码、平台限定的视图构造描述语言体现主题格调很易变GUI 设计人员的审美和想法、非布局类款式、图标与图片行为交互逻辑不易变交互设计人员的想法 业务逻辑很易变业务规定组件架构表格中列出的 UI 组件形成元素都能够作为独自的组件存在。如果把 UI 组件看作是「最终产品」的话,那么 UI 组件形成元素所对应的那些组件就是「两头产品」。 在软件工程中,「组件(component)」个别是指软件的可复用块,好比制造业所应用的「构件」。这是一个比拟宽泛的概念,它能够是软件包,能够是 web 服务,也能够是模块等。 但在前端眼里,「组件」通常是指页面上的视图单元,即「UI 组件」。能够说,「UI 组件」是「组件」的子集。 欧雷《聊聊前端 UI 组件:外围概念》 鉴于上述起因,这里须要特地阐明下:上文所说的「作为独自的组件存在」中的「组件」是指「软件的可复用块」,而不是「UI 组件」。 格调组件在上篇文章中提到了「虚构组件」的概念—— 在持续往下之前,先引入一个「虚构组件」的概念。正如它的名字所示,是一个虚构的,理论不存在的,只是概念上的组件。它是几个主题格调属性的汇合。 欧雷《聊聊前端 UI 组件:组件特色》 与之类似,「格调组件」也是一些主题格调属性的汇合,大略包含: 如果要用代码来体现的话,能够借助 CSS 预处理器中的变量。这里用 Sass 来举例: // 主题色$sc--primary: #cce5ff !default;$sc--secondary: #e2e3e5 !default;$sc--info: #d1ecf1 !default;$sc--success: #d4edda !default;$sc--warning: #fff3cd !default;$sc--danger: #f8d7da !default;// 文本色$sc--text-primary: #303133 !default;$sc--text-secondary: #696c71 !default;$sc--text-heading: #2c405a !default;$sc--text-regular: #333 !default;$sc--text-placeholder: #c0c4cc !default;// 字体尺寸$sc--font-size: 14px !default;$sc--font-size-lg: 16px !default;$sc--font-size-sm: 12px !default;// 字体粗细$sc--font-weight-light: 300 !default;$sc--font-weight-normal: 400 !default;$sc--font-weight-bold: 700 !default;// 边框粗细$sc--border-width: 1px !default;// 边框色彩$sc--border-color: #dcdfe6 !default;// 边框圆角$sc--border-radius: 4px !default;$sc--border-radius-lg: 6px !default;$sc--border-radius-sm: 2px !default;格调组件与体现复用的继承密切相关—— ...

September 12, 2023 · 3 min · jiezi

关于软件设计:聊聊前端-UI-组件组件特征

本文是文章系列「聊聊前端 UI 组件」的第二篇,内容与本系列的上篇文章《聊聊前端 UI 组件:外围概念》有所关联,如果还没看过,倡议去看下。 本文的次要内容是依据特色对前端 UI 组件进行建模,让咱们尽可能充沛地理解它的方方面面,并为如何设计以及建设一个组件体系打下基础。 组件形成从关注点拆散的角度合成 UI 组件,并剖析其各局部的易变性。 形成元素一个残缺的具备性能的 UI 组件的形成,有构造(structure)、体现(presentation)和行为(behavior)这三个方面。为什么强调是「残缺的具备性能的 UI 组件」?是因为它是一个最全的特色汇合,而其余的「UI 组件」可能会短少一些特色,从而使剖析不那么欠缺。 看到「构造」、「体现」与「行为」这三个词,作为一名有教训的前端开发者,很天然地就会想到很久很久之前开始始终提倡的前端开发的关注点拆散——HTML 负责组织页面构造,CSS 负责网页内容的体现款式,JS 则负责用户与网页之间的交互,它们各自扮演着不同且相辅相成的角色。 然而在这里,它们的含意会有所不同—— 经 HTML 组织后的网页内容是「构造」没错,但它仅仅是代码层面的,未被 CSS 所影响的构造。最终的视觉出现,也就是视觉层面的「构造」,应该还包含 CSS 的布局类款式,如定位(positioning)、浮动(float)等。 CSS 中的那些非布局类款式,如色彩、字体、文本、边框、尺寸、留白等类别的款式,以及图标、图片,皆为「体现」。这些个别还被称为「主题格调」或者「皮肤」。 可在 JS 中运行的事件零碎以及进行逻辑解决的函数和对象办法,算是「行为」——这就是 UI 组件的交互逻辑了。当然了,与交互逻辑相交融的业务逻辑,也是「行为」的一部分。 易变性依据形成 UI 组件的每个局部的性质,会影响 UI 组件相应局部的易变性——对于组件复用来说,该局部是绝对稳固的还是绝对不稳固。 GUI 倒退了几十年,人机交互的图形元素及布局形式曾经绝对固定,只有不是呈现像 Google Glass 之类的革命性交互设施,就不会产生重大扭转。 欧雷《我来聊聊面向模板的前端开发》 如上所述,将来到底会呈现什么样的变革性交互方式无从得悉,但我认为,只有是须要用眼去看且用手去操作,交互方式就逃不离这几十年来未有啥改革的模式。因而,UI 组件的视觉构造和交互逻辑是最不易变的,且无论是交互模式还是触发事件都是可枚举的。 如果单纯从最终的 HTML 构造上来看,它也算是不易变的,但在古代前端开发中,HTML 的构造根本是动静生成的,并且强依赖于 React、Vue 这类没有统一标准的 JS 库/框架。另外,还存在着像 WXML、AXML 这类平台限定的视图构造描述语言。因为写法不统一,这就使页面内容构造变得不那么稳固。 一般来说,离用户越近的货色越容易产生扭转。在网站、利用中离用户最近的是 GUI,而在 GUI 中离用户最近的则是主题格调,这是对用户来说最直观的货色。主题格调会随着 GUI 设计人员(通常是设计师)的审美和想法而扭转,所以它是最易变的。 因为业务逻辑是进行业务相干解决的外围所在,如果业务规定变了,相应的代码实现也得跟着扭转,所以业务逻辑也是很易变的。业务逻辑对于一个网站、利用来说是十分必要且重要的,但对 UI 组件来说,它就没那么必要了,更谈不上重要。在前端的 GUI 层面,业务逻辑理当是交互逻辑的延长。 ...

September 11, 2023 · 1 min · jiezi

关于软件设计:聊聊前端-UI-组件核心概念

本文是一个文章系列的第一篇,次要阐明几个基本概念以及所要探讨的指标主体,目标是对立认知上的「上下文」以尽量避免因信息不对称而造成了解阻碍。 这一系列文章是对于前端 UI 组件的,我想通过这个系列静下心来好好聊聊与之相干的内容。 每个名词都是概念,就像一个「数据包」,依据其被「压缩」的信息量,要真正地了解一个词语可能须要大量的常识储备。 基本概念咱们要聊的是「前端 UI 组件」,这个词能够进一步拆分成「前端」、「UI」和「组件」这三个词。所以,要想弄明确「前端 UI 组件」是什么,得先把组成它的三个词搞懂。 UI?GUI?平时在议论一个软件的视觉方面的问题时,总会用到「UI」这个英文缩写,有时也会说「GUI」。尽管它们是不同的含意,但在大多数状况下,咱们是将这两个词划等号了。在这里,我试图帮大家将这两个词辨别开,就算用法仍然不变,至多可能意识到它们的区别。 人与机器间,更确切地说是与人造零碎间,是如何进行互动的以及如何更好地交互,是人们始终在摸索的——也就是「人机交互」这个词所代表的。「人造零碎」能够是各种各样的物理机器,也能够是计算机系统和软件。 「交互」的实质就是人/物体间的信息替换,即信息从一个人/物体输入,输出到另一个人/物体中。因而,两个人/物体、输入形式、输出形式是交互的几个基本要素。 在人机交互的场景中,进行互动的对象是人和人造零碎。人通过敲击、触摸、谈话等形式输入信息,通过视觉、听觉、触觉等形式输出信息;人造零碎则通过文本、图形、声音等形式输入信息,通过将人以各种形式产生的信息转化为电流的形式输出信息。 在人机交互中起到信息替换作用的那块空间,叫做「人机交互界面」,也叫「用户界面」。「UI」就是「用户界面」所对应的英文单词「User Interface」的缩写。 不同的交互方式和档次产生了不同的「用户界面」,如:基于文本的「命令行界面(Command-line Interface)」、基于图形的「图形用户界面(Graphical User Interface)」、基于语音的「自然语言用户界面(Natural-language User Interface)」等等。 其中,「图形用户界面」是目前比拟宽泛应用的,它的英文缩写就是「GUI」。 前端开发所谓的「前端开发」就是利用 web 前端技术进行 GUI 相干的开发工作,专门从事这类工作的人被称为「前端开发者」。 在以前,「前端开发者」是指「页面重构工程师」和「前端开发工程师」;随着业务和技术的倒退,「页面重构工程师」慢慢退出历史舞台,「前端开发者」根本与「前端开发工程师」划等号,并且称说变得更精简——「前端工程师」。 在职业产生扭转的同时,作为一个「前端开发者」,作为一名「前端工程师」,企业和业界的冀望变高了,所承当的职责变重了——这是一次职业降级,也是一次行业荡涤——适应的人变更强了,不适的人被淘汰了。 时至今日,「前端开发」的含意也不是当初单纯地写写页面做做网站,还涵盖了前端工程相干的 CLI 工具、挪动端和桌面端的客户端利用、服务端中比拟凑近前端的局部等等等等——这俨然是一个「客户端工程师」所做的工作——没错!「前端开发」实质上就是「客户端开发」的一个分支,只不过这点越来越被强化了,并且「客户端开发」越来越趋势对立,能够称之为「泛客户端开发」。 无论工作内容和职业职责怎么变,只有是做这行,所要解决的外围问题是不变的——人与人造零碎之间如何更好地进行互动。 组件?控件?在软件工程中,「组件(component)」个别是指软件的可复用块,好比制造业所应用的「构件」。这是一个比拟宽泛的概念,它能够是软件包,能够是 web 服务,也能够是模块等。 但在前端眼里,「组件」通常是指页面上的视图单元,即「UI 组件」。能够说,「UI 组件」是「组件」的子集。你可能还总会听到「控件(control)」这个词。放轻松,别抓头,它只是「UI 组件」的一个别名而已。 一般的组件通用性很差,也就是说,它根本只能用于某个特定的零碎且不能被替换。有一种组件,它是基于标准化的接口标准开发进去的,能用在任何对接了该接口的零碎,也能被任何合乎该接口标准的组件替换——它就是「可替换组件」,就像制造业所应用的「标准件」。 可替换的 UI 组件是前端 GUI 开发从手工作坊到主动拆卸的关键所在。 相干概念下面通过三个比拟根本的概念论述了「前端 UI 组件」是什么,上面再来说说会对其产生重大影响的几个概念—— 设计体系所谓的「设计体系」,即「Design System」,是与 UI 组件非亲非故的一个概念。能够认为,设计体系是 UI 组件的外观及交互的理论依据,而 UI 组件是设计体系的具体实现。 设计体系的存在是为了辅助像网站、利用等数字产品的设计与开发,它作为惟一的参考而让不同团队的人(如产品经理、设计师和开发人员等)能够一起参加到数字产品的建设当中。 基于设计体系设计并开发进去的产品无论是在观感上还是体验上都可能放弃肯定的一致性,建立产品形象并流传品牌价值。 设计体系所涵盖的内容,包含但不限于设计语言、格调指南、模式库、UI 组件、品牌语言及与之相干的应用阐明文档——设计体系自身不是一个交付物,但它是由一些交付物组成的,并会随着产品、工具和技术等的更新而一直进化。 从下面所列出的设计体系的涵盖内容中能够看出,形成它的元素有无形的,像模式、UI 组件、指南及给设计师和开发人员所应用的工具等;还有一些是有形的,像品牌价值观、合作形式、思维形式和独特信念等。 格调指南和模式库是比拟重要的两个交付物:格调指南着眼于一些图形款式及其用法,如色彩、字体、图片等;模式库则集成了具备性能的 UI 组件及其用法。 ...

September 10, 2023 · 1 min · jiezi

关于软件设计:架构师日记深入理解软件设计模式-京东云技术团队

作者:京东批发 刘慧卿 一 设计模式与编程语言1.1 什么是设计模式设计模式(Design pattern) :由软件开发人员在软件开发中面临常见问题的解决方案,是通过长时间的试验积攒总结进去的,它使设计更加灵便和优雅,复用性更好。从实用的角度来看,它代表了某一类问题的最佳实际。 设计模式到底解决了开发过程中的哪些难题呢,它又是如何来解决的呢? 其外围是:复用和解耦。使不稳固依赖于稳固、具体依赖于形象,以此加强软件设计适应变动的能力。 1.2 什么是编程范式要探讨设计模式和编程语言的关系,还得从编程范式谈起。编程范式一词最早来自 Robert Floyd 在 1979 年图灵奖的颁奖演说,是程序员对待程序的观点,代表了程序设计者认为程序应该如何被构建和执行的认识,与软件建模形式和架构格调有严密关系。 以后支流的编程范式有三种: 1.结构化编程(structured programming) 2.面向对象编程(object-oriented programming) 3.函数式编程(functional programming) 这几种编程范式之间的关系如下: 1.起初是非结构化编程,指令(goto指令)能够轻易跳转,数据能够轻易援用。起初有了结构化编程,人们把 goto 语句去掉了,束缚了指令的方向性,过程之间是单向的,但数据却是能够全局拜访的; 2.起初面向对象编程的时候,人们罗唆将数据与其严密耦合的办法放在一个逻辑边界内,束缚了数据的作用域,靠关系来查找; 3.到函数式编程的时候,人们束缚了数据的可变性,通过一系列函数的组合来形容数据,从源到指标映射规定的编排,两头它是无状态的; 编程范式是形象的,编程语言是具体的。编程范式是编程语言背地的思维,要通过编程语言来体现。C 语言的支流编程范式是结构化编程,而 Java 语言的支流编程范式是面向对象编程,起初 Java8 开始反对 Lambda 表达式,将函数式编程范式的内容交融进来,同时新诞生的语言一开始就反对多范式,比方 Scala,Go 和 Rust 等。 从结构化编程到面向对象编程,再到函数式编程,形象水平越来越高(离图灵机模型越来越远),与畛域问题的间隔越来越近。直观的来讲,就是解决事实问题的效率晋升了,灵活性和执行效率随之有所降落。 设计模式无论用什么语言实现都是能够的,然而因为语言的各自差异化特点,不是每种语言都完满或对立实现各种设计模式。比方Java外面有策略模式,那是因为Java8之前不反对办法传递,不能把一个办法当作参数传给他人,所以有了策略模式。而JavaScript等语言能够间接传函数,就基本没必要造一个策略模式进去。 1.3 什么是多态个性面向对象编程语言有三大个性:封装、继承和多态。 1.封装即信息暗藏或数据保护,“数据结构"通过裸露无限的拜访接口,受权内部仅能通过"数据结构"提供的办法(函数)来拜访其外部的数据; 2.继承的益处是能够实现代码复用,但不应适度应用,如果继承的档次过深就会导致代码可读性和可维护性变差。 因而倡议少用继承而多用组合模式; 3.多态能够分为变量的多态,办法的多态,类的多态。通常强调的是类的多态,多态的实现是指子类能够替换父类,在理论代码运行过程中调用子类的办法实现; 多态能够说是面向对象中最重要的一个个性,是解决我的项目中紧偶合的问题,进步代码的可扩展性和可复用性的外围,是很多设计模式、设计准则、编程技巧的代码实现根底。 多态比拟直观的了解就是去实现某个动作,当不同的对象去实现时会产生出不同的状态,其作用范畴能够是办法的参数和办法的返回类型。 多态这种个性也须要编程语言提供非凡的语法机制来实现,Java 中多态能够通过"子类继承父类+子类重写父类办法+父类援用指向子类对象"的形式实现,还能够通过"接口语法"的形式实现。C++中则应用virtual(虚函数)关键字来实现。 像一些动静语言如 Python 也能够通过 duck-typing 的语法实现,另外 Go 语言中的"隐藏式接口"也算是 duck-typing。 Python 语言,实现多态示例如下: class MyFile: def write(self): print('I write a message into file.') class MyDB: def write(self): print('I write data into db. ') def doIt(writer): writer.write()def demo(): myFile= MyFile() myDB = MyDB() doIt(myFile) doIt(myDB )二 设计模式与架构模式2.1 理解架构模式对给定上下文的软件架构中常见问题的一种通用的可复用的解决方案,能够为设计大型软件系统的各个方面提供相应的领导。它不仅显示了软件需要和软件结构之间的对应关系,而且指定了整个软件系统的组织和拓扑构造,提供了一些设计决策的基本原理,常见的架构设计模式如下: ...

May 5, 2023 · 6 min · jiezi

关于软件设计:为什么说过早优化是万恶之源

 Donald Knuth(高德纳)是一位计算机科学界的驰名学者和计算机程序设计的先驱之一。他被誉为计算机科学的“圣经”《计算机程序设计艺术》的作者,提出了驰名的“大O符号”来形容算法的工夫复杂度和空间复杂度,开发了TeX零碎用于排版科技文献,取得过图灵奖、冯·诺伊曼奖、美国国家迷信奖章等多项荣誉。明天要说的就是他所提出的一条软件设计重要准则 Premature optimization is the root of all evil 过早优化是万恶之源。 为什么说“过早优化是万恶之源”? 我认为过早优化代码会让人陷入到谬误的指标中去,从而漠视掉了最重要的指标。举个很简略的例子,你须要疾速构建一个产品来抢占用户,你当下最重要的指标是让这个产品疾速上线,而不是把这个产品打造的好用(在中国互联网下,这样的事不可胜数),如果你只关注到后者体验、性能问题而漠视了速度,在当下高度竞争的市场之下,你基本毫无机会。 当然下面这个例子是从理性的层面说的,对很多程序猿来说也可能波及不到产品层面的内容。咱们从软件设计的层面,感性的来说,过早优化可能会导致以下的一些问题: 减少代码的复杂性:适度优化可能会导致代码的复杂性减少,从而升高代码的可读性和可维护性。如果代码过于简单,可能会导致开发人员难以了解和保护代码,从而减少开发成本和工夫。消耗开发工夫和资源:适度优化可能会导致开发人员破费大量工夫和资源在代码的性能优化上,而疏忽了其余重要的开发工作。这可能会导致我的项目进度延误和开发成本减少。升高代码的可移植性:适度优化可能会导致代码的可移植性升高。如果代码过于依赖于特定的硬件或操作系统,可能会导致代码无奈在其余环境中运行。升高代码的可扩展性:适度优化可能会升高代码的可扩展性。如果代码过于依赖于特定的算法或数据结构,可能会导致代码无奈适应将来的需要变动。过早优化的典型案例 在软件工程史上因为适度关注软件性能导致我的项目最终失败的案例亘古未有,比方我上面要说的一些我的项目,在软件工程史上都是十分出名的我的项目(当然可能有些新生代程序员曾经不晓得了)。 IBM OS/360操作系统:在20世纪60年代,IBM公司开发了OS/360操作系统,这是过后最大的软件工程我的项目之一。在开发过程中,IBM公司过于关注代码的性能问题,导致代码的复杂性减少,开发工夫延误,最终导致我的项目的失败。我通晓这个我的项目还是在我最近在浏览的一本软件工程经典书籍《人月神话》中,也举荐大家浏览下,这个我的项目尽管最终失败了,但也给整个软件工程畛域留下了贵重的教训。Netscape Navigator浏览器:在20世纪90年代,Netscape公司开发了Navigator浏览器,这是过后最风行的浏览器之一。在开发过程中,Netscape公司过于关注代码的性能问题,导致代码的复杂性减少,开发工夫延误,最终导致浏览器市场份额重大降落。Windows Vista操作系统:在21世纪初,微软公司开发了Windows Vista操作系统,这是过后最大的软件工程我的项目之一。在开发过程中,微软公司过于关注代码的性能问题,导致代码的复杂性减少,开发工夫延误,最终导致操作系统的用户体验不佳,市场反应不佳。话说这个操作系统我还用过呢,用户界面还是很漂亮的,很多UI设计也被沿用到了Window7中。如何辨认过早优化 在软件开发过程中,如何判断是否过早优化呢?这里有一些概括性的判断规范,能够简略参考下: 是否存在性能问题:如果代码还没有性能问题,那么过早优化就是不必要的。因而,在进行优化之前,应该先测试代码的性能,确定是否存在性能问题。是否优化了将来可能产生的问题:如果优化的是将来可能产生的问题,而不是以后存在的问题,那么就可能是过早优化。在进行优化之前,应该优先思考以后存在的问题,而不是将来可能产生的问题。是否就义了代码的可读性和可维护性:如果优化代码会导致代码的复杂性减少,升高代码的可读性和可维护性,那么就可能是过早优化。在进行优化之前,应该优先思考代码的可读性、可维护性和可扩展性。是否节约了大量的开发工夫和资源:如果优化代码会节约大量的开发工夫和资源,而不是进步代码的性能和效率,那么就可能是过早优化。在进行优化之前,应该评估优化的老本和收益,确定是否值得进行优化。 判断是否过早优化须要依据具体情况进行评估。在进行优化之前,应该先测试代码的性能,确定是否存在性能问题。同时,也应该优先思考代码的可读性、可维护性和可扩展性,防止适度优化。 总结 作为一名在IT畛域摸爬滚打多年的工程师,我深有体会地意识到过早优化是软件开发中的一大陷阱。在软件开发的初期,咱们可能会过于关注代码的性能问题,而疏忽了代码的可读性、可维护性和可扩展性。这种做法可能会导致代码的复杂性减少,升高代码的可读性和可维护性,甚至可能会节约大量的开发工夫和资源。 在软件开发过程中,咱们应该防止过早优化,而是优先思考代码的可读性、可维护性和可扩展性。当须要进行性能优化时,应该在代码的根底上进行优化,通过剖析性能瓶颈、优化算法和数据结构等办法来进步代码的性能和效率。同时,咱们也应该意识到,性能优化并不是软件开发的惟一指标,咱们还应该重视代码的可读性、可维护性和可扩展性,以便保障代码的品质和可靠性。

April 2, 2023 · 1 min · jiezi

关于软件设计:DatenLord前沿技术分享-No15

1、 演讲题目应用 TLA+ 为分布式算法验证正确性 2、演讲工夫2023年1月8日上午10:30 3、演讲人原野 达坦科技(DatenLord) 4、引言随着计算机领域的倒退,软件变得越来越宏大简单。特地是在并发与分布式畛域,因为其具备极高的复杂性,传统的基于“教训”的软件设计与验证形式曾经不能满足需要,因而咱们须要一种更好的形式验证软件的正确性——应用 TLA+。 5、内容简介本次分享中,会介绍形式化标准语言 TLA+ 的个性与思维,并从与传统编程不同的角度,利用简略的数学知识,为一些简略的算法编写 TLA+ specification。 6、直播预约欢迎您预约直播,或者登陆腾讯会议观看直播: 会议号:581-8301-3525

January 6, 2023 · 1 min · jiezi

公理设计由奇怪海战引发的软件设计思考

前几天看到了一个博客,推荐了《公理设计》一书,还有其相关的文档以及视频。简单了解了一下,增深了一些对软件设计的理解,特此也推荐给大家。 公理设计理论将设计建立在科学公理、定理和推论的基础上,由麻省理工学院教授 Nam. P. Suh 领导的研究小组于 1978 年提出,适用于各种类别的设计活动。软件设计当然也属于一类工程设计过程,下面我们就来看一下两者的关联。 奇怪的海战首先从1862年11月13日的一场海战讲起。这场海战“标志着蒸汽动力铁甲舰新时代的到来。为了便于理解,我这里对舰船名称进行了修改,想了解的朋友可以百度 U.S.S. Monitor battles C.S.S. Virginia. 南方叛军的大大号战舰,体型庞大,非常凶悍。已经击沉了两艘联邦军舰。北方政府军则只派出小小号,一艘非常小,火力也小多的军舰。 大大号顾名思义,它船体特别的大,但是都是固定炮塔,两侧和首尾有很多门炮。而小小号虽然小,却有一个可以旋转的炮台。 我们可以理解为一条战舰需要有两个基础功能:调整航行方向和调整炮击方向。 对于大大号,这两个功能需求是耦合 couple 的,要改变炮击方向,就需要将船只转向。而对于小小号,这两个功能需求则是解耦合 decouple 的,航行方向与炮击方向无关,炮击方向可以独立调整。 于是小小号一直尽量守在大大号的射击死角攻击,而大大号虽然火力猛烈则必须不断通过改变航线来调整炮击方向,于是就不断绕圈。这两条船打了4个小时,大大号不得不撤退了,小小号获得了胜利。 由此可见功能之间的解耦十分重要,它增加了便捷性和灵活性。 工科生最爱的映射矩阵 书中由海战作为引子,介绍了设计过程中的四个域(Domain): CNs:Customer Needs,客户域,就是客户描述的一大堆自然语言也说不清楚的事情,什么高端大气上档次之类的东西。FRs:Functional Requirements,功能域,从 CNs 域到 FRs 域的变换,就是把客户漫无边际的需求翻译成一些可定量的参数,比如战舰控制系统的 FR 是控制航行方向和控制开炮方向。DPs:Design Parameters,设计参数,或者叫物理域,实现 FRs 的物理参数,比如航向控制器和炮塔控制器。PVs:Process Variables,过程变量,或者叫过程域,是描述实现功能过程中涉及的过程变量。相邻域之间的映射,可以看成目标(做什么?)和手段(怎样做?)之间的对应关系。设计过程是相邻域中特征向量之间映射和转换过程。 例如,用户域元素映射到功能域的过程,实际上是将用户需求转变成产品功能要素的过程,即产品规划;功能域向物理域的映射过程是产品的设计过程;从物理域到过程域的映射则可看成“加工产品”的过程。 其中最为重要的是FRs(功能需求)到DPs(设计参数)的映射,这也是我们软件开发过程中最长接触的步骤,需求文档有了,如何进行代码设计并实现。 书中以矩阵向量的方式讲述了 FRs (功能需求) 和 DPs (设计参数) 的映射关系,也就是上图中由 A 变量组成的矩阵代表着 FPs 到 DPs 的映射。不同的矩阵代表着不同的映射关系,其实我们不需要关心矩阵各个位置的具体值如何计算,只需简化的了解如果 FP 和 DP 有关联,则矩阵相应位置上的值为1,否则为0。 比如说小小号上的情况,有两个功能需要:FR1(调整航向)和FR2(调整开炮方向);以及两个设计参数:DP1(船舵)和DP2(旋转炮塔) 其中转动船舵的时候,船会转向,所以A11这里是X,同时船身上的炮塔也跟着船一起转向,所以也影响开炮方向FR2,因此A21也是X。 而在旋转炮塔的时候,不影响船的航行方向,所以A12这里是0。 好的设计?所以,基于上边这个映射矩阵,好的设计应该有两个特点: 首先FRs(功能需求)的数量N,应当等于DPs (设计参数)的数量M。每一个FR(功能需求)与且只与一个DP(设计参数)相互关联。也就是说映射矩阵是一个对角矩阵,对角线上有值,其他位置都是0。《程序员修炼之道》中也提及了类似的思想,也就是正交性一节。那一节的提示是消除无关事务之间的影响,正好和这里映射矩阵是对角矩阵不谋而合。当映射举证是对角矩阵时,说明 FR 和 DP 一一对应,不会有交叉影响。当某一个 FR也就是需求发生变更时,只需要修改一个DP。 ...

November 3, 2019 · 1 min · jiezi

软件开发什么是过度设计

软件设计(架构)往往在项目开发中起到非常关键性的作用,至少它是能够工作。良好的软件设计包含了:灵活性、可伸缩性、可行性、可复用性、安全性,通过该一系列的定义,使我们影响到了软件功能的设计和特征。 (一)、什么是过度设计过度设计一词在英文中称为"over design",over意思是太多,design意思是设计、构思,通过教科书上面的解释,意味着你设计的或构思的太多了,即为过度设计。 什么是过度设计?设计出来的系统比恰到好处要复杂或臃肿的多,过度的封装、继承、接口或是大量的无用配置方法,其实就是用户需要一把杀鸡的刀,而你却设计出了杀牛刀或是电锯。 过度设计通常来自于开发者将问题过于复杂化或是前瞻性欠缺。 在我们日常所犯的错误中,大部分是来自于前者,至于后者的欠缺,需要一定的项目经验和洞察力来支撑,能够合理的预判和考虑需求会哪个方向发展。 在前者,问题复杂化会引入大量额外的代价,如成本上升,系统缺陷增大、提升维护成本、降低系统性能。而高性能和可维护性都是系统的隐性需求,如果这些也没实现好,那就可能属于设计错误。 但是从客观角度来看,能够进行过度设计的,多半设计能力高于设计不足的,过度的设计改回来的成本也比设计不足的改过去的成本低的多,在此需要更多地去权衡"利与弊"。 (二)、过度设计案例以系统充值为例,最初你设计的系统只需要一个支付宝功能,你的数据库设计如下: id primary key int //主键user_id int//充值用户status int //-1充值失败,0充值中,1充值成功order_no string //第三方支付系统订单号amount decimal //金额但是没过多久,你的系统需要接入另一个支付系统-微信,需要区分用户是通过微信还是支付宝充值的,于是你的数据库设计便成了以下模样 id primary key int //主键user_id int//充值用户status int //-1充值失败,0充值中,1充值成功order_no string //第三方支付系统订单号platform string //第三方交易平台amount decimal //金额但是你想了下,感觉可能以后需要接入银联支付,需要记录是哪张银行卡支付的,然后你又想了下,既然已经接入了银联支付,那顺便就再完善以下,支持国际支付,如美元充值,港币充值,然后需要记录上当地充值的汇率及当地支付时间,最终你花了一天的时间,将数据库改成了这样 id primary key int //主键user_id int//充值用户status int //-1充值失败,0充值中,1充值成功order_no string //第三方支付系统订单号platform string //第三方交易平台amount decimal //金额currency string //货币:CNY USD HKD bank_id int //银行卡IDrate decimal // 充值汇率local_pay_time //当地支付时间完成基本设计后,你花了4周的时间完成编码和测试,最终交付了上去,然而系统的初衷只是需要简单区分的是微信充值或支付宝充值,而因过度设计带来的额外成本和缺陷是非常巨大的,为此我们需要尽力让自己做的恰到好处并且避免过度设计。 (三、)如何避免过度设计?避免过度设计的最佳方法就是“不要设计的太远”,未了解实际未来,就做出了各种预设和判断,为系统增加了额外的负担。 正如scrum(敏捷开发)所倡导的Evolutionary Design(演进式设计),将每一次的重构和迭代都映射和更新到最新的设计中来,从而最大限度的满足系统的功能性需求和非功能性需求。 当你手里握着一把锤子时,不要把所有看到的,都当成钉子。

September 8, 2019 · 1 min · jiezi

设计模式之建造者设计模式

这是设计模式系列的第二篇——建造者设计模式,我希望推送的文章是一个系列的,尽量保持一样的写作风格,尽量把我理解的阐述清楚,关于建造者设计模式主要从以下几个方面来学习,具体如下: 概述本质关键概念具体实现总结概述建造者设计模式(Builder Pattern)属于创建型设计模式,主要用于创建复杂的对象,可将复杂对象的构建过程抽象出来,通过不同实现的构建者和装配者最终组装出不同的对象,可以非常方便的增加不同实现的构建者、组装者而不用修改以前的代码。 本质建造者设计模式(Builder Pattern)分离了对象子组件的构造过程和组装过程,实现了构建与组装的解耦,不同的构建器相同的组装顺序以及相同的构建器不同的组装顺序都可以创建出不同的对象,使得构建与组装充分解耦,进而实现构建算法与组装算法的解耦,从而实现更好的复用。 关键概念构建者(Builder):构建不同的子组件且返回子组件或者提供获取复杂对象的方法,将构建过程抽象成接口或抽象类,方便扩展具体的不同的构建者。组装者(Dirctor):通过某个具体的构建者构建相关的子组件,同时对外提供组成复杂产品对象的方法。当需要生成复杂对象时,直接通过某个具体的组装者获得想要的具体对象即可,至于组装过程与构建过程使用者不需要关心,分别由具体的组装者与具体的构建者内部完成。当然复杂对象可以理解为具有很多属性的对象。 具体实现下面以手机的组装过程来说明建造者设计模式的具体实现,产品类如下: /** * 产品 * @author jzman */public class Phone { private Screen screen; private Camera camera; private Cpu cpu; //省略getter、setter、toString 方法 //...}//子组件class Screen{ private String name; //...}//子组件class Camera{ private String name; //...}//子组件class Cpu{ private String name; //...}抽象的构建者: /** * 构建者 * @author jzman */public interface PhoneBuilder { Screen builderScreen(); Camera builderCamera(); Cpu builderCpu();}具体的构建者: /** * 具体的构建者 * @author jzman */public class MiPhoneBuilder implements PhoneBuilder{ @Override public Screen builderScreen() { System.out.println("构建屏幕..."); return new Screen("Mi-screen"); } @Override public Camera builderCamera() { System.out.println("构建相机..."); return new Camera("Mi-camera"); } @Override public Cpu builderCpu() { System.out.println("构建屏幕..."); return new Cpu("Mi-cpu"); }}抽象的组装者: ...

May 31, 2019 · 1 min · jiezi

被误读的设计模式

目录概要 <!-- TOC depthFrom:1 depthTo:6 withLinks:1 updateOnSave:1 orderedList:0 --> 设计模式的开山之作对设计模式的误解关于使用设计模式的3个问题无处不在的设计模式如何解释设计模式<!-- /TOC --> 设计模式的开山之作1994年10月21日,有四个哥们儿出版了一本书,名字叫做《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software)。 这四个哥们儿后来以“四人帮”(Gang of Four,GoF)著称,而他们的《设计模式:可复用面向对象软件的基础》一书也就成为了设计模式的开山之作。 对设计模式的误解之前发布了几篇介绍设计模式的的博客,看到有的读者对设计模式的评论是: 看了也不会,会了也不用设计模式有使用场景的,不能生搬硬套我写了那么多年代码,从来没用过设计模式关于使用设计模式的3个问题这里我不得不说,大家对设计模式是有很多误解的,面对设计模式我们至少要思考下面3个问题: 首先,什么是设计模式?设计模式简而言之就是一些常见软件设计问题的标准解决方案。 正如设计模式的开山之作《设计模式:可复用面向对象软件的基础》一书中所说的那样: 所有结构良好的面向对象体系结构中都包含了许多设计模式。设计面对向软件比较困难,而设计可复用的面向对象软件就更加困难。内行的设计者知道:不是解决任何问题都要从头做起。他们更愿意复用以前使用过的解决方案。当找到一个好的解决方案,他们会一遍又一遍地使用。这些经验是他们成为内行的部分原因。可以说,如果问题是我们的敌人,代码是我们的剑,设计模式就是高手心中的剑谱。 接着,怎么使用设计模式《Head First 设计模式》一书的作者里曼说,使用设计模式有3个层次: • Beginner —— 初级选手,在编程的时候无处不用设计模式,认为用的模式越多设计就越好。 • Intermediate —— 中级选手,在编程的时候知道何时该用什么设计模式,而什么时候不该用。 • Zen —— 到了禅的境界,“他山之石,可以攻玉”。设计模式被用来简化设计,让设计更优雅。 非要把设计模式硬塞到设计之中,那只是初级菜鸟的层次。 最后,设计模式在实际生产中使用的多不多?设计模式本身就是源自实际问题的优秀解决方案的总结,因此在很多基础的架构和框架里,都可以看到设计模式的影子。 比如Java开发者经常使用的Spring框架,从创建型的工厂模式、单例模式、原型模式,再到结构型的享元模式、代理模式,再到行为型的观察者模式、模板模式,在其源码中随处可见。 无处不在的设计模式设计模式不是空中楼阁,也不是只有面试和吹牛的时候才能放在嘴边的炫耀资本,而是一个优秀的开发者可以让自己的设计更加优雅的不可或缺的好帮手。 并且设计模式也并不是软件行业特有的现象,很多行业中有经验的从业者都会使用各种“模式”来优化自己的设计。 就拿剧本创作举例,如果一个剧作家要写一个剧本,并不是完全凭空创建的,也有各种业内普遍应用的模式,例如“悲剧英雄模式”(《麦克白》、《哈姆雷特》)、“凄美爱情模式”(《罗密欧与朱丽叶》、《梁山伯与祝英台》)等等,都是剧作家可以使用的“设计模式”。 如何解释设计模式一般而言,要介绍一个设计模式,至少要包含如下4个要素: 问题(Problem)描述了我们遇到了哪些问题,也就是某个设计模式的使用场景。 解决方案(Solution)为了解决上面遇到的问题,我们想到了哪些解决方案。其中最具有普遍性的方案往往就是我们的设计模式的内容。 效果(Consequence)设计模式就像一个模板,可以为解决不同的问题提供思路。而解决问题的效果就是衡量一个设计模式在某个场景下是否适合的关键因素,一般我们要衡量的方面有灵活性、可移植性、可扩充性、性能等。 模式名称(Pattern Name)一个好的名字,可以让我们记住某个模式,并且可以望名知意,使其更便于传播。甚至作为我们思维方式的一部分保留下来。 以上面4个要素为基础,我们解释设计模式可以分为如下几个方面: 模式名称:模式的名字模式别名:模式的其他名字或者昵称模式分类:模式属于那种类型模式意图:回答模式是干什么的,为了解决什么问题等问题模式结构:模式包含哪些角色,以及这些角色之间的关系模式适用性:哪些场景适合使用该模式模式实现:通过例子来展示模式的实现过程已知应用:该模式已经被使用在了什么地方相关模式:模式中用到的模式,与该模式有替代关系的模式发布/订阅模式 和 事件驱动模型 从原理到实战的系列文章: 设计模式之发布订阅模式(1) 一文搞懂发布订阅模式设计模式之发布订阅模式(2) Redis 发布/订阅模式设计模式之发布订阅模式(3) 深入Spring Events事件驱动模型设计模式之发布订阅模式(4) Guava Eventbus 事件处理设计模式之发布订阅模式(5) Spring Events源码解析 ...

May 7, 2019 · 1 min · jiezi

什么是SOLID原则(第3部分)

让我们从最后一个 SOLID 原则开始吧,即依赖倒置原则(Dependency Inversion Principle,简称 DIP)(不要和依赖注入Dependency Injection ,DI 弄混淆了)。这个原则所说的是高级模块不应该依赖具象的低级模块,它们都应该依赖相应模块的抽象层。我仍将使用自行车的示例来尝试给你解释这个原则。首选看下这个 Bike 接口:interface Bike { void pedal() void backPedal()}MountainBike 和 ClassicBike 这两个类实现了上面的接口:// 山地车class MountainBike implements Bike { override void pedal() { // complex code that computes the inner workings of what happens // when pedalling on a mountain bike, which includes taking into // account the gear in which the bike currently is. } override void backPedal() { // complex code that computes what happens when we back pedal // on a mountain bike, which is that you pedal in the wrong // direction with no discernible effect on the bike }}// 传统自行车class ClassicBike implements Bike { override void pedal() { // the same as for the mountain bike with the distinction that // there is a single gear on a classic bike } override void backPedal() { // complex code that actually triggers the brake function on the // bike }}正如你所看到的,踩脚踏板(pedal)会让自行车向前行驶,但是山地车 MountainBike 因为有多个齿轮,所以它的 pedal 会更加复杂。另外,向后踩脚踏板(back pedal)时,山地车不会做任何事,而传统自行车 ClassicBike 则会触发刹车操作。我之所以在每个方法的注释中都有提到“complex code”,是因为我想指出我们应该把上述代码移动到不同的模块中。我们这样做是为了简化自行车类以及遵循单一职责原则(自行车类不应该担起在你向前或向后踩脚踏板时究竟发生了什么的计算工作,它们应该处理有关自行车的更高级别的事情)。为了做到这一点,我们将为每种类型的 pedalling 创建一些行为类。class MountainBikePedalBehaviour { void pedal() { //complex code }}class MountainBikeBackPedalBehaviour { void backPedal() { // complex code }}class ClassicBikePedalBehaviour { void pedal() { // complex code }}class ClassicBikeBackPedalBehaviour { void backPedal() { // complex code }}然后像下面这样使用这些类:// 山地车class MountainBike implements Bike { override void pedal() { var pedalBehaviour = new MountainBikePedalBehaviour() pedalBehaviour.pedal() } override void backPedal() { var backPedalBehaviour = new MountainBikeBackPedalBehaviour() backPedalBehaviour.backPedal() }}// 传统自行车class ClassicBike implements Bike { override void pedal() { var pedalBehaviour = new ClassicBikePedalBehaviour() pedalBehaviour.pedal() } override void backPedal() { var backPedalBehaviour = new ClassicBikeBackPedalBehaviour() backPedalBehaviour.backPedal() }}这个时候,我们可以很清楚地看到高级模块 MountainBike 依赖于某些具体的低级模块 MountainBikePedalBehaviour 和 MountainBikeBackPedalBehaviour。ClassicBike 以及它的低级模块同样如此。根据依赖倒置原则,高级模块和低级模块都应该依赖抽象。为此,我们需要以下接口:interface PedalBehaviour { void pedal()}interface BackPedalBehaviour { void backPedal()}除了需要实现上面的接口外,行为类的代码与之前无异:class MountainBikePedalBehaviour implements PedalBehaviour { override void pedal() { // same as before }}剩下的其他行为类同上。现在我们需要一种方法将 PedalBehaviour 和 BackPedalBehaviour 传递给 MountainBike 和 ClassicBike 类。我们可以选择在构造方法、pedal() 、pedalBack() 中完成这件事。本例中,我们使用构造方法。class MountainBike implements Bike { PedalBehaviour pedalBehaviour; BackPedalBehaviour backPedalBehaviour; public MountainBike(PedalBehaviour pedalBehaviour, BackPedalBehaviour backPedalBehaviour) { this.pedalBehaviour = pedalBehaviour; this.backPedalBehaviour = backPedalBehaviour; } override void pedal() { pedalBehaviour.pedal(); } override void backPedal() { backPedalBehaviour.backPedal(); }}ClassicBike 类同上。我们的高级模块(MountainBike 和 ClassicBike)不再依赖于具体的低级模块,而是依赖于抽象的 PedalBehaviour 和 BackPedalBehaviour。在我们的例子中,我们应用的主模块可能看起来向下面这样:class MainModule { MountainBike mountainBike; ClassicBike classicBike; MountainBikePedalBehaviour mountainBikePedalBehaviour; ClassicBikePedalBehaviour classicBikePedalBehaviour; MountainBikeBackPedalBehaviour mountainBikeBackPedalBehaviour; ClassicBikeBackPedalBehaviour classicBikeBackPedalBehaviour; public MainModule() { mountainBikePedalBehaviour = new MountainBikePedalBehaviour(); mountainBikeBackPedalBehaviour = new MountainBikeBackPedalBehaviour(); mountainBike = new MountainBike(mountainBikePedalBehaviour, mountainBikeBackPedalBehaviour); classicBikePedalBehaviour = new ClassicBikePedalBehaviour(); classicBikeBackPedalBehaviour = new ClassicBikeBackPedalBehaviour(); classicBike = new ClassicBike(classicBikePedalBehaviour, classicBikeBackPedalBehaviour); } public void pedalBikes() { mountainBike.pedal() classicBike.pedal() } public void backPedalBikes() { mountainBike.backPedal(); classicBike.backPedal(); }}可以看到,我们的 MainModule 依赖了具体的低级模块而不是抽象层。我们可以通过向构造方法中传递依赖来改善这种情况:public MainModule(Bike mountainBike, Bike classicBike, PedalBehaviour mBikePB, BackPedalBehaviour mBikeBPB, PedalBehaviour cBikePB, BackPedalBehaviour cBikeBPB)…现在,MainModule 部分依赖了抽象层,部分依赖了低级模块,这些低级模块也依赖了那些抽象层。所有这些模块之间的关系不再依赖于实现细节。在我们到达应用程序中的最高模块之前,为了尽可能地延迟一个具体类的实例化,我们通常要靠依赖注入和实现了依赖注入的框架。你可以在 这里 找到更多有关依赖注入的信息。我们可以将依赖注入视为帮助我们实现依赖倒置的工具。我们不断地向依赖链中传递依赖关系以避免具体类的实例化。那么为什么要经历这一切呢?不依赖于具象的一个优点就是我们可以模拟一个类,从而使测试更容易进行。我们来看一个简单的例子。interface Network { public String getServerResponse(URL serverURL);}class NetworkRequestHandler implements Network { override public String getServerResponse(URL serverURL) { // network code implementation }}假设我们还有一个 NetworkManager 类,它有一个公共方法,通过使用一个 Network 的实例返回服务器响应:public String getResponse(Network networkRequestHandler, URL url) { return networkRequestHandler.getServerResponse(url)}因为这样的代码结构,我们可以测试代码如何处理来自服务器的“404”响应。为此,我们将创 NetworkRequestHandler的模拟版本。我们之所以可以这么做,是因为 NetworkManager 依赖于抽象层,即 Network,而不是某个具体的 NetworkRequestHandler。class Mock404 implements Network { override public String getServerResponse(URL serverURL) { return “404” }}通过调用 getResponse 方法,传递 Mock404 类的实例,我们可以很容易地测试我们期望的行为。像 Mockito 这样的模拟库可以帮助你模拟某些类,而无需编写单独的类来执行此操作。除了易于测试,我们的应用在多变情景下也能应对自如。因为模块之间的关系是基于抽象的,我们可以更改具体模块的实现,而无需大范围地更改代码。最后同样重要的是这会让事情变得更简单。如果你有留意自行车的示例,你会发现 MountainBike 和 ClassicBike 类非常相似。这就意味着我们不再需要单独的类了。我们可以创建一个简单的实现了 Bike 接口的类 GenericBike,然后山地车和传统自行车的实例化就像下面这样:GenericBike mountainBike = new GenericBike(mbPedalB, mbBackPedalB);GenericBike classicBike = new GenericBike(cbPedalB, cbBackPedalB);我们减少了一半数量的具体自行车类的实现,这意味着我们的代码更容易管理。总结所有这些原则可能看起来有点矫枉过正,你可能会排斥它们。在很长的一段时间里,我和你一样。随着时间的推移,我开始逐渐把我的代码向增强可测试性和更易于维护的方向转变。渐渐地,我开始这样来思考事情:“如果只有一种方法可以把两个部分的内容分开,并将其放在不同的类中,以便我能……”。通常,答案是的确存在这样的一种方法,并且别人已经实现过了。大多数时候,这种方法都受到 SOLID 原则的启发。当然,紧迫的工期和其他现实生活中的因素可能会不允许你遵守所有这些原则。虽然很难 100% 实现 SOLID 原则,但是有比没有强吧。也许你可以尝试只在那些当需求变更时最容易受影响的部分遵守这些原则。你不必过分遵循它们,可以把这些原则视为你提高代码质量的指南。如果你不得不需要制作一个快速原型或者验证一个概念应用的可行性,那么你没有必要尽力去搭一个最佳架构。SOLID更像是一个长期策略,对于必须经得起时间考验的软件非常有用。在这篇由三部分组成的文章中,我试图给你展示了一些有关 SOLID的我发现比较有趣的东西。关于 SOLID,还有很多看法和解释,为了更好地理解和从多个角度获取知识,请多阅读些其他文章。我希望这篇文章对你有所帮助。……如果你还没有看过另两个部分,这里是它们的链接,第1部分 和 第2部分 。如果你喜欢这篇文章,你可以在我们的 官方站点 上找到更多信息。 ...

December 23, 2018 · 3 min · jiezi

什么是SOLID原则(第2部分)

翻译自:What’s the deal with the SOLID principles? (part 2)在文章的 第1部分,我们主要讨论了前两个 SOLID 原则,它们分别是单一职责原则和开闭原则。在这一部分,我们将按照首字母缩略词中的顺序来处理接下来的两个原则。让我们启程吧!L在 SOLID 原则中,最具神秘色彩的就是里氏替换原则(Liskov Substitution Principle,简称 LSP)了。此原则以 Barbara Liskov 的名字命名,他在 1987年 首次提出了这一原则。里氏替换原则要阐述的内容是:如果对象 A 是对象 B 的子类,或者对象 A 实现了接口 B(本质上讲,A 就是 B 的一个实例),那么我们应该能够在不做任何特殊处理的情况下,像使用一个对象 B 或者 B 的一个实例那样使用对象 A。为了理清思路,让我们看一个关于多个自行车的示例。Bike 基类如下:class Bike { void pedal() { // pedal code } void steer() { // steering code } void handBrakeFront() { // hand braking front code } void handBrakeBack() { // hand braking back code }}山地自行车类 MountainBike 继承自基类 Bike (译者注:山地车有通过齿轮的机械原理调整档位的特性):class MountainBike extends Bike { void changeGear() { // change gear code }}MountainBike 类遵循了里氏替换原则,因为它能够被当作一个 Bike 类的对象使用。如果我们有一个自行车类型数组,并用 Bike 和 MountainBike 的实例对象来填充它,那么我们完全可以正确无误地调用 steer() 、pedal() 等 Bike 基类的所有方法。所以,我们可以在不经过特殊处理的情况下,把 MountainBike 类型的元素当作 Bike 类型的元素来使用。现在想象一下,我们添加了一个名为 ClassicBike 的类,如下所示:class ClassicBike extends Bike { void footBrake() { // foot braking code }}这个类代表了一种经典自行车,你可以通过向后踩踏板来进行制动。这种自行车没有手刹。基于此,如果我们有一个 ClassicBike 类型的元素混在了上述的自行车数组中,我们仍然能够无误地调用 steer 和 pedal 方法。但是,当我们尝试调用 handBrakeFront 或者 handBrakeBack 的时候,问题就暴露出来了。取决于具体的实现,调用这些方法可能导致系统崩溃或者什么也不会做。我们可以通过检查当前元素是否是 ClassicBike 的实例来解决这个问题:foreach(var bike in bikes) { bike.pedal() bike.steer() if(bike is ClassicBike) { bike.footBrake() } else { bike.handBrakeFront() bike.handBrakeBack() }}如你所见,假如没有类似上面的类型判断,我们就不能再把一个 ClassicBike 的实例看作一个 Bike 实例了。这显然违背了里氏替换原则。有多种方法可以解决这个问题,当我们讨论到 SOLID 中的 I 原则时,就会看到一个解决之道。遵循里氏替换原则的一个有趣的后果就是你编写的代码将很难不符合开闭原则。【译者注】原文中,上图有个标题“There is no spoon”,这句话是电影《黑客帝国》的一句台词。面向对象编程存在的一个问题就是我们常常忘记正在打交道的数据,以及处理这些数据和真实世界里对象的关系。现实生活中的有些事情无法在代码中直接建立模型,因此我们必须牢记:抽象本身并不神奇,底层的数据仅是数据而已(并不是一个真正的自行车)。忽视里氏替换原则可能会让你遇到各种麻烦。拿 Donald (之前一篇文章 中的一个开发者) 来说,他写了一个 String 的子类,名叫 SuperSmartString 。这个子类做了所有的事情并覆写了父类 String 中的一些方法。他的这种编码方式显然违背了里氏替换原则。之后,他在他的代码中全都使用子类 SuperSmartString 的实例,而且还把这些实例视同 String 实例。不久,Donald 就注意到了一些“奇怪”、“神秘”的 bug 开始四处出现。当这些问题出现时,程序员就该开始抱怨之旅了,编程语言、编译器,编码平台、操作系统,甚至是市长和上帝都要跟着挨批评了。这些“神奇的” bug 其实可以通过遵守里氏替换原则来避免。就算不是为了代码质量,单单是为了程序员应该头脑清晰的职业属性,这个原则也应该受人尊敬。如果你的工作项目稍有复杂,那么只需少许的 SuperSmartStrings 和 ClassicBike 们就能让你工作不堪忍受。【译者注】关于里氏替换原则的相关内容远不止上文提到的这些,比如覆写(override)与重载(overload)的区别、子类需要个性化时该怎么做等等都需要我们关注。I至此,我们还剩下两个原则。I 代表的是接口隔离原则(Interface Segregation Principle,简称 ISP)。这个很容易理解。它说的是我们应该保持接口短小,在实现时选择实现多个小接口而不是庞大的单个接口。我会再次使用自行车的例子,但是这次我用一个 Bike 接口而不是 Bike 基类:interface Bike { void pedal() void steer() void handBrakeFront() void handBrakeBack()}MountainBike 类必须实现这个接口的所有方法:class MountainBike implements Bike { override void pedal() { // pedal implementation } override void steer() { // steer implementation } override void handBrakeFront() { // front hand brake implementation } override void handBrakeBack() { // back hand brake implementation } void changeGear() { // change gear code }}目前尚好。对于有问题的带有脚刹功能的 ClassicBike 类,我们可以采用下面这种笨拙的实现:class ClassicBike implements Bike { override pedal() { // pedal implementation } override steer() { // steer implementation } override handBrakeFront() { // no code or throw an exception } override handBrakeBack() { // no code or throw an exception } void brake() { // foot brake code }}在这个例子中,我们不得不重写手刹的两个方法,尽管不需要它们。正如前所述,我们打破了里氏替换原则。比较好的一个做法就是重构这个接口:interface Bike() { void pedal() void steer()}interface HandBrakeBike { void handBrakeFront() void handBrakeBack()}interface FootBrakeBike { void footBrake()}MountainBike 类将实现 Bike 和 HandBrakeBike 接口,如下所示:class MountainBike implements Bike, HandBrakeBike { // same code as before}ClassicBike 将实现 Bike 和 FootBrakeBike ,如下所示:class ClassicBike implements Bike, FootBrakeBike { override pedal() { // pedal implementation } override steer() { // steer implementation } override footBrake() { // code that handles foot braking }}接口隔离原则的优势之一就是我们可以在多个对象上组合匹配接口,这提高了我们代码的灵活性和模块化。我们也可以有一个 MultipleGearsBike 接口,在它里面添加 changeGear() 方法。现在,我们就可以构建一个拥有脚刹和换挡功能的自行车了。此外,我们的类现在也遵循了里氏替换原则,ClassicBike 与 MountainBike 都能够看作 Bike 而毫无问题了。如前所述,遵循里氏替换原则也有助于开闭原则的实现。……如果你还没看过第 1 部分的内容,可以在 这里 查看。在 第3部分 我们将探讨最后一个 SOLID 原则。如果你喜欢这篇文章,你可以在我们的 官方站点 上找到更多信息。 ...

December 22, 2018 · 2 min · jiezi

什么是SOLID原则(第1部分)

翻译自:What’s the deal with the SOLID principles?(part 1)即使你是一个初级开发人员,你也可能听说过 SOLID 原则。它们无处不在。工作面试时,你也许听过这样的问题:“你是如何评估代码质量的呢?又是如何区分代码的好坏呢?”一般的答案类似这样:“我尽量保持文件足够小。当文件变得很大时,我将移动部分代码到其他文件中。”。最糟糕的答案之一是:“我看到了就知道了。”当你用此类的描述回答之后,面试官通常会问:“你听说过 SOLID 原则吗?”。那么,什么是 SOLID 原则呢?它为什么会被经常提到呢?维基百科 - SOLID 是这样描述的:“在面向对象的计算机编程中,术语 SOLID 是对五大软件设计原则的助记缩写,这五个原则旨在让软件设计更易理解、更灵活、更易于维护。它与 GRASP 软件设计原则并无关系。这些软件设计原则是 Robert C. Martin 提出的众多原则中的一个子集。虽然它们适用于任何面向对象的设计,但是 SOLID 原则也可以形成诸如敏捷开发或者自适应软件开发等方法论的核心理念。Martin 在 2000年 的论文《设计原则与设计模式》中介绍了 SOLID 原则的理论。”。如果正确地遵循了 SOLID 原则,那么它将引导你写出维护性和可测试性更好的高质量代码。如果需求变更,你也可以轻松地修改代码(与不遵守 SOLID 的代码相比)。我认为向你提及这些内容是非常重要的:这些原则关注的是可维护性、可测试性以及和人类思维相关的更高质代码,而不是你为之编码的计算机。工作在以性能为主的领域的开发者各自有着不同的编程方法。由于在我们生活的世界里,人工时间成本比机器时间成本更昂贵,所以大多数开发者都不在高性能需求的领域里工作,而且他们常常被鼓励去使用面向对象的编程方法。在这种情况下,大部分开发者都能很好地应用 SOLID 原则。从本文开始,我将按照单词首字母缩写的顺序尽力向你解释每一个原则。为了简洁起见,我会把这个主题分为 3 个部分。本文是我将向你介绍的前两个原则的第 1 部分。好了,让我们从字母 S 开始吧。SSOLID 中的 “S” 表示的是单一职责原则(Single Responsibility Principle,简称 SRP),它是最容易理解也可能是最容易让人忽视的一个原则。此原则意味着一个类应该只做且只能做一件事情。如果一个类未能达到它的目的,那么你就可以看着它并说“你做了一件事……”。举例来说,假如我们需要从网络上获取一些 JSON 数据,然后解析它,并把结果保存在本地数据库中。根据我们正在编码的平台,这种工作可以使用为数不多的代码来实现。由于代码量不多,我们可能会想把所有的逻辑全部扔到一个类中。但是,根据单一职责原则,这将会是一个糟糕的做法。我们可以清楚地区分 3 个不同的职责:从网络上获取 JSON 数据,解析数据,保存解析的结果到数据库中。基于此,我们应该有 3 个类。第 1 个类应该只处理网络。我们给它提供一个 URL,然后接收 JSON 数据或者在出现问题时,收到一个错误信息。第 2 个类应该只解析它接收到的 JSON 数据并以相应的格式返回结果。第 3 个类应该以相应的格式接收 JSON 数据,并把它保存在本地数据库中。为什么非要这么麻烦呢?通过这样分离代码,我们能获得什么好处呢?其中一个好处就是可测试性。对于网络请求类,我们可以使用一个测试的 URL 分别在请求成功和发生错误的测试用例下来观察它的正确行为。为了测试 JSON 模块,我们可以提供一个模拟的 JSON 数据,然后查看它生成的正确数据。同样的测试原则也适用于数据库类(提供模拟数据,在模拟的数据库上测试结果)。有了这些测试,如果我们的程序出了问题,我们可以运行测试并查看问题发生在哪个地方。可能是服务器上的某些部分发生了改变,导致我们接收了有损数据。或者数据是正常的,但是我们在 JSON 解析模块中遗漏了什么,致使我们不能正确地解析数据。又或者可能我们正尝试插入数据的数据库中不存在某个列。通过这些测试,我们不必猜测问题出在哪个地方。看到了问题所在,我们就努力地去解决它。除了可测试性,我们还拥抱了模块化。如果项目需求变更,服务器返回数据的格式是 XML 或者其他的自定义格式而非 JSON,那么我们所要做的就是编写一个处理数据解析的新模块,然后用这个新的代替 JSON 模块。或者可能因为一些奇葩的理由,上述两者我们都需要,而网络模块再根据一些规则调用正确的模块。如果我们有一些本地的 JSON 文件需要解析并将解析的数据发给其他模块时该怎么办呢?那么,我们可以把本地的 JSON 发送给我们的解析模块,然后获取结果并把它用在需要的地方。如果我们需要以 Flat 的格式(译者注:Flat File)而不是数据库的形式本地保存数据呢?同样没问题,我们可以用一个新的模块来替换数据库模块。如你所见,这个看似简单的原则有很多优势。通过遵守这个原则,我们已经能够想象的到我们的代码库在可维护性方面会有重大改进。O字母“O”表示的是开闭原则( Open-Closed Principle,简称 OCP)。常言道,我们的类应该对扩展开发,对修改关闭。什么意思呢?我的理解是,我们应该以插件式的方式来编写类和模块。如果我们需要额外的功能,我们不应该修改类,而是能够嵌入一个提供这个额外功能的不同类。为了解释我的理解,我将使用一个经典的计算器示例。这个计算器在最开始只能执行两种运算:加法和减法。计算器类看起来像下面这样(本文中的代码不是用特定语言编写的):class Calculator { public float add(float a, float b) { return a + b } public float subtract(float a, float b) { return a — b }}我们像下面这样使用这个类:Calculator calculator = new Calculator()float sum = calculator.add(10, 2) //the value of sum is 12float diff = calculator.subtract(10, 2) //the value of diff is 8现在,我们假设客户希望为这个计算器添加乘法功能。为了添加这个额外的功能,我们必须编辑计算器类并添加乘法方法:public float multiply(float a, float b) { return a * b}如果需求又一次改变,客户又需要除法,sin,cos,pow以及众多的其他数学函数,我们不得不一次又一次编辑这个类来添加这些需求。根据开闭原则,这并不是一个明智的做法。因为这意味着我们的类可以修改。我们需要让它屏蔽修改,而对扩展开放,那么我们该怎么做呢?首先,我们定义一个名为 Operation 的接口,这个接口只有一个名为 compute 的方法:interface Operation { float compute(float a, float b)}之后,我们可以通过实现 Operation 接口来创建操作类(本文中提供的大多数示例也可以通过继承和抽象类来完成,但我更喜欢使用接口)。为了重建简单的计算器示例,我们将编写加法和减法类:class Addition implements Operation { public float compute(float a, float b) { return a + b }}class Subtraction implements Operation { public float compute(float a, float b) { return a — b }}我们的新计算器类只有一个名叫 calculate 的方法,在这个方法中,我们可以传递操作数与操作类:class Calculator { public float calculate(float a, float b, Operation operation) { return operation.compute(a, b) }}我们将像下面这样使用我们的新类:Calculator calculator = new Calculator()Addition addition = new Addition()Subtraction subtraction = new Subtraction()float sum = calculator.calculate(10, 2, addition) //the value of sum is 12float diff = calculator.calculate(10, 2, subtraction) //the value of diff is 8现在如果我们需要添加乘法,我们将创建这样的一个乘法运算类:class Multiplication implements Operation { public float compute(float a, float b) { return a * b }}然后通过添加以下内容在上面的示例中使用它:Multiplication multiplication = new Multiplication()float prod = calculator.calculate(10, 2, multiplication) // the value of prod is 20我们终于可以说我们的计算器类对修改关闭,对扩展开放了。看一下这个简单的例子,你可能会说将这些额外的方法添加到原始的计算器类中也没什么大问题,还有就是可能更好的实现也就意味着编写更多的代码。诚然,在这个简单的情景中,我更赞同你的说法。但是,在现实生活里的复杂情景下,遵守开闭原则编码将大有裨益。也许你需要为每个新功能添加远不止那三个方法,也许这些方法非常复杂。然而通过遵循开闭原则,我们可以用不同的类外化新的功能。它将有助于我们以及他人更好地理解我们的代码,而这主要是因为我们必须专注于较小的代码块而不是滚动无休止的文件。为了更好地可视化这个概念,我们可以把计算器类视为第三方库的一部分,并且无法访问其源码。好的实现就是编写它的善良的人们遵守了开闭原则,使它对扩展开放。因此,我们可以使用自己的代码扩展其功能,并轻松地在我们的项目中使用它。如果这听起来仍让人犯傻,那就这样想吧:你刚刚为客户编写了一个很棒的软件,它完成了客户想要的一切。你尽最大能力编写了所有的内容,并且代码质量令人惊叹。数周后,客户想要新的功能。为了实现它们,你必须潜心投入到你的漂亮代码中,修改各种文件。这样做,有可能代码质量会受到影响,特别是在截止日期紧张时。如果你已经为你的代码编写了测试(这也是你应该做的),那么这些修改可能会破坏一些测试,你还必须修改这些测试。这与遵守了开闭原则编写的代码形成了鲜明的对比。要实现新功能,你只需编写新代码即可。旧代码保持不变。你所有的旧测试仍然有效。因为我们不是生活在一个完美的世界中,所以在某些跑偏的情况下,你可能仍然会对旧代码的某些部分进行细微的更改,但这与非开闭原则带来的修改相比则可以忽略不计。除此之外,遵循开闭原则的编码方式还能让你在心理上获得极大的愉悦体验。其中一个就是,你只需要编写新代码,而无须为了实现新功能对你引以为傲的代码痛下杀手。通过为新功能编写新代码,而不是修改旧代码,高涨的团队士气将随之而来。这可以提高生产效率,从而减少工作压力,改善生活质量。我希望你能看到这个原则的重要性。不过令人沮丧的是,在一个真实的项目中,主要是由于缺乏魔法水晶球的能力,我们很难预见未来以及应该如何、在哪里应用这个原则。但是,知道了开闭原则的确有助于在需求来临时识别出可能的用例。在一开始的实现中,当客户想要给这个计算器添加乘法和除法功能时,我们随手将这两个方法添加到了 Calculator 类中。接下来,当他还要 sin、cos 时,我们也许会对自己说:“等会儿……”。等待过后,我们开始重构代码以适配开闭原则来避免将来可能遇到的麻烦。现在,当客户还想要 tan、pow 以及其他功能时,我们早就搞定了。……你可以在 什么是SOLID原则(第2部分) 阅读下两个 SOLID 原则。如果你喜欢这篇文章,你可以在我们的 官方站点 上找到更多信息。 ...

December 21, 2018 · 2 min · jiezi

优秀工程师必备的三大思维,你拥有哪些?

阿里妹导读:不同岗位、不同职责的技术人对工程师思维的深度要求是不一样的,但从多维度去思考却应是每个技术人都应该具备的素养。本文整理自阿里巴巴高级技术专家至简在团队内部的个人分享,希望通过对工程师思维的分析和解读,让大家能正确对待那些在现实工作中看上去与本职岗位无关,却对团队效能影响极大的一些点和一些事。作者简介:至简,阿里巴巴高级技术专家,是集团Service Mesh方向的重要参与者和推动者。曾出版《专业嵌入式软件开发——全面走向高质高效编程》一书,坚信和倡导软件设计是软件质量之根本,并对软件开发的复杂性本质有着深刻的认识,对如何高质高效实施软件开发有着自己独到的见解和方法。在社会分工的背景下,软件行业的工程师群体被划分成了开发、测试、产品等诸多岗位,以协作的方式共同完成价值创造。高度依赖软件的互联网行业正以全新的方式改善着人们的生活,同时在改善的道路上对价值创造的效能提出了更高的要求,而背后是对个体与团队的协作效能有着更高的诉求。专人专岗的协作模式在进一步改善团队的协作效能时所面临的最大挑战在于“岗位墙”,即岗位间衔接不可避免会出现一些模糊地带,而这些模糊地带又很容易相互忽视,导致失去关注而很大程度地拉低了团队效能。比如,开发工程师会认为保证质量是测试工程师单方面的职责;开发工程师不关注用户体验而只需关注实现需求,等等。此外,这种协作模式也会固化个体的思维和心智模式,将个体的思维和心智框定在所处岗位之内,以致对于岗位之外的内容不能很好地理解,使得个体在整个协作活动中会缺乏同理心、系统性,从而影响工作幸福感。相信这些现实工作场景读者并不陌生:开发工程师对产品工程师所提出的用户体验方面的需求会认为过于吹毛求疵;产品工程师因不理解技术的实现原理而提出天马行空、不接地气的需求(我们在此不讨论创新这一特例);测试工程师因为不理解工程效率的内涵而将自己的工作变成了体力活;开发工程师不清楚自己对于软件质量的责任,而将那些本因自己做好的琐碎工作心安理得地交给测试工程师去做;辛辛苦苦所开发出来的功能,用户抱怨难用。这些问题发生的最终结果,一定是团队协作效能的低下。那么在没有找到比专人专岗更好的协作模式的情形下,我们该如何发挥个体的力量去改善团队的协作效能呢?改善的起点在于全面地梳理工程师思维,帮助工程师个体在职场和职业发展中建立起更为全面的思维和视野,以促使每个工程师在协作过程中能最大程度地发挥个体能力去推动团队协作效能的提升。我将工程师思维分解为产品、技术和工程三大思维。每个维度主要关注的内容通过几个关键字去表达,如下图所示。下面针对每种思维需要关注的每个词以图中从上至下的顺序去解释。由于解释是基于关键词去展开的,所以段落之间的衔接可能会显得生硬,还请读者见谅。产品思维产品思维的起源是用户(或客户)价值。用户价值是通过技术手段以产品或服务的形态去解决用户的痛点,或带去爽点。毫无疑问,工程师在日常工作中应时刻关注并理清自己的工作与用户(或客户)价值的联系,并且应该通过聚焦于用户价值去安排工作的优先级和分配自己的精力。当用户价值足够时,产品能否在市场中立足并真正收获收益,首先考验的是产品的用户体验。良好的用户体验一定是站在用户的角度,基于用户心智来塑造概念,由于概念存在理解和解释成本,所以塑造的概念应足够轻、少且易掌握。概念一旦塑造出来则概念间的关系也随之确定,这些关系基本上决定了产品与用户的交互流程。好的产品体现于“易用”二字,其极致在于迎合用户的本能反应并符合各种生活或专业常识。所有产品都存在演进的过程,所创造的用户价值也在被不断地挖掘与探索,那时不同的细化价值需要通过产品特性去区分和表达。特性也是产品差异化的一种体现,特性也间接地确定了软件实现层面的功能模块边界。作为开发工程师,也需要对产品特性有非常透彻的理解,并能将其很好地抽象并转化为软件实现层面的功能模块。特性需要考虑通过售卖license等形式进行开启或关闭去实现售卖,这一点对于2B的产品甚是必要。为了产品更好地演进,需要通过数据闭环的形式去检验创造用户价值的效果,让产品的开发、运营、营销工作做到有的放矢。在产品价值创造的道路上,最害怕的事莫过于只顾低头干做加法,做得多却无人关心收效。而我们通过数据化闭环的形式,不仅能让整个产品大团队聚焦于核心价值,还能帮助团队在探索用户价值的道路上理性地做减法。大多情形下,做减法远难于做加法。技术思维技术思维的源头是需求。需求可以分成市场需求、系统需求、特性需求等不同层次,回答的是技术层面“做什么”的问题。显然,清晰表达的需求以及对需求的精确理解才能确保将事做对。毋容置疑,需求一旦出现偏差所导致的浪费是非常严重的,也正因如此工程师对于需求的质量相当重视。需求一旦确立,会基于模块化的思想拆分成多个功能模块去降低实现的局部复杂度,最终将所有功能模块“拼接”在一起去实现整体需求。每个功能模块会安排给一个人或一个团队负责,由于功能模块是需求分解后的产物,容易导致工程师在实现的过程中只看到“树木”而忘记了“森林”。性能是工程师在实现一个功能模块时不得不关注的,特别是当功能模块被运用于高频、时效性敏感、算力有限的场合时性能将尤其被关注。在现实中有时会存在工程师乐于追求性能的极致去体现自己的技术实力,甚至出现过早追求性能而滑入过度设计的误区。毫无疑问,一个正规的团队,对于功能模块的开发工作多会以项目制、多个迭代的方式去完成交付。不少工程师这里会有一个误区,忘记了敏捷思想所倡导的“项目计划的目的是为了适应变化”,而是将“按时交付”当作是天职,各种赶工爬到终点时却毫不意外地看到了“一地鸡毛”的景象。在迈向第四次工业革命的道路上,人工智能、大数据、机器学习,Kubernetes、Istio、Knative、Go、Dart、Flutter等新技术不断冲击着工程师已掌握的技能。快速跟上技术的迭代步伐是每个有追求的工程师不断提升自己专业素养的表现之一。工程师的内心一定不缺乏对新技术的追求,憧憬自己所掌握的技术具有一定的先进性。工程思维工程思维的起点是流程。流程的背后是科学,以既定的步骤、阶段性的输入/输出去完成价值创造,通过过程控制确保最终结果让人满意。由于流程涉及每一个工程师的工作质量与效率,其含义不只在于定义、工具化、检查等内容,而是应基于工程师的日常工作习惯,将流程与工程师的工作环境无缝整合。“无缝”体现于流程中的概念与工程师群体已建立的专业常识相一致、没有增加毫无价值的负担,根本仍是确保易用性。机制的含义是通过对所需解决问题的分析,以一种模式去解决同类问题。机制应体现一定的系统性,而非“头痛治头,脚痛治脚”。系统性不是一开始就能被洞察到,可能在演进的过程中逐步发现和完善的,因而需要工程师在工作的过程中不时回顾并付诸实践去落实。对于工程师来说,机制是通过系统性的软件设计去达成的。可以说产品质量直接决定了工程师的工作和生活幸福感。一个质量不可靠的产品一定会给用户和工程师自己带去麻烦,甚至造成无法挽回的经济损失并造成负面的社会影响。对于工程师来说,那势必打乱个体的工作与生活节奏。为了让产品的质量做到可靠,单元测试、静态分析、动态分析等确保工程质量的手段应成为工程师的基本工作内容,通过将这些手段与CI(Continuous Integration)流程进行整合去持续构建起对软件产品的质量信心。在互联网行业,除了软件产品的质量得可靠外,风险可控是另一个不能忽视的内容。而风险可控是建立于系统性机制和质量可靠之上的。对于服务端软件来说愈是如此。风险往往出现于资源使用的极端场景,当从外部涌入的过多事务远超软件产品的处理能力时,需要有一定的机制让整个产品能相对平滑地应对,或是扩充资源、或是限制涌入事务的流量。软件所需的机器成本是比较容易忽视的话题,软件成本不只与软件性能相关,还与软件之间的依赖、技术方案等因素相连。当一个软件需要从公司的内部对外输出时,平时忽视对成本的关注就会暴露出成本问题。比如,为了运行某个软件需要数量庞大的计算资源,所导致的资金开销对于客户来讲很可能是无法接受的。至此,大致介绍完了自己所理解的工程师思维。延伸了解工程师思维的价值在于,工程师个体需要在工作中逐步建立起产品、技术和工程三大思维,以便用更为全面的视角去看待日常工作中所面临的困境和困惑。当站在单一的思维去看待所面临的问题时可能觉得不合理,但从三大思维层面去审视时所得到结论可能完全相反。从团队协作的角度,只有团队中有更多的个体从多维度去进行思考,才容易发现岗位间衔接的那些无人问津的灰色地带,进而通过补位、助攻去更大程度地发挥团队的效能。显然,不同岗位、不同职责的工程师对于这三大思维的深度要求是不一样的,但从多维度去思考却应是每个工程师都应该具备的素养。最后,我也给读者留下一些问题,同样期待您在留言区分享自己的思考。本文作者:至简阅读原文本文来自云栖社区合作伙伴“阿里技术”,如需转载请联系原作者。

December 13, 2018 · 1 min · jiezi