不代表任何内容的类-是否正确?


42

我只是在设计应用程序,不确定我是否正确理解SOLID和OOP。类应该做一件事情并且做得很好,但另一方面,它们应该代表我们所使用的真实对象。

就我而言,我对数据集进行特征提取,然后进行机器学习分析。我假设我可以创建三个类

  1. FeatureExtractor
  2. 数据集
  3. 分析仪

但是FeatureExtractor类不代表任何东西,它的作用使它比类更像是一个例程。它只有一个将要使用的函数:extract_features()

创建不代表一件事而是做一件事的类是否正确?

编辑:不确定是否重要,但我正在使用Python

并且如果extract_features()看起来像这样:是否值得创建一个特殊的类来保存该方法?

def extract_features(df):
    extr = PhrasesExtractor()
    extr.build_vocabulary(df["Text"].tolist())

    sent = SentimentAnalyser()
    sent.load()

    df = add_features(df, extr.features)
    df = mark_features(df, extr.extract_features)
    df = drop_infrequent_features(df)
    df = another_processing1(df)
    df = another_processing2(df)
    df = another_processing3(df)
    df = set_sentiment(df, sent.get_sentiment)
    return df

13
这看起来很完美。考虑到您列出为模块的三件事是可以的,您可能希望将它们放在不同的文件中,但这并不意味着它们必须是类。
Bergi '18

31
请注意,在Python中使用非OO方法非常普遍并且可以接受。
jpmc26 '18

13
您可能对域驱动设计感兴趣。实际上,“类应该表示来自现实世界的对象”是错误的……它们应该表示域中的对象。该域通常与现实世界紧密地联系在一起,但是根据应用程序的不同,某些事物可能会或可能不会被视为对象,或者某些“实际上”是分开的事物最终可能会在应用程序域内部链接在一起或完全相同。
Bakuriu

1
随着您对OOP的逐渐熟悉,我认为您会发现类很少与现实世界的实体一对一对应。例如,下面的论证试图补习班都在单一类真实世界的实体相关联的功能的作文非常频繁反模式:programmer.97things.oreilly.com/wiki/index.php/...
凯文-恢复莫妮卡

1
“它们应该代表我们合作的真实对象。” 不必要。许多语言都有代表字节流的流类,这是一个抽象概念,而不是“真实对象”。从技术上讲,文件系统也不是“真实对象”,它只是一个概念,但是有时有些类代表文件系统或文件系统的一部分。
法拉普

Answers:


96

班上应该做一件事并且做好

是的,通常这是一个好方法。

但另一方面,它们应该代表我们合作的真实对象。

不,那是恕我直言的常见误解。一个好的初学者对OOP的访问通常是“从代表现实世界中事物的对象开始”,这是事实。

但是,您不应止步于此

可以(并且应该)使用类以各种方式来构造程序。从现实世界建模对象是这一方面的一个方面,但不是唯一的一个方面。为特定任务创建模块或组件是类的另一个合理用例。“功能提取器”可能就是这样的模块,即使它仅包含一个公共方法extract_features(),如果不包含很多私有方法以及某些共享状态,我也会感到惊讶。因此,拥有一类FeatureExtractor将为这些私有方法引入自然的位置。

旁注:在像Python这样的语言中,支持单独的模块概念的人也可以FeatureExtractor为此使用模块,但是在这个问题的上下文中,恕我直言,这是可以忽略的差异。

而且,“特征提取器”可以想象为“提取特征的人或机器人”。那是一个抽象的“事物”,也许不是您在现实世界中会发现的事物,但是名称本身是一个有用的抽象,它使每个人都可以知道该类的职责是什么。因此,我不同意此类不会“代表任何东西”。


33
我特别喜欢您提到内部状态问题。我经常发现这是决定是否将某些东西做成Python类或函数的决定性因素。
David Z

17
您似乎推荐“经典”。不要为此创建类,它是Java样式的反模式。如果是函数,则使其成为函数。内部状态无关紧要:函数可以具有该状态(通过闭包)。
康拉德·鲁道夫'18

25
@KonradRudolph:您似乎错过了这与执行一个函数无关。这是关于一段代码的,它需要多个函数,一个通用名称以及可能的某些共享状态。为此,使用模块而不是类来定义模块概念的语言可能是明智的。
布朗

8
@DocBrown我不得不不同意。这是具有一个公共方法的类。在API方面,这功能是无法区分的。有关详细信息,请参见我的答案。如果需要创建表示状态的特定类型,则可以使用单方法类。但是这里不是这种情况(即使那样,有时也可以使用函数)。
康拉德·鲁道夫'18

6
@DocBrown我开始明白您的意思了:您正在谈论从内部调用的函数extract_features?我只是假设它们是来自其他地方的公共职能。公平地说,我同意,如果它们是私有的,则它们可能应该与一起进入一个模块(但仍然:不是一个类,除非它们共享状态)extract_features。(也就是说,您当然可以在该函数中本地声明它们。)
Konrad Rudolph

44

布朗博士(Doc Brown)是现场专家:类不需要代表现实世界的对象。它们只需要有用。类从根本上来说仅仅是附加类型,在现实世界中,类intstring对应于什么?它们是抽象的描述,而不是具体的,有形的东西。

也就是说,您的情况很特殊。根据您的描述:

并且如果extract_features()看起来像这样:是否值得创建一个特殊的类来保存该方法?

您绝对正确:如果您的代码如图所示,则将其放入类是没有用的。有一个著名的演讲认为,Python中类的这种使用具有代码味道,并且简单的函数通常就足够了。您的情况就是一个很好的例子。

类的过度使用是由于OOP在1990年代成为Java的主流。不幸的是,当时的Java缺少几种现代语言功能(例如闭包),这意味着许多概念如果不使用类就很难表达或无法表达。例如,直到最近,Java中才有带有状态的方法(即闭包)。取而代之的是,您必须编写一个类来承载状态,并且该类公开了一个方法(称为invoke)。

不幸的是,这种编程风格远远超过了Java(部分原因是很有影响力的软件工程书,否则它非常有用),即使是在不需要这种解决方法的语言中也是如此。

在Python中,类显然是一个非常重要的工具,应自由使用。但是它们并不是唯一的工具,没有理由在没有意义的地方使用它们。一个常见的误解是,自由功能在OOP中没有位置。


补充一点是有用的,如果示例中调用的函数实际上是私有函数,则将它们封装在模块或类中将是完全合适的。否则,我完全同意。
Leliel

11
记住“ OOP”并不意味着“编写一堆类”也很有用。函数是Python中的对象,因此无需将其视为“非OOP”。相反,它只是简单地重用了内置的类型/类,而“重用”是编程中的圣旨之一。将其包装在类中将阻止重用,因为没有任何东西与此新的API兼容(除非__call__定义了,在这种情况下,请使用函数!)
Warbo,

3
同样关于“类与独立功能”的主题:eev.ee/blog/2013/03/03/…–
Joker_vD

1
难道不是这样吗,尽管在Python中“自由函数”也是具有公开方法的类型的对象__call__()?这与匿名内部类实例真的有很大不同吗?从语法设计上来说,可以肯定,但是从语言设计上来说,这似乎与您在本文中没有明显的区别。
JimmyJames

1
@JimmyJames对。关键是它们为特定目的提供相同的功能,但使用起来更简单。
康拉德·鲁道夫

36

我只是在设计应用程序,不确定我是否正确理解SOLID和OOP。

在这里已经20多年了,我也不知道。

班上应该做一件事并且做好

这里很难出错。

它们应该代表我们合作的真实对象。

真的吗?让我向您介绍有史以来最受欢迎和最成功的课程:String。我们将其用于文本。它代表的现实世界对象是:

渔夫将10条鱼吊在一根绳子上

为什么不呢?并不是所有的程序员都痴迷于钓鱼。在这里,我们使用一种称为隐喻的东西。可以为不存在的事物建立模型。这个想法必须明确。您正在读者心中创造图像。这些图像不必是真实的。只是容易理解。

一个好的OOP设计会将消息(方法)聚集在数据(状态)周围,以便对这些消息的反应可以根据数据而有所不同。如果这样做能够模拟现实世界中的事物,那就显得有些困惑。如果没有,哦。只要对读者有意义,就可以了。

现在确定,您可以这样考虑:

悬挂在字符串上的喜庆字母显示为“让事情成真!”

但是如果您认为这必须在现实世界中存在,然后才能使用隐喻,那么您的编程职业将涉及很多手工艺。


1
一连串漂亮的图片……
Zev Spitz

在这一点上,我不确定“字符串”是否是隐喻的。它只是在编程领域具有特定的含义,就像classand tablecolumn... 一样
。– Kyralessa

@Kyralessa,你以为是教新手这个隐喻,或者让它对他们来说是魔术。请从相信魔术的编码人员那里救我。
candied_orange

6

谨防!SOLID在任何地方都没有说过一个类只能“做一件事”。如果是这样的话,类将只能有一个单一的方法,而类和函数之间并没有真正的区别。

SOLID说,一个班级应该代表一个责任。这些有点像团队中人员的职责:驾驶员,律师,扒手,图形设计师等。这些人员中的每个人都可以执行多项(相关的)任务,但都属于一项职责。

这样做的目的是-如果需求有变化,理想情况下,您只需要修改一个类。这只是使代码更易于理解,更易于修改并降低了风险。

没有规则,对象应该代表“真实的事物”。由于OO 最初是为模拟而发明的,所以这只是一种货真价实的知识。但是您的程序不是模拟程序(很少有现代OO应用程序可用),因此该规则不适用。只要每个班级都有明确的职责,您就可以了。

如果一个类实际上只有一个方法该类没有任何状态,则可以考虑使其成为独立函数。这非常好,并且遵循KISS和YAGNI原则-如果可以使用函数来解决问题,则无需进行授课。另一方面,如果您有理由相信您可能需要内部状态或多个实现,则最好先将其设置为类。您将不得不在这里运用自己的最佳判断。


+1表示“如果可以使用函数来解决,则无需上课”。有时有人需要说出真相。
tchrist

5

创建不代表一件事而是做一件事的类是否正确?

总的来说还可以。

如果没有一些更具体的描述,那么FeatureExtractor该类到底应该做什么是很难说的。

无论如何,即使FeatureExtractor公开只公开了一个公共extract_features()功能,我也可以考虑用一个Strategy类来配置它,这将决定提取的精确程度。

另一个示例是带有Template函数的类。

还有更多的基于类模型的行为设计模式


当您添加一些代码进行说明时。

并且如果extract_features()看起来像这样:是否值得创建一个特殊的类来保存该方法?

线

 sent = SentimentAnalyser()

恰好包含了我的意思,即你可以用策略配置一个类。

如果您具有SentimentAnalyser该类的接口,则可以FeatureExtractor在其构造时将其传递给该类,而不是直接耦合到函数中的该特定实现。


2
我看不出增加复杂性(一个FeatureExtractor类)只是为了引入更多复杂性(SentimentAnalyser该类的接口)的原因。如果需要进行解耦,则extract_features可以将get_sentiment函数作为参数(load调用似乎独立于函数,仅出于其作用而被调用)。还要注意,Python没有/鼓励接口。
Warbo

1
@warbo-即使将函数作为参数提供,通过使其成为函数,也可以限制可能适合函数格式的潜在实现,但是如果需要在一次调用和调用之间管理持久状态,接下来(例如a CacheingFeatureExtractor或a TimeSeriesDependentFeatureExtractor),则对象将更合适。仅因为当前不需要某个对象并不意味着就永远不会存在。
Jules

3
@Jules首先,您将不需要它(YAGNI),其次,Python函数可以引用持久状态(闭包)(如果您不需要),其三,使用函数不会限制任何内容,因为任何对象都带有__call__方法将在您需要时兼容(不需要),第四,添加包装器,就像FeatureExtractor使代码与以前编写的所有其他代码不兼容一样(除非您提供__call__方法,在这种情况下,函数显然会更简单) )
Warbo

0

除了模式和所有花哨的语言/概念:您偶然发现的是JobBatch Process

归根结底,即使是纯粹的OOP程序也需要某种方式来驱动,才能真正执行工作。一定有一个入口点。例如,在MVC模式中,“ C”控件从GUI接收click等事件,然后编排其他组件。在经典的命令行工具中,“主要”功能将执行相同的操作。

创建不代表一件事而是做一件事的类是否正确?

您的班级代表一个实体,它会做某事并协调其他所有事情。您可以将其命名为ControllerJobMain或任何想到的名称。

并且如果extract_features()看起来像这样:是否值得创建一个特殊的类来保存该方法?

这取决于情况(我不熟悉在Python中完成此操作的通常方式)。如果这只是一个小的一次性命令行工具,那么使用方法而不是类应该没问题。当然,程序的第一个版本可以摆脱一种方法。以后,如果您发现最终使用了数十种这样的方法,甚至可能混入了全局变量,那么该将其重构为类了。


3
请注意,在这种情况下,调用独立过程“方法”可能会造成混淆。包括Python在内的大多数语言都将它们称为“函数”。“方法”是绑定到类的特定实例的函数/过程,与您使用的术语相反:)
Warbo,

诚然,@ Warbo。或者我们可以称它们为过程或defun或sub或...; 它们可能是与实例无关的类方法(原文如此)。我希望温柔的读者能够抽象出预期的含义。:)
AnoE

@Warbo很高兴知道!我遇到的大多数学习材料都指出,术语功能和方法是可以互换的,并且仅仅是语言相关的偏好。
Dom

@Dom 通常,(“纯”)“功能”是从输入值到输出值的映射。“程序”是一种也会引起影响的功能(例如删除文件);两者都是静态调度的(即在词法范围内查找)。“方法”是从值(称为“对象”)动态调度(查找)的函数或(通常)过程,该值自动绑定到方法的(隐式this或显式self)参数。对象的方法是相互“公开”的递归,因此替换foo导致所有self.foo调用都使用此替换。
华宝

0

我们可以将OOP视为系统行为进行建模。请注意,该系统不必存在于“现实世界”中,尽管有时有时可以使用现实世界中的隐喻(例如“管道”,“工厂”等)。

如果我们想要的系统太复杂而无法一次建模,则可以将其分解为较小的部分,然后对它们进行建模(“问题域”),这可能涉及进一步分解,依此类推,直到我们得到行为匹配的部分。 (或多或少)某些内置语言对象的对象,例如数字,字符串,列表等。

一旦有了这些简单的部分,我们就可以将它们组合在一起以描述较大部分的行为,然后可以将它们组合为更大的部分,依此类推,直到我们可以描述整个域所需的所有组件为止。系统。

在此“组合在一起”阶段,我们可以编写一些类。当没有现有对象以我们想要的方式运行时,我们编写类。例如,我们的域可能包含“ foos”,称为“ bars”的foos集合和称为“ bazs”的bar的集合。我们可能会注意到foos很简单,可以使用字符串进行建模,因此我们做到了。我们发现bar要求其内容遵守某些特定的约束,该约束与Python提供的任何内容都不匹配,在这种情况下,我们可以编写一个新类来强制执行此约束。也许bazs没有这种特性,所以我们可以用一个列表来代表它们。

请注意,我们可以写这些组件(FOOS,酒吧和bazs)中的每一个新的类,但我们并不需要对是否已经存在一些与正确的行为。尤其是,要使一个类有用,它需要“提供”某些东西(数据,方法,常量,子类等),因此,即使我们有许多层的自定义类,我们最终也必须使用一些内置功能。例如,如果我们为foos编写了一个新类,则它可能仅包含一个字符串,那么为什么不忘记foo类并让bar类包含这些字符串呢?请记住,类也是内置对象,它们只是一个特别灵活的对象。

一旦有了我们的领域模型,我们就可以采用这些模型的某些特定实例,并将它们安排到我们要建模的特定系统的“模拟”中(例如“用于...的机器学习系统”)。

一旦有了这个模拟,我们就可以运行它,并且嗨,我们有了一个适用于...(或我们建模的任何东西)的机器学习系统(一个模拟)。


现在,在您的特定情况下,您正在尝试对“功能提取器”组件的行为进行建模。问题是,是否有任何行为像“功能提取器”的内置对象,还是需要将其分解为更简单的东西?看起来特征提取器的行为与函数对象非常相似,因此我认为将它们用作模型会很好。


学习这类概念时要记住的一件事是,不同的语言可以提供不同的内置功能和对象(当然,有些甚至不使用诸如“对象”之类的术语!)。因此,在一种语言中有意义的解决方案在另一种语言中可能没有多大用处(甚至可以应用于同一语言的不同版本!)。

从历史上看,许多 OOP文献(尤其是“设计模式”)都集中在Java上,而Java与Python完全不同。例如,Java类不是对象,直到最近Java才有函数对象,Java进行严格的类型检查(鼓励使用接口和子类),而Python鼓励进行鸭式打字,Java没有模块对象,Java整数/浮动/等 不是对象,Java中的元编程/自省需要“反射”,依此类推。

我并不想尝试使用Java(作为另一个示例,很多OOP理论都围绕Smalltalk展开,而Smalltalk又与Python截然不同),我只是想指出我们必须非常仔细地考虑上下文和开发解决方案的约束条件,以及是否与我们所处的情况相符。

在您的情况下,函数对象似乎是一个不错的选择。如果您想知道为什么某些“最佳实践”准则没有提及函数对象作为可能的解决方案,那可能仅仅是因为这些准则是为Java的旧版本编写的!


0

务实地说,当我有“做一件重要的事情,应该分开的杂项”,而没有清晰的家时,我将其放在一Utilities节中,并将其用作我的命名约定。即。FeatureExtractionUtility

忘记一个类中方法的数量;今天的一种方法明天可能需要增加到五种方法。重要的是一个清晰且一致的组织结构,例如用于杂项功能的公用事业区域。

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.