结构化设计和面向对象设计的异同

1,277 阅读4分钟

0.前言

软考的题目中通常会涉及结构化和面向对象两种理念,让大家选择哪种更适合复杂的、大型的、需求多变场景。刷过题的都知道要选面向对象,但为啥要选面向对象呢?

教材、真题中还大量提到结构化思想等同于面向过程编程,面向对象思想对应面向过程编程。这些概念之前都是生背,好像理解了,但也说不清楚为什么。

在读完《软件设计的哲学》这本书后,得到一些启发。

1.引子

本文中的案例思想来自《软件设计的哲学》这本书书

如果要做一个文件读写的模块(这里的文件是内部有一定复杂结构的文件,例如配置文件、excel文件之类,而非简单的无结构文本),我们要做的事很简单,将文件读取出来、对读取出的内容做一些修改、保存文件。 顶层流程必然这样设计:

graph LR
读取文件 --> 处理文件 --> 保存文件

基于上述流程会设计三个核心类和一个控制类,类结构图如下:

classDiagram
Main -->Reader
Main -->Processor
Main -->Writer

class Main {
main()
}

class Reader{
+read()
}
class Processor{
process()
}
class Writer{
write()
}

上述几个类的职责非常明确,Main做主控、Reader读文件、Writer写文件、Processor处理文件。

2.问题

至今为止,一切都非常合理,我们用的是面向对象语言,也用到了类,但这真的是面向对象吗?

让我们深入思考Reader和Writer两个类的核心流程:

  • Reader.read()
graph TB
打开文件 --> 以二进制形式读取文件内容 --> 将文件内容转换为程序可读的内部数据结构
  • Writer.write()
graph TB
将程序生成的数据结果转换为文件的二进制内容 --> 写入文件 --> 关闭文件 

上面两个步骤中,RederWriter需要将二进制文件转为内部数据结构、或者将内部数据结构转为二进制文件内容,即两者都需要知晓文件的内部结构

上述两个类,虽然将文件结构封装到内部,但仍然存在一些知识的共享,如果开发ReaderWriter的的两个人,那么就存在一些沟通成本,即需要共识文件结构,开发完成后进行联调和修订。这样显然有问题,那么问题在哪里呢?

我们可以对类结构进行重构,将与文件结构相关的内容剥离到公共类中,公共类负责文件结构的encode和decode:

classDiagram
Main -->Reader
Main -->Processor
Main -->Writer

Reader --> Codec
Writer --> Codec

class Main {
main()
}

class Reader{
+read()
}
class Processor{
process()
}
class Writer{
write()
}

class Codec {
decode()
encode()
}

问题好像是解决了,但类变多了(对于Java来说也不是什么大事,Java写代码类就是多)

我们考虑下这个功能的演进:现在需要增加V2版本,V2版本功能更多、而且与V1版本的存储结构不兼容,那么CodeC就需要抽象了:

classDiagram
Main -->Reader
Main -->Processor
Main -->Writer

Reader --> Codec
Writer --> Codec

class Codec
<<interface>> Codec

CodecV1 --|> Codec
CodecV2 --|> Codec

class Main {
main()
}

class Reader{
+read()
}
class Processor{
process()
}
class Writer{
write()
}

class Codec {
decode()
encode()
}

class CodecV1 {
decode()
encode()
}

class CodecV2 {
decode()
encode()
}

完成上述结构调整后,Reader和Writer类的内部代码要改吗?可能有两种答案:

  • 答案1:需要修改,因为需要根据类型构造Codec的实例
  • 答案2:不需要修改,因为上述Codec实例可以由Main注入,或者Spring等机制动态注入

其实答案1和答案2都是修改,只是修改点不同。答案2可能更差,因为文件的版本和Main函数其实是没关系的,它不需要知道这种知识

回想上面的过程,我们用的是面向对象语言、也有类,类之间还有关联关系、也有公共抽象,但这就是最好结果吗?

虽然文件结构都隐藏起来了,但对于MainProcessor两个类来说,仍然可以见到Codec这个类,也知道文件有encode()encode()这两个函数,更重要的是如果未来文件结构、存储方式发生变更,那么ReaderWriterCodec这三个类要一起变更,显然违反了面向对象中的开放关闭原则

3. 根因

面向对象设计最根本要解决的问题是信息隐藏,对外暴露的信息越少越好,那么程序结构将越稳定,需求变化后修改的范围会越少。

上面问题的跟进,是在用面向对象的语言对结构化设计的结果进行实现

如果将文件读写都放到同一个类中实现,那么文件结构将不再对外暴露,所有关于文件读写、文件结构的变更将封装在文件类中:

classDiagram
Main -->File
Main -->Processor

class Main {
main()
}

class File{
read()
write()
}
class Processor{
process()
}

main()函数的调用流程可以看成这样:

graph LR
File.read --> Processor.process --> File.write

流程不变,但整体变简单了很多,Main需要、也仅仅能看到自己关心的模块和函数。 至于File,可以是一个模块、也可以是一个类,内部的文件结构变更再与外部无关。

4.结论

个体以及团队的认知能力都是有限的,在有限的认知能力水平下,只有更好的隐藏不必要对外的信息,才能做到更快的迭代演进,这也是面向对象设计能应对复杂的、大型的、需求多变场景的根本原因。

能否应对复杂的、大型的、需求多变场景与语言本身无关,只与结构、信息隐藏有关 ,即使是C语言,也能通过合理的模块设计、合理的动态/静态链接库封装实现上述目的。