应用SOLID原则


13

我对SOLID设计原则很陌生。我了解它们的原因和好处,但是我未能将它们应用到一个较小的项目中,我想将其重构为使用SOLID原理的实际练习。我知道没有必要更改运行正常的应用程序,但是无论如何我都希望对其进行重构,这样我才能获得未来项目的设计经验。

该应用程序具有以下任务(实际上还有很多任务,但让我们保持简单):它必须读取一个包含数据库表/列/视图等定义的XML文件,并创建一个SQL文件,该文件可用于创建ORACLE数据库架构。

(注意:请不要讨论为什么我需要它或为什么我不使用XSLT等,这是有原因的,但是它们不在主题之列。)

首先,我选择仅查看表和约束。如果忽略列,则可以通过以下方式声明它:

约束是表的一部分(或更准确地说,是CREATE TABLE语句的一部分),并且约束也可以引用另一个表。

首先,我将说明应用程序现在的外观(不应用SOLID):

目前,该应用程序具有“表”类,该类包含表所拥有的约束的指针列表以及引用该表的约束的指针列表。每当建立连接时,也会建立向后连接。该表具有createStatement()方法,该方法依次调用每个约束的createStatement()函数。所述方法本身将使用到所有者表和引用表的连接,以检索它们的名称。

显然,这根本不适用于SOLID。例如,存在循环依赖关系,这些依赖关系根据所需的“添加” /“删除”方法和一些大型对象析构函数使代码膨胀。

所以有两个问题:

  1. 我应该使用依赖注入来解决循环依赖吗?如果是这样,我想约束应该在其构造函数中接收所有者(以及可选的被引用)表。但是,如何在单个表的约束列表上运行呢?
  2. 如果Table类都存储自身的状态(例如,表名,表注释等)以及到约束的链接,那么考虑“单一职责原则”,这是一个还是两个“职责”?
  3. 如果情况2是正确的,我是否应该在管理链接的逻辑业务层中创建一个新类?如果是这样,1.显然不再相关。
  4. 应该将“ createStatement”方法作为Table / Constraint类的一部分,还是应该将它们移出?如果是这样,去哪儿?每个数据存储类(例如,表,约束等)一个管理器类?还是为每个链接创建一个管理器类(类似于3)?

每当我尝试回答这些问题之一时,我都会发现自己在某个地方盘旋。

如果您包括列,索引等,问题显然变得更加复杂,但是如果你们帮助我解决简单的Table / Constraint问题,我也许可以自己解决其他问题。


3
您使用什么语言?您能否发布至少一些框架代码?在没有看到实际代码的情况下,很难讨论代码质量和可能的重构。
彼得Török

我正在使用C ++,但是我试图将其排除在讨论范围之外,因为您可能会遇到任何语言的问题
Tim Meyer

是的,但是模式和重构的应用取决于语言。例如,@ back2dos在下面的回答中建议了AOP,这显然不适用于C ++。
彼得Török

请参考programmers.stackexchange.com/questions/155852/...更多关于SOLID原则
LCJ

Answers:


8

您可能从不同的角度出发,在此处应用“单一责任原则”。您显示给我们的只是(或多或少)您应用程序的数据模型。SRP在这里意味着:确保您的数据模型仅负责保存数据-少不多。

所以,当你要读你的XML文件,创建从它的数据模型,并写SQL,你应该有什么不能做的就是实现什么到你的Table类,这是XML或SQL具体。您希望您的数据流如下所示:

[XML] -> ("Read XML") -> [Data model of DB definition] -> ("Write SQL") -> [SQL]

因此,唯一应放置XML特定代码的地方是一个名为的类Read_XML。SQL专用代码的唯一位置应该是Write_SQL。当然,也许您打算将这两个任务拆分为更多的子任务(并将您的类拆分为多个管理器类),但是您的“数据模型”不应对此层承担任何责任。因此,请勿createStatement在您的任何数据模型类中添加a ,因为这会使您的数据模型对SQL负责。

当您描述表负责保存表的所有部分(名称,列,注释,约束...)时,我没有发现任何问题,这就是数据模型背后的想法。但是您描述的“表”还负责其某些部分的内存管理。这是特定于C ++的问题,在Java或C#之类的语言中,您将不会轻易面对它。摆脱这些责任的C ++方法是使用智能指针,将所有权委派给另一个层(例如,boost库或您自己的“智能”指针层)。但是请注意,您的循环依赖关系可能会“激怒”某些智能指针实现。

关于SOLID的更多信息:这是一篇不错的文章

http://cre8ivethought.com/blog/2011/08/23/software-development-is-not-a-jenga-game

通过一个小例子解释SOLID。让我们尝试将其应用于您的案例:

  • 您不仅需要类Read_XMLWrite_SQL,还需要管理这两个类的交互的第三类。让我们称之为ConversionManager

  • 应用DI原则可能意味着在这里:ConversionManager不应创建的实例 Read_XML,并Write_SQL通过自身。而是可以通过构造函数注入这些对象。并且构造函数应该具有这样的签名

    ConversionManager(IDataModelReader reader, IDataModelWriter writer)

IDataModelReader从哪里Read_XML继承的接口,与IDataModelWriter相同Write_SQL。这ConversionManager为扩展提供了一个开放空间(您很容易提供不同的读者或作家)而无需更改它-因此我们以开放/封闭原则为例。想一想,当您想支持另一个数据库供应商时,您将需要更改什么-理想情况下,您不必更改数据模型中的任何内容,只需提供另一个SQL-Writer。


尽管这是SOLID的非常合理的练习,但(注明)请注意,它要求“获取者”和“设定者”使用相当贫乏的数据模型,这违反了“老派的Kay / Holub OOP”。这也让我想起了臭名昭著的史蒂夫·叶格(Steve Yegge)
user949300

2

好吧,在这种情况下,您应该应用SOLID的S。

一个表包含在其上定义的所有约束。约束包含它引用的所有表。简单明了的模型。

您所坚持的是执行反向查找的能力,即找出某个表被哪个约束引用的能力。
因此,您真正想要的是索引服务。那是完全不同的任务,因此应该由另一个对象来执行。

要将其分解为非常简化的版本:

class Table {
      void addConstraint(Constraint constraint) { ... }
      bool removeConstraint(Constraint constraint) { ... }
      Iterator<Constraint> getConstraints() { ... }
}
class Constraint {
      //actually I am not so sure these two should be exposed directly at all
      void addReference(Table to) { ... }
      bool removeReference(Table to) { ... }
      Iterator<Table> getReferencedTables() { ... }
}
class Database {
      void addTable(Table table) { ... }
      bool removeTable(Table table) { ... }
      Iterator<Table> getTables() { ... }
}
class Index {
      Iterator<Constraint> getConstraintsReferencing(Table target) { ... }
}

关于索引的实现,有3种方法:

  • getContraintsReferencing方法实际上可以只DatabaseTable实例进行整体爬网,并对实例进行爬网Constraint以获得结果。根据它的成本和需要的频率,它可能是一个选择。
  • 它也可以使用缓存。如果您的数据库模型一旦定义就可以更改,则可以通过在实例TableConstraint实例发生变化时触发信号来维护高速缓存。一个稍微简单的解决方案是Index建立一个整体的“快照索引”,Database然后将其丢弃。如果您的应用程序在“建模时间”和“查询时间”之间有很大的区别,那当然是可能的。如果很有可能同时执行这两个操作,那么这是不可行的。
  • 另一种选择是使用AOP拦截整个创建调用并相应地维护索引。

非常详细的答案,到目前为止,我很喜欢您的解决方案!如果我为Table类执行DI,并在构造过程中为其提供约束列表,您会怎么想?无论如何,我都有一个TableParser类,在这种情况下,它可以充当工厂或与工厂一起工作。
Tim Meyer

@Tim Meyer:DI不一定是构造函数注入。DI也可以通过成员函数来完成。表是否应该通过构造函数获取所有部分取决于您是否只希望在构造时添加这些部分并且以后再也不要更改,或者是否要逐步创建表。那应该是您设计决策的基础。
Doc Brown

1

解决循环依赖的方法是发誓永远不会创建它们。我发现编码测试优先是一种强大的威慑力。

无论如何,循环依赖始终可以通过引入抽象基类来打破。这是典型的图形表示形式。这里的表是节点,外键约束是边。因此,创建一个抽象的Table类和一个抽象的Constraint类,以及一个抽象的Column类。然后,所有实现都可以依赖于抽象类。这可能不是最好的表示形式,但是它是对相互耦合的类的改进。

但是,您怀疑,解决此问题的最佳方法可能不需要跟踪对象关系。如果只想将XML转换为SQL,则不需要约束图的内存表示形式。如果您想运行图算法,那么约束图会很好,但是您没有提到它,因此我认为这不是必需的。您只需要一个表列表和一个约束列表,以及每个要支持的SQL方言的访问者。生成表,然后生成表外部的约束。在需求改变之前,将SQL生成器耦合到XML DOM不会有任何问题。为明天保存明天。


这就是“(实际上要更多,但让我们保持简单)”的作用。例如,在某些情况下,我需要删除一个表,因此我需要检查是否有任何约束在引用该表。
Tim Meyer
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.