当前位置: 首页 > news >正文

(翻译)terry crowley: 写给程序员


写在翻译前:
在hackerson偶然阅读到了terrycrowley的这篇文章,从技术本质的探讨到成为技术管理者,软件工程思想以及团队管理经验方面说的非常不错,很受启发,想翻译一下,希望能为程序员指点迷津。

译文(原文:点击打开链接):
在2016年十月离开微软时,在这家公司已度过了近二十一年。在软件行业三十五年的经验里,我花了一些时间整理出了我所学到的经验,整理出了这篇文章(篇幅较长,为此感到很抱歉)。


要成为出色的程序员,你需要了解很多方面:深入理解编程语言,API,算法与数据结构,操作系统和以及不同的工具等等。而他们持续在变化——新的语言和编程环境层次不穷,总有一些大家正在使用的热门工具或语言。因此掌握前沿的技术并达到精通是很重要的。木匠需要知道如何选择合适的锤子和钉子来完成一项工作。


并且,我发现有一些概念和方法能够长时间的适用于很广泛的场景,甚至长达几十年。我们已经看到底层设备的性能发生了不同数量级的变化,而对一些系统设计方法的思考仍然是有意义的。它们比任何的具体实现更为基础。了解这些主题对于系统的分析和设计非常有帮助。


谦虚与自负
这不仅限于编程,对于在一个不断变化的领域,人们需要对谦卑和自我的平衡进行调节。总是有更多的东西要学习;如果你愿意学习,总有可以帮助你学习的那个人。人需要谦卑来让你承认所不知道的东西,并需要自我来树立信心去掌握新领域并应用所知道的东西。我所看到的最大的挑战是,当有人在某一领域工作了很长时间,以至于“忘记了”他有多善于学习新事物。最好的学习来自于实践,做东西,可以仅仅是一个原型哪怕是黑一个软件。我所知道的最好的程序员,都对技术有很广泛的了解,同时也深入研究并掌握了一些技术,成为了专家。最好的学习机会往往发生在你真正面临难题的时候。


关于端到端的争论
早在1981年,杰里·萨尔茨(Derry Reed)和戴夫·克拉克(Dave Clark)正在做早期的互联网分布式系统,并写出了他们对端对端论证的描述。在互联网上有很多错误信息,所以回去阅读原始论文很有用。他们谦虚地认为,这不是他们的发明——从他们的角度来看,这只是一个普遍的工程策略,适用于许多领域,而不仅仅是通信。他们只是收集了一些例子并记录了下来。其中一个解释是:


在系统中实现某些功能时,只有在对系统的每个节点都了解的情况下,才能正确并完整的实现。在某些情况下,对于系统某内部组件做局部优化可能出于性能的原因。


SRC论文称这只是一场“争论”,尽管它被提升为维基百科以及其他地方作为“原则”。事实上,最好只是把它看作是一个争论。正如他们所说,系统设计师所遇到的最难问题之一就是如何确定系统组件的责任。结论是,分解功能时权衡利弊,隔离复杂度,设计一个能够随需求不断演化,高可靠并且高性能的系统。而没有一套简单的规则可以遵循。


网络的大部分讨论都集中在通信系统上,但是端到端的论据适用于更广泛的情况。分布式系统中的一个例子是“最终一致性”。能够接受最终一致的系统,可以通过让系统中某元素保持暂时不一致的状态,进而来优化和简化设计,因为扩大了解决这些不一致的端到端的范围。可扩展的订购系统(例如由亚马逊使用的)的示例做的不错,不是每个请求都会到达阻塞节点的库存控制服务。这样就可能出现系统的两个节点同时卖了最后一本书,但系统的总流程中会包含端到端的检测机制,例如,通知客户该书已被修改。在订单完成之前,一本书最末端可能被仓库内的某辆车快递。一旦您意识到端到端检测系统是必需的并且已经做到位了,系统的内部设计就可以再次被优化。


事实上,端到端的方法之所以如此强大,是因为系统在性能与功能上设计所获得的灵活性。这种端对端思维使系统内部性能的设计很灵活,使得整个系统的设计更加健壮,每个组件可以独立变化。这使得端到端的方法很有弹性。


端到端方法的其中一个含义是,您要非常小心地添加逻辑层和功能,它们会使系统的性能设计失去灵活性(或其他的灵活性,但性能,特别是系统延迟受的影响最大)如果能够使逻辑层保证系统的性能0代价,端对端方法就可以利用这些性能代价来对其他需求进行优化。如果您用掉这些性能代价,即使提供了一些增值功能,也失去了设计的灵活性。


当您有一个足够大而复杂的系统,并将内部组件分到整个团队时,系统端到端的设计需要团队间的交流。而团队的自然倾向是对各自组件进行扩展,这样会导致端到端设计的灵活性降低。


应用端对端方法的挑战之一是最终结果的确定。 “小跳蚤具有较小的跳蚤...等等“。


专注复杂度
编码是一门非常精确的艺术,程序的正常运行需要每一行代码的正确执行。但这是具有误导性的。程序的整体复杂性与它内部组件相互交互的复杂性并不相等。最健壮的程序会让系统中最重要的部分简单直接,同时以简单的方式与系统中其他组件进行交互,达到隔离复杂度的目的。复杂度的隐藏可以与其他设计方法(如信息隐藏和数据抽象)同构,但如果专注于确定复杂性以及如何隔离,有不同的设计敏感性。


我一直使用的例子是以前的视频终端编辑器如VI和EMACS使用的屏幕重绘算法。早期的视频终端实现了绘画角色的核心动作的控制顺序以及显示功能,还有显示上的优化,例如向上或向下滚动当前行或在行内插入新行或是移动字符。这些命令中的每一项都具有不同的成本,并且这些成本在不同制造商的设备上是有区别的。 全屏应用程序,如文本编辑器,需要尽快刷新屏幕,因此需要对控制顺序进行优化,以完成屏幕的状态切换。


应用程序之所以这样设计是为了隐藏了复杂度。修改文本缓冲区(功能最多的创新)的系统部分完全不用关心这些更改如何转换为屏幕更新命令的。因为计算最佳命令顺序的性能成本小于实际执行更新命令的性能成本。在系统设计中,性能分析的目的是确定如何以及在何处隐藏复杂度,这是系统设计中的常见策略。屏幕更新的过程可以与底层文本缓冲区中的更改操作异步,并与缓冲区顺序无关。缓冲区如何改变并不重要,只要改变了就行。这种异步的组合形式,消除了组件之间的相互作用中顺序上的依赖,然后对它们的交互进行批处理,这是一种隐藏耦合复杂度的常见方法。


隐藏复杂性的关键不是所隐藏的组件,而是组件的隐藏者。在端到端的流程中,组件的提供者需要对组件的使用负责。他们需要有清晰的视野,系统的其余部分是如何与组件进行交互的,以及是否存在复杂度的泄漏。而这点做不好,通常导致“这个组件很难使用”。也意味着它没有正确的隐藏组件的复杂度或没有定义清晰的功能边界来隐藏复杂度。


分层与组合
系统设计师的基本职责是决定如何将系统分解成组件和层;要确定哪部分做哪部分买。开源公司可能希望在“做与买”的决策中保持资金流不变。大项目的一个重要因素是随着时间的推移如何做决定。这些设计选择不仅仅在项目开始需要进行评估,而且要随产品的不断发展而进行评估。


下文包含了系统分解的一些内容,需要长时间的学习。
层是泄露的。层(或抽象)从根本上是泄漏的。这些泄漏可能会立即产生后果,但也会随着时间的推移而产生后果。其中一个后果是,层会很更多的特征渗透出去。它们可能是关于性能或逻辑顺序的假设,而层并没有对其明确定义。这意味着你更容易被组件的内部行为变化所影响。第二个后果是,你会很依赖组件的内部行为,如果要改变它,后果和挑战可能比您想象的要大。


层的功能泄露。你所使用的组件将具有比实际需要更多的功能。在某些情况下,是否使用这个层的决定是基于它将来的用途。你使用它,因为你想“上火车”,并利用该组件现有的功能,在它之上创建程序,导致会承担一些后果。
 1)组件通常会根据您实际不需要的功能进行权衡。 2)组件会把所不需要的功能的复杂度与约束带入项目中,这些约束将阻碍组件的演化。 3)应用中会有更多的表面积被泄漏。其中一些是由于真正的“抽象泄露”所致,一些会显著(但通常控制不良)增加对组件全部功能的依赖。由于办公室足够大,我们发现,即使系统包含若干层,但最终只会对系统中某部分的功能完全掌握。虽然这似乎是乐观的(我们更完全地利用组件),但所有的使用并不是同等重要的。由于这种低使用率,我们最终要花费巨大的成本从一层转移到另一层。 4)额外的功能带来了复杂性以及被错误使用的几率。如果1个XML验证的API被指定为XML树的一部分,那么就可以动态地下载模型的定义。而如果它在代码时被错误地打开,这将导致w3c.org-Web服务器性能大大下降以及(无意的)分布式拒绝服务攻击。(这些俗称为“地雷”API)
 
层会随演化被替代。随需求与系统的演化,组件会被淘汰。最终需要替换整层或组件。而层内组件间又有相互依赖。这意味着上述问题变得很严重。


‘做还是买’的决定将改变。这并不意味着当初的决定是错误的。通常,项目启动时没有适当的组件可以用,而项目后期找到了合适的。又或者当初使用的组件不再符合现在的需求,并且需求明确,容易理解,有足够的理由来创建自己的组件。这意味着,需要密切关心层泄露问题,系统中的层之间,所引用的层都存在泄露问题。


层会变厚。一旦定义了一个层,它就会开始增加功能。厚层的难度在于,它往往会降低您在下层上持续创新的能力。在某种意义上说,这就是为什么操作系统公司厌恶基于其核心功能之上的后层,创新速度会放慢。避免这种情况的一个严格的方法是禁止适配器中的状态存储。MFC采用这种方法构建在Win32之上。短期内不可避免地会需要往现有层堆积功能(导致上述所有最终的问题),因为这样做比起重构更便宜。而了解其中弊端的系统设计人员会找机会重构以简化组件,而不是被动的增加越来越多的功能。


爱因斯坦宇宙观
数十年来,我一直在设计异步分布式系统,有次在微软的内部谈话中,这位SQL架构师Pat Helland引用了这个话题。 “我们生活在一个爱因斯坦世界-不存在同时性的东西。” 在构建分布式系统时,几乎所有要构建的系统都是分布式的-你无法隐藏系统分布式这个自然属性。这只是在物理学上的解释。这是我一直觉得远程过程调用(RPC)的原因之一,特别是“透明”的RPC明确地试图隐藏分布式的这个属性,从根本上来说就是错误的。我们需要拥抱系统的分布式性质,因为系统实现包括完整的系统设计和用户体验。


分布式设计包含以下几点:
您从一开始就要考虑到用户体验的含义,而不是尝试在错误出现时再打补丁。


使用异步来进行组件的交互。同步是不可能的。如果出现同步,那是因为某些内部层已经在试图隐藏异步调用,试图掩盖(但不可能成功)系统运行时行为的基本特征。


需要对状态机的交互进行设计,其中的状态代表健壮的、长寿命的系统状态。


你认识到失败是可预期的。在分布式系统中检测故障的唯一有保证的方法是等待时间太长。这意味着取消操作是首选。系统中的层(可能是UI)需要确定是否已经等待太久,并取消交互。取消只是重置状态并和回收资源。还没有办法能够在系统中可靠地进行冒泡取消。有时可能会有低成本,但不可靠的方式来尝试进行冒泡取消,作为性能优化的一个考量。


取消不是回滚,因为它只是回收本地的资源和状态重置。如果需要回滚,则需要进行端到端取消。


永远不会知道分布式组件的真正状态。即使能够检测到状态,但它可能已经改变了。当您发送操作时,它可能在传输中丢失,也可能会被处理了但响应时丢失,或者可能需要大量时间来处理,在未知的时间后状态发生改变。需要一些方案诸如幂等操作,或有效地重新发现远程状态的机制,而不是去期望分布式组件可以并行进行可靠的状态追踪。可以在“最终一致性”的概念方案中进行查找。


你应该接受异步并设计它,而不是试图隐藏它。当您看到像幂等或不变性这样在分布式设计中应用的技术时,要将其视为探寻宇宙本质的方法,而它们不仅仅是工具箱中的一个设计工具。


谈谈性能
我确信唐·克努兹一定会为后人误解他的“过早优化是所有邪恶的根源”而感到遗憾。事实上,,性能在60年里(或下一个十年,取决于分立晶体管,真空管和机电继电器投资这些趋势)得到了令人难以置信的指数级改善,这些都是所有我们在行业中所看到的惊人创新,以及“软件世界”对经济变化产生的波动。


认识到这个指数变化的一个关键是,当系统的所有组件都经历指数变化时,这些指数是不同的。因此,硬盘容量的增长与内存容量或CPU速度不同,或内存与CPU之间的延迟不同。即使趋势是由相同的底层技术所驱动,指数也不尽相同。延迟的改进从根本上依赖带宽的改进。在短期内指数变化趋势看似呈线性,但随着时间的推移变化趋势可能会出现很大的变化。而这种变化会影响性能与组件的关系,进而设计系统的重新设计。


这样做的结果是,在当下有意义的设计决策在几年后可能就不再有意义了。而在某些情况下,二十年前不错的设计,权衡一下利弊,在现在依然是不错的设计。现代内存映射的特征看起来更像是早期的过程交换,而不像按需求分页。(这有时会导致像我自己这样的老程序员认为“这和我们在70年代使用的方法一样”,而忽略了在这40年里,并没有产生多大意义的事实)。


物体的基本约束是三维与光速。我们回到爱因斯坦的宇宙。在记忆上是有层次结构的, 它们是物理学规律的基础。存储,IO,内存,计算和通信。这些元素的相对容量,延迟和带宽会发生变化,但系统设计是想这些元素如何组合在一起,以及它们之间的平衡和权衡。吉姆·格雷(Jim Gray)做了这个分析。


从3D基本原理与光速中得出的推论是,大部分的性能分析需要做三件事情:本地,本地,本地。无论是在磁盘上打包数据,管理处理器缓存层次结构,如何将数据打包到通信包中,数据如何打包在一起,随时间推移访问数据的方式以及如何在组件之间传输数据,是性能的根本问题。应该专注于如何在较少的数据上写较少的代码,并尽量在空间和时间上尽量采用本地操作。


JonDevaan曾经说“对数据进行设计,而不是代码”。这通常也意味着在审查系统结构时,我对于代码是如何交互的兴趣较少。我更想看看数据是如何相互影响和流动的。如果有人尝试通过对代码结构描述来解释系统,而并不了解数据流的速率和数量,他们就其实并不了解系统。


内存的层次结构也意味着我们总是有缓存。即使某些系统层试图隐藏它。缓存是基本的,但也是危险的。缓存尝试利用代码的运行时行为来改变系统中不同组件之间的交互模式。它们固然需要对该行为进行建模,即使该模型在缓存是如何填充和无效缓存以及测试缓存命中时是隐式的。如果模型不够好或随着行为的变化而变差,则缓存将无法正常运行。一个简单的指导方针是缓存必须有检测机制,因为应用程序的行为会发生变化,组件的性能也会不断变化,它们的行为将随时间而变差。每个有经验的程序员都有缓存的失败经历。


幸运的是,我早年的职业生涯花在了互联网发源地BBN。很自然会认为组件之间是异步通信的,这是系统自然连接的方式。流量控制和排队理论是通信系统的基础,也是任何异步系统运行的常见方式。流量控制本质上是资源管理(管理channel的能力),但资源管理的粒度更细。流量控制本质上是一个端到端的职责,所以以端到端的方式思考异步系统是非常自然的。在这种情况下,缓冲区膨胀的故事是非常值得去思考的,因为它体现了如果缺乏端到端行为的动态与技术“改进”(路由器中的较大缓冲区)的理解,会导致网络基础设施建设的长期问题。


“光速”的概念是很有助于分析系统的一个概念。光速分析不讨论当前的性能如何,而是问“这个设计在理论上可以实现的最好性能是怎样的?”所传输的信息真实内容是什么?改变的速度?组件之间的潜在延迟和带宽是多少?光速分析法迫使系统设计师深入了解当前设计能否可以目标性能,还是要重新考虑其他方法。它还强调要深入理解性能消耗在哪里,以及这是否是因为系统中内在的或潜在地不当行为所造成的。从建设性的角度来看,它迫使系统设计者了解所构建模块的真实性能特征,而不是侧重于功能特性。


我花了很多时间来构建图形应用程序。位于终端用户在系统中会(自然而然)定义一些约束。人类视觉和神经系统不会察觉到变化。而系统会自然地受到限制,系统设计者可以利用(必须利用)这些约束,例如。通过虚拟化(限制需要将多少底层数据模型映射到视图数据结构中),或是通过将屏幕更新速率限制为人类视觉系统所能够感知的限度。


复杂度的本质
我的整个职业生涯艰辛而复杂。为什么系统和应用程序会变得复杂?为什么一个应用程序领域的开发不会随着时间的推移,基础设施的强大而变得越来越容易,而不是越来越困难且更受限制。事实上,我们管理复杂度的关键方法之一是“走开”,重新开始。通常,新的工具或语言会迫使我们从头开始,这样开发人员最终同时获得工具的好处与重新开始的好处。重新开始,什么是根本?并不是说一些新工具,平台或语言不是一件好事,但我可以保证它们不会解决复杂性增长的问题。控制复杂性增长的最简单方法是所开发的系统由较少开发人员来完成,并且这是个小系统。


当然,在许多情况下,“走开”是不二选择。办公室业务是建立在价值和复杂的资产基础上的。随着OneNote,Office从“Word”的复杂中“走开”,为了开创一个全新的维度来保持产品创新。Sway是另一个例子,从Office中脱离开辟全新的产品线。对于Word,Excel和PowerPoint的webapp而言,最终决定要与数据格式保持分离,这个决定为以后产品的发生也产生了重大意义。


弗雷德·布鲁克(FredBrook)的“没有银弹”关于软件开发中事故与本质的讨论,对我影响很大。在软件建模的本质上,嵌入了很多不可避免的复杂度。我刚刚重新阅读了这篇文章,并且发现令人惊讶的是,影响未来开发人员生产力的两大趋势,越来越强调“创建与购买”中的“购买”。已经预示了开源和云基础设施的变化。另一个趋势是转向更"有机"或 "生物"繁衍的增长方式,而不是更纯粹的建构主义。一个现代读者认为,应该转变为敏捷方法,持续集成。而这个文章是在1986年!


我非常重视斯图尔特.考夫曼关于复杂度基本性质的工作。考夫曼建立了一个简单的布尔网络模型("NK模型"),然后探索其基本数学结构的应用,如系统中相互作用分子,遗传网络, 生态系统,经济系统和(有限的)计算机系统,以了解行为有序的数学基础,及其与混沌行为的关系。在一个高度连接的系统中,自然就有一个相互冲突的约束系统,这使得它(数学上)很难将该系统向前发展(被看作是在崎岖的环境中的优化问题)。控制这种复杂性的一个基本方法是将系统中元素独立出来进行批处理,并限制元素之间的相互联系(在NK模型中基本上减少"N"和"K")。当然,对于系统设计者来说,应用复杂度的隐藏、信息隐藏和数据抽象技术以及使用松散的异步耦合来限制组件间的交互是很常见的。


我们经常面临的一个挑战是,系统演化的许多方法都是跨维度的。实时协同是 Office 应用程序的一个非常具体的 (和复杂的) 最近的示例。


我们数据模型的复杂度往往等同于"权力"。设计用户体验的一个挑战是,我们需要将一组有限的用户手势完成到基础数据模型状态空间中的转换。增加状态空间的维度必然会在用户手势中产生歧义。这只是数学上的解释。而确保系统保持"易于使用"的基本方法是对基础数据模型进行约束。


管理
我一开始在高中担任领导角色(学生会主席!),并总是发现承担更大的责任是很自然的。同时,我一直很自豪,我能够继续做为一个全职程序员经历每个管理阶段。副总裁最后把我推向了边缘,远离了日常编程。而我想回到编程,因为我所离开的工作,是一个令人难以置信的充满创造性和充实的活动 (也许也会在你解决bug时有点沮丧)。


尽管在我到微软时,我已经当了十多年的"经理",但我在1996年之后,才真正了解管理层。微软非常强调"工程领导力是技术领导力"。这与我的观点一致,帮助了我成长承担更大的管理责任。


在我到来时最让我共鸣的是办公室里的透明文化。经理的工作是设计并使用透明的过程来驱动项目。透明度不仅仅是简单、自动或善意的问题,它需要应用到系统中。最好的透明度是能够跟踪每个工程师日常活动中的输出(完成工作项目、打开和修复bug、完成用户故事)。要当心主观的判断。


我曾经说我的工作是设计反馈回路。透明过程为每个参与者提供了一种方法-从单个工程师到经理到执行,使用数据来驱动过程和结果,并了解他们在整个项目目标中所扮演的角色。最终,透明度是辅助管理的一个重要工具--管理者可以在距离问题最接近的人身上进行更多的控制,因为他们能够直接观察到正在取得的进展。当然,这个过程是需要协调的。


关键是,目标被映射到了正确的框架(包括关键资源限制,如船舶时间表)。如果一个决策需要放在管理链中完成,并且决策不断的在管理链上下移动,这通常反映了管理层中的框架很糟糕。


当我真正意识到作为一个领导者内化的重要性时,我在BeyondSoftware公司。当时随着工程师经理的离开(后来聘请我去了FrontPage),当时的四个组长都犹豫着要担当经理这个角色--因为我们不知道要呆多久。我们在技术上都非常出色,因而相处得很好,所以我们决定一起领导这个项目。而这产生了很糟糕的后果。一个明显的问题是,我们还没有一个合理分配资源的策略--而这是管理的重要职责之一!当你知道自己是个负责的人时,你会觉得自己的责任是缺失的。我们之间没有一个能够制定统一目标以及定义约束的领导者。


我现在还记得,第一次我认识到了倾听对于领导者的重要性。我刚刚在Word、OneNote、服务器发布和文本服务方面担当了开发经理的角色。关于我们是如何组织文本服务团队的, 有了一个很大的争议,我亲自问每一个关键的参与者,听他们说什么,然后进行整合,并把我所听到的全部写下来。当我向其中一个主要参与者展示这篇汇总的时候, 他的反应是 "哇,你真的听到了我要说的话"!作为一个经理所面临的重要问题,是倾听每个组员的心声(从平台到持续集成团队)。倾听是一个积极的过程,它包括尝试理解某些观点,然后写下所学到的东西并通过测试来验证自己的理解。当需要作出关键决定的时候,在打电话的过程中每个人都已经知道他们的意见得到了倾听 (不管他们是否同意最终决定)。


这是以前的工作,而作为FrontPage的开发经理,在那里我提高了通过部分信息做决策的能力。你等待的时间越长,你就需要更多的信息来做决定。等待的时间越长,灵活性就越小。有时你只需要打个电话。


设计一个组织需要创建张力。你希望增加资源领域的空间,以便在一组更大的资源中应用一致的优先级框架。但是资源域越大,就越难拥有做出正确决策所需的信息。组织结构就是要平衡这两个因素。软件增加了这个问题的复杂度,因为软件的特性可以在任意维度上贯穿设计。Office已使用共享团队来解决这些问题(优先级和资源),方法就是使用跨部门团队来与正在构建的团队共享工作 (添加资源)。


当你在爬管理阶梯时,你和你的新同事不会因为突然变得更聪明,因为你承担了更多的责任。这使得整个组织比高层领导更聪明。让每个级别在一致的框架内拥有自己的决策是实现这一目的的关键。保持倾听并对组织负责,解释你所作出的决定背后的原因也很重要。害怕做一个愚蠢的决定可能是一个有用的动力,它可以确保你清楚地表达你的推理,并倾听听所有可能的声音。


结论
我从大学毕业时的第一份工作的面试,在结束时,招聘人员问我是否对"系统"或"应用程序"感兴趣。我并没有理解这个问题。而在之后的职业生涯,我经历了软件栈每一层中所出现的一些困难的、有趣的问题。保持学习!

相关文章:

  • 推荐x61使用nhc软件控制风扇
  • azure 最佳实践-- 系统运维
  • 全角字符unicode码对应表
  • azure 最佳实践 -- 尽量使用托管服务
  • azure 最佳实践 -- 使用正确的数据存储
  • 变形金刚2影院版完整字幕
  • azure 最佳实践-- 为演化而设计
  • 做真正Hacker的乐趣──自己动手去实践
  • azure 最佳实践 -- 随业务演化的架构
  • 看完只有沉默的爱
  • 重温TCP-IP学习笔记——1/3
  • 嵌入式GUI设计第一阶段回顾
  • 重温TCP-IP学习笔记- 2/3
  • Windows Mobile 6.5 Widgets开发初体验
  • 重温TCP-IP学习笔记 3/3
  • JS 中的深拷贝与浅拷贝
  • DataBase in Android
  • es6要点
  • Eureka 2.0 开源流产,真的对你影响很大吗?
  • iBatis和MyBatis在使用ResultMap对应关系时的区别
  • JAVA 学习IO流
  • javascript数组去重/查找/插入/删除
  • js
  • js 实现textarea输入字数提示
  • macOS 中 shell 创建文件夹及文件并 VS Code 打开
  • Octave 入门
  • Promise面试题,控制异步流程
  • python 装饰器(一)
  • Redux系列x:源码分析
  • 从tcpdump抓包看TCP/IP协议
  • 基于Android乐音识别(2)
  • 记录:CentOS7.2配置LNMP环境记录
  • 开源地图数据可视化库——mapnik
  • 七牛云 DV OV EV SSL 证书上线,限时折扣低至 6.75 折!
  • 驱动程序原理
  • 如何使用Mybatis第三方插件--PageHelper实现分页操作
  • 数据结构java版之冒泡排序及优化
  • 微信小程序--------语音识别(前端自己也能玩)
  • 一道闭包题引发的思考
  • 主流的CSS水平和垂直居中技术大全
  • (10)工业界推荐系统-小红书推荐场景及内部实践【排序模型的特征】
  • (2.2w字)前端单元测试之Jest详解篇
  • (a /b)*c的值
  • (附源码)python房屋租赁管理系统 毕业设计 745613
  • (十五)devops持续集成开发——jenkins流水线构建策略配置及触发器的使用
  • (一)Thymeleaf用法——Thymeleaf简介
  • (终章)[图像识别]13.OpenCV案例 自定义训练集分类器物体检测
  • (转)Android学习系列(31)--App自动化之使用Ant编译项目多渠道打包
  • (转载)Linux网络编程入门
  • .bat批处理(八):各种形式的变量%0、%i、%%i、var、%var%、!var!的含义和区别
  • .htaccess配置常用技巧
  • .NET Core 成都线下面基会拉开序幕
  • .NET core 自定义过滤器 Filter 实现webapi RestFul 统一接口数据返回格式
  • .net core开源商城系统源码,支持可视化布局小程序
  • .net mvc 获取url中controller和action