Java中的大类分解

12
我刚开始学习Java,并且好奇在Java中是否有良好的对象分解实践?让我描述一个问题。 在大型软件项目中,总是有一些名为“core”或“ui”的大类,它们往往具有许多方法,并且旨在作为较小类之间的中介者。例如,如果用户单击窗口上的按钮,则此窗口类将发送消息到'ui'类。 此“ui”类捕获此消息并通过调用其成员对象之一的方法对应地采取行动,或者如果是“退出应用程序”或“启动网络连接”之类的内容,则向应用程序“核心”发布消息。
这些对象很难分解,因为它们只是许多小应用程序对象之间的中介者。但是,在应用程序中拥有具有数百或数千个方法的类,即使这些方法是从一个对象委派到另一个对象的平凡任务,也不太方便。 C#通过允许将类实现拆分为多个源文件来解决此类问题:您可以按任意方式划分God对象,并且它将起作用。
有没有在Java中分解这些对象的实践?
5个回答

20

一种开始拆分如此庞大的对象的方法是首先找到由大对象管理的一组良好的字段或属性子集,这些字段相关并且不与对象的其他字段或属性交互。然后,只使用这些字段创建一个新的、更小的对象。也就是说,将全部逻辑从大类移动到新的较小类中。在原始的大类中,创建一个委托方法,只需简单地将请求传递下去。这是一个很好的第一步,只涉及改变大对象,虽然它并没有减少方法数量,但它可以大大减少大类中需要的逻辑。

经过几轮这样的操作后,您可以通过直接指向新的较小对象来删除部分委托,而不是通过以前处于所有事情中间的巨大对象。

请参见维基百科的委托模式讨论,以获取示例。

举个简单的例子,如果您有一个人员对象来代表公司员工,那么您可以创建一个薪资对象来跟踪与薪资相关的值,一个评级对象来跟踪员工的评级,一个奖项对象来跟踪人员赢得的奖项,等等。

换句话说,如果您最初拥有一个包含以下方法的大类,其中每个方法都包含业务逻辑和许多其他方法:

...
public boolean isManagement() { ... }
public boolean isExecutive() { ... }
public int getYearsOfService() { ... }
public Date getHireDate() { ... }
public int getDepartment() { ... }
public BigDecimal getBasePay() { ... }
public BigDecimal getStockShares() { ... }
public boolean hasStockSharePlan() { ... }
...

那么这个大对象可以在它的构造函数中创建一个新创建的对象StaffType,一个新创建的对象PayInformation和一个新创建的对象StaffInformation,最初这些方法在大对象中看起来像:

// Newly added variables, initialized in the constructor (or as appropriate)
private final StaffType      staffType;
private final StaffInformation staffInformation;
private final PayInformation payInformation;

...

public boolean isManagement() { return staffType.isManagement(); }
public boolean isExecutive() { return staffType.isExecutive(); }
public int getYearsOfService() { return staffInformation.getYearsOfService(); }
public Date getHireDate() { return staffInformation.getHireDate(); }
public int getDepartment() { return staffInformation.getDepartment(); }
public BigDecimal getBasePay() { return payInformation.getBasePay(); }
public BigDecimal getStockShares() { return payInformation.getStockShares(); }
public boolean hasStockSharePlan() { return payInformation.hasStockSharePlan(); }
...

原来存放在大对象中的所有逻辑已经被移到这三个新的较小对象中。通过这种改变,您可以将大对象分解成更小的部分,而无需触及使用大对象的任何内容。然而,随着时间的推移,您会发现一些大对象的客户端只需要访问其中一个可分割组件。对于这些客户端,他们可以直接使用小对象而不是使用大对象并委托给特定对象。但即使这种重构从未发生过,将不相关项目的业务逻辑分离到不同的类中也会改进事情。


在原始的大类中,创建一个委托方法,只需简单地将请求传递下去。<- 是指一个方法吗?还是针对神对象分解后的每个函数都要创建一个方法? - grigoryvp
@Eye of Hell:我扩展了我的答案来回答你的问题。 - Eddie
在StaffType内部的代码将非常难以访问god对象的方法。您需要将god对象的引用进行依赖注入,以便在staffType中使用。这是在Java中执行此类操作的推荐方式吗? - grigoryvp
重点是您可以在StaffType中不需要引用任何移入StaffType的内容之外的任何项的情况下执行此操作。如果上帝对象的每个字段确实依赖于对象的每个其他字段,那么将对象分解为困难的任务。 - Eddie
他们并不是真正的依赖关系,只是倾向于通过高级应用逻辑来调用它。例如,如果核心接收到“主窗口最小化”消息,它将访问配置管理器以获取设置,尝试图标管理器以可选地显示托盘图标,并使用十几个UI部件管理器来隐藏它们。 - grigoryvp
很多 UI 都会通过将事件传递给子组件来避免神对象反模式。"主窗口最小化"事件被设置为图标管理器和其他 UI 组件的一部分,然后它们基于这个事件来决定下一步该做什么。 - Laplie Anderson

1
下一步的逻辑可能是将BigClass更改为Java包。接下来,为每个相关功能组创建新对象(在每个类中指出该对象是新包的一部分)。
这样做的好处是减少依赖和提高性能。
  1. 无需导入整个包/BigClass即可获取少量方法。
  2. 相关功能的代码更改不需要重新编译/部署整个包/BigClass。
  3. 使用较小的类,分配/释放对象时使用的内存较少。

如何处理包内有多个对象的情况?例如,我将“核心”类分解为“核心”包,现在它有core.ui.main、core.ui.log等。现在一个日志窗口想要通知核心用户点击了“排序”按钮。它该如何调用core.ui.log.sort()?所有方法都必须是静态的吗? - grigoryvp
你可以这样做:导入 core.ui.*; 然后实例化 log 或以静态方式调用 sort() 方法,不过这取决于你的代码。 - Dana the Sane

0
我见过一些情况,这些情况通过继承来解决:假设类Big负责5个不同的事情,而且(由于各种原因)它们都必须在同一个类中。因此,您选择任意的继承顺序,并定义如下内容:
BigPart1 // all methods dealing with topic #1
BigPart2 extends BigPart1 // all methods dealing with topic #2
...
Big extends BigPart4 // all methods dealing with the last topic.

如果您真的可以将事物分层,使得故障有意义(即Part2实际上使用了Part1的东西,但反之则不然等),那么也许这样做有一些意义。

我看到过这种情况是在WebWorks中,一个类拥有大量的getter / setter方法--setter用于依赖注入(例如,在执行期间向对象传递URL参数)和getter用于使值可供各种页面模板访问(我认为是JSP)。

因此,逻辑上将Breakdown分组,例如假设类被称为MyAction,则存在MyActionBasicArgs(基本CGI参数的字段和setter),由MyActionAdvancedArgs(高级选项args)延伸,并由MyActionExposedValues(getter)延伸,再由MyActionDependencies(由Spring依赖注入使用的setter,非CGI args)延伸,最后是包含实际execute()方法的MyAction。

由于WebWorks中依赖注入的工作方式(或至少是当时的工作方式),必须是一个巨大的类,因此以这种方式进行分解可以使维护更加容易。但首先,请务必仔细考虑您的设计,尽可能避免单个巨大类的出现。


不幸的是,如果程序部分按层次结构组织,则情况会很好。在现实生活中,核心所寻找的东西包括“XML解析”,“网络服务器”,“网络客户端”,“用户界面”,“日志设施”等等。在这种情况下,很难从另一个委托组继承一个委托组 :) - grigoryvp
1
我觉得这个想法不太好。在BigPart4中,一个方法很容易意外地覆盖了父类的方法。虽然有趣,但最好不要滥用Java的继承机制。 - Tim Frey
@Eye of Hell 我认为这里的想法是不以经典继承方式为前提,而是将方法划分为逻辑组,使得 .java 文件易于阅读。客户端代码始终引用 Big,其中仍包含所有方法。 - Tim Frey
这将防止一个神对象方法调用另一个,如果“核心”是监督者和中介,这将非常普遍 :(. 例如,如果我将“网络”移动到父类中,它将失去调用其子“XML解析器”方法的能力,反之亦然 :( - grigoryvp
1
真的真的不是一个好主意。请不要这样做。每当有选择时,优先使用包含而不是组合! - Bill K
BillK: 我想你是指组合优于继承吗? Outlaw: 我不一定提倡这个答案,但你可以启用缺失的 @Override 注释来防止这种情况。 - Jeff Axelrod

0

是的,C#提供了部分类。我猜这就是你所说的:

C#通过允许将类实现分解为多个源文件来解决这个问题:您可以按任意方式划分大型对象,并且它将正常工作。

这确实有助于使庞大的类更易管理。然而,我发现当需要扩展代码生成器创建的代码时,最好使用部分类。当一个类像你所说的那样大时,几乎总是可以通过适当的面向对象设计将其分解为较小的类。使用部分类绕过了更正确的面向对象设计,这有时是可以接受的,因为最终目标是稳定、可靠、易于维护的代码,而不是面向对象代码的教科书式范例。然而,很多时候,将大型对象的代码放入同一类的大量较小的部分类实例中并不是理想的解决方案。

如果可能找到“上帝”对象属性的子集彼此不相互作用,则每个子集都可以成为新对象类型的良好候选。但是,如果这个“上帝”对象的所有属性都相互依赖,那么您就没有太多的方法来分解该对象。


0

我不知道为什么你会有这样一个如此庞大的类。

我想,如果你正在使用GUI构建器代码生成并且懒得处理它,你可能会陷入这种情况,但是除非你自己掌控,否则codegen通常会变得很糟糕。

任意拆分单个类是可怕的解决方案,是一种可怕的人为问题。(例如,代码重用将变得几乎不可能)

如果必须使用GUI构建器,请让它构建较小的组件,然后使用这些小组件来构建更大的GUI。每个组件应该只做一件事,并且做得很好。

尽量避免编辑生成的代码。将业务逻辑放入生成的“框架”中是一种可怕的设计模式。大多数代码生成器在这方面并不是很有帮助,因此请尝试仅进行一次最小编辑以从外部类中获取所需内容(考虑MVC,其中生成的代码是您的视图,而您编辑的代码应该在您的模型和控制器中)。

有时候你可以从Frame对象中暴露getComponents方法,通过迭代容器获取所有组件,然后动态地将它们绑定到数据和代码上(通常绑定到名称属性上效果很好)。我一直都能够安全地使用表单编辑器,而且所有的绑定代码往往非常容易抽象和重用。
如果你不是在谈论生成的代码——那么在你的“上帝”类中,它是否只做一项小工作并且做得很好?如果不是,就将一个“工作”分离出来,放到它自己的类中,并委托给它。
你的上帝类是否完全因素分解?当我看到像这样的巨大类时,通常会看到很多复制/粘贴/编辑行。如果有足够的相似性可以复制、粘贴和编辑某些部分,那么就足够将这些行因素分解成一个代码块了。
如果你的大类是GUI类,请考虑使用装饰器——可重用并将东西移出主类。双倍的收获。
我想你问题的答案是,在Java中我们只需使用良好的面向对象编程来确保问题不会在第一时间出现(或者我们不会——Java当然不比其他语言免疫你所谈论的问题)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接