在程序员的生涯中,无论是在跳槽还是晋升的时候都有遇到各种各样被考察的情况,随着软件开发工作资历不断积累,这些考察要求和条件逐渐变高。

不会单纯用工作年限来判断候选人是否合格,因为毕竟写三年 CRUD 业务和做三年系统研发的工作经历和工作质量是大不相同的,这样会存在不小的问题,尤其是在招聘高级开发工程师时。而算法题面试也一样,跳槽必刷题,已经很难分辨候选人是当场想出来的,还是刷题记住的答案。传统的八股文、算法、前沿技术源码等知识内容已经不再新鲜,而一些"软技能"像系统设计面试将会考察环节中较为重要的一环。

为了能让面试更有效,采用系统设计题来考察高级开发工程师就是很好的选择。因为系统设计题覆盖面广,不像算法题那样在网上四处流传,候选人想刷也没法刷,只能拼技术。

这些软技能并不会直接考察所了解到的某个知识点或者是某一片知识点所连接起来的部分知识面,而是去查看一位工程师在整体层面和理解应用程序设计上的功底是否深厚;这类问题不像算法题那样在网上四处流传,想刷题也没法刷,只能拼技术。虽然不会牵扯到某个知识点,比如某个控件是如何实现并说出内部实现原理,但是会让谈下这个控件的优点有哪些或如何在原有的基础上进行改进;

这样能让整个考察环节变得更加有效,面试官就能准确地判断出候选人的技术实力,同时从而给到合适的待遇和定级。

在面试系统设计题时,又该怎么评价候选人呢?对此,大家可以参考下图的总结。

在本文中,我将分享客户端中关于如果被问及到如何设计一个系统的设计问题该如何进行回答,以及我是如何去解决系统设计问题的秘诀。听起来很有趣?……keep reading

系统设计问题涉及的问题

Eason去观看了很多不同类型的系统设计教程视频,并做了一些关于这方面的总结,我觉得这些总结不仅仅是适用于系统设计,也是另一种对自己技术栈的要求和查漏补缺的方式;下面是罗列出来程序设计中必须要考虑到的方面:

1.功能需求——首先需要定义应用的用例和一些功能。

2.非功能性要求——定义性能、经验和规模要求。

3.假设——定义问题的边界、任何规模约束、特征等。

4.客户端-服务器通信——定义连接选项,如 HTTP 请求、轮询、服务器端事件。

5.API 设计——定义所构建的功能端点。

6.数据模型——定义对象的数据模型字段。

7.应用流程(用例流程) ——定义并执行正在设计的功能的用户流程。

8.性能和工具——定义大家将如何收集数据和指标以查看应用程序的性能——内存利用率、CPU 利用率。

9.ADA — 确保大家定义了辅助功能以及应用程序的可访问性。

10.国际化——随着应用程序在国际上的发展,需要处理这款系统应用的国际适配。

11.安全性——定义如何去保护应用程序。

具体实现

现在,让我们通过一个例子来说明设计一个系统为何需要以上的这些结构:假设题目是被要求设计一个能展现出日常生活中的趣闻趣事App;

1. 功能需求

显示我周围有趣的地方列表。

向下滚动时加载更多

2. 非功能性需求

该列表应该加载非常快或具有低延迟

滚动性能

数据异步加载且显示不应错位

列表不应抖动

3.假设

每天有多少活跃用户?用户量级多少?

API 是否可用?我们需要明智地使用它们

4.客户端-服务器通信

常规 HTTP 请求——这是客户端向服务器请求数据/资源的最通用用例场景。例如,获取提要中提要项目的列表一般会采用哪些轮询方式?

定期轮询- 客户端可以不断向服务器发出请求以获取最新信息,但很多时间服务器可能没有任何更新可提供。这不仅导致大部分时间响应为空,而且在多次设置 HTTP 连接时会浪费资源。

长轮询——如果我们有一个用例,我们知道内容不会从服务器频繁更新。在这种情况下,我们可以做的就是使用长轮询。客户端会与服务器建立 HTTP 连接,并保持与服务器的连接打开,以便服务器可以在有任何推送时向客户端推送更新。这样我们就节省了打开和关闭与服务器连接的无用工作。并需要记住,这个长轮询连接也可能超时,因此在这种情况下需要重新启动连接。

WebSockets——在客户端和服务器都可以发起通信并且客户端和服务器之间不断来回的情况下,我们可以使用 WebSockets。这个用例的一个很好的例子是聊天应用程序。

服务器端事件 (SSE) —客户端与服务器建立持久性和长期连接。服务器使用此连接发送数据。客户端处于侦听模式,因为只有服务器才能与客户端通信。如果客户端需要与服务器通信,则需要使用不同的协议。这个用例是说,我们有一个社交媒体应用程序,列出了朋友的提要。但是现在,如果在我们使用应用程序时刚刚发生了更新,客户端可以通过服务器端事件 (SSE) 协议轻松获取这些更新。

5. API 设计

API 通过协议相互通信。常见的协议有 SOAP、REST 和 GraphQL。有关这些的详细信息虽然并不是本文的一部分,不过大家感兴趣可以自己去拓展一下。对于我们这个展示感兴趣内容的App,REST 应该没问题,它基本适用于大多数的场景。根据我们的要求,需要设计以下类型的Api:

a) 我们需要一个地点列表

b) 由于我们列出了附近的地点,因此我们想到的一件事是 API 调用需要某种位置信息来返回数据,且可以是我们的查询参数。

c) 分页处理。考虑到这是一个移动应用程序,我们没有像台式机或笔记本电脑那样的无限资源,如计算/连接和电源,我们需要小心获取的数据量,不能同时一次性全部加载出来或者把数据下拉下来。因此,最好分块获取数据并在需要时获取更多数据。所以我们需要有能力分页请求数据,这就是分页的用武之地。Offset、KeySet 和 Cursor Based 是不同类型的分页服务器支持。对于我们的用例,我们可以选择其中任何一个。但我个人更偏向喜欢用偏移分页。所以我们会有一个页码和页数限制来限制资源的数量。考虑到这一点,我们的 API 可能看起来像这样?

GET — /places?lat={}&long={}&page={}&pageLimit={}

6. 数据模型

在设计某个系统模型的时候,得把基础架构给先定好,好比系统应用是高楼,数据模型则是地基;在本文中,将向大家介绍如何是高效并有意义的潜在数据模型。考虑到整个需求涉及到的功能,所以将数据模型设计成下述图表(简化后的模型)👇

7. 应用流程

以下是我们可以可能需要用到的任何设计架构模式。

可以是:

  • MVC——模型视图控制器
  • MVVM——模型视图视图模型
  • MVP——模型视图Presenter
  • VIPER等

对于一般较为简单的用例,我采用比较多的是MVVM。大家平时应该使用常用并有意义的模式,并且在工作上也同样可以采取照搬的方式套用过去,从而提高工作效率,减少造轮子的时间。

8. 性能

这节本来是可以放在解决非功能性需求和任何其他与性能相关部分,因为是考察环节中比较重要的一点所以再次拿出来展开和大家聊一下,App设计中有哪些方法可以用来提高应用程序性能?

  1. 布局优化:
  • 删除布局中无用的控件和层次,其次有选择地使用性能比较低的ViewGroup。
  • 采用标签,标签,ViewStub.
  • 避免过度绘制
  1. 绘制优化:
  • onDraw中不要创建新的局部对象。
  • onDraw方法中不要做耗时的任务,循环执行方法后十分抢占CPU的时间片,这会造成View的绘制过程不流畅
  1. 内存泄漏优化:
  • 在开发过程中避免写出有内存泄漏的代码
  • 通过一些分析工具比如MAT来找出潜在的内存泄露,然后解决。
  1. 响应速度优化:响应速度优化的核心思想就是避免在主线程中做耗时操作。

  2. ListView/RecycleView及Bitmap优化

  • 使用ViewHolder模式来提高效率
  • 异步加载:耗时的操作放在异步线程中
  • ListView/RecycleView的滑动时停止加载和分页加载
  • 对加载图片进行压缩,避免加载图片多大导致OOM出现。
  1. 线程优化:线程优化的思想就是采用线程池,避免程序中存在大量的Thread。

  2. 其他性能优化的建议

    ① Show Skeleton - 某种东西正在加载的感知概念让用户感觉数据加载速度更快。在加载数据时显示一些 Skeleton 会对感知的用户体验产生巨大影响

    ② 使用具有适当尺寸的图像。如果 API 支持高度和宽度作为查询参数,那么大家将能拿到对图片显示有意义的结果。用户的眼睛无法分辨图像大小之间的差异,但它因为加载的数据较少从而对应用程序的性能产生巨大影响。

    ③ 使用内容交付网络 (CDN)进行静态图像缓存,以帮助更快地交付内容。在我们的App中,将 CDN 用于图像和静态内容,将地点描述文本和图像缓存至云端。

    ④ 在后台线程上加载数据,这样主线程永远不会被阻塞,UI 永远不会卡住。

以上是一些常见优化性能的做法,大家也可以在面试期间去主动深入了解其他的方法。

9.可访问性

我们始终希望每个人都使用我们的应用程序。为了让它真正具有包容性,我们需要在我们的应用程序中处理可访问性。以下是我们可以做的一些事情来解决可访问性问题:

  • 确保颜色对比度正确
  • 每个目标尺寸至少为 44pt
  • 为可访问性标注添加自定义调用
  • 使用适当的配色方案
  • 支持动态字体类型
  • 对例如GTXilib 的库使用自动检查,以便他们可以确保每个 UI 元素都有一个标签、适当的特征、标签不是多余的。

上面提到的做法可以应用于任何移动应用程序。大家也可以在 Google 和Apple网站上阅读有关可访问性的更多信息。

10. 国际化/本地化

应用程序的国际化为其用户提供了本地和个人体验,这对应用程序的使用和采用很有帮助。通常,我们可以通过为应用程序中的内容/文本使用不同的本地化字符串文件 strings.xml 来支持这一点。

大家还可以使用高级技术,例如将设备区域设置发送到服务器,然后服务器返回本地化文件。根据需要,大家可以采取不同的路线来实现本地化,这也是Goolge 官方对于本地化的指南。

11. 安全

安全是一个非常广泛和复杂的话题。作为移动应用程序的开发人员,大家需要在平时日常工作中学会并遵循一些提示和技巧:

  • 使用签名和加密数据
  • 编译与反编译
  • NDK与反汇编
  • 加壳与脱壳

结论

系统设计的重点是考察我们作为工程师从整体层面来看待/理解应用程序设计的能力,而不仅仅是单个部分知识点或知识面的细节。关于系统设计的答案,从来就不存在最优解,毕竟世上本来就没有完美的答案。