只有一个(公共)方法的类是否有问题?


51

我目前正在从事一个软件项目,该项目对视频监控镜头进行压缩和索引编制。压缩的工作方式是分割背景和前景对象,然后将背景另存为静态图像,并将前景另存为子画面。

最近,我开始复习我为该项目设计的一些课程。

我注意到有许多类只有一个公共方法。其中一些类是:

  • VideoCompressor(使用一种compress方法,该方法接收类型的输入视频RawVideo并返回类型的输出视频CompressedVideo)。
  • VideoSplitter(使用一种split方法,该方法接收类型的输入视频RawVideo并返回2个输出视频的向量,每个类型均为RawVideo)。
  • VideoIndexer(使用index接收类型为type的输入视频RawVideo并返回类型为video的视频索引的方法VideoIndex)。

我发现自己实例每个班只是为了让像电话VideoCompressor.compress(...)VideoSplitter.split(...)VideoIndexer.index(...)

从表面上看,我确实认为类名足以说明其预期功能,实际上它们是名词。相应地,它们的方法也是动词。

这真的有问题吗?


14
这取决于语言。在像C ++或Python这样的多范式语言中,这些类不存在任何业务:它们的“方法”应该是自由函数。

3
@delnan:即使在C ++中,即使不需要完整的“ OO功能”,也通常使用类来创建模块。确实,可以使用命名空间代替将“自由函数”组合到一个模块中,但是我看不出有什么好处。
布朗

5
@DocBrown:C ++为此提供了名称空间。实际上,在现代C ++中,您经常对方法使用自由函数,因为它们可以(静态地)对所有参数重载,而方法只能对调用方进行重载。
Jan Hudec 2014年

2
@DocBrown:这是问题的核心。当函数足够复杂时,使用仅具有一种“外部”方法的名称空间的模块就可以很好地工作。因为名称空间不假装代表任何东西,也不假装是面向对象的。类确实可以,但是像这样的类实际上只是名称空间。当然,在Java中您无法使用函数,因此就是结果。对Java感到羞耻。
Jan Hudec 2014年

Answers:


87

不,这不是问题,相反。这是班级模块化和明确责任的标志。从该类用户的角度来看,精益接口很容易掌握,并且会鼓励松耦合。这具有许多优点,但几乎没有缺点。我希望以这种方式设计更多组件!


6
这是正确的答案,我已经赞成,但是您可以通过解释一些优点来使它更好。我认为OP的直觉告诉他一个问题是其他问题……也就是说,VideoCompressor确实是一个界面。Mp4VideoCompressor是一个类,应该可以与另一个VideoCompressor互换,而无需将VideoSplitter的代码复制到新的类中。
pdr 2014年

1
抱歉,您错了。具有单个方法的类应该只是一个独立的函数。
Miles Rout 2014年

3
@MilesRout:您的评论表明您误解了这个问题-OP用一个公共方法而不是一个方法编写了一个类(他的意思是一个具有多个私有方法的类,正如他在一条评论中告诉我们的那样)。
布朗

2
“具有单个方法的类应该只是一个独立的函数。” 关于该方法是什么的粗略假设。我有一类用一种公共方法。BEHIND IT是该类中的多个方法,再加上5个完全不了解客户端的其他类。SRP的出色应用将产生清晰,简单的分层类结构的界面,这种简单性一直体现在最上层。
Radarbob 2015年

2
@Andy:问题是“这是一个问题”,而不是“这是否符合特定的OO定义”。而且,OP表示没有状态的类的问题绝对没有迹象。
布朗

26

它不再是面向对象的。由于这些类不代表任何内容,因此它们只是功能的容器。

这并不意味着它是错误的。如果功能足够复杂或具有通用性(即,参数是接口,而不是具体的最终类型),则可以将该功能放在单独的模块中

从那里开始,取决于您的语言。如果该语言具有自由功能,则应将其作为模块导出功能。为什么不假装它是一堂课。如果该语言没有像Java这样的自由函数,则可以使用单个public方法创建类。好吧,这仅显示了面向对象设计的局限性。有时功能只是更好的匹配。

在一种情况下,您可能需要一个具有单个公共方法的类,因为它必须实现具有单个公共方法的接口。无论是用于观察者模式,依赖注入还是其他。在这里,它再次取决于语言。在具有一流函子(C ++(std::function或模板参数),C#(委托),Python,Perl,Ruby(proc),Lisp,Haskell等)的语言中,这些模式使用函数类型,不需要类。Java没有(但是,在版本8中会)具有函数类型,因此您使用单方法接口和相应的单方法类。

当然,我不主张编写单个巨大的功能。它应该具有私有子例程,但是它们可以对实现文件(在C ++中为文件级静态或匿名名称空间)或仅在公共函数内部实例化的私有帮助器类(是否要存储数据?)私有。


12
“为什么假装不是一堂课呢?” 使用对象而不是自由函数可以进行状态和子类型化。在需求不断变化的世界中,这可能证明是有价值的。迟早需要使用视频压缩设置或提供备用压缩算法。在此之前,“类”一词只是简单地告诉我们,此功能属于一个易于扩展且可互换的软件模块,具有明确的职责。这不是OO真正的目的吗?

1
好吧,如果它只有一个公共方法(并且某种意义上没有保护的方法),那么它实际上就不能扩展。当然,在这种情况下,压缩参数成为函数对象可能是有意义的(某些语言对此有单独的支持,有些则没有)。
Jan Hudec 2014年

3
这是对象和函数之间的基本联系:一个函数同一个对象同构,只有一个方法,没有字段。闭包对于使用单一方法和某些字段的对象是同构的。返回所有都在同一变量集上关闭的多个函数之一的选择器函数与对象同构。(实际上,这是用JavaScript编码对象的方式,除了使用字典数据结构而不是选择器函数。)
JörgW Mittag 2014年

4
“它不再是面向对象的。因为那些类不代表任何东西,所以它们只是函数的容器。” -这是错的。我认为这种方法更多是面向对象的。OO并不表示它代表真实世界的对象。大量的编程处理抽象概念,但这是否意味着您不能应用面向对象的方法来解决它?当然不。它更多地与模块化和抽象有关。每个对象都有一个责任。这样可以很容易地围绕程序进行更改。没有足够的空间来列出OOP的众多优点。
Despertar 2014年

2
@Phoshi:是的,我明白。我从来没有声称功能性方法也不会奏效。但是,这显然不是主题。视频压缩器或视频透传器或任何仍然是对象的完美有效候选者。

13

可能有理由将给定方法提取到专用类中。这些原因之一是允许进行依赖注入。

假设您有一个名为的类VideoExporter,它最终应该能够压缩视频。一个干净的方法是拥有一个接口:

interface IVideoCompressor
{
    Stream compress(Video video);
}

可以这样实现:

class MpegVideoCompressor : IVideoCompressor
{
    // ...
}

class FlashVideoCompressor : IVideoCompressor
{
    // ...
}

并像这样使用:

class VideoExporter
{
    // ...
    void export(Destination destination, IVideoCompressor compressor)
    {
        // ...
        destination = compressor(this.source);
        // ...
    }
    // ...
}

一个糟糕的选择是拥有一个VideoExporter拥有大量公共方法并且可以完成所有工作的工作,包括压缩。这将很快成为维护方面的噩梦,很难添加对其他视频格式的支持。


2
您的答案不能区分公共方法和私有方法。可以理解为建议将类的所有代码都放在一个方法中,但是我想那不是您的意思吗?
Doc Brown

2
@DocBrown:私有方法与此问题无关。他们可以参加内部帮手课程或其他任何课程。
Jan Hudec 2014年

2
@JanHudec:目前的文字是“一个不正确的选择是拥有一个拥有大量方法的VideoExporter”-但应该是“一个不正确的选择就是拥有具有众多公共方法的VideoExporter ”。
Doc Brown

@DocBrown:在这里同意。
Jan Hudec 2014年

2
@DocBrown:谢谢您的发言。我编辑了答案。最初,我以为这个问题(也是我的答案)仅是关于公共方法的。似乎并不那么明显。
2014年

6

这表明您要将函数作为参数传递给其他函数。我猜您的语言(Java?)不支持它;如果是这样,这并不是设计上的失败,而是您选择的语言的缺点。这是语言坚持要求所有事物都必须是一类的最大问题之一。

如果您实际上并没有传递这些伪函数,那么您只需要一个自由/静态函数。


1
从Java 8开始,您有了lambda,因此几乎可以传递函数。
Silviu Burcea 2014年

公平的说法,但是不会正式发布一段时间,而且某些工作场所向新版本迁移的速度很慢。
2014年

发行日期是三月。此外,EAP版本在JDK 8中非常受欢迎:)
Silviu Burcea 2014年

尽管Java 8 lambda只是仅使用一种公共方法定义对象的简捷方式,除了节省了一些样板代码之外,它们在这里没有任何区别。
2013年

5

我知道我参加晚会很晚,但是每个人似乎都错过了这一点:

这是一个众所周知的设计模式,称为:策略模式

当有几种可能的策略可以解决子问题时,将使用策略模式。通常,您定义一个在所有实现上强制执行合同的接口,然后使用某种形式的依赖注入为您提供具体的策略。

例如,在这种情况下,您可以拥有interface VideoCompressor和然后有几个替代实现,例如class H264Compressor implements VideoCompressorclass XVidCompressor implements VideoCompressor。从OP尚不清楚其中是否包含接口,即使没有接口,也可能只是原作者在需要时打开了实施策略模式的大门。哪一个本身就是好的设计。

OP经常发现自己实例化类来调用方法的问题是她没有正确使用依赖项注入和策略模式的问题。与其在需要的地方实例化它,不如说包含类应该包含一个带有strategy对象的成员。并且该成员应该被注入,例如在构造函数中。

在许多情况下,策略模式仅使用一种doStuff(...)方法即可生成接口类(如您所示)。


1
public interface IVideoProcessor
{
   void Split();

   void Compress();

   void Index();
}

您所拥有的是模块化的,这很好,但是如果您将这些职责归为IVideoProcessor,那么从DDD的角度来看可能更有意义。

另一方面,如果拆分,压缩和索引没有任何关系,那么我将它们保留为单独的组件。


由于每个功能都需要更改的原因有些不同,因此我认为像这样将它们放在一起会违反SRP。
Jules

0

这是一个问题-您是在设计的功能方面,而不是数据上工作。您实际拥有的是3个独立的功能,这些功能已经过OO标准化。

例如,您有一个VideoCompressor类。为什么要使用旨在压缩视频的类-为什么没有在其视频类上压缩该类型每个对象包含的(视频)数据的Video类?

设计OO系统时,最好创建代表对象的类,而不是代表可以应用的活动的类。在过去,类被称为类型-OO是一种扩展语言并支持新数据类型的方法。如果您这样想OO,那么您将获得一种更好的类设计方法。

编辑:

让我尝试更好地解释一下自己,想象一下具有concat方法的字符串类。您可以实现这样的事情,其中​​从类实例化的每个对象都包含字符串数据,因此您可以说

string mystring("Hello"); 
mystring.concat("World");

但是OP希望它像这样工作:

string mystring();
string result = mystring.concat("Hello", "World");

现在在某些地方可以使用类来保存相关功能的集合,但这不是OO,这是一种使用语言的OO功能来帮助更好地管理代码库的便捷方法,但绝不是任何一种OO设计”。在这种情况下,对象是完全人为的,像这样简单地使用,因为该语言无法提供更好的方法来解决此类问题。例如。在诸如C#之类的语言中,您将使用静态类来提供此功能-它重用了类机制,但是您不再需要实例化一个对象来仅调用其上的方法。最终您会string.IsNullOrEmpty(mystring)得到一些我认为比差的方法mystring.isNullOrEmpty()

因此,如果有人问“我如何设计类”,我建议考虑类将包含的数据,而不是类所包含的功能。如果您追求“一个类是一堆方法”,那么最终您将编写“更好的C”样式代码。(如果要改进C代码,这不一定是一件坏事),但它不会为您提供最佳的OO设计程序。


9
-1,我完全不同意。初学者的OO设计仅适用于像a这样的数据对象,Video并且倾向于过度使用具有此类功能的此类,这些通常以混乱的代码结尾,每个类的LOC大于10K。先进的OO设计将功能分解为较小的单元(例如)VideoCompressor(并让一个Video类成为数据类或的门面VideoCompressor)。
Doc Brown

5
@DocBrown:但此时它不再是面向对象的设计,因为VideoCompressor并不代表object。这没什么不对,只是表明了面向对象设计的局限性。
Jan Hudec 2014年

3
啊,但是将1个函数转换为一个类的初学者OO根本不是真正的OO,它只会鼓励大规模的解耦,最终导致成千上万个无法维护的类文件。我认为最好将类视为“数据包装器”而不是“功能包装器”,这将使初学者更好地理解如何考虑OO程序。
gbjbaanb 2014年

4
@DocBrown实际上准确地表明了我的关注;我确实有一个Video类,但是压缩方法绝非易事,因此我实际上会将compress方法分解为其他多个私有方法。在阅读了不要创建动词类
yjwong 2014年

2
我想这可能是确定有一Video类,但它会组成VideoCompressor,一个VideoSplitter和其他相关类,应在良好的面向对象的形式,是具有高度凝聚力个人类。
埃里克·金

-1

ISP(接口分离原则)说,没有客户端应该被迫依赖于它不使用方法。好处是多方面的,而且很明显。您的方法完全尊重ISP,这很好。

同样尊重ISP的另一种方法是,例如,为每个方法(或具有高内聚性的方法集)创建一个接口,然后让一个类来实现所有这些接口。这是否是更好的解决方案取决于方案。这样做的好处是,当使用依赖项注入时,您可以使一个客户端具有不同的协作者(每个接口一个),但最终所有协作者都将指向同一对象实例。

对了你说

我发现自己实例化了每堂课

,这些类似乎是服务(因此是无状态的)。您是否考虑过让它们单身?


1
通常,单例是反模式。请参阅Singletons:解决自1995年以来就没有的问题
Jan Hudec 2014年

3
对于其他情况,尽管接口分离原理是一个很好的模式,但这个问题是关于实现的,而不是接口,因此我不确定它是否相关。
Jan Hudec 2014年

3
我不想讨论单例是模式还是反模式。我为这个问题提出了一个可能的解决方案(甚至有个名字),这取决于他来决定是否适合。
diegomtassis 2014年

关于ISP是否相关,问题的主题是“只有一个方法的类是一个问题吗?”,与之相反的是有很多方法的类,当您创建一个方法时就成为问题。客户直接依赖它...正是罗伯特·马丁(Robert Martin)的施乐(xerox)示例,这使他制定了ISP。
diegomtassis 2014年

-1

是的,有问题。但并不严重。我的意思是,您可以像这样构造代码,并且不会发生任何不良情况,它是可维护的。但是这种结构存在一些弱点。例如,考虑如果视频的表示形式(如果您将其分组在RawVideo类中),则需要更新所有操作类。或者考虑一下,您可能会在运行时中看到多种视频表示形式。然后,您将必须将表示形式与特定的“操作”类进行匹配。同样从主观上讲,为要进行的每个操作拖拽一个依赖项也是很烦人的。并在每次您决定不再需要该操作或需要新操作时更新传递的依赖关系列表。

这实际上也违反了SRP。有些人只是将SRP视为划分职责的指南(并通过将每个操作视为不同的职责而走到了很远),但他们忘记了SRP也是划分职责的指南。根据SRP的职责,出于相同原因而进行的更改应归为一组,以便在发生更改时将其本地化为尽可能少的类/模块。对于大类,只要在同一个类中具有多个算法就没有问题,只要这些算法是相关的(即,共享一些在该类之外不应该知道的知识)。问题是大类的算法完全没有关联,并且由于不同的原因而改变/变化。

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.