管理多少个非OO代码库?


27

我总是看到抽象是OO为管理代码库提供的非常有用的功能。但是如何管理大型非OO代码库?还是那些最终变成了“ 泥浆大球 ”?

更新:
似乎每个人都在认为“抽象”只是模块化或数据隐藏。但是恕我直言,这也意味着必须使用“抽象类”或“接口”,这对于依赖注入和测试是必须的。非OO代码库如何管理这一点?而且,除了抽象之外,封装还有助于管理大型代码库,因为它定义并限制了数据与函数之间的关系。

使用C,非常有可能编写伪OO代码。我对其他非OO语言了解不多。那么,这是管理大型C代码库的方式吗?


6
请以与语言无关的方式描述一个对象。它是什么,如何修改,它应该继承什么,应该提供什么?Linux内核充满了分配的结构,其中包含许多帮助程序和函数指针,但是对于大多数人来说,这可能无法满足面向对象的定义。但是,它是维护良好的代码库的最佳示例之一。为什么?因为每个子系统的维护者都知道他们职责范围内的内容。
蒂姆·波斯特

请以与语言无关的方式描述您如何看待正在管理的代码库以及OO与之相关的事情。
David Thornley,2010年

@Tim Post我对Linux内核源代码管理感兴趣。您能否进一步描述系统?也许作为一个例子的答案?
Gulshan 2010年

7
在过去,我们模拟和存根的单独链接用于单元测试。依赖注入只是其中几种技术中的一种。条件编译是另一种。
Macneil 2010年

我认为将大型代码库(OO或其他)称为“托管”是很困难的。在您的问题中更好地定义中心术语会很好。
tottinge

Answers:


43

您似乎认为OOP是实现抽象的唯一方法。

虽然OOP当然非常擅长于此,但这绝不是唯一的方法。大型项目也可以通过不妥协的模块化(只需看看Perl或Python,两者都擅长于此,而ML和Haskell等功能语言也是如此)以及使用诸如模板(在C ++中)的机制,就可以保持可管理性。


27
+1另外,如果您不知道自己在做什么,则可以使用OOP编写“泥浆大球”。
拉里·科尔曼

C代码库呢?
Gulshan

6
@Gulshan:许多大型C代码库都是OOP。仅仅因为C没有类并不意味着OOP不能通过一点努力就可以实现。此外,C使用标头和PIMPL习惯用法允许良好的模块化。不如现代语言中的模块舒适或强大,但再次足够好。
康拉德·鲁道夫

9
C允许在文件级别进行模块化。该接口位于.h文件中,而公共可用函数位于.c文件中,私有变量和函数将static附加访问修饰符。
David Thornley,2010年

1
@Konrad:虽然我同意OOP不是唯一的方式,但我认为OP可能严格考虑C,它既不是功能性语言也不是动态语言。因此,我怀疑提及Perl和Haskell对他/她是否有用。实际上,我发现您的评论对OP更为相关和有用(这并不意味着您不能花一点精力就可以实现OOP);您可能会考虑将其添加为带有其他详细信息的单独答案,可能会附有代码段或几个链接来支持。这至少将赢得我的投票,很有可能赢得OP的投票。:)
Groo

11

模块,(外部/内部)功能,子例程...

正如Konrad所说,OOP并不是管理大型代码库的唯一方法。事实上,在它之前(在C ++ *之前)已经编写了很多软件。


*是的,我知道C ++并不是唯一支持OOP的工具,但是从某种程度上讲,这种方法才开始变得惯用。
Rook


6

实际上,由于维护此类系统的人员已经进行了一段时间(出于愤世嫉俗的态度是工作保障),因此很少进行更改(例如,考虑社会保障退休计算)和/或根深蒂固的知识。

更好的解决方案是可重复验证,我的意思是自动测试(例如单元测试)和遵循规定步骤的人工测试(例如回归测试)“而不是四处查看,看看有什么突破”。

为了开始使用现有代码库进行某种类型的自动化测试,我建议阅读Michael Feather的“有效使用遗留代码”,其中详细介绍了将现有代码库带入某种可重复测试框架OO的方法。这导致了其他人已经回答的想法,例如模块化,但是这本书描述了在不破坏事物的前提下正确的做法。


迈克尔·费瑟(Michael Feather)的书+1。当您对庞大的丑陋代码库感到沮丧时,请(重新阅读):)
Matthieu 2010年

5

尽管基于接口或抽象类的依赖注入是进行测试的一种很好的方法,但这不是必需的。别忘了几乎所有语言都具有函数指针或eval,它们可以执行您可以使用接口或抽象类做的任何事情(问题是它们可以做更多的事情,包括许多不好的事情,而它们却不能做到)本身提供元数据)。这样的程序实际上可以通过这些机制实现依赖注入。

我发现严格使用元数据非常有帮助。在OO语言中,代码位之间的关系(通过某种程度)由类结构定义,以一种足以标准化的方式来实现,例如具有反射API。在程序语言中,自己发明那些会有所帮助。

我还发现代码生成在过程语言(与面向对象的语言相比)中更为有用。这保证了元数据与代码同步(因为它是用于生成元数据的),并为您提供了一些类似于面向方面的编程的切入点的东西-您可以在需要时插入代码的地方。有时,这是在我可以确定的环境中进行DRY编程的唯一方法。



2

即使没有抽象,大多数程序也被分解为某种类型的部分。这些部分通常与特定的任务或活动相关,您以与处理抽象程序中最特定的部分相同的方式进行工作。

在中小型项目中,有时使用纯正的OO实现实际上更容易做到这一点。


2

抽象,抽象类,依赖项注入,封装,接口等并不是控制大型代码库的唯一方法;它只是控制代码的唯一方法。这是公正且面向对象的方式。

主要秘诀是在编码非OOP时避免考虑OOP。

在非OO语言中,模块化是关键。在C语言中,正如David Thornley在评论中提到的那样,实现了这一点:

该接口位于.h文件中,.c文件中具有公共可用的函数,而私有变量和函数具有附加的静态访问修饰符。


1

管理代码的一种方法是按照MVC(模型-视图-控制器)体系结构将其分解为以下类型的代码。

  • 输入处理程序-此代码处理诸如鼠标,键盘,网络端口之类的输入设备,或诸如系统事件之类的更高级抽象。
  • 输出处理程序-此代码处理使用数据来操纵外部设备(例如监视器,灯,网络端口等)的情况。
  • 模型-此代码用于声明持久性数据的结构,验证持久性数据的规则以及将持久性数据保存到磁盘(或其他持久性数据设备)中。
  • 视图-此代码处理格式化数据,以满足各种查看方法的要求,例如Web浏览器(HTML / CSS),GUI,命令行,通信协议数据格式(例如JSON,XML,ASN.1等)。
  • 算法-此代码将输入数据集尽可能快地重复转换为输出数据集。
  • 控制器-此代码通过输入处理程序获取输入,使用算法解析输入,然后通过可选地将输入与持久性数据组合或仅对输入进行转换,然后通过模型将转换后的数据保存在持久性中,从而使用其他算法对数据进行转换软件,以及可选地通过查看软件转换数据以渲染到输出设备上。

这种代码组织方法适用于用任何OO或非OO语言编写的软件,因为通用设计模式通常对于每个领域都是通用的。同样,除了算法之外,这些类型的代码边界通常是最松散的耦合,因为它们将数据格式从输入链接到模型,然后链接到输出。

系统演进通常采取让您的软件处理更多种输入或更多种输出的形式,但是模型和视图是相同的,控制器的行为也非常相似。或者,随着时间的流逝,即使输入,模型,算法相同且控制器和视图相似,系统仍可能需要支持越来越多的不同种类的输出。或者可以扩展系统以为相同的一组输入,相似的输出和相似的视图添加新的模型和算法。

OO编程使代码组织变得困难的一种方式是,因为有些类与持久性数据结构紧密地联系在一起,而有些则没有。如果持久性数据结构与级联的1:N关系或m:n关系密切相关,则很难确定类边界,直到您在对系统进行了编码之前就已经对其进行了有意义的编码。当持久性数据的架构发生变化时,与持久性数据结构相关的任何类都将难以发展。处理算法,格式化和解析的类不太可能受到持久性数据结构的模式更改的影响。使用MVC类型的代码组织可以更好地将最混乱的代码更改隔离到模型代码中。


0

当使用缺少内置结构和组织功能的语言(例如,如果没有名称空间,程序包,程序集等)或无法充分控制该大小的代码库时,自然而然的反应就是开发我们自己的策略来组织代码。

该组织策略可能包括与以下内容有关的标准:应在何处保存不同的文件,在某些类型的操作之前/之后需要发生的事情,命名约定和其他编码标准,以及很多“这是如何设置的” -别惹它!” 输入注释-只要它们解释原因,这些注释才有效!

由于该策略很可能最终会根据项目的特定需求(人员,技术,环境等)进行定制,因此很难提供一种千篇一律的解决方案来管理大型代码库。

因此,我认为最好的建议是采纳特定于项目的策略,并将其作为关键优先事项:记录结构,原因,进行更改的过程,进行审核以确保其得到遵守,至关重要的是:在需要更改时进行更改。

我们大多数人都熟悉重构类和方法,但是对于使用这种语言的大型代码库,需要在必要时重构组织策略本身(包括文档)。

推理与重构相同:如果您觉得系统的整体结构混乱,您将在处理系统的较小部分时出现思维障碍,并且最终会使它恶化(至少这是我的观点)它)。

注意事项也相同:使用回归测试,确保在重构​​出错时可以轻松还原,并进行设计以首先促进重构(否则您将不会这样做!)。

我同意,这比重构直接代码要复杂得多,并且很难从可能不理解为什么需要这样做的经理/客户那里确认/隐瞒时间,但这也是最容易导致软件腐烂的项目类型由不灵活的顶层设计引起的...


0

如果您要询问大型代码库的管理,那么您正在询问如何在相对粗糙的层次上保持代码库的结构良好(库/模块/子系统的构建/使用名称空间/在正确的位置放置正确的文档)等等。)。OO原则,特别是“抽象类”或“接口”,是在非常详细的级别上保持内部代码干净的原则。因此,对于OO或非OO代码,保持大型代码库可管理的技术没有不同。


0

处理方式是找出所用元素的边界。例如,C ++中的以下元素具有清晰的边框,必须仔细考虑边框之外的任何依赖项:

  1. 自由功能
  2. 成员功能
  3. 宾语
  4. 接口
  5. 表达
  6. 构造函数调用/创建对象
  7. 函数调用
  8. 模板参数类型

结合这些元素并重新认识它们的边界,您几乎可以在c ++中创建所需的任何编程风格。

例如,对于一个函数而言,它是要认识到从一个函数调用其他函数很不好,因为它会引起依赖关系,相反,您应该只调用原始函数参数的成员函数。



-2

Emacs是一个很好的例子:

Emacs架构

Emacs组件

Emacs Lisp测试使用skip-unlesslet-bind进行功能检测和测试装置:

有时,由于缺少先决条件,因此没有必要进行测试。可能没有编译必需的Emacs功能,要测试的功能可能会调用外部二进制文件,而该外部二进制文件可能在测试计算机上不可用,您将其命名。在这种情况下,宏skip-unless可用于跳过测试:

 (ert-deftest test-dbus ()
   "A test that checks D-BUS functionality."
   (skip-unless (featurep 'dbusbind))
   ...)

运行测试的结果不应取决于环境的当前状态,并且每个测试应使其环境保持其在其所在状态中的状态。尤其是,测试不应取决于任何Emacs定制变量或钩子,并且如果必须对Emacs的状态或Emacs外部状态(例如文件系统)进行任何更改,则无论更改是通过还是失败,都应在返回之前撤消这些更改。

测试不应依赖于环境,因为任何此类依赖关系都可能使测试变脆或导致仅在某些情况下发生且难以复制的故障。当然,被测代码可能具有影响其行为的设置。在这种情况下,最好对let-bind所有此类设置变量进行测试,以在测试期间设置特定的配置。该测试还可以设置许多不同的配置,并对每个配置运行被测代码。

和SQLite一样。这是它的设计:

  1. sqlite3_open()→打开与新的或现有的SQLite数据库的连接。sqlite3的构造函数。

  2. sqlite3→数据库连接对象。由sqlite3_open()创建并由sqlite3_close()销毁。

  3. sqlite3_stmt→准备好的语句对象。由sqlite3_prepare()创建,并由sqlite3_finalize()销毁。

  4. sqlite3_prepare()→将SQL文本编译为字节代码,它将完成查询或更新数据库的工作。sqlite3_stmt的构造函数。

  5. sqlite3_bind()→将应用程序数据存储到原始SQL的参数中。

  6. sqlite3_step()→将sqlite3_stmt前进到下一个结果行或完成。

  7. sqlite3_column()→sqlite3_stmt在当前结果行中的列值。

  8. sqlite3_finalize()→sqlite3_stmt的析构函数。

  9. sqlite3_exec()→一个包装函数,对一个或多个SQL语句字符串执行sqlite3_prepare(),sqlite3_step(),sqlite3_column()和sqlite3_finalize()。

  10. sqlite3_close()→sqlite3的析构函数。

sqlite3架构

令牌生成器,解析器和代码生成器组件用于处理SQL语句,并将其转换为虚拟机语言或字节码的可执行程序。粗略地说,这三层实现了sqlite3_prepare_v2()。前三层生成的字节码是准备好的语句。虚拟机模块负责运行SQL语句字节代码。B-Tree模块将数据库文件组织到具有有序键和对数性能的多个键/值存储中。Pager模块负责将数据库文件的页面加载到内存中,以实现和控制事务,并创建和维护日记文件,以防止崩溃或电源故障后数据库损坏。OS Interface是一个瘦的抽象,提供了一组通用的例程,以使SQLite适应在不同的操作系统上运行。粗略地说,最下面的四层实现sqlite3_step()

sqlite3虚拟表

虚拟表是在打开的SQLite数据库连接中注册的对象。从SQL语句的角度来看,虚拟表对象看起来像任何其他表或视图。但是在后台,对虚拟表的查询和更新将调用虚拟表对象的回调方法,而不是对数据库文件进行读写。

虚拟表可能表示内存中的数据结构。或者它可能代表磁盘上非SQLite格式的数据视图。或者,应用程序可以按需计算虚拟表的内容。

这是虚拟表的一些现有和假定用途:

全文搜索界面
使用R树的空间索引
内省SQLite数据库文件(dbstat虚拟表)的磁盘内容
读取和/或写入逗号分隔值(CSV)文件的内容
像访问数据库表一样访问主机的文件系统
启用R等统计数据包中数据的SQL操作

SQLite使用多种测试技术,包括:

三种独立开发的测试工具
部署后的配置中100%的分支测试覆盖率
数百万个测试案例
内存不足测试
I / O错误测试
碰撞和断电测试
模糊测试
边值测试
禁用的优化测试
回归测试
格式错误的数据库测试
广泛使用assert()和运行时检查
Valgrind分析
未定义的行为检查
检查清单

参考文献

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.