当前位置: 首页 > 科技观察

浅谈对开发者友好的软件设计

时间:2023-03-16 23:42:18 科技观察

面向开发者的软件,相对于普通用户只在有限的场景下使用,还可能集成、扩展、二次开发等,所以在代码或者设计层面你也应该考虑如何尽可能对开发人员更友好。本文试图从三个不同的角度来解释和讨论哪些设计是正确的:LeastSurprise(最少恐惧原则)Guide,NotBlame(不责怪用户,尽量引导)KeepItSimple,Stupid(尽可能简单尽可能)结合实际案例。开发者友好。LeastSurprise(最少惊吓原则)不要吓到用户!通常在一个特定的领域,人们会在该领域的语境中形成一系列的约定俗成和常识,比如:走在墙上,头很疼,但墙通常不会倒塌在网上填完表格后页面并按下提交按钮,页面将跳转到命令的末尾。添加--help通常会返回命令的使用方法。因此,我们的软件所显示的行为在其领域内应尽可能保持一致。性,明显的,可预测的。1.单一控制源作为用户,您通常希望软件提供源清晰且行为一致的配置。如果有很多种不同的方式来实现类似的配置效果,用户就会一头雾水,不知道该用哪一种。Spring框架在发展多年后,由于其出色的灵活性设计,反过来也导致了一定程度的理解难度。例如,当你想在SpringSecurity中配置自定义认证时,你可以:以上三种方式都可以满足认证需求,包括官方文档在内的很多资料都会尝试使用其中的一两种来配置认证。如果用户对它的设计原理不是很了解(比如刚刚入门),看到这么多不同的配置方式,很容易造成混乱和恐慌。2.无歧义在某些情况下,用户在使用我们的软件时必须进行一定的配置。从用户的角度来看,对于配置项,用户期望的是能够一眼看出配置的内涵。如果配置项不明确,用户会感到困惑。这是一篇讨论TiDB交互性的文章中的示例:TiDB5.0中引入了一个配置开关:tidb_allow_mpp=ON|OFF(默认=ON)这个开关旨在禁止优化器使用TiFlash来执行查询,如果设置为ON,优化器会根据实际情况选择是否使用TiFlash。因此,虽然配置为ON,但实际是否使用TiFlash取决于优化器的判断。“就像房间里控制电灯的开关,关了灯就不会亮,开灯也不一定会亮。”这种有歧义的开关的存在,很容易让用户产生误解和误会。面对以上问题,文中给出的修改建议是改为:tidb_allow_mpp=ON|OFF|AUTO这个AUTO确实能让用户一目了然。3.遵循约定有许多设计、语言级别或领域约定和规范,软件开发人员通常默认遵循这些约定和规范。这里有一个在《重构 2》中查询和修改分离的例子:有时方法名甚至被省略,变成了getTotalOutstanding()。通常遇到以getXXX开头的函数,用户会默认该函数是幂等的。如果他们在使用后发现呼叫动作有一些副作用(例如,每次呼叫都会发送账单),这会让用户感到困惑。.(Rust的厉害之处在于找到get_xxx(&mutself)的方法定义时会自动高亮警告)再举个例子:通常类似于上述的“移动”操作,from在前,from在前to在后面,如果我们的函数反过来,to在前面,from在后面,就是在欺骗用户。但是考虑到上面操作的两个参数都属于string类型,我们没有办法限制用户以fromfirstandthento的形式传递参数(也许用户喜欢intelassembly语法?),所以更好的方法可能是:Guide,NotBlame(不要责怪用户,尽量引导)RTFM,是老头子在指导新人吗?还是软件作者对外展党的口诛笔伐?每当我们看到用户报错显示HttpCode400,是不是感觉一阵抄袭?你好?“用户错误”是用户自己的问题,与开发者无关,是这样吗?1.报错,然后呢?当用户执行错误操作时,我们的软件应该向用户反馈详细的错误信息。但除此之外,还有很多事情可以做:上面展示了Rust编译器的编译错误,从上到下:告诉我们错误原因是“缺少生命周期标志”,错误代码是E0106并且是“linear_probe_hash_table.rs”文件的第17:26个字符错误,箭头表示代码错误的位置。“帮助”部分告诉我们“可以考虑使用‘a符号’”,最后用波浪线给出修正后的效果。有人说WritingRust是“编译器驱动开发”,从编译器的保姆级错误信息来看,确实如此。2.帮助用户识别而不是记忆。经过一些步骤复杂的配置操作后,用户可能不知道在最终执行之前要做什么。我们的软件应该帮助用户检查和识别问题(也就是类似dry-run的能力),从而减少出错的概率。我们知道Terraform的工作流程是Write->Plan->Apply。在写入tf文件(Write)之后,执行操作(Apply)之前,有一个Plan阶段,用于告知客户下一步操作的执行计划和可能的影响。Plan会将资源的当前状态与用户的预期状态进行比较,给出一个对系统没有实质性影响的执行计划。如果用户发现执行计划与自己的预期不符,可以返回修改。3.交互式文档虽然用户一开始可能只花30秒浏览文档,但是当深入使用我们的软件时,查看文档是必须的。传统文档不仅看起来枯燥乏味,而且由于缺乏反馈,用户很难记住文档试图传达的知识。(来源:https://arthas.aliyun.com/doc/arthas-tutorials.html?language=en&id=arthas-basics)上图为Arthas提供的交互式文档(学习课程),通过在线“游乐场+引导用户完成“任务”形式,加强反馈,根据阶段给予奖励,可以很好的提升体验。KeepItSimple,Stupid(尽量保持简单)用户希望我们的软件易于使用、易于理解、易于扩展。开发者需要在API、设计、协作等方面保证简单,但简单很难。1、耐心与好奇心成反比。当我们尝试使用一个新的包、工具等时,我们首先面临的是如何引用和安装它。我们会去首页看README,但是人的耐心通常是很有限的……下图是Prometheus的GetStarted页面:(来源:https://prometheus.io/docs/introduction/first_steps/)它不仅有一大段文字,甚至还有配置文件,无形中给用户造成不小的心理负担。如果用户想尝试,可能要找半个小时的空余时间,鼓起勇气,端正身体,然后按照文档开始尝试。再来看看rustup的主页:(来源:https://rustup.rs/)相比之下,深色背景的命令一目了然,在shell中30秒即可执行完毕,所以任何人可以接近在本地零负担快速搭建一个rust环境。自述文件或主页通常是用户接触我们软件的第一个地方。如何抓住用户的好奇心,确实需要认真研究。2、简单就是美简单的美体现在如何优雅地解决问题。在Golang中启动一个go-routine的操作可谓极其简单:不需要导入任何包,也没有其他需要理解和记忆的相关关键字,甚至连go-routine本身的引用都不返回(如何管理go-routine是另一回事)。正是这种简单易用的设计,让程序员在想开始go-routine的时候可以放心。3.约定大于配置。环境和配置自动默认设置,减少了用户一开始需要做的决定,也降低了上手的难度和用户的心理负担。RubyonRails较早地实践了这一概念,并在其框架内应用了许多约定,以降低初学者的入门门槛并提高专家的工作效率。SpringBoot甚至完全是为了方便用户使用Spring框架而创建的。通过一系列自动配置、条件配置等方式,用户只需极少量的配置(甚至零配置)即可“JustRun”。针对不同的使用场景,用户可以选择不同的自定义配置项。这时候,如何优雅的让用户只关心自己想要的配置呢?功能选项构建实体时,需要许多必需和可选参数,传统的两种方法:全部作为传入函数,或者为每个参数写一个包装函数传入一个配置类(或结构体)。以上方法都存在一些问题,更好的方法是使用可变参数。配置。以创建grpcserver为例:不同的用户对配置的关注点可能不同。上面的代码不仅设置了超时时间、消息大小、拦截器等,其他的配置不需要关心。这样的设计可以方便用户灵活选择自己想要的或者自定义的配置项。4.不仅易于使用,而且免费“不用的东西,你不用付费。而且:你用的东西,你不能用手写代码了。”以上是BjarneStroustrup对C++零成本抽象原理的描述。符合零成本抽象原则的函数和特性不会产生任何全局开销(不使用就不会产生开销),任何比这个函数抽象层次低的手写代码也不会有更好的性能。事实上,对于用户来说,为了提升用户体验而付出一定的代价通常是值得的,比如使用泛型让代码更优雅,而代价可能是代码扩展或者运行时开销。然而,对于实现零成本抽象的功能,它不仅提高了用户体验,而且不会引入任何额外的成本。显然,设计一个既好用又免费的功能是非常困难的,但它带来的价值也是巨大的。Rust引入Ownership和Borrowing的概念,让自动内存管理在编译时完成,省去手动申请内存释放的成本和运行时GC的成本。这一特性让Rust迅速受到用户的追捧和蜂拥而至。5.关注结果,而不是过程。如果允许用户直接描述ta想要的结果,那么用户就不必指定具体的工作过程。下面的代码描述了java语言中单词统计的实现:先将单词映射到(word,count)-pair,然后聚合相同的单词,最后得到结果。这是一种程序方法。但是,如果使用SQL的声明式实现,请看下图:SQL语言只是描述了用户想要的结果,用户不需要询问,也不需要关心得到这个结果的过程。另外,在K8S声明式API设计中,除了可以灵活描述结果状态外,还可以保证操作的幂等性,用户体验非常好。显然,声明式API比过程式API具有更高的抽象级别,但这也意味着声明式API更难实现。常见的声明式API的实现多是基于解决特定领域的问题,不具备图灵完备性。结束语本文主要讨论构建对开发人员友好的软件需要包含的三个要素,并通过一些示例证明这些要素本身的必要性。综上所述,我们认为对开发者友好的软件:首先应该遵循一些领域内的常识和约定,以免用户在使用过程中感到困惑。其次,尽量引导用户做出正确的操作,同时降低试错成本,提升学习体验。最后,设计和交互应尽可能简单,以便于使用、理解和扩展。【本文为专栏作家《ThoughtWorks》原创稿件,微信公众号:Thinkworker,转载请联系原作者】点此查看该作者更多好文