应用程序设计

三年工作经验之成果,简要概述了应用程序的设计过程~
以下没有参考任何文档,纯属瞎编!

一、模块

划分模块是为了让大型应用程序的结构更加清晰,在对程序进行重构或者修改模块的功能时更加方便。模块一般需要根据功能类型区分,一般功能包括以下几种:

  • 日志记录:用于程序运行过程的打点记录,在程序运行出错时能够定位到出错位置和相关必要数据
    • DEBUG日志:调试时使用,包括详细的调试信息,程序出错时辅助调试
    • INFO日志:记录程序运行记录,打卡记录
    • ERROR日志:记录程序出错记录
  • 数据记录:程序相关的大量数据条目记录,一般与数据库接壤
  • 配置记录:程序配置相关,配置参数则使用文本文件记录(JSON格式、YAML格式等)
  • 会话控制:网络连接模块,用于控制两个程序之间的通信
    • 会话协议:两个程序的会话需要制定明确的会话协议
  • 任务控制:多线程程序
    • 任务启动、停止、回收控制
    • 卡死线程处理
  • 缓存控制:缓存文件数据或者网络数据(辅助快速加载)

1.1 日志记录

对于一个日志记录模块,最主要的功能就是提供日志记录功能。其它功能如下:

  • 日志的输出等级:可以选择日志输出的最低等级(低于此等级的日志不输出)
  • 日志输出位置:可以输出到终端、保存到文件。
    • 日志保存到文件时,对于不同模块的日志可以选择保存到到不同的文件;当文件过大时,将日志内容归档管理。

1.2 数据记录

在对接到应用程序的上层部分,一般需要根据程序的具体需求定制(比如存储什么数据,许需要哪些数据操作接口(增删查改之类的))。在数据记录的底层部分,可以支持多种数据库类型,方便数据库的切换,或者使用不同的数据库完成不同的需求(瞎猜的,实际没有遇到过)。

该模块可以设计为细腰型结构,包括上层对接具体需求,中层管理,底层聚合多种数据库类型。这种结构一方面是便于新增新的功能或者支持新的数据库;另一方面是方便移植到另一个应用程序上(只需要替换上层部分即可)。细腰型结构示例:

graph TD
    A(数据需求1) --> B(管理)
    C(数据需求2) --> B(管理)
    B(管理) --> D[Mysql]
    B(管理) --> E[SQLITE]

1.3 配置记录

配置模块和日志模块一样,都是应用程序的基础。配置用来初始化一些应用程序的参数,如果把这些参数写入到程序内部,那么在需要调整参数的时候就会很麻烦(重新编译程序),所以需要与应用程序隔离的文件来存储一些在程序启动时可能会发生改变的内容。

配置模块需要对应用程序提供的基本功能是根据key来获取对应的值,即获取某个参数的值;配置模块也可以添加一些高级功能,例如向配置文件写入一个参数的值;当程序运行时,如果监测到配置发生改变时,对程序中对应的参数重新初始化。

配置的存储需要与应用程序的语言相适应。C/C++一般使用一行即为一个参数配置(使用“:”或者“=”隔离参数的key和value);Python可以很方便的使用json库,所以可以使用JSON文件作为配置文件。

1.4 会话控制

会话控制模块一般用于本地或者网络上的两个进程间的通信。通信则一般分为同步和异步方式,同步通信方式就是进程A向进程B索要数据,A等着B完成,然后拿到数据;异步方式是进程A向进程B索要数据,确定B收到索要数据之后A立即返回,B开始准备数据,当B数据准备好之后将数据发送给A,A拿到数据。

会话控制模块一般分为两层,上层是会话协议层,下层是网络传输层,会话协议层主要定义两个程序之间的通信协议(A:你今天吃什么?B:我吃番茄炒鸡蛋),网络传输部分主要是定义信息传输的方式(怎么把”你今天吃什么?“这句话发出去),一般有Socket(TCP/UDP)、HTTP等。

graph TD
    subgraph 从
    E(从:会话协议) <--> C(从:网络传输)
    end
    subgraph 主
    A(主:会话协议) <--> B(主:网络传输)
    end

1.5 任务控制

一般的多任务程序中都需要任务管理,多任务即为多线程任务,多线程任务包括临时任务、持久化任务等等。任务控制模块可以提供任务控制功能,基本的包括启动任务和终止任务,更复杂点的包括任务的活性监测(监测线程是否卡住)。

多任务模块需要考虑的点有:

  • 基本功能:
    • 启动任务:开启一个新的线程,此时需要数据传入和函数传入(不同的类型其处理函数也不一样,处理函数也需要不同的数据,需要考虑的是怎么把开启新线程的接口统一:封装统一的数据结构相对来说是一种扩展性很强的方案)
    • 终止任务:当任务没有分离时,使用的计算机语言一般会提供回收的方式
  • 活性监测:任务开启时注册一个定时器,当定时器结束时说明任务还没有退出(如果进程提前退出则说明该任务实例已经被销毁(注意销毁任务实例的定时器处理))

1.6 缓存控制

缓存控制的目的是存储一些临时性的数据,用来保存数据或者加速访问某些内容。一般应用是可以缓存其它模块的数据内容(比如会话控制的会话数据和任务控制的任务数据)或者需要加速访问的内容(从文件中读取的一些内容、从网络上获取到的内容,这些内容如果重新获取对于一个对运行时间有要求的程序来说时间成本过大。但是要注意,这些内容短期内不会发生变化,如果这些内容每次获取都不一样,那么或许重新获取是无法避免的)

缓存控制的设计比较灵活,一般需要根据实际的应用程序设计(具体问题具体分析)。对于一些常规性的存储内容(其它模块需要存储的数据)则可以统一实现,减少重复造轮子的可能性。

二、逻辑

2.1 必要逻辑

计算机语言都会(所有的)包含一些必要的逻辑,包括:

  • 数据操作:保存数据、打印数据
  • 顺序执行:逐条执行代码
  • 循环处理:循环执行某段代码
  • 条件处理:根据条件判断是否执行某段代码
  • 函数封装:将某一个小功能封装成函数

2.2 类的概念

继承和多态(待补充)

对于含有类概念的计算机语言(C++、Python):

类的概念比函数封装更进一步。函数封装只会封装单个的功能;类则封装了数据和多个功能,即包含了数据和处理数据的方法。类的一个实例通常称为对象(类可以描述为实例的蓝图,设计图),对象的出现使得类区别于数据和方法的集合。每一个类的对象都有自己的数据空间,使得不同的对象互相隔离。

模块的出现使得同类型的操作得以聚合。例如一个SQLITE数据库操作类,其中数据区域可以包括数据库的位置信息;操作方法包括增删查改等常用方法的集合。

将操作方法聚合成类和聚合成文件(无类)的区别。将操作方法封装成类时,数据定义保存在类中,方法定义保存在类中,当我们操作不同位置的数据库时,可以实例化多个对象,这些对象的区别仅有其中的数据区域(数据库的位置);将操作方法汇聚成文件时,当我们操作不同位置的数据库时,可以定义空间来存储不同的数据库位置,当调用某一个数据库方法时,需要传入一些必要的内容(例如数据库的位置或者数据库操作句柄)等。

使用类的优势:类将数据定义和方法定义封装到一起,第一是类的进一步封装使得代码边界更加清晰,数据定义易于管理;第二是减少了不必要的外部数据的传入(类初始化时传入并在数据区域统一管理)。

2.3 模块的概念

对于含有模块概念的计算机语言:

模块的概念比类的概念更宽泛,类是一种类型(奔驰)的方法的封装,模块可以认为是一类对象(奔驰、奥迪)的封装,实际上模块的分类包括日志记录模块,数据记录模块(里面可能包含了Mysql数据库操作对象和SQLITE数据库操作对象等多种类封装)。

模块的优势:模块将同一属性的内容封装(放)到一起,第一是方便不同模块的管理,方便模块替换(只要对外的API是不改变的,那么模块很容易被替换或者修改内部功能),便于代码的维护(在修改某一部分的功能时,直接到对应的模块修改);

三、细节

计算机语言只是实现目的的一种工具,不同的语言在对模块的实现上有不同的优势,在选用语言来完成某一功能时,需要考虑如下内容:

  • 功能复杂性:
    • 是否需要拆分成不同模块,模块之间的耦合
  • 运行的平台:
    • 运行环境的配置
  • 运行速度要求:编译语言和脚本语言
  • 时间控制精度要求:我之前用Shell写一个程序时,程序的时间很难控制(定时运行)
  • 底层模块依赖:是否有精力进行从砖头开始垒房子,或者快速从墙片开始垒房子(一样结实的)
语言 优势 弱势 平台 分类
C 1、快 1、数据管理
2、打地基
1、编译运行 高级
C++ 1、快 1、打地基 1、编译运行 高级
Python 1、库多 1、较慢(边解释边运行) 1、打包运行
2、解释器运行
脚本
Shell 1、无需编译 1、数据管理
2、复杂处理
1、解释器运行(Linux) 脚本

四、语言组合

不同的语言是可以互通的,这样可以解决某一个语言的缺陷。例如Python调用C实现的动态库(以解决Python运行慢的问题)(实例:Numpy底层使用C语言编写,内部解除了GIL(全局解释器锁),其对数组的操作速度不受Python解释器的限制,效率远高于纯Python代码)。

其它代码互相调用的例子不再赘述。

五、程序设计

在需求定义完成之后,就可以开始着手程序的架构设计,架构是语言无关的,最终无论用什么语言去实现这个程序都可以,只是存在难易度和细节的调整问题。难易度和细节指的是一个语言是否具有良好的生态(有没有现成的稳健的依赖包,或者自己有没有积攒一些常用的代码段等),另外就是程序在与外部对接的过程,有没有封装良好的接口。

程序架构的设计是基于需求的目标的,是一个有目的的设计。为了完成最终的目标,我们需要让各个模块按照我们目标运作起来。动起来是最基本的,我们还需要考虑之后的维护过程,怎么让代码更好的去维护,另外还有考虑代码的复用性,这些代码能不能封装起来在之后的项目中使用。

软件工程中有一个名为“高内聚、低耦合”的概念,我们在写代码之前就需要考试考虑好这个问题,低耦合是为了让基本不相关的内容分离设计,高内聚是让一个模块恰好做一件事。在低耦合方面,如果日志模块和缓存模块纠缠在一起,再加一个数据库模块的话难道要再重写一个日志模块?再高内聚方面,一个日志模块不需要分散开写成多个部分,日志模块就是提供一个稳定的标准的日志接口,里面怎么实现是不需要调用者关注的。本人觉得做好“高内聚、低耦合”就能得到一份好维护,复用性强的代码。

做好程序架构的设计之后,就可以开始做语言的选型,从划分的各个模块入手开始做代码的编写。在做语言的选型问题的时候,要考虑与外部对接的难易度,例如与数据库的对接,与外部接口的对接等等,有的语言例如python对这些有很好的支持,但是C++就需要仔细考虑怎么对接的问题。