在最初学习软件开发时,我非常热衷于寻找最佳的编程语言、平台、库、框架、模式和架构。我当时以为,通过找到最好的东西并专注于这些主题,排除其他因素,我就可以避免浪费宝贵的时间。
虽然我很早就意识到,采用基于项目的学习方法(而不是逐个主题地罗列清单)来缩小范围非常重要,但找到最适合这项工作的工具却是另一回事。
我从事产品编程、客户端应用程序开发十余年,解答过成千上万初级和中级开发人员的问题,我自己也曾苦苦思索过这些问题,因此我会尽我所能地解释如何找到最好的东西。
本文主要面向初中级开发人员,旨在为他们提供一些解决难题的实用方案。你无需丰富的编程经验即可阅读本文。
如何找到好东西
我邀请你跟随一些基本的非技术性示例,这些示例将为本文的其余部分奠定基础。这些示例对某些人来说可能听起来有些傻,但我在其中融入了一些概念模式,你可以将其应用于编程语言、工具和概念——以及几乎所有可以用“好”、“坏”、“最好”或“最差”等词语来形容的事物。
假设你想解决保持水分充足的问题,想买一个水瓶。
你可能认为,要找到最合适的水瓶,需要考虑以下几个方面:
公众舆论和评论
专家意见和评论
制造商的描述和声誉
购买并测试水瓶(最好不要全部都测试,因为那样通常会花费太多时间和金钱)
以上这些因素都需要考虑,因为未经公众充分检验的产品必然存在不确定性。专家评论可以帮助你做出决定,但你也考虑到这些专家的偏见和动机。此外,你还应该考虑制造商在质量、设计和客户服务方面是否拥有良好的口碑,还是仅仅为了追求利润最大化。
经过一番调查研究,可得出以下几个方案:
自动售货机里的一瓶塑料瓶装水
一款高科技金属瓶,你甚至可以在里面煮东西
一款简约但采用符合道德规范的可重复灌装、不含双酚A的塑料水瓶
但是,我们会注意到,确定哪个方案最好,几乎没有人能达成一致意见。通常会有一些共同的观点,但从来没有出现过所有专家都给出相同评价或建议的情况。
仔细思考之后,很明显地,我们需要考虑如何、何时以及何地使用这个水瓶。换句话说,你需要考虑它的使用情境或场合。
假设我需要在三种不同的情合下做出这个决定:
我们站在高速公路某山谷的一个休息站里,面前有一台自动售货机,里面摆满了用充满微塑料的瓶子灌装的廉价水。但你别无选择,而且非常口渴。
我正在一家露营用品商店里,为即将前往新西兰的露营之旅做准备。这次旅行有很多徒步旅行,而且很难找到过滤水。
你正在购物网站上寻找可以每天带到办公室的东西,以避免因饮水不足而引起的脱水头痛。
总体上说,我不应该忽视已有信息、专家意见、受欢迎程度、评分、评论、用户评价等。但我永远不可能找到适用于所有情况的最好水瓶。
最好的“任何东西”取决于正在试图解决的问题以及该问题的背景(例如需求或情况等词语也适用)。
换句话说,这些事物都没有绝对或固定的价值——它们的价值始终是相对的。我们称之为“适用性法则”。
现在,让我们再来讨论一些与软件设计和开发直接相关的例子。
“适用性法则”同样适用于编程语言,就像寻找最佳水瓶一样——尽管细节和背景有所不同。世界上并不存在适合每个人、团队、问题或功能的最佳编程语言。
这里有一些具体的细节和背景信息可以提供,或许能帮助大家找到答案。我们将介绍选择编程语言的具体细节和一些通用思路。这些模式也适用于框架、库以及编程的大多数场合。
首先,你需要对受欢迎程度以及专家和“影响者”的意见持怀疑态度。
我的建议是,对于任何做出以下声明的人都要格外谨慎:
“X”语言是最好的(当然,说“X”语言是我最喜欢的也完全可以接受)。
“X”语言是最糟糕的,非常糟糕,已经过时了,是垃圾,毫无用处,等等。
通常有三类人会发表这类言论:
有些所谓的专家,其实是在表达自己的个人偏好,却将其当作不容置疑的事实来陈述。
非专业人士鹦鹉学舌地重复上述群体的观点,或者尚未理解适宜性法则的人。
乌合之众与参与人群
值得注意的是,一个人可能在某些问题上是专家,但这并不能保证他对所有问题的看法都能达到专家水平。
但这并不意味着你应该不假思索地否定专家意见。要同时考虑专家的过往记录以及他们对发言语境的关注程度。
让我们来看两个例子:
专家A表示:“Python拥有机器学习开发方面最好的工具、支持和生态系统。”
专家B说:“Python是最好的编程语言。”
如果你一直关注我的论点,就会发现专家 B显然缺乏精确性(无论是有意还是无意),而专家 A 的情况则不同。无论专家 A的说法是否属实(顺便一提,我写过一些 Python 代码,但没有机器学习方面的经验),你都能看出他们考虑到了细节和上下文。
要寻找像专家 A这样的人士!
不过也要承认,我见过很多用高级语言编写的难读代码的例子——请不要这样做。
从事嵌入式系统开发的人员可能希望使用 C 或 C++ 等语言进行开发,以优化性能或克服内存和处理能力的限制。
但在企业系统中,由于需要运行在各种平台上,并且与业务需求、规则和现实世界的对象(产品、用户等)紧密交织,底层语言并不那么流行。毕竟,中高层管理人员通常只关注那些能够提升用户体验的底层优化。
我总体上很喜欢优化,但始终牢记,在小数据集(即n较小)或高处理能力下,一切都会很快。简单来说,有时像可读性这样的人类需求远比对效率的微不足道的优化重要得多。
具有讽刺意味的是,像 Java 这样的语言(结构非常严谨且冗长)与像 JavaScript 这样的语言(与Java恰恰相反,取决于你的使用方式)的主要缺点和优点,根据上下文而有所不同。
说到企业系统,使用结构体、类型、接口、类、线程、并发原语以及类似的编程结构可以带来诸多好处。它既能保护系统免受安全隐患的影响,又能通过类型层次结构、接口、协议、抽象等方式提供一定的灵活性。
此外,研究设计模式可以教会你一些可重复使用的解决方案,以解决自通用计算机诞生以来(或之后不久)就遇到的问题。
但是,适用性原则在这里仍然适用。
有时,我们只需要编写一个简单的脚本,比如将一些数据从一个 SQL 数据库导入到另一个数据库。也许你知道如何用更函数化的方法来解决问题,代码里面不需要也不鼓励使用对象、类或结构体。有一天,你会意识到,试图在所有情况下都应用设计模式、架构、层次结构和类似的构造,实际上制造的问题和解决的问题一样多。稍后我们会更详细讨论。
大多数现代语言的设计者和维护者都明白,开发者更喜欢灵活性。我们中的许多人都希望避免过早优化和不必要的结构,但也不希望因为不小心让程序把 1.23356 和“Rhinoceros”相加而导致代码崩溃。
关键在于,结构或缺乏结构既是福也是祸,这取决于它在什么情况下被使用。
我并不是说语言流行度无关紧要,也不是说你应该从最冷门的“小众”编程语言入手。我无意贬低这些“小众”编程语言,但冷门本身通常并不是什么好事。
关键在于,许多(或许是大多数)对编程语言发表意见的人,并没有在各种平台、语言和环境下进行过丰富的实践经验。如果一个人只用过Python,并且乐在其中,那么他自然会倾向于把Python看得比其他语言更高。
我们人类往往倾向于找到最适合自己的方法,然后誓死捍卫它。但从更具体的例子来看,我认识几十位经验丰富的中高级开发人员,他们精通 Java 和其他编程语言。尽管 Java 仍然是全球最流行的语言之一,但我认识的这些开发人员中,只有一位在有选择的情况下更倾向于使用 Java 编写代码。
不要误以为对很多人有效的方法就是你最后才需要尝试的方法。我曾多次尝试使用 Haskell 等语言,这让我学到了很多关于编写更具函数式(以及函数式纯函数式)特性的代码的好处。
但我完全没有打算将 Haskell 作为构建图型应用程序的首选解决方案。
你最应该考虑的问题之一,你选择的编程语言是否能帮助你找到工作,这是很多人关心的问题。
一些有影响力的人非常喜欢建议人们选择某种编程语言,因为它在 GitHub 上的公开提交最多,或者选择另一种编程语言,因为它有最多的程序员使用(但实际上,这一点非常难确定)。
让我换个角度思考:假设最常见的编程语言和平台组合是 JavaScript 和 Web。再假设我们有相当确凿的网页招聘信息数据,证实目前市场上最多的职位正是针对这种组合。最后假设,出于某种原因,你非常讨厌 JavaScript,却乐于使用 PHP 构建网站。
你会听到有人说 PHP 是一种快过时的语言,也是找工作的死路一条。
如果你仔细寻找,你会发现市场上有很多招聘信息都在寻找能够开发、扩展和维护现有代码库的 PHP 开发人员。而且,由于 PHP开发人员的职位空缺与申请人数的比例远高于JavaScript 开发人员,你获得面试机会的可能性也会大大增加。事实上,我的团队最近就招聘了几位 PHP 开发人员!
这部分内容对网页开发者来似乎无关紧要,但对于那些希望针对特定硬件或操作系统进行开发的人来说是重要的事。简而言之,如果你没有一台安装了 macOS 和 Xcode 的电脑,那么开发 iOS 应用将会非常困难。就我个人而言,2014 年我选择 Android 开发,部分原因是之前学过一些 Java 知识——还有一个重要的考虑因素是我当时有一部 Android 手机。
虽然可以通过付费使用远程设备(例如通过在线服务使用远程 Mac)来解决这个问题,但我多年前使用这类服务的经验是,它们并不好用。
想想你拥有哪些资源,以及这些资源如何与你想要开发的产品或想要为之工作的公司相匹配。如果你除了一个装有网络浏览器的廉价电脑之外几乎一无所有,但仍然想开发图形用户界面(GUI)应用程序,那么Web开发可能是一个不错的选择。
这个话题值得单独写一篇文章。一年前,我会告诉你,人工智能充其量只能编写一些基础代码,帮助你学习一些东西,而这些东西可能正确,也可能错误。
现在情况变化真大!虽然如果我复制粘贴自己不理解或未经测试的代码,我的工作就做不好,但人工智能绝对已经成为我开发的强大助力。
回到我们讨论的主题,这和选择编程语言有什么关系?嗯,虽然我之前说过流行度并非总是决定性因素,但根据语言学习模型(LLM)的运作机制,流行度确实是一个考量因素。就通用性而言,像 Python、JavaScript 和 Java 这样的语言可能拥有最多的训练数据。而我的经验是,我常用的语言,比如 Kotlin、TypeScript 和 Swift,表现也相当不错。
但开发 Android 或 iOS 应用时,我却很少遇到一个在 Web 开发中不太常见的奇怪副作用。这些平台和 SDK 不断变化,拥有数以万计的第三方库、数十种架构,以及关于最佳实践和反模式的无穷无尽的观点,这意味着 LLM(生命周期管理)在处理复杂性或具体性方面可能会遇到严重问题。
我预计随着 LLM 服务改进正确性检查或其他减少幻觉的方法,这个问题将会得到解决。
在选择编程语言时,我能提出的最重要一点或许就是避免沉没成本谬误。在我最初几年兼职学习期间,由于学习Java对我来说实在太难了,我根本没想过要学习第二门语言。
大约12年后,我已经用Java、Kotlin、Swift、C++、TypeScript和SQL编写过一些相当复杂的代码。此外,我还涉猎过C、Python、JavaScript、Racket、Haskell、Objective-C、Visual Basic和C#等语言的代码编写。
并非我刻意去学习这些东西——我通常不会去学习与眼前问题无关的东西。而是这些学习机会随着我的个人和职业兴趣自然而然地出现了。
学习任何通用编程语言的基础知识或接近精通,都会对其他语言的学习有所帮助。的确,如果一个人没有计算机科学基础就去学习 Python 或 JavaScript,那么他/她将很难理解操作系统层面或更底层的工作原理。
我也遇到过一些人,他们的 C/C++/汇编编程水平可能比我高得多,但他们在大学或学院里却从未编写过任何玩具程序。
继续学习,努力在个人兴趣和职业目标之间找到平衡。
接下来的话题都围绕着一个问题展开,我们会反复探讨这个问题:“它解决的问题比它制造的问题多吗? ”
在继续之前,这里先给出一个关于库和框架之间关系的有用但并非最终定论的定义。你还会找到其他定义,但在这个行业里,对于这类话题的共识少得惊人。
对我来说,库就是你可以从别处获取并用来构建各种东西的代码。它可以是单行代码,也可以是大型复杂的子系统——通常介于两者之间。我可以给你一个冗长而严谨的定义,但这不适合当前的语境(因为合适!)。
例如,Java 的 Math 库 (java.lang.Math) 就提供了以下功能:“ Math 类包含用于执行基本数值运算的方法,例如初等指数函数、对数函数、平方根函数和三角函数。 ”
有些人会把“框架”和“库”这两个词互换使用,我对此没有异议。我理解的框架是指可以围绕其构建各种东西的东西,它不一定与解决某个特定的问题领域(例如数学)有关。
例如 RxJava 就是一个例子,它是一个相当复杂的框架,可以用来绑定和管理整个应用程序中的数据流。我已经在近十几个应用程序中使用过这个框架,这些应用程序在原理上都截然不同。
从根本上讲,我确实认为框架是一种库——它们只是目标不同,而且通常占用资源更多。
在选择库和框架时,我会问自己以下问题:
与我自己编写的解决方案相比,它解决的问题是否比它带来的问题更多?
它被维护得好吗(定期更新、作者积极响应、有公司支持)?
它是否有完善的文档(现在我们可以利用人工智能来实现这一目的,这已经不是什么问题了)?
它具有什么样的足迹?
我们来看两个例子。为了避免冒犯任何人,我不会提及具体的平台或这些库的名称。但它们都曾/现在用于移动开发(尽管它们解决的是任何平台上常见的 GUI 问题)。
首先,我最喜欢的库之一只有一个功能:将图像加载到用户界面中。
尽管设备性能比以往更强,但在智能手机上加载大型图像进行显示仍然存在问题。移动操作系统可能会终止占用过多系统资源的程序(即进程)。
这个库处理了我所关心的所有图片加载方面的问题:
将图像加载到特定控件中。
显示合适的负载指示器
显示可选的错误或回退状态,告知用户出现问题。
处理异步加载(通过 URL/URI)、处理和压缩可能很大的比特流(即图像数据)的复杂性
不会不必要地增加打包后的应用程序大小
不会频繁更改其公共 API(例如更改函数名称,这会导致人们在版本更新时实现的功能失效)。
它解决的问题,正是我正要处理的问题。
其次,我最不喜欢的库之一也只负责一项功能:分页。这里的分页指的是将数据分块加载到应用程序中。这在购物车或社交媒体应用程序中特别常见。
我所想到的库是这样解决这个问题的:
将客户端应用程序的每一层(从前端到后端)与其依赖项紧密耦合
这种紧密耦合使得测试变得困难,需要采取一些额外的步骤。
它能很好地处理分页的核心问题,除非你需要自定义或特殊情况。
频繁更改其面向公众的 API
(总体上)解决了一个问题,我很乐意自己编写解决方案。
由于类型设置过于单一且缺乏灵活性,与其他框架的兼容性不佳。
持续更新了几年,然后就被弃用了。
虽然没有大幅增加打包应用程序的大小,但肯定比我自己的解决方案要大。
正如各位所见,即使是能较好地解决核心问题的方案,也可能经不起这个简单的考验:它解决的问题是否比它带来的问题更多?我自己也写过几次分页代码,再以后除非有人能非常有力地说服我,否则我不会轻易尝试。
最佳实践和原则数不胜数,我无法一一详述。
这里我将着重解释为何我将编程原则与不可更改/不可违背的规则区分开来。正如我当初寻求最佳编程语言一样,我也希望找到最佳原则,从而编写出最佳代码。
问题在于,我遇到的所有编程原则都必然受到“适用性法则”的制约。我将以一个个人经验为例,指出我们之前提出的问题——“它解决的问题是否比创造的问题更多”——在这里同样适用。
这条原则可以概括为:如果发现重复代码,就应该将其提取到一个单独的模块(文件、函数、类、库等等)中。简单来说,将重复代码提取到单独模块中的行为可以看作是一个抽象的过程。
坦白讲,这个想法的提出者和倡导者们认为它远比这复杂得多。但许多开发者从未费心去深入研究这些细微差别——他们也不应该费这个劲。我之所以会遇到这些细微差别,仅仅是因为我过度运用了这个理念。
有些情况下,代码重复反而更好:
我们有一组类似的模块(例如类似的组件或业务规则),但由于不同的原因,它们在不同的地方被使用。
我们有一组类似的模块,这些模块可能会因为各种原因而发生变化(例如,产品团队和客户的不同优先级带来的快速变化的需求)。
我们特意将某些协同工作的模块放在不同的包、文件或目录中,以避免模块/模块组之间相互影响。
我们发现需要在抽象层中添加关于某个特定实现的细节,但这些细节并不适用于其他实现(也就是说,这是一个糟糕的抽象)。
以上列举的所有内容都是我过去遇到的情况总结。关键在于,我总体上同意避免代码重复。我也知道在某些情况下,我更倾向于代码重复。因为适用性!
一般来说,你可以把所有编程原则,比如YAGNI(你不需要它)、DRY(不要重复自己)、SRP(单一职责原则)(以及SOLID原则的其他方面),甚至像敏捷和瀑布这样的软件开发方法都理解为同一种原则。在实际应用中,你可以把它们当作指导方针,帮助你避免一些常见问题。但是,一个拥有足够创造力和经验的人总能找到这样一种情况:遵循这些原则反而会带来更多的问题。
很多时候,你需要反复运用这些原则,才能真正理解“过度”的含义。但要注意,当其中一条原则在你面前失效时,不要矫枉过正。我也犯过这样的错误,不得不重新调整。
迄今为止,我还没有遇到过一条普遍适用的编程原则。有些原则很接近,但我总能想到它们在某些情况下并非最佳选择。比如一条很好的原则:永远编写你能写出的最简洁的代码。换句话说,不要无缘无故地增加额外的复杂性。
假设你公司的价值体系或激励机制不太完善,导致你人为地夸大自己的工作表现。这还需要我多说吗?
接下来,我将结合适用性原则,探讨软件系统中的模式和架构。
软件架构是我唯一自认为擅长的领域,我阅读过许多关于设计模式的书籍。所以只要有机会,我都会尽力提供一些关于这些主题的实用信息。
总结一下我关于这个主题的所有文章、课程和公开演讲:最佳软件架构取决于项目与个人需求。
理解这一核心概念的一个方法是问问自己:最适合医院的架构设计是否也适合两居室公寓?显而易见的答案是,我们或许会预期这两类不同的需求之间存在一些共通性(例如门、窗、某种类型的卫生间等等)。但是,两居室公寓的理想架构设计,甚至仅仅是门窗设计,都不可能与医院的架构设计相同。
简而言之,你永远找不到一种架构能够完美适用于所有项目和需求。
以下是我比较熟悉的一些架构:
模型-视图-控制器(MVC)
模型-视图-演示器(MVV)
模型-视图-视图模型(MVVM)
整洁架构(Clean Architecture,罗伯特·C·马丁风格)
模型-视图-意图
颇令人困惑的是,这些架构的实现方式多种多样——几乎和开发者的实现方式一样多!MV-VM 是移动开发中比较常见的架构之一,而对于某些人来说,MV-VM 似乎是一种单一的架构,但我至少能想到五种不同的实现方式。
以下是我针对这些架构的一些使用建议:
要警惕复杂的架构(尤其是 Clean Architecture,因为很多人在这方面犯了严重的错误)中增加不必要的复杂性。
不要试图让项目需求去适应架构——反其道而行之(最好的判断标准是,你会发现由于你使用的架构,你试图实现的某些东西变得不必要地困难)。
不要为了保持一致性而盲目地套用相同的模式,而应该在同一应用程序的不同功能中采用不同的方法。
别误会,我非常喜欢学习设计模式,而且在我开发的大多数 GUI 应用中都会用到一些关键模式。观察者模式(也叫发布-订阅模式或 Pub-Sub 模式)在需要将多个异步数据源连接起来时非常实用。我也很喜欢看到库开发者提供优秀的构建者模式,方便我使用他们的 API。
我认为理解桥接模式或外观模式等基础模式,可以教会你如何将细节隐藏在抽象层之后,这实际上比描述这些模式的那些听起来吓人的专业术语要简单得多。
但我日常工作中很少花时间思考或研究设计模式。相反,我总是思考那些催生这些模式的态度和原则:
提倡松耦合代码(分离依赖项和参数的创建与使用,合理使用抽象)
编写只做一件事的类、接口、协议和函数(尽管这“一件事”可能是一个宏观目标,而不是一个微观操作)。
尽可能避免复杂性(这种复杂性的常见来源是过度使用抽象概念)。
并非假装每个复杂的问题都有简单的解决方案(也就是说,尽可能简单,但不能更简单)。
避免过早优化
再次强调,我只会运用这些原则和态度,前提是它们解决问题的能力大于其带来的问题。设计模式如果应用过于严苛,可能会违背许多原则——尤其是在避免复杂性和过早优化方面。
不要试图让项目需求去迎合你的设计模式。相反,你应该思考哪些模式可能适合你的项目需求,并根据需要进行调整。
我写作这篇文章的目的是为了传达三点信息:
如何选择编程语言以及如何避免在探讨这类话题时容易陷入的陷阱进行了实用性概述。
一个兼具哲学性和实用性的框架,可用于评估任何事物的适用性——重点在于软件的学习和开发。
我如何处理其他主题,例如工具、架构和模式。
虽然考虑工作机会和现有硬件等因素很重要,还请不要忽视个人兴趣的重要性。就我所知,认知(学习如何学习)与兴趣密切相关,兴趣与动机和记忆力密不可分。我们不可能总是只做自己感兴趣的事情,但我建议你尽可能多地寻找个人需求与实际需求之间的交集。
最后,我鼓励大家思考其他领域,或许可以在这些领域探索适用性原则和部落主义思维的问题。
变化是永恒的,价值是相对的。
作者:洛逸
本篇文章为 @ 行动的大雄 创作并授权 21CTO 发布,未经许可,请勿转载。
内容授权事宜请您联系 webmaster@21cto.com或关注 21CTO 公众号。
该文观点仅代表作者本人,21CTO 平台仅提供信息存储空间服务。
请扫描二维码,使用微信支付哦。