如何完成一个软件工程--以Menu项目为例
如何完成一个软件工程--以Menu项目为例
参考资料:代码中的软件工程
项目源码地址:menu_code
一、环境配置
这里使用VScode完成整个项目的开发。
1、下载安装相关插件

2、下载MinGW,并完成GDB、GCC等相关环境变量的配置
这里掠过具体步骤,当最后可以查到GDB、GCC相关版本信息时即为配置完成。


3、在VScode内修改相关json文件
如图所示,主要为下图中的四个文件。

4、编译运行测试用例
这里我编写了一个简单的Hello.c程序,可以看到成功完成运行,说明VScode的C/C++编译环境已配置完成。

二、代码的成长
UML三巨头之一的Ivar Jacobson曾说“银弹不存在,我们需要的仅仅是明智的软件开发方法( smart software development ),软件必须从一个小的可运行的 skinny system 开始,逐渐充实生长称为 full-fledge 的成熟系统。” 所以这个目录项目也是这样,从 hello world 开始不断迭代调试使代码长的越来越像一个命令行的菜单小程序。写代码要小步快跑不断迭代,罗马不是一天建成的,不要期望一撮而就。
很明显的可以看出来本目录项目经过了7个版本的更迭,最后才完成了一个较为完善的目录项目。

此外为了方便使用者能够方便阅读,往往需要在程序块头部添加相关注释,以方便使用者了解本项目的意义和内容。

此外,代码风格应当遵循规范,应当简明、易读、无二义性,以期让人更好的读懂项目中的源码,常见的代码风格规范由孟老师总结如下:



三、模块化设计
模块化(Modularity)是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。这个做法背后的基本原理是关注点的分离 (SoC, Separation of Concerns),而关注点的分离的思想背后的根源是由于人脑处理复杂问题时容易出错,把复杂问题分解成一个个简单问题,从而减少出错的情形。
模块化软件设计的方法如果应用的比较好,最终每一个软件模块都将只有一个单一的功能目标,并相对独立于其他软件模块,使得每一个软件模块都容易理解容易开发。因此,软件设计中的模块化程度便成为了软件设计有多好的一个重要指标,一般我们使用耦合度(Coupling)和内聚度(Cohesion)来衡量软件模块化的程度。
耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合(Tightly Coupled)、松散耦合(Loosely Coupled)和无耦合(Uncoupled)。一般在软件设计中我们追求松散耦合。

而内聚度是指一个软件模块内部各种元素之间互相依赖的紧密程度。理想的内聚是功能内聚,也就是一个软件模块只做一件事,只完成一个主要功能点或者一个软件特性(Feather)。
在Menu项目下,我们可以看到从lab3.3开始引入模块化的设计,利用linklist.h文件声明了一些函数,然后在linklist.c完成了这一部分函数的具体实现。
相关功能函数在linklist.h的声明:

相关函数在linklist.c内的具体实现方式:

最后在menu.c文件导入linklist.h即可完成相关函数的使用:


四、可重用接口
尽管已经做了初步的模块化设计,但是分离出来的数据结构和它的操作还有很多菜单业务上的痕迹,我们要求这一个软件模块只做一件事,也就是功能内聚,那就要让它做好链表数据结构和对链表的操作,不应该涉及菜单业务功能上的东西;同样我们希望这一个软件模块与其他软件模块之间松散耦合,就需要定义简洁、清晰、明确的接口。这时进一步优化这个初步的模块化代码就需要设计合适的接口。
接口就是互相联系的双方共同遵守的一种协议规范,在我们软件系统内部一般的接口方式是通过定义一组API函数来约定软件模块之间的沟通方式。换句话说,接口具体定义了软件模块对系统的其他部分提供了怎样的服务,以及系统的其他部分如何访问所提供的服务。在面向过程的编程中,接口一般定义了数据结构及操作这些数据结构的函数;而在面向对象的编程中,接口是对象对外开放(public)的一组属性和方法的集合。函数或方法具体包括名称、参数和返回值等。
接口规格是软件系统的开发者正确使用一个软件模块需要知道的所有信息,那么这个软件模块的接口规格定义就必须清晰明确地说明正确使用本软件模块的信息。一般来说,接口规格包含五个基本要素:
- 接口的目的;
- 接口使用前所需要满足的条件,一般称为前置条件或假定条件;
- 使用接口的双方遵守的协议规范;
- 接口使用之后的效果,一般称为后置条件;
- 接口所隐含的质量属性。
接下啦介绍Call-in方式的函数接口和Callback方式的函数接口。
Call-in方式的函数接口如图所示,函数参数通常不包含别的函数,全是数据传入。

而Call-back方式的函数,他的参数中往往还会包含的别的函数作为参数,如图所示。 SearchLinkTableNode()函数中使用了Conditon()函数作为参数。

五、线程安全
线程(thread)是操作系统能够进行运算调度的最小单位。它包含在进程之中,是进程中的实际运作单位。一个线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。一般默认一个进程中只包含一个线程。有了多核多线程CPU,操作系统就可以让不同进程运行在不同的CPU核的不同线程上,从而大大减少进程调度进程切换的资源消耗。
借助函数调用堆栈可以将我们写的函数调用代码整理成一个顺序执行的指令流,也就是一个线程,每一个线程都有一个独自拥有的函数调用堆栈空间,其中函数参数和局部变量都存储在函数调用堆栈空间中,因此函数参数和局部变量也是线程独自拥有的。除了函数调用堆栈空间,同一个进程的多个线程是共享其他进程资源的,比如全局变量是多个线程共享的。
如果我们的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。 线程安全问题通常是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行读写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
Menu项目中从lab7.1开始在linlist.c引入头文件pthread.h实现线程安全的设计,并在可能影响线程安全的函数中加入了锁机制。


六、总结
感谢孟老师这几节课的教导,让我知道了从头开发一个软件工程项目应走的流程,从最开始的想法构造,伪代码设计,再到后来的代码实现,一步步完善这个项目的功能,最后完成一个完整的项目,让我感觉受益匪浅,为我在之后设计软件的过程中打下了良好的基础。
浙公网安备 33010602011771号