代码中的软件工程——Menu实例

  结合孟老师课上的理论知识和Menu实例,对工程化编程进行一个回顾,从模块化设计、可重入接口到线程安全三个方面进行分析,以加深对工程化编程的理解。

一、编译与调试环境配置

  VS Code C/C++环境配置:

  • 首先下载VS Code,安装包下载完成后,直接进行安装即可。
  • 安装C/C++扩展。

  • 下载MinGW,并安装gcc,g++,gdb。

  • 配置环境变量。

  • 在dos命令窗口输入gcc或g++,查看环境变量是否配置成功,如下情况,则配置成功。

  • 配置VS Code C\C++环境。进入调试界面添加配置环境,选择 C++(GDB/LLDB),再选择 g++.exe,之后会自动生成 launch.json 配置文件。

  • 编辑 launch.json 配置文件。
    View Code
  • 返回.cpp文件,按F5进行调试,会弹出找不到任务"task g++",选择 "配置任务",会自动生成 tasks.json 文件,编辑 tasks.json 文件。
    View Code
  • 运行C语言程序。

  至此,VS Code可以运行C\C++程序,在准备运行Menu程序时,发现源代码中包含一些Linux中的头文件(例如<pthread.h>  <arpa/inet.h>),所以还是选择使用Ubuntu中运行源文件,Ubuntu中VS Code配置方法与Windows中基本一致,运行情况如下,

二、模块化设计

  模块化是在软件系统设计时保持系统内部各部分相对独立,以便每一部分可以被独立地进行设计和开发。模块化软件设计如果应用的比较好,最终每一个软件模块都将只有一个单一的功能目标,并相对独立独立于其他软件模块。从而整个软件系统也更容易定位软件缺陷,而且整个系统的更新和维护也更容易,一般使用耦合度和内聚度来衡量软件模块化程度。

  耦合度一般指软件模块之间的依赖程度,一般分为紧密耦合、松散耦合和无耦合,一般在软件设计中追求松散耦合。内聚度则是指一个软件模块内部各元素之间互相依赖的紧密程度。

  对于Menu小程序,它将数据结构和它的操作与菜单业务处理进行分离处理,虽然汉字同一个源文件中,但可以认为有了初步的模块化。程序中定义DataNode结构体,里面存放了该条指令、对于该指令的描述、对应的处理函数、以及指向下一个指令结点的指针。将每条指令作为一个节点存放起来,并将其与对应的操作相分离。定义了linktable.c和linktabke.h源文件,将对链表的操作与菜单业务相分离,后期,又在linktable.c中定义了LinkTable结构体,进一步将linktable.h中不属于接口调用的内容拿出来,有效的隐藏了软件内部的实现细节,加强了程序的模块化程度。

三、可重用接口

  进行了模块化设计之后我们往往将设计的模块与实现的源代码文件有个映射对应关系,因此需要将数据结构和它的操作独立放到单独的源文件中,这就需要设计合适的接口,以便模块之间相互调用。上一节我们将对链表数据结构和对链表的操作与菜单业务功能分离之后,为了实现一个模块与其他模块之间松散耦合,就需要定义清晰明确的接口,在程序分别在linktable.c和menu.h中定义了函数接口。

  同时使用callback(回调)机制,对于回调,看到了一个比较浅显易懂的例子:你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。给Linktable增加了Callback方式的接口,需要两个函数接口,一个是call-in方式函数,例如SearchLinkTableNode函数,其中一个函数作为参数,这个作为参数的函数就是callback函数,利用callback函数参数使Linktable的查询接口更加通用,有效的提高了接口的通用性。

  之前提到了模块之间的耦合度,在接口定义时也要考虑接口之间的耦合度,由于在程序中定义了cmd为全局变量,这样很容易造成公共耦合,我们可以通过传递参数来消除这种公共耦合,同时为了维持接口的可重用性,在SearchConditon函数中,没有直接传递char*类型的参数,而是没有指定传入数据的类型,在函数内部再将其进行强制转换。

   在定义一些通用接口时,可以从三个方面下手:

  • 参数化上下文。通过参数传递上下文的信息,而不是隐含的依赖上下文环境。
  • 移除前置条件。就是对于函数的形参的形式进行优化,例如: int sum(int a,int b); ,sum函数只能有两个参数,我们可以尽量消除这种前置条件,例如:  int sum(int[] a,int length);  。
  • 简化后置条件。对于 int sum(int[] a,int length); 返回结果为a中下标0~length的元素和,这里可以改为 int sum(int[] b); b代表a中0~length的元素,同时可以直接获取到b的长度,返回b中所有元素的和。

四、可重入函数与线程安全

  可重入函数指可以由多于一个任务并发使用,而不必担心数据错误。在多线程编程时,多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。线程安全问题大多是由全局变量及静态变量引起的,局部变量逃逸也可能导致线程安全问题。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

  对于可重入函数,要保证多个线程不能同时进入临界区,例如AddLinkTableNode函数,在线程进入临界区之前,要对临界区进行加锁操作,执行完毕之后再释放锁,可以保证线程互斥的进入临界区,进而保证线程安全。

  虽然每个函数都是可重入的,但组合起来可能会出现线程安全问题。例如,一个线程进行天剑操作时,随后另一个线程进行查找操作,由于第一个线程还未执行完毕,查找过程就会出现错误,所以要全局性的考虑线程安全问题。以上可以得出:不可重入的函数一定不是线程安全的;可重入的函数不一定是线程安全的。 

五、写在最后

  在此之前也有接触过软件体系结构、设计模式等课程,但是当时比较轻视这些课程,直到真正着手开发一个小项目时才意识到了它的重要性。通过课上的理论学习,结合对Menu实例的一步步分析,从编译环境配置、模块化设计、可重用接口到线程安全,对工程化编程有了进一步的了解,对自身以后的编程习惯有很大的影响,对外可以提高软件产品的质量,对内可以大大规范开发过程,提高开发效率。

  【参考】代码中的软件工程

      工程化编程实战--代码中的软件工程

posted @ 2020-11-08 16:40  Dem0_zhu  阅读(283)  评论(0)    收藏  举报