App development summary

The architecture varies from person to person, and different architects have different views; the architecture also varies from project to project, and different projects have different requirements, and the corresponding architecture will also be different. However, there are some things that are general, that all architects need to consider, and that all projects will have requirements, such as how to design the API? How is the architecture layered? How to separate development environment and production environment? In the past few years, I have been responsible for the research and development of apps, including catering, social networking, smart home, e-commerce, news media and so on. When you have a certain experience, you will always have some of your own experience. The following content is a summary of my experience on the above issues based on my experiences.

Start with the API

The core thing of an app is actually data, and the main source of data is API. The project I was in charge of before has suffered a lot because of the pit of the API. Therefore, I will start with the API for the architecture design of the App project in the future.

develop safety mechanisms

The first thing to consider when designing an API is the security mechanism of the API. The last project I was in charge of was attacked twice because of API security issues. After analysis, there are mainly two vulnerabilities: one is the lack of a way to verify the caller's security, and the other is that the data transmission is not secure enough. Then, the security mechanism of API is mainly to solve these two problems:

  1. Ensure that the caller of the API is an App authorized by itself;
  2. Ensure the security of data transmission.

The solution to the first problem, I mainly adopt the way of designing the signature. For each client, Android, iOS, WeChat, assign an AppKey and AppSecret respectively. When calling the API, add the AppKey to the request parameter list, and add the AppSecret and all parameters together to generate a signature string according to a certain signature algorithm, and then bring the signature string along with the API when calling the API. After the server receives the request, it queries the corresponding AppSecret according to the AppKey in the request, and generates a signature string according to the same signature algorithm. When the signature generated by the server is consistent with the signature brought by the request, it means this The caller of the request is authorized by itself, proving that the request is secure. Moreover, each end has a Key, which is also convenient for identification and statistics of different ends. In order to prevent AppSecret from being acquired by others, this AppSecret is usually written in the code. In addition, the signature algorithm also needs to have a certain degree of complexity, and it cannot be easily cracked by others. In addition, adding another timestamp to the parameter list can also prevent partial replay attacks.

The solution to the second problem is mainly to use HTTPS. HTTPS automatically compresses and encrypts the request data because of the addition of the SSL security protocol. In certain programs, it can prevent monitoring, hijacking, and retransmission, mainly to prevent man-in-the-middle attacks. Apple has adopted HTTPS by default since iOS9. And on how to use HTTPS in Android, Google has also given a lot of security advice. However, most apps do not follow the security recommendations. The main reason is that the SSL certificate is not checked for security, which has become a big loophole. The middleman can use this loophole to pass the check with a fake certificate, and thus can hijack the SSL certificate. All data is gone. Therefore, for security reasons, it is recommended to perform strong verification on the SSL certificate, including whether the signing CA is legal, whether the domain name matches, whether it is a self-signed certificate, whether the certificate has expired, etc.

Standardization of interface protocols

The data returned by the API is generally transmitted in JSON format. However, JSON values ​​have only six data types:

  • Number: Integer or floating point number
  • String: String
  • Boolean:true 或 false
  • Array: The array is enclosed in square brackets []
  • Object: The object is enclosed in curly braces {}
  • Null: empty type

Most of the pitfalls about the API that I have encountered are caused by errors in the conversion of JSON data and entity objects, and there are various errors, including some very strange errors.

The most troublesome thing is to deal with the Date type, because JSON itself does not have a Date type. Therefore, the JSON library will convert the Date type data to String when serializing it. At this time, different environments, different platforms, and different JSON parsing libraries, the converted results are often different. For example, the result you may get on the development machine is "2016-1-1 17:11:11", but when you put it on the server, the result becomes "Jan 1,2016 5:11:11 PM". It will undoubtedly fail when deserializing. Later, I canceled all Date types and used timestamps to represent them uniformly, so there was no more trouble of conversion.

In addition, the developer of the interface sometimes wrongly converts some data to String, which causes the client to use it with an exception due to the wrong type. For example, when 1, which was originally a number, is converted to "1", an error occurs when the client performs an operation, or an error occurs when using switch to judge, or other situations that cannot be converted occur; for example, when it is empty, the JSON is correct The ground indicates that it should be null, but if it becomes "null" when converted to String, then the problem comes. I have encountered the program crash caused by this wrong conversion several times. The first time, It took a whole day to locate the problem.

Also, because the developers of the interface are different, there are many times that the parameter names with the same meaning in different interfaces are different. For example, for an interface with paging data, there are generally parameters for the current page. Developer A may name the parameter currentPage, and the first page starts from 0; developer B names another interface as currPage, and the first page But it starts from 1; C developers also named it presentPage in another interface, and the first page starts from 0 again. The developer of the client is also drunk.

Each technical team generally has an interface protocol document, which mainly includes the description of each interface, input parameters, output results, etc., but it is generally not rigorous, and there is no unified standard in many places, which is prone to many pitfalls. Therefore, it is very important to have a unified standard and strictly enforced interface protocol. In addition to specifying each interface, including the specific data type of each data in the interface, the content of the protocol also needs to specify a set of shared data dictionaries and other information that needs to be uniformly defined, such as signature algorithms. Once there is this unified standard and strictly enforced interface protocol, many problems will be solved.

interface versioning

We have more than once caused errors in the old version of the app due to changes in the interface, and the change is not necessarily a modification of the interface itself. It may be that a new data structure has been added to the bottom layer, and the interface returns the new data to the client. , but the old version of the client cannot be parsed, resulting in an error.

In order to solve the compatibility problem of the interface, it is necessary to do a good job of interface version control. In terms of implementation, there are generally two approaches:

  1. Each interface has its own version, generally add a version parameter to the interface;
  2. The entire interface system has a unified version. Generally, the version number is added to the URL, such as http://api.domain.com/v2.

Usually, the update of the small version adopts the first method. Our approach is to do different branch processing according to different version numbers. The update of the major version uses the second method. At this time, it is basically a new interface system, which is relatively independent from the old version.

When there are more and more versions, maintenance becomes a big problem, we don't have as much energy to maintain all versions, so versions that are too old are generally not maintained anymore. At this time, if a user is still using the old version that is about to be discarded, the user needs to be reminded to upgrade to the new version.

Architecture layering

After the design of the API is completed, I will then consider the overall architecture of the App project. How to structure the whole, I have also done a lot of attempts. In the early days, Android completed all operations in Activity, including interface data processing, business logic processing, and calling API. Later, it was found that the Activity became more and more bloated, the code became more and more complex, and it was difficult to maintain. So I began to think about how to split and how to achieve loose coupling and high cohesion.

前面也说过,一个App的核心就是数据,那么,从App对数据处理的角色划分出发,最简单的划分就是:数据管理、数据加工、数据展示。相应的也就有了三层架构:数据层、业务层、展示层。它们之间的关系如下图,数据层是三层中的最底层,往下,它接入API;往上,它向业务层交付数据。业务层夹在三层中间,属于数据的加工厂,将数据层提供上来的数据加工成展示层需要展示的数据。展示层处于三层中的最上层,主要就是将从业务层取得的数据展示到界面上。

数据层

数据层是数据管理者,主要任务就是封装API,并将数据结果交付给上层,中间会再加个数据缓存。整个主流程如下图:

  1. 业务层向数据层请求数据;
  2. 数据层检查缓存中有没有请求需要的数据;
  3. 如果有缓存数据,则直接返回缓存数据;
  4. 如果没有缓存数据,则从网络API获取数据,并将数据加入缓存,然后返回数据。

调用网络API时,还要判断网络状态,根据不同状态做不同处理。如果网络不可用,就无需发起请求了。网络可用时,也要区分是连接WIFI还是连接移动网络。连接移动网络时,一般需要限制调用比较耗流量的请求。曾经,我们没有对移动网络状态下的请求进行限制,结果,测试时流量DuangDuangDuang地一下子就不见了十几M。连接WIFI时,则无需设置这种限制,而且还可以预先请求一些接口,比如请求当前分页数据时,可以将下一页的数据也预先请求。

缓存也需要缓存策略,不同的接口需要做不同的缓存处理。首先,缓存只适用于获取数据的接口,对于修改数据的接口则不适用。其次,不同接口缓存时间一般也不同,对于很少变动的数据缓存时间可以设置长一些,而频繁变动的数据缓存时间则比较短,甚至不进行缓存。最后,缓存数据因为比较多,我们一般保存在数据库,而对于调用频率高、最新的数据,还会在内存中也拥有一份缓存,不过缓存时间比较短。请求缓存数据时,会先检查内存缓存中有没有,有则直接将缓存的数据返回,没有才从数据库获取。

那么,如何将数据交付给业务层呢?这是整个数据层模块与外部交互的部分,当与外部交互的时候,一般都要符合面向接口编程的原则,因此只要提供开放的数据接口就可以了。对于接口的参数需要说明一下,上面提到的参数有appKey、version、currentPage这几个,还有签名sign、时间戳time,其实可以分为两类:系统参数和业务参数。像appKey、version、sign、time这些属于系统参数,而currentPage,或username之类的则属于业务参数。数据层开放的数据接口的参数只需要包含业务参数就可以了,业务层并不需要关心系统参数是什么,系统参数在数据层内部封装API时指定就可以了。

业务层

业务层是数据加工者,主要就是从数据层获取数据,然后经过业务逻辑处理后转化成展示层需要的数据。业务层因为夹在数据层和展示层中间,起着承上启下的作用。也因此,业务层很容易沦落为只是一个数据的中转站,主要就是因为对业务层具体的作用和职责没有理解清楚。

这里用一个例子来说明业务层具体的工作吧,就举个用户注册的例子。用户注册时,界面上需要用户提供手机号、短信验证码、密码、确认密码。那么,最简单的操作就是,带上这些参数调用数据层的注册接口。好了,问题来了,注册接口并没有提供确认密码的参数。那好,调用注册接口之前先判断下密码和确认密码是否一致,不一致则返回错误提示给用户,一致了才调用注册接口。好了,第二个问题来了,用户等网络请求等了一段时间后,请求结果返回说手机号少了一位。下一次,又等了一段时间,这次又返回说手机号多了一位。就因为一个小错误要让用户等那么久,用户肯定有意见。后台也有意见,各种非法的请求都发过来,是嫌服务器压力不够大啊。那好,调用接口之前对这些参数做有效性检查吧,手机号要规范,短信验证码只能为六位数字,密码不能少于六位。终于注册成功了,第三个问题又来了,注册接口是没有返回用户的accessToken的,只有登录接口才会返回。让用户手动再登录一下?这用户体验不太好啊。正确的姿势应该是注册成功后再自动调用一次登录接口,如果因为网络问题第一次登录失败,后面还需要再自动调用多一次,如果还是调用失败,才让用户手动登录。

上面的例子中,对参数的有效性检查,注册成功后的自动登录,都属于业务逻辑的处理,也就是说都是业务层的工作。

业务层交付给展示层的数据也是通过接口的方式,不过,和数据层交付给业务层时不同的是:交付给展示层的数据应该是通过异步回调返回的。因为获取数据是一个比较耗时的任务,通过异步回调才不会阻塞UI主线程。

展示层

展示层作为数据展示者,它只要关心数据如何展示就可以了。不过,数据如何展示却不是那么简单。展示层是三层架构中最复杂的一层了,要考虑的东西远远多于其他两层,涉及的东西包括但不限于界面布局、屏幕适配、图片资源、文本资源、颜色资源等等。在开发一段时间后,展示层出现代码混乱是最常见的。因此,做好展示层,就需要保持高质量的代码。要保持高质量代码,我觉得至少应该遵循几条基本的原则:

  1. 保持规范性:定义好开发规范,包括书写规范、命名规范、注释规范等,并按照规范严格执行;
  2. 保持单一性:布局就只做布局,内容就只做内容,各自分离好,每个方法、每个类,也只做一件事情;
  3. 保持简洁性:保持代码和结构的简洁,每个方法,每个类,每个包,每个文件,都不要塞太多代码或资源,感觉多了就应该拆分。

所谓无规矩不成方圆,展示层的设计,要从开发规范开始。一份好的开发规范,是保证代码有较高的可读性的基础。iOS方面,苹果已经有一套Coding Guidelines,主要属于命名方面的规范。当我们制定自己的开发规范时,首先就要遵守苹果的这份规范,在此基础上再加上自己的规范。Android方面,我也在我的博客中分享过一套(Android技术积累:开发规范),主要分为书写规范、命名规范、注释规范三部分。

最重要的不是开发规范的制定,而是开发规范的执行。如果没有按照开发规范去执行,那开发规范就等于形同虚设,那代码混乱的问题依然得不到解决。

说到单一性,面向对象设计中,有一个基本原则就是单一职责原则,它规定一个类应该只有一个发生变化的原因。保持单一性是减低耦合度的关键标准,其目的就是各方面的解耦。而我这里说的单一性不只是规定类的单一,也包括界面的单一、方法的单一、资源文件的单一等。

界面的单一,首先是界面的布局和界面的数据应该分离。另外,界面数据的获取和展示也应该分离。一句话,保持界面的单一性就是要保持界面上每个维度都做好分离,从界面的布局,到数据的获取,数据的检查,数据的展示。

方法的单一,则表现为一个方法是对一个行为的封装。行为又可以拆分为多个步骤,每个步骤其实也是更细化的行为。因此,方法嵌套方法是一种常态。那么,保持方法的单一性,关键不在于怎么定义这个方法的行为,而在于这个行为要怎么拆分成更细的行为。举个例子,通常在Activity的onCreate方法,做初始化操作,细分出来就分为了:控件的初始化、逻辑变量的初始化、数据的初始化。数据的初始化又可以再细分:数据的获取、数据的展示。每个细化的行为都应该封装为一个独立的方法,这样,才真正符合方法的单一性。

资源文件的单一,主要是指Android的各类资源文件,包括存放字符串的strings.xml,存放字符串数组的arrays.xml,存放颜色值的colors.xml,存放尺寸值的dimens.xml,等等。资源文件的单一,是说所有相关的资源信息要在资源文件里定义并引用到代码或布局文件里,而不是在代码或布局文件里直接定义。这样做,可以很方便地做各种适配和修改,比如支持国际化,比如不同分辨率的屏幕用不同尺寸值。iOS则没有提供和Android一样的资源文件分离的机制,但可以参考Android的做法自己去实现。

环境分离

每个App项目,至少都会有两个环境:测试环境和生产环境。多的甚至有四个环境:开发环境、测试环境、预生产环境和生产环境。开发人员经常需要在环境之间切换,测试人员也同样。经常出现测试人员今天需要测试环境的最新版本,叫App开发人员打包一个给她,明天需要切换到生产版本,再叫App开发人员打包一个生产环境的给她。我们知道,一个App,在一台手机上要么只能是测试环境的,要么只能是生产环境的。测试人员要测试两个环境,只能不断替换不同环境的同个App,这实在太麻烦了。为了解决此问题,最好的方案就是环境分离,不同环境有不同的App。

一个App的唯一标识,Android是用包名,iOS是用Bundle Identify。那么,在一个系统想安装不同环境的App,只要每个环境App的包名和Bundle Identify不同即可。比如,生产版的包名和Bundle Identify命名为com.mydomain.myapp,测试版的包名和Bundle Identify则命名为com.mydomain.myapp.beta,这样,Android和iOS都会识别为两个不同的App了。

不过,只改包名和Bundle Identify是不够的,应用图标和应用名称也要修改,不然安装之后很难区分哪个App是哪个环境的。一般做法就是,非生产环境的App图标就是在生产图标的基础上添加一个环境标签,同时App的应用名称也是在生产的基础上添加环境后缀名。另外,因为包名和Bundle Identify不同了,微信、微博、百度地图等这些第三方平台也都需要为不同环境的App分别申请不同的appID。

实现上,最笨的方法就是拷贝当前工程,然后修改,缺陷很明显,维护成本很高。不过,好在Android和iOS都有很方便的修改方式。

Android有了Gradle,可以设置多个不同的Flavors,每个Flavor都有一个applicationId属性,其实就是App的包名。比如,生产版和测试版的设置如下:

productFlavors {
    myapp {
        applicationId "com.mydomain.myapp"
    }
    myappBeta {
        applicationId 'com.mydomain.myapp.beta'
    }
}

这样,其实就有两个App了。然后,源代码新建一个和main同级的目录,命名为myappBeta,然后,将图标、名称和第三方设置之类的,和main保持一样的位置、文件名、属性等,就可以替换成环境相关的了。

iOS则可以通过创建多个环境的Target来实现环境分离,不同Target可以设置不同的Bundle Identify、Bundle display name、更换图标。另外,每个Target也各自有自己的一份plist文件的,环境变量和第三方设置之类的,都可以设置在相应的plist文件里。

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=326953594&siteId=291194637