怎么设计一个简单又直观的接口?

news2024/11/27 17:40:16

文章目录

  • 问题的开端
    • 为什么从问题开始?
    • 自然而来的接口
  • 一个接口一件事情
    • 减少依赖关系
    • 使用方式要“傻”
  • 小结

开放的接口规范是使用者和实现者之间的合约。既然是合约,就要成文、清楚、稳定。合约是好东西,它可以让代码之间的组合有规可依。但同时它也是坏东西,让接口的变更变得困难重重。

接口设计的困境,大多数来自于接口的稳定性要求。摆脱困境的有效办法不是太多,其中最有效的一个方法就是要保持接口的简单直观。那么该怎么设计一个简单直观的接口呢?

问题的开端


软件接口的设计,要从真实的问题开始。

一个解决方案,是从需要解决的现实问题开始的。要解决的问题,可以是用户需求,也可以是现实用例。面对要解决的问题,我们要把大问题分解成小问题,把小问题分解成更小的问题,直到呈现在我们眼前的是公认的事实或者是可以轻易验证的问题。

比如说,是否可以授权一个用户使用某一个在线服务呢?这个问题就可以分解为两个小问题:

  • 该用户是否为已注册的用户?
  • 该用户是否持有正确的密码?

我们可以使用思维导图来描述这个分解。
在这里插入图片描述

  • 分解问题时,我们要注意分解的问题一定要“相互独立,完全穷尽”(Mutually Exclusive and Collectively Exhaustive)。这就是MECE原则。使用MECE原则,可以帮助我们用最高的条理化和最大的完善度理清思路。

如何理解这个原则呢?这也是金字塔原理的一个重要思想。

先来说一下“相互独立”这个要求。问题分解后,我们要仔细琢磨,是不是每一个小问题都是独立的,都是可以区分的事情。

我们以上面的分解为例子,仔细看会发现这种划分是有问题的。因为只有已经注册的用户,才会持有正确的密码。而且,只有持有正确密码的用户,才能够被看作是注册用户。这两个小问题之间,存在着依赖关系,就不能算是“相互独立”。

我们要消除掉这种依赖关系。

变更后,就需要两个层次的表达。第一个层次问题是,该用户是否为已注册的用户?这个问题,可以进一步分解为两个更小的问题:用户持有的用户名是否已注册? 用户持有的密码是否匹配?

该用户是否是已注册的用户?

a. 用户名是否已注册?

b.用户密码是否正确?

在这里插入图片描述

  • 除了每一项都要独立之外,我们还要琢磨,是不是把所有能够找到的因素,都找到了?也就是说,我们是否穷尽了所有的内容,做到了“完全穷尽”?

你可能早已经注意到了上述问题分解的缺陷。如果一个服务,对所有的注册用户开放,上面的分解就是完备的。否则,我们就漏掉了一个重要的内容,不同的注册用户,可以访问的服务可能是不同的。也就是说如果没有访问的权限,那么即使用户名和密码正确也无法访问相关的服务。

如果我们把漏掉的加上,这个问题的分解可以进一步表示为

该用户是否是已注册的用户?

a. 用户名是否已注册?

b.用户密码是否正确?

2.该用户是否有访问的权限?

在这里插入图片描述
完成上述的分解后,对于是否授权用户访问一个服务这个问题,我们就会有一个清晰的思路了。

为什么从问题开始?

为什么我们要遵循“相互独立,完全穷尽”的原则呢?

只有完全穷尽,才能把问题解决掉。否则,这个解决方案就是有漏洞的,甚至是无效的。

只有相互独立,才能让解决方案简单。否则,不同的因素纠缠在一起,既容易导致思维混乱,也容易导致不必要的复杂。

还有一个问题,我们也要清楚地理解。那就是,为什么要从问题开始呢?

从问题开始,是为了让我们能够找到一条主线。然后,围绕这条主线,去寻找解决问题的办法,而不是没有目标地让思维发散。这样,也可以避免需求膨胀和过度设计。

比如说,如果没有一条主线牵制着,按照面向对象编程的思路,我们看到“用户”两个字,马上就会有无限的联想。是男的还是女的呀?姓啥名谁呀?多大岁数了?家住哪儿啊?一系列问题都会冒出来,然后演化成一个庞大的对象。但事实上,对于上面的授权访问问题,我们根本不需要知道这些。

自然而来的接口

把大问题分解成小问题,再把小问题分解成更小的问题。在这个问题逐层分解的过程中,软件的接口以及接口之间的联系,也就自然而然地产生了。这样出来的接口,逻辑直观,职责清晰。对应的,接口的规范也更容易做到简单、稳定。

还记得我们前面说过的Java的命名规范吗?Java类的标识符使用名词或者名词短语,接口的标识符使用名词、名词短语或者形容词,方法的标识符使用动词或者动词短语。这背后的逻辑是,Java类和接口,通常代表的是一个对象;而Java的方法,通常代表的是一个动作。

我们在分解问题的过程中,涉及到的关键的动词和动词短语、名词和名词短语或者形容词,就是代码中类和方法的现实来源。比如,从上面的问题分解中,我们很容易找到一个基础的小问题:用户名是否已注册。这个小问题,就可以转换成一个方法接口。

我们前面讨论过这个接口。下面,我们再来看看这段使用过的代码,你有没有发现什么不妥的地方?

/**
 * Check if the {@code userName} is a registered name.        
 *
 * @return true if the {@code userName} is a registered name.
 */
boolean isRegisteredUser(String userName) {
    // snipped
}

不知道你看到没有,这个方法的命名是不妥当的。

根据前面的问题分解,我们知道,判断一个用户是不是注册用户,需要两个条件:用户名是否注册?密码是否正确?

上面例子中,这个方法的参数,只有一个用户名。这样的话,只能判断用户名是不是已经被注册,还判断不了使用这个用户名的用户是不是真正的注册用户。

如果我们把方法的名字改一下,就会更符合这个方法的职能。

/**
 * Check if the {@code userName} is a registered name.        
 *
 * @return true if the {@code userName} is a registered name.
 */
boolean isRegisteredUserName(String userName) {
    // snipped
}

如果你已经理解了我们前面的问题分解,你就会觉得原来的名字有点儿刺眼或者混乱。这就是问题分解带给我们的好处。问题的层层简化,会让接口的逻辑更直观,职责更清晰。这种好处,也会传承给后续的接口设计。

一个接口一件事情

我们提到过一行代码只做一件事情,一块代码只做一件事情。一个接口也应该只做一件事情。

如果一行代码一件事,那么一块代码有七八行,不是也应该做七八件事情吗?怎么能说是一件事情呢?这里我们说的“事情”,其实是在某一个层级上的一个职责。授权用户访问是一件完整、独立的事情;判断一个用户是否已注册也是一件完整、独立的事情。只是这两件事情处于不同的逻辑级别。也就是说,一件事情,也可以分几步完成,每一步也可以是更小的事情。有了逻辑级别,我们才能分解问题,接口之间才能建立联系。

对于一件事的划分,我们要注意三点。

  • 一件事就是一件事,不是两件事,也不是三件事。

  • 这件事是独立的。

  • 这件事是完整的。

如果做不到这三点,接口的使用就会有麻烦。

比如下面的这段代码,用于表示在不同的语言环境下,该怎么打招呼。在汉语环境下,我们说“你好”,在英语环境下,我们说“Hello”。

/**
 * A {@code HelloWords} object is responsible for determining how to say
 * "Hello" in different language.
 */
class HelloWords {
    private String language = "English";
    private String greeting = "Hello";

    // snipped 

    /**
     * Set the language of the greeting.
     *
     * @param language the language of the greeting.
     */
    void setLanguage(String language) {
        // snipped 
    }

    /**
     * Set the greetings of the greeting.
     *
     * @param language the greetings of the greeting.
     */
    void setGreeting(String greeting) {
        // snipped 
    }

    // snipped 
}

这里涉及两个要素,一个是语言(英语、汉语等),一个是问候语(Hello、你好等)。上面的这段代码,抽象出了这两个要素。这是好的方面。

看起来,有两个独立的要素,就可以有两个独立的方法来设置这两个要素。使用setLanguage()设置问候的语言,使用setGreeting()设置问候的问候语。看起来没什么毛病。

但这样的设计对用户是不友好的。因为setLanguage()和setGreeting()这两个方法,都不能表达一个完整的事情。只有两个方法合起来,才能表达一件完整的事情。

这种互相依赖的关系,会导致很多问题。 比如说:

  1. 使用时,应该先调用哪一个方法?

  2. 如果语言和问候语不匹配,会出现什么情况?

  3. 实现时,需不需要匹配语言和问候语?

  4. 实现时,该怎么匹配语言和问候语?

这些问题,使用上面示例中的接口设计,都不好解决。 一旦接口公开,软件发布,就更难解决掉了。

减少依赖关系

有时候,“一个接口一件事情”的要求有点理想化。如果我们的设计不能做到这一点,一定要减少依赖关系,并且声明依赖关系。

一般来说一个对象,总是先要实例化,然后才能调用它的实例方法。构造方法和实例方法之间,就有依赖关系。这种依赖关系,是规范化的依赖关系,有严格的调用顺序限制。编译器可以帮我们检查这种调用顺序。

但是,我们自己设计的实例方法之间的依赖关系,就没有这么幸运了。这就要求我们弄清楚依赖关系,标明清楚依赖关系、调用顺序,以及异常行为。

下面的这段代码,摘录自OpenJDK。这是一个有着二十多年历史的,被广泛使用的Java核心类。这段代码里的三个方法,有严格的调用顺序要求。要先使用initSign()方法,再使用update()方法,最后使用sign()方法。这些要求,是通过声明的规范,包括抛出异常的描述,交代清楚的。

/*
 * Copyright (c) 1996, 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * <snipped>
 */

package java.security;

import java.security.InvalidKeyException;
import java.security.PrivateKey;
import java.security.SignatureException;
import java.security.SignatureSpi;

/**
 * The Signature class is used to provide applications the functionality
 * of a digital signature algorithm. Digital signatures are used for
 * authentication and integrity assurance of digital data.
 * 
 * <snipped>
 * 
 * @since 1.1
 */
public abstract class Signature extends SignatureSpi {
    // snipped

    /**
     * Initialize this object for signing. If this method is called
     * again with a different argument, it negates the effect
     * of this call.
     *
     * @param privateKey the private key of the identity whose signature
     * is going to be generated.
     *
     * @exception InvalidKeyException if the key is invalid.
     */
    public final void initSign(PrivateKey privateKey)
            throws InvalidKeyException {
        // snipped
    }

    /**
     * Updates the data to be signed or verified, using the specified
     * array of bytes.
     *
     * @param data the byte array to use for the update.
     *
     * @exception SignatureException if this signature object is not
     * initialized properly.
     */
    public final void update(byte[] data) throws SignatureException {
        // snipped
    }

    /**
     * Returns the signature bytes of all the data updated.
     * The format of the signature depends on the underlying
     * signature scheme.
     *
     * <p>A call to this method resets this signature object to the state
     * it was in when previously initialized for signing via a
     * call to {@code initSign(PrivateKey)}. That is, the object is
     * reset and available to generate another signature from the same
     * signer, if desired, via new calls to {@code update} and
     * {@code sign}.
     *
     * @return the signature bytes of the signing operation's result.
     *
     * @exception SignatureException if this signature object is not
     * initialized properly or if this signature algorithm is unable to
     * process the input data provided.
     */
    public final byte[] sign() throws SignatureException {
        // snipped
    }    

    // snipped
}

然而,即使接口规范里交待清楚了严格的调用顺序要求,这种设计也很难说是一个优秀的设计。用户如果不仔细阅读规范,或者是这方面的专家,很难第一眼就对调用顺序有一个直观、准确的认识。

这就引出了另一个要求,接口一定要“皮实”

使用方式要“傻”

所有接口的设计,都是为了最终的使用。方便、皮实的接口,才是好用的接口。接口要很容易理解,能轻易上手,这就是方便。此外还要限制少,怎么用都不容易出错,这就是皮实。

上面的OpenJDK例子中,如果三个方法的调用顺序除了差错,接口就不能正常地使用,程序就不能正常地运转。既不方便,也不皮实。

小结

1、从真实问题开始,把大问题逐层分解为“相互独立,完全穷尽”的小问题;

2、问题的分解过程,对应的就是软件的接口以及接口之间的联系;

3、一个接口,应该只做一件事情。如果做不到,接口间的依赖关系要描述清楚。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1349243.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Stable Diffusion API入门:简明教程

Stable Diffusion 是一个先进的深度学习模型&#xff0c;用于创造和修改图像。这个模型能够基于文本描述来生成图像&#xff0c;让机器理解和实现用户的创意。使用这项技术的关键在于掌握其 API&#xff0c;通过编程来操控图像生成的过程。 在探索 Stable Diffusion API 的世界…

爱思唯尔的KBS——模板、投稿、返修、接收的总结

第二篇论文终于是接受了QAQ&#xff0c;被审稿人疯狂拖时间&#xff0c;KBS是真难绷啊 由于之前发布过关于爱思唯尔旗下的ESWA博客&#xff0c;KBS和ESWA是类似的&#xff0c;因此本篇博客主要说下区别以及期间碰到的各种情况&#xff0c;有疑问依然可以在评论区说&#xff0c;…

【C语言】函数

函数是什么&#xff1f; “函数”是我们早些年在学习数学的过程中常见的概念&#xff0c;简单回顾一下&#xff1a;比如下图中&#xff0c;你给函数 f(x)2*x3 一个具体的x,这个函数通过一系列的计算来返回给你一个结果(图示如下)。 这就是数学中函数的基本过程和作用。但是你…

48、激活函数 - 梯度消失和梯度爆炸

简单介绍下梯度消失和梯度爆炸,这个不是重点,但是我觉得有必要再深入了解这个概念,以及很多激活函数为什么是可以防止梯度消失的。 梯度消失和梯度爆炸实际上是在神经网络训练过程中经常会遇到的两类问题,这两类问题都与梯度有关。 什么是梯度 在神经网络训练中,梯度是指…

Final Cut 视频剪辑快速入门,小白上手视频课的制作

本文是一个快速入门教程&#xff0c;如果您是0视频处理基础&#xff0c;又想录制网课或是一些对效果要求不高的视频那么这篇教程足够使用了。 本文主要用Final Cut处理视频课&#xff0c;本文是笔者在制作视频课过程中逐渐摸索的&#xff0c;如果您想制作一些比较专业的视频&a…

mysql原理--Explain详解

1.概述 一条查询语句在经过 MySQL 查询优化器的各种基于成本和规则的优化会后生成一个所谓的 执行计划 &#xff0c;这个执行计划展示了接下来具体执行查询的方式&#xff0c;比如多表连接的顺序是什么&#xff0c;对于每个表采用什么访问方法来具体执行查询等等。设计 MySQL 的…

【LMM 003】生物医学领域的垂直类大型多模态模型 LLaVA-Med

论文标题&#xff1a;LLaVA-Med: Training a Large Language-and-Vision Assistant for Biomedicine in One Day 论文作者&#xff1a;Chunyuan Li∗, Cliff Wong∗, Sheng Zhang∗, Naoto Usuyama, Haotian Liu, Jianwei Yang Tristan Naumann, Hoifung Poon, Jianfeng Gao 作…

uni-app 前后端调用实例 基于Springboot 下拉刷新实现

锋哥原创的uni-app视频教程&#xff1a; 2023版uniapp从入门到上天视频教程(Java后端无废话版)&#xff0c;火爆更新中..._哔哩哔哩_bilibili2023版uniapp从入门到上天视频教程(Java后端无废话版)&#xff0c;火爆更新中...共计23条视频&#xff0c;包括&#xff1a;第1讲 uni…

UDS诊断(ISO14229-1) 11服务

文章目录 功能简介应用场景请求和响应1、请求2、子功能3、肯定响应4、否定响应 报文示例UDS中常用 NRC参考 功能简介 11服务&#xff0c;即 ECUReset&#xff08;ECU重置&#xff09;服务&#xff0c;用来控制MCU进行重启&#xff0c;重启分为硬件重启和软件重启。 应用场景…

事件循环的理解

1.单线程 Js是一个单线程的语言,代码只能一行一行去执行,遇到同步的代码就直接执行了,如果遇到异步的代码怎么办&#xff1f; 不可能等到异步的代码执行完&#xff0c;在去执行后面同步的代码。 2.主线程 遇到同步的代码,就在主线程里面直接执行了。 3.任务队列 遇到异步的…

Vue中使用JavaScript中的requestAnimationFrame动画循环实现循环滚动效果-demo

效果 requestAnimationFrame是一个由浏览器提供的 JavaScript 方法&#xff0c;用于在下一次浏览器重绘之前执行指定的回调函数。它接受一个回调函数作为参数&#xff0c;并返回一个整数值&#xff0c;可以用于取消动画循环。 使用 requestAnimationFrame 可以创建平滑的动画效…

【AIGC-图片生成视频系列-5】I2V-Adapter:一种用于视频扩散模型的通用图像生成视频适配器

目录 一. 项目与贡献概述 二. 方法详解 a. 整体框架图 b. 帧相似性先验 三. 一般化图像生成动画结果 四. 基于个性化 T2I 模型的动画结果 五. 结合ControlNet动画结果 六. 项目论文和代码 七. 个人思考与总结 在快速发展的数字内容生成领域&#xff0c;焦点已从文本到…

C#中的Attribute详解(下)

C#中的Attribute详解&#xff08;下&#xff09; 一、Attribute本质二、Attribute实例化三、Attribute实例化的独特之处四、元数据的作用五、自定义Attribute实例六、Attribute的附着目标七、附加问题 一、Attribute本质 从上篇里我们可以看到&#xff0c;Attribute似乎总跟pu…

Linux基础知识点(五-信号)

一、信号的基本概念 1.1 信号的概念 信号&#xff08;signal&#xff09;&#xff0c;又称为软中断信号&#xff0c;用于通知进程发生了异步事件&#xff0c;它是Linux系统响应某些条件而产生的一个事件&#xff0c;它是在软件层次上对中断机制的一种模拟&#xff0c;是一种异…

12.29最小生成数K算法复习(注意输入输出格式),校园最短路径(通过PRE实现路径输出,以及输入输出格式注意)

7-2 最小生成树-kruskal算法 分数 15 const int maxn 1000; struct edge {int u, v, w; }e[maxn]; int n, m, f[30]; bool cmp(edge a, edge b) {return a.w < b.w; } int find(int x) {if (f[x] x) {return x;}else {f[x] find(f[x]);return f[x];} } //int arr[100…

简单FTP客户端软件开发——VMware安装Linux虚拟机(命令行版)

VMware安装包和Linux系统镜像&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1UwF4DT8hNXp_cV0NpSfTww?pwdxnoh 提取码&#xff1a;xnoh 这个学期做计网课程设计【简单FTP客户端软件开发】需要在Linux上配置 ftp服务器&#xff0c;故此用VMware安装了Linux虚拟机&…

数据结构模拟实现LinkedList双向不循环链表

目录 一、双向不循环链表的概念 二、链表的接口 三、链表的方法实现 &#xff08;1&#xff09;display方法 &#xff08;2&#xff09;size方法 &#xff08;3&#xff09;contains方法 &#xff08;4&#xff09;addFirst方法 &#xff08;5&#xff09;addLast方法 …

python开发的app有哪些,python如何开发小软件

这篇文章主要介绍了python开发的app有哪些&#xff0c;具有一定借鉴价值&#xff0c;需要的朋友可以参考下。希望大家阅读完这篇文章后大有收获&#xff0c;下面让小编带着大家一起了解一下。 Python 无处不在&#xff0c;可以说是现代的 C 编程语言&#xff0c;你可以在任何地…

JavaScript的三种引入的方式

目录 (一).什么是JS1.1JS的特点1.2JS的组成 (二).JS引用的三种方式2.1标签引用&#xff08;或嵌入式)2.2文件引用&#xff08;外链式&#xff09;2.3行内式 (三).JS三种引用方式的优缺点1.行内方式&#xff1a;2.标签引用&#xff08;或嵌入式&#xff09;&#xff1a;3.文件引…

Codeforces Round 900 (Div. 3)(A-F)

比赛链接 : Dashboard - Codeforces Round 900 (Div. 3) - Codeforces A. How Much Does Daytona Cost? 题面 : 思路 : 在序列中只要找到k&#xff0c;就返回true ; 代码 : #include<bits/stdc.h> #define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)…