软件架构
软件架构设计是从宏观角度思考软件设计的方式。软件架构的优劣决定了一个软件系统的上限。
单程序结构
常见程序的结构可以分成两种:one-shot 和 deamon;one-shot 程序读取命令行参数后,一下子从前执行到尾,处理完数据后,直接退出程序;而 deamon 程序会在解析完命令行参数和配置文件后进入死循环,并通过阻塞式系统调用,监听和等待它感兴趣的下层响应,并在响应发生时被操作系统唤醒,然后执行业务逻辑,除非触发关闭命令或者异常,否则将会按照期望一直运行下去。
// deamon 程序
int main(int argc, char *argv[]) {
Args* args = parseArgs(argc, argv);
while(true) {
// 阻塞式调用,监听客户端的请求
void* incoming = listen(args);
// 业务层寻找处理该请求的函数
void* handler = findHandler(args, incoming);
// 分发该请求
void* result = distribute(args, incoming, handler);
// 响应请求
response(incoming, result);
}
return 0;
}
前者,较为简单,略;后者是我们平时编写多数程序的结构,包括前端的 GUI 客户端程序和后端的 server 程序。
deamon 程序
deamon 程序的结构可以被概括为一个“前中后”三层结构——前端视图层、中间业务层、后端持久层。视图层面向自己的客户端,监听客户端的请求和操作,解析和翻译操作,并将请求的任务分发给中间业务层;中间层负责进行主体复杂逻辑的实现和繁重工作的处理,它在处理业务的过程中可能需要依赖外界的持久化数据,或者将数据进行持久化,它会选择性地调用后端持久层,进行数据持久化的操作;后端持久层通过系统 IO 调用,向进程外发送和获取数据,或是作为数据的直接管理者,代替进程中的其他部分管理数据。
子程序和函数
进一步向更小的粒度分析,我们发现,一个程序往往由一个个子程序构成,或者直白点,在多数的编程语言中,它叫做函数。然而我们需要从更底层的 C 语言和汇编的角度去分析函数是什么?函数是进程的内存空间中的一段数据,它记录的是一段 CPU 可执行的机器指令序列,我们可以告知 CPU 这段内存中的数据是指令,并应当读取并执行它;同时,在执行之前,可以携带一定的指针,指向一些数据,并要求令这些数据作为函数执行时的一部分上下文,函数在执行期间,可以访问他们,最后函数应当将处理数据的结果,保留到一个指定的 Return 指针中进行返回,让函数的调用者能够通过访问该指针拿到自己想要的数据和结果。这个过程可以被抽象成一个管道,它从进口端接受一些数据,并使用这些数据进行计算,产生一个返回值,并从管道的出口端返回。
函数有如下几个重要特点:
函数嵌套调用,函数内部可以调用另一个函数,从而形成嵌套,大大减少重复代码的书写;为了实现这个特性,大多数的编译器采用调用栈来实现;
系统调用,系统调用不是用户自己编写的,它存在于进程中,在进程创建的时候就被操作系统一同加载到了进程的内存空间,用户可以直接调用这些函数实现逻辑复用和受限资源的访问;这些函数区别于用户自行实现的函数,他们是系统层提供的 API 接口;
系统调用有一个特点那就是外界交互。程序期望和外界发生交互,或者说 IO 操作。IO 操作有一个非常大的特点就是,可能是失败的,它可能返回一个程序无法处理的结果或者没有结果,那么程序将会就此崩溃,或者是主动退出;
函数复用,函数的目的就是为了复用程序逻辑,避免重复书写相同的代码。
作用域
进程从操作系统那里申请一个块巨大的内存空间,它是一个简洁的字节数组,但是却也非常的简陋,我们需要对内存进行划分使用和管理,而不是随意使用,其中一个重要的手段便是作用域。作用域是为了划分和隔离子程序,拆解问题,提供资源的隔离。
我们在一个内存区域中划分一小块进行使用,在区域中存放了一些数据,接着进行了一些计算,然后释放和关闭了这个作用域,不同的作用域在不同的时期被展开和闭合,并且彼此之间的数据资源存在一定的隔离性。作用域需要程序设计时被声明,在运行时依照声明被展开;展开时,可以将状态传入一个域,在域展开的过程当中,状态可以被改变,在域使用完毕时,可以将闭合,释放资源,同时传出一些状态。
当我们在一个作用域中声明和创建了一个变量的时候,该数据的大小可能会发生变化,我们会对其进行重新赋值。
从一定角度看,程序就是状态 + 作用域的管理。
父子作用域
我们划分并隔离不同的子程序,但是最后却又要让子程序之间发生交互和数据交换,发生父子嵌套和兄弟并列,这体现了封装和接口的思想。父作用域只需调用一个接口,便可以完成一个复杂动作,而无需关系其底层细节。
作用域可以被分为,全局作用域和局部作用域。
- 全局作用域中的内容被整个程序可见,并且早于所有局部作用域被展开,晚于所有局部作用域被闭合。全局作用域只有一个。全局作用域是所有作用域的父代。
- 函数调用产生一个局部作用域,
在不同的语言中,提供了不同的作用域抽象,拥有更多的层级,包括模块作用域、类作用域、函数作用域、块作用域。但是不管如何,我们需要着重关注的是父作用域和子作用域之间的关系——父作用域依赖于子作用域。
- 当一个作用域依赖于其他作用域的时候,它的功能就受到了别人的影响,为了让这个作用域正常工作,就需要保证它依赖的所有作用域正常工作;
- 作用域之间的接口应该保持稳定,当接口更变时,需求同时改变依赖者的逻辑;
- 作用域之间应该避免循环依赖,因为循环依赖往往是程序划分不正确的外在体现,或者从哲理的角度讲,循环依赖其实是在解决“蛋生鸡”的问题,但是这是没有办法解决的;
作用域之间需要通信,特别是对于兄弟组件或者是没有父子关系的组件之间进行通信,通信的基本方式是:基于父组件进行共享,然而这会造成一些额外的问题,后续将会展开讨论。
生命周期
作用域和状态都有生命周期,一个作用域从被展开到关闭,走完了它全部的生命周期;一个变量从创建,到修改,再到销毁,走完了它全部的生命周期。在一些语言中,不同的语法和特性会产生不同的生命周期,管理生命周期也是管理状态和作用域的一部分。
- 在资源不再使用的时候及时关闭资源,从而节约资源,提高系统的效率;
- 访问状态资源应当在适当的作用域中,并且不应该在该资源的生命周期已经结束之后继续访问该资源;