如何在关系数据库驱动的应用程序中设计不好的数据库中创建更好的OO代码


19

我正在编写一个Java Web应用程序,该应用程序主要由一堆相似的页面组成,其中每个页面都有多个表和一个适用于这些表的过滤器。这些表上的数据来自SQL数据库。

我将myBatis用作ORM,在我的情况下,这可能不是最佳选择,因为数据库设计不良,而mybatis是面向数据库的工具。

我发现我正在编写很多重复的代码,因为由于数据库的不良设计,我不得不为类似的事情编写不同的查询,因为这些查询可能非常不同。也就是说,我无法轻松地将查询参数化。这会传播到我的代码中,而不是通过一个简单的循环来填充表中列上的行,我将代码改为:

得到一个数据(p1,...,pi);

得到B数据(p1,...,pi);

获得C数据(p1,...,pi);

获得D数据(p1,...,pi); ...

当我们有带有不同列的不同表时,这很快就会爆炸。

这也增加了我使用“ wicket”的复杂性,实际上是将对象映射到页面中的html元素。因此,我的Java代码成为了数据库和前端之间的适配器,这使我创建了许多布线,样板代码,并在其中混入了一些逻辑。

正确的解决方案是在ORM映射器上包裹一个额外层,该额外层为db提供一个更均匀的接口,还是有更好的方法来处理我正在编写的这些意大利面条式代码?

编辑:有关数据库的更多信息

该数据库主要保存电话信息。较差的设计包括:

具有人工ID作为主键的表与域知识无关。

没有唯一,触发器,检查或外键。

具有通用名称的字段,这些字段匹配不同记录的不同概念。

只能通过与其他具有不同条件的表交叉才能分类的记录。

应为数字或日期存储为字符串的列。

综上所述,到处都是凌乱/懒惰的设计。


7
是否可以更正数据库设计?
RMalke 2013年

1
请说明数据库的设计不佳。
图兰斯·科尔多瓦

@Renan Malke Stigliani不幸的是,并非如此,因为有依赖于它的旧版软件,但是我以一些略有不同的设计来镜像某些表并填充它们,从而简化了代码。不过我并不为此感到自豪,我宁愿不重复的表胡乱
DPM

1
这本书可能会给你SOM的如何开始修复datbase problesm并保持遗留代码工作eideas:amazon.com/...
HLGEM

4
您列出的大多数问题。。。不是。如今,使用替代键而不是自然键实际上是一种非常标准的建议;根本没有“差劲的设计”。就“不良设计”而言,缺少约束和使用不合适的列类型是一个更好的例子,但是它实际上根本不会影响您的应用程序代码(除非您计划滥用这些问题?)。
ruakh 2013年

Answers:


53

面向对象特别有价值,因为会出现这些类型的场景,并且它为您提供了合理设计抽象的工具,从而可以封装复杂性。

真正的问题是,您在哪里封装这种复杂性?

因此,让我退后一步,谈谈我在这里指的是什么“复杂性”。您的问题(据我了解;如果我错了,请纠正我)是一个持久性模型,该模型对于完成数据所需的任务不是有效可用的模型。它可能对其他任务有效且可用,但对您的任务无效。

那么,当我们拥有的数据不能为我们的方法提供良好的模型时,我们该怎么办?

翻译。这是您唯一可以做的。这种翻译就是我上面提到的“复杂性”。因此,既然我们接受了将要转换的模型,我们需要确定几个因素。

我们需要同时翻译两个方向吗?两种方向的翻译方式是否相同:

(Tbl A,Tbl B)-> Obj X(读取)

对象X->(Tbl A,Tbl B)(写)

还是插入/更新/删除活动代表不同类型的对象,以便您以Obj X的形式读取数据,但是从Obj Y插入/更新了数据?其中,你不妨去这两种方式,如果没有更新/插入/删除可能是重要的因素,在那里,你想放的翻译。


您在哪里翻译?

回到我在这个答案中所做的第一句话;OO允许您封装复杂性,而我在这里指的是事实,您不仅应该,而且如果您希望确保它不会泄漏并渗入所有代码中,则必须封装该复杂性。同时,重要的是要认识到您不能拥有完美的抽象,因此不必担心拥有一个非常有效和可用的抽象

现在又 您的问题是:您将这种复杂性放在哪里?好吧,你有选择。

你可以做到这一点使用存储过程的数据库。这具有经常不能很好地与ORM配合使用的缺点,但这并不总是正确的。存储过程可以带来一些好处,包括经常带来的性能。但是,存储过程可能需要大量维护,但是要由您来分析您的特定方案,并确定维护是否比其他选择更多或更少。我个人对存储过程非常熟练,因此,可用人才的这一事实减少了开销。永远不要低估根据您知道的事情做出决策的价值。有时,次优解决方案可能比正确的解决方案更理想,因为您或您的团队比最佳解决方案更了解如何创建和维护它。

数据库中的另一个选项是视图。取决于您的数据库服务器,这些可能是高度最佳或次优的,甚至根本无效,缺点之一可能是查询时间,具体取决于数据库中可用的索引选项。如果您不需要进行任何数据修改(插入/更新/删除),则视图将成为更好的选择。

越过数据库,您将拥有使用存储库模式的旧时机。这是一种经过时间检验的方法,非常有效。缺点往往包括样板,但是结构合理的存储库可以避免这种情况,即使这些确实导致不幸的样板数量,存储库也往往是简单的代码,易于理解和维护,并提供良好的API /抽象。另外,存储库还具有单元可测试性,这是数据库内选项所失去的。

诸如自动映射器之类的工具可能使使用ORM可行,从而可以在数据库模型从orm到可用模型之间进行转换,但是其中一些工具在维护/理解行为方面有些棘手,更像魔术。尽管它们可以使开销代码最小化,但在很好理解的情况下却可以减少维护开销。

接下来,您将从数据库中走得越来越,这意味着将有更多的代码要处理未翻译的持久性模型,这将是真正令人不快的。在这些情况下,您要谈论的是将翻译层放在您的UI中,听起来好像您现在正在做。这通常是一个非常糟糕的主意,并且会随着时间的流逝而恶化。


现在让我们开始疯狂地讲话。

Object不是唯一的终端都是,所有的抽象存在。多年来,计算机科学的研究甚至在此之前的数学研究中,都已经有了大量的抽象概念。如果要开始发挥创意,让我们开始谈论已经研究过的已知抽象。

有演员模型。这是一种有趣的方法,因为它说您要做的就是将消息发送到其他代码,从而将所有工作有效地委派给该其他代码,这对于封装所有代码的复杂性非常有效。只要您向演员发送一条消息,说“我需要将Obj X发送到Y”,并且您有一个容器等待位置Y的响应,然后处理Obj X,就可以这样做。您甚至可以发送一条消息来指示“我需要Obj X并完成计算Y,Z”,然后您甚至不需要等待;翻译发生在消息传递的另一端,如果您不需要阅读结果,可以继续进行翻译。出于您的目的,这可能会稍微滥用参与者模型,但这要视情况而定;

另一个封装边界是过程边界。这些可以非常有效地用于隔离复杂性。您可以将转换代码创建为Web服务,其中的通信是使用SOAP,REST的简单HTTP,或者如果您确实想要自己的协议(不建议)。STOMP并不是一个较差的较新协议。或者,将普通的守护程序服务与系统本地的公共存储管道一起使用,以使用您选择的任何协议再次快速通信。这实际上有一些不错的好处:

  • 您可以运行多个进程,同时为旧版本和较新版本提供翻译支持,从而允许您更新翻译服务以发布对象模型V2,然后在以后单独更新使用代码以与新对象一起使用模型。
  • 您可以做一些有趣的事情,例如将流程固定在性能核心上,通过使该流程成为唯一一个具有安全性特权运行的流程来接触该数据,还可以通过这种方法获得一定程度的安全性。
  • 当您在谈论过程边界时,您将获得一个非常牢固的边界,该边界将保持固定,从而确保长时间以来将抽象泄漏降至最低,因为在翻译空间中编写代码将无法在翻译空间之外被调用,因为它们不会共享流程范围,从而确保按合同确定一组固定的使用方案。
  • 异步/非阻塞更新的能力更加简单。

缺点显然是比通常需要的维护更多,通信开销会影响性能和维护。


封装复杂性的方法有很多种,可以将复杂性放在系统中越来越奇怪和奇怪的地方。使用高阶函数的形式(通常使用策略模式或各种其他奇怪形式的对象模式来伪装),您可以做一些非常有趣的事情。

是的,让我们开始谈论一个单子。您可以以非常小巧的特定功能的独立方式创建此转换层,这些功能执行必要的独立转换,但将所有这些转换功能隐藏在看不见的地方,因此外部代码几乎无法访问它们。这样做的好处是减少了对它们的依赖,使它们可以轻松更改而不会影响太多外部代码。然后,您创建一个可以接受任何高级OO模型类型对象的高阶函数(匿名函数,lambda函数,策略对象,但是您需要对其进行结构化)的类。然后,使用适当的转换方法让接受这些函数的基础代码执行文字执行。

这将创建一个边界,其中所有翻译不仅存在于边界的另一端,而且远离您的所有代码。它仅在那一侧使用,从而使您的其余代码甚至不知道有关该边界的入口点在哪里的任何信息。

好吧,是的,这的确在发疯,但谁知道呢?您可能只是疯了(认真地,不要从事疯狂程度低于88%的单子,这确实有造成人身伤害的危险)。


4
哇,这是一个非常全面的答案。如果只有SE允许我,我会不止一次投票。
Marjan Venema 2013年

11
电影版本何时发布?
yannis

3
@JimmyHoffa Bravo先生!!! 我将为这个答案添加书签,并在女儿长大时向她展示。
Tombatron

4

我的建议:

创建数据库视图,以:

  1. 给列起有意义的名字
  2. 进行“与具有不同条件的其他表的交叉”,这样您就可以隐藏这种复杂性。
  3. 将存储为字符串的数字或日期分别转换为数字和日期。
  4. 根据某些条件在没有的地方创建唯一性。

这个想法是创建一个外观,在外观上模仿一个更好的设计。

然后,使ORM与该外观相关,而不是与实际表相关。

但是,这并不能简化插入。


使用数据库视图看起来像是一个好主意,也是最优雅的操作过程,可以从最低层面抽象出丑陋的外观,出于某种原因,我没有考虑过。谢谢。
DPM

3

我可以看到您现有的数据库架构如何导致您编写更具体的代码和查询任务,而这些任务本来可以通过设计更好的架构抽象出来的,但是这不会妨碍您编写良好的面向对象代码的能力。

  • 记住SOLID原则
  • 编写可以轻松进行单元测试的代码(通常遵循SOLID原则)。
  • 将您的业务逻辑与显示逻辑分开。
  • 阅读Apache Wicket文档和示例-该框架可能比您想象的节省了更多样板代码,因此学习如何有效地使用它。
  • 将必须处理数据库的逻辑放在单独的层中,该层提供业务逻辑可以使用的干净接口。这样,如果您(或将来的维护者)有机会改进架构,那么他们可以这样做而无需对业务逻辑进行太多更改。

当您发现自己使用的数据库架构并不完美时,很容易会遇到各种使工作变得更加困难的方法,但是在某些时候,您必须撇开这些抱怨并充分利用它。

尽管架构不完善,也可以将其视为利用您的创造力编写干净,可重用,易于维护的代码的机会。


1

在回答有关更好的面向对象代码的最初问题时,我建议使用讲SQL的对象。ORM本质上违反面向对象的原理,因为它对一个对象进行操作,并且OOP中的对象是一个自给自足的实体,它具有用于实现其目标的所有资源。我确信这种方法可以使您的代码更简单。

谈到问题空间,即您的域,我将尝试确定聚合根。这些是您域的一致性边界。边界必须始终保持其一致性。聚合通过域事件进行通信。如果您的系统足够大,则可能应该在子系统上对其进行拆分(称为SOA,微服务,自包含系统等)。

我还会考虑使用CQRS -它可以大大简化您的写入和读取方面。确保阅读Udi Dahan 关于该主题的文章

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.