如果我的函数很复杂并且有很多变量,我应该创建一个类吗?


40

这个问题在某种程度上与语言无关,但并非完全与语言无关,因为面向对象编程(OOP)在Java中是不同的,而Java没有Python的一流功能。

换句话说,我对用Java之类的语言创建不必要的类感到内gui,但是我觉得在像Python这样少用的语言中可能会有更好的方法。

我的程序需要多次执行相对复杂的操作。该操作需要大量的“簿记”,必须创建和删除一些临时文件,等等。

这就是为什么它还需要调用许多其他“子操作”的原因-将所有内容放入一个巨大的方法中并不是很好,模块化,可读性强等。

现在这些是我想到的方法:

1.制作一个仅具有一个公共方法的类,并将子操作所需的内部状态保留在其实例变量中。

它看起来像这样:

class Thing:

    def __init__(self, var1, var2):
        self.var1 = var1
        self.var2 = var2
        self.var3 = []

    def the_public_method(self, param1, param2):
        self.var4 = param1
        self.var5 = param2
        self.var6 = param1 + param2 * self.var1
        self.__suboperation1()
        self.__suboperation2()
        self.__suboperation3()


    def __suboperation1(self):
        # Do something with self.var1, self.var2, self.var6
        # Do something with the result and self.var3
        # self.var7 = something
        # ...
        self.__suboperation4()
        self.__suboperation5()
        # ...

    def suboperation2(self):
        # Uses self.var1 and self.var3

#    ...
#    etc.

我看到的这种方法的问题是,此类的状态仅在内部有意义,并且除了调用其唯一的公共方法外,它无法对其实例执行任何操作。

# Make a thing object
thing = Thing(1,2)

# Call the only method you can call
thing.the_public_method(3,4)

# You don't need thing anymore

2.制作一堆没有类的函数,并在它们之间传递各种内部需要的变量(作为参数)。

我看到的问题是我必须在函数之间传递很多变量。同样,这些功能将彼此紧密相关,但不会将它们分组在一起。

3.与2.类似,但是将状态变量设为全局而不是传递它们。

这根本没有好处,因为我必须使用不同的输入多次执行该操作。

有没有第四种更好的方法?如果没有,那么哪种方法更好?为什么?有什么我想念的吗?


9
“我看到的这种方法的问题是,此类的状态仅在内部有意义,并且除了调用其唯一的公共方法外,不能对其实例执行任何操作。” 您已经将此作为问题进行了表述,但并非您认为如此的原因。
Patrick Maupin 2015年

@Patrick Maupin-你是对的。我真的不知道,这就是问题所在。感觉就像我在为某个东西使用类而应该使用其他东西,而Python中还有很多东西我还没有探索过,所以我认为也许有人会建议一些更合适的东西。
iCanLearn

也许是要弄清楚我要做什么。就像,我没有看到使用普通类代替Java中的枚举的任何特别错误,但是仍然有些事情枚举更为自然。所以这个问题实际上是关于我尝试做的事情是否有更自然的方法。
iCanLearn


1
如果仅将现有代码分散到方法和实例变量中,并且对其结构没有任何更改,那么您将一无所获,但会失去清晰度。(1)是个坏主意。
usr

Answers:


47
  1. 使一个类仅具有一个公共方法,并将子操作所需的内部状态保留在其实例变量中。

我用这种方法看到的问题是,此类的状态仅在内部有意义,并且除了调用其唯一的公共方法外,对它的实例无能为力。

选项1是正确使用封装的一个很好的例子。您希望内部状态对外部代码隐藏。

如果那意味着您的类只有一个公共方法,那就去吧。维护起来会容易得多。

在OOP中,如果您有一个只做一件事的类,公共表面很小,并且隐藏了所有内部状态,那么您(就像查理·辛恩(Charlie Sheen)会说的那样)就是WINNING

  1. 制作一堆没有类的函数,并在它们之间传递各种内部需要的变量(作为参数)。

我看到的问题是我必须在函数之间传递很多变量。同样,这些功能将彼此紧密相关,但不会被分组在一起。

选项2的凝聚力低。这将使维护更加困难。

  1. 与2.类似,但是将状态变量设为全局而不是传递它们。

选项3与选项2一样,具有较低的内聚力,但严重得多!

历史表明,全局变量带来的残酷维护成本超过了它的便利性。这就是为什么您总是听到像我这样的老屁对封装的抱怨。


获胜的选择是#1


6
#1是一个非常难看的API。如果我决定为此创建一个类,则可能会创建一个委托给该类的公共函数,并将整个类设为私有。ThingDoer(var1, var2).do_it()do_thing(var1, var2)
user2357112 2015年

7
尽管选项1显然是赢家,但我会更进一步。与其使用内部对象状态,不如使用局部变量和方法参数。这使得可重入公共功能,这是更安全的。

4
在选项1的扩展中,您还可以做一件事:保护结果类(在名称前添加下划线),然后定义模块级别的函数def do_thing(var1, var2): return _ThingDoer(var1, var2).run(),使外部API更加美观。
Sjoerd Job Postmus

4
我不遵循您对1的推理。内部状态已隐藏。添加类不会改变。因此,我不明白您为什么推荐(1)。实际上,根本没有必要公开类的存在。
usr

3
对我来说,实例化然后立即调用单个方法然后再也不使用的任何类都是主要的代码异味。它对一个简单的函数是同构的,仅在数据流被遮盖的情况下(因为实现的中断部分通过在抛弃型实例上分配变量而不是传递参数来进行通信)。如果您的内部函数使用了太多参数,以至于调用变得复杂而笨拙,那么隐藏该数据流时代码就不会变得那么简单
2015年

23

我认为#1实际上是一个不好的选择。

让我们考虑一下您的功能:

def the_public_method(self, param1, param2):
    self.var4 = param1
    self.var5 = param2 
    self.var6 = param1 + param2 * self.var1
    self.__suboperation1()
    self.__suboperation2()
    self.__suboperation3()

suboperation1使用哪些数据?它是否收集子工序2使用的数据?当您通过将数据存储在自身上来传递数据时,我无法确定功能之间的关系。当我看一下self时,一些属性来自构造函数,一些属性来自对the_public_method的调用,还有一些属性是在其他地方随机添加的。我认为,这是一团糟。

那第二号呢?好吧,首先,让我们看一下第二个问题:

同样,这些功能将彼此紧密相关,但不会被分组在一起。

它们将在一个模块中在一起,因此它们将完全组合在一起。

我看到的问题是我必须在函数之间传递很多变量。

我认为这很好。这样可以在算法中明确显示数据依赖性。通过将它们存储在全局变量或自身中,您可以隐藏依赖项并使它们看起来不太糟,但它们仍然存在。

通常,当出现这种情况时,这意味着您没有找到正确的方法来分解问题。您发现拆分成多个函数很尴尬,因为您试图以错误的方式拆分它。

当然,在没有看到您的实际功能的情况下,很难猜测这是一个好建议。但是,您可以在此处暗示您要处理的内容:

我的程序需要多次执行相对复杂的操作。该操作需要大量的“簿记”,必须创建和删除一些临时文件等。

让我选择一个符合您描述的安装程序示例。安装程序必须复制一堆文件,但是如果您在整个过程中都将其取消,则需要倒回整个过程,包括放回替换的所有文件。此算法看起来像:

def install_program():
    copied_files = []
    try:
        for filename in FILES_TO_COPY:
           temporary_file = create_temporary_file()
           copy(target_filename(filename), temporary_file)
           copied_files = [target_filename(filename), temporary_file)
           copy(source_filename(filename), target_filename(filename))
     except CancelledException:
        for source_file, temp_file in copied_files:
            copy(temp_file, source_file)
     else:
        for source_file, temp_file in copied_files:
            delete(temp_file)

现在,将逻辑除以必须进行注册表设置,程序图标等操作,结果一团糟。

我认为您的#1解决方案看起来像:

class Installer:
    def install(self):
        try:
            self.copy_new_files()
        except CancellationError:
            self.restore_original_files()
        else:
            self.remove_temp_files()

这的确使整体算法更加清晰,但隐藏了不同部分之间通信的方式。

方法2看起来像:

def install_program():
    try:
       temp_files = copy_new_files()
    except CancellationError as error:
       restore_old_files(error.files_that_were_copied)
    else:
       remove_temp_files(temp_files)

现在,它明确显示了数据如何在函数之间移动,但是这很尴尬。

那么该函数应该如何编写?

def install_program():
    with FileTransactionLog() as file_transaction_log:
         copy_new_files(file_transaction_log)

FileTransactionLog对象是上下文管理器。当copy_new_files复制文件时,它通过FileTransactionLog进行操作,该文件用于进行临时复制并跟踪已复制的文件。在例外情况下,它将复制原始文件,在成功情况下,它将删除临时副本。

之所以有效,是因为我们发现了任务的更自然分解。以前,我们将有关如何安装应用程序的逻辑与有关如何恢复已取消安装的逻辑混合在一起。现在,事务日志处理有关临时文件和簿记的所有详细信息,并且该功能可以集中在基本算法上。

我怀疑您的案子在同一条船上。您需要将簿记元素提取到某种对象中,以便可以更简单,更优雅地表达复杂的任务。


9

由于方法1的唯一明显缺点是其使用方法不理想,因此我认为最好的解决方案是进一步推动封装:使用类,但还要提供一个独立的函数,该函数仅构造对象,调用方法并返回:

def publicFunction(var1, var2, param1, param2)
    thing = Thing(var1, var2)
    thing.theMethod(param1, param2)

这样,您就可以使用最小的代码接口,并且内部使用的类实际上只是公共函数的实现细节。调用代码永远不需要了解您的内部类。


4

一方面,问题是语言不可知的。但另一方面,实现取决于语言及其范式。在这种情况下,它是Python,它支持多种范例。

除了您的解决方案外,还有可能以一种更实用的方式使操作无状态完成,例如

def outer(param1, param2):
    def inner1(param1, param2, param3):
        pass
    def inner2(param1, param2):
        pass
    return inner2(inner1(param1),param2,param3)

这一切都归结为

  • 可读性
  • 一致性
  • 可维护性

但是-如果您的代码库是OOP,则突然之间某些部分以(更多)功能样式编写时,它将破坏一致性。


4

为什么可以编码时设计?

对于已读的答案,我持相反观点。从这个角度来看,所有答案,甚至坦率地说,问题本身都集中在编码机制上。但这是一个设计问题。

如果我的函数很复杂并且有很多变量,我应该创建一个类吗?

是的,因为这对您的设计有意义。它本身可能是一个类,也可能是其他某个类的一部分,或者它的行为可能在各个类之间分布。


面向对象的设计是关于复杂性

OO的重点是通过根据系统本身封装代码来成功构建和维护大型,复杂的系统。“适当的”设计表明一切都在某个类中。

OO设计主要通过遵循单一职责原则的重点课程来自然地管理复杂性。这些类在整个系统的呼吸和深度中提供结构和功能,并交互和集成这些维度。

鉴于此,经常有人说悬挂在系统上的功能-太普遍了的通用实用程序类-是一种代码味道,表明设计不足。我倾向于同意。


OO设计自然可以管理复杂性 OOP自然会引入不必要的复杂性。
Miles Rout

“不必要的复杂性”使我想起了同事的无价评论,例如“哦,太多了。” 经验告诉我,榨汁是值得的。最好的情况下,我实际上看到的是整个类,它们的方法长1-3行,并且每个类都这样做,这可以最大限度地降低任何给定方法的部分复杂度:单个简短的 LOC可以比较两个集合并返回重复项-当然,后面还有代码。不要将“很多代码”与复杂的代码混淆。无论如何,没有什么是免费的。
–radarbob

1
许多以复杂方式交互的“简单”代码比少量复杂代码更糟糕。
Miles Rout 2015年

1
少量的复杂代码包含了复杂性。有复杂性,但只有那里。它不会泄漏出去。当您有很多单独的简单部件以一种非常复杂且难以理解的方式协同工作时,这会更加令人困惑,因为复杂性没有边界或墙。
Miles Rout 2015年

3

为什么不将您的需求与标准Python库中存在的需求进行比较,然后查看其实现方式?

注意,如果不需要对象,仍可以在函数中定义函数。在Python 3中,有一个新的nonlocal声明允许您更改父函数中的变量。

您可能仍然发现在函数中包含一些简单的私有类来实现抽象和整理操作很有用。


谢谢。您是否知道标准库中听起来像我所遇到的问题的所有内容?
iCanLearn 2015年

我发现很难使用嵌套函数引用某些东西,实际上我nonlocal在当前安装的python库中找不到任何地方。也许您可以textwrap.py从中得到一个TextWrapper类,但您也可以从中获得一个def wrap(text)功能,该功能可以简单地创建一个TextWrapper 实例,.wrap()在其上调用方法并返回。因此,使用一个类,但添加一些便利功能。
meuh 2015年
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.