Должен ли я создать класс, если моя функция сложна и имеет много переменных?

40

Этот вопрос несколько не зависит от языка, но не полностью, поскольку объектно-ориентированное программирование (ООП) отличается, например, в Java , которая не имеет функций первого класса, чем в Python .

Другими словами, я чувствую себя менее виноватым за создание ненужных классов в таком языке, как Java, но я чувствую, что может быть лучший способ в менее шаблонных языках, таких как 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
«Проблема, с которой я сталкиваюсь при таком подходе, состоит в том, что состояние этого класса имеет смысл только внутри, и не может ничего делать с его экземплярами, кроме как вызывать их единственный публичный метод». Вы сформулировали это как проблему, но не то, почему вы думаете, что это так.
Патрик Мопин
@ Патрик Мопин - Ты прав. И я действительно не знаю, в этом проблема. Такое ощущение, что я использую классы для того, для чего нужно использовать что-то еще, и есть много вещей в Python, которые я еще не исследовал, поэтому я подумал, что, возможно, кто-то предложит что-то более подходящее.
iCanLearn
Может быть, речь идет о том, чтобы понять, что я пытаюсь сделать. Например, я не вижу ничего особенно неправильного в использовании обычных классов вместо перечислений в Java, но все же есть вещи, для которых перечисления более естественны. Так что этот вопрос действительно о том, есть ли более естественный подход к тому, что я пытаюсь сделать.
iCanLearn,
3
Возможный дубликат Какова реальная ответственность класса?
Комнат
1
Если вы просто разберете свой существующий код на методы и переменные экземпляра и ничего не измените в его структуре, то вы ничего не выиграете, но потеряете ясность. (1) это плохая идея.
USR

Ответы:

47
  1. Создайте класс, который имеет только один открытый метод и сохраняет внутреннее состояние, необходимое для подопераций, в своих переменных экземпляра.

Проблема, которую я вижу с этим подходом, состоит в том, что состояние этого класса имеет смысл только внутри, и не может ничего делать с его экземплярами, кроме вызова их единственного открытого метода.

Вариант 1 является хорошим примером инкапсуляции, используемой правильно. Вы хотите, чтобы внутреннее состояние было скрыто от внешнего кода.

Если это означает, что у вашего класса есть только один публичный метод, пусть будет так. Это будет намного легче поддерживать.

В ООП, если у вас есть класс, который выполняет ровно одну вещь, имеет небольшую открытую поверхность и скрывает все свое внутреннее состояние, то вы (как сказал бы Чарли Шин) ПОБЕДИЛИ .

  1. Создайте группу функций без класса и передайте различные внутренние переменные между ними (в качестве аргументов).

Проблема, которую я вижу с этим, состоит в том, что я должен передать много переменных между функциями. Кроме того, функции будут тесно связаны друг с другом, но не будут сгруппированы вместе.

Вариант 2 страдает от низкой когезии . Это усложнит обслуживание.

  1. Как 2. но сделайте переменные состояния глобальными вместо их передачи.

Вариант 3, как и вариант 2, страдает от низкой когезии, но гораздо серьезнее!

История показала, что удобство глобальных переменных перевешивается из-за жестокой стоимости обслуживания. Вот почему вы слышите, как старые пукающие, как я, все время разглагольствуют об инкапсуляции.


Вариант победы # 1 .

MetaFight
источник
6
# 1 - довольно уродливый API. Если бы я решил создать для этого класс, я бы, вероятно, сделал одну публичную функцию, которая делегирует классу, и сделал бы весь класс приватным. ThingDoer(var1, var2).do_it()против do_thing(var1, var2).
user2357112 поддерживает Monica
7
Хотя вариант 1 является явным победителем, я бы пошел еще дальше. Вместо использования внутреннего состояния объекта используйте локальные переменные и параметры метода. Это делает общественную функцию реентерабельной, что безопаснее.
4
Еще одну вещь, которую вы могли бы сделать в расширении опции 1: сделать результирующий класс защищенным (добавив подчеркивание перед именем), а затем определите функцию уровня модуля def do_thing(var1, var2): return _ThingDoer(var1, var2).run(), чтобы сделать внешний API немного красивее.
Sjoerd Job Postmus
4
Я не слежу за вашими аргументами в пользу 1. Внутреннее состояние уже скрыто. Добавление класса не меняет этого. Поэтому я не понимаю, почему вы рекомендуете (1). На самом деле нет необходимости вообще разоблачать существование класса.
USR
3
Любой класс, который вы создаете, а затем немедленно вызываете один метод, а затем никогда не используете его, является для меня основным запахом кода. Это изоморфно простой функции, только с непрозрачным потоком данных (так как разорванная часть реализации связывается, назначая переменные в одноразовом экземпляре вместо передачи параметров). Если ваши внутренние функции принимают так много параметров, что вызовы являются сложными и громоздкими, код не становится менее сложным, когда вы скрываете этот поток данных!
Бен
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? Собирает ли он данные, используемые suboperation2? Когда вы передаете данные, сохраняя их на себе, я не могу сказать, как связаны части функциональности. Когда я смотрю на себя, некоторые атрибуты взяты из конструктора, некоторые из вызова the_public_method, а некоторые добавляются случайным образом в других местах. На мой взгляд, это беспорядок.

Как насчет № 2? Ну, во-первых, давайте посмотрим на вторую проблему с ним:

Кроме того, функции будут тесно связаны друг с другом, но не будут сгруппированы вместе.

Они будут в модуле вместе, поэтому они будут полностью сгруппированы вместе.

Проблема, которую я вижу с этим, состоит в том, что я должен передать много переменных между функциями.

На мой взгляд, это хорошо. Это делает явными зависимости данных в вашем алгоритме. Сохраняя их как в глобальных переменных, так и в самом себе, вы скрываете зависимости и заставляете их казаться менее плохими, но они все еще там.

Обычно, когда возникает такая ситуация, это означает, что вы не нашли правильный способ разложить вашу проблему. Вам неудобно разделять на несколько функций, потому что вы пытаетесь разделить их неправильно.

Конечно, не видя вашей реальной функции, трудно угадать, что было бы хорошим предложением. Но вы даете намек на то, с чем имеете дело здесь:

Моя программа должна выполнить относительно сложную операцию несколько раз. Эта операция требует много «бухгалтерии», должна создавать и удалять некоторые временные файлы и т. Д.

Позвольте мне выбрать пример чего-то, что соответствует вашему описанию, установщик. Установщик должен скопировать кучу файлов, но если вы отмените его частично, вам нужно перемотать весь процесс, включая возврат всех файлов, которые вы заменили. Алгоритм для этого выглядит примерно так:

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)

При этом у вас будет наименьший возможный интерфейс к вашему коду, и класс, который вы используете внутри, действительно станет просто деталью реализации вашей публичной функции. Код вызова не должен знать о вашем внутреннем классе.

cmaster
источник
4

С одной стороны, вопрос как-то не зависит от языка; но с другой стороны, реализация зависит от языка и его парадигм. В данном случае это Python, который поддерживает несколько парадигм.

Помимо ваших решений, существует также возможность более полного выполнения операций без сохранения состояния, например

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

Все сводится к

  • читабельность
  • консистенция
  • ремонтопригодность

Но -Если ваша кодовая база является ООП, она нарушает согласованность, если вдруг некоторые части написаны в (более) функциональном стиле.

Томас Джанк
источник
4

Зачем дизайн, когда я могу быть программированием?

Я предлагаю противоположный взгляд на ответы, которые я прочитал. С этой точки зрения все ответы и, честно говоря, даже сам вопрос сосредоточены на механике кодирования. Но это проблема дизайна.

Должен ли я создать класс, если моя функция сложна и имеет много переменных?

Да, так как это имеет смысл для вашего дизайна. Это может быть сам по себе класс или часть какого-то другого класса, или его поведение может быть распределено между классами.


Объектно-ориентированный дизайн - это сложность

Задача ОО состоит в том, чтобы успешно создавать и поддерживать большие, сложные системы, инкапсулируя код в терминах самой системы. «Правильный» дизайн говорит, что все в каком-то классе.

ОО-дизайн, естественно, управляет сложностью в первую очередь с помощью целевых классов, которые придерживаются принципа единой ответственности. Эти классы дают структуру и функциональность во всем дыхании и глубине системы, взаимодействуют и интегрируют эти измерения.

Учитывая это, часто говорят, что функции, свисающие с системы - слишком распространенный общий класс полезности - это запах кода, указывающий на недостаточный дизайн. Я склонен согласиться.

radarbob
источник
ОО-дизайн, естественно, управляет сложностью ООП, естественно, вносит ненужную сложность.
Майлз Рут
«Ненужная сложность» напоминает мне о бесценных комментариях от коллег, таких как «о, это слишком много классов». Опыт подсказывает мне, что сок стоит выжать. В лучшем случае я вижу практически целые классы с методами длиной 1-3 строки, и каждый класс выполняет свою часть, сложность любого данного метода сводится к минимуму: один короткий LOC, сравнивающий две коллекции и возвращающий дубликаты - конечно, за этим стоит код. Не путайте «много кода» со сложным кодом. В любом случае, ничего не бесплатно.
радаробоб
1
Множество «простого» кода, который взаимодействует сложным образом, НАМНОГО хуже, чем небольшое количество сложного кода.
Майлз Рут
1
Небольшое количество сложного кода содержало сложность . Есть сложность, но только там. Это не вытекает. Когда у вас есть множество индивидуально простых частей, которые работают вместе действительно сложным и трудным для понимания способом, это намного более запутанно, потому что нет никаких границ или стен сложности.
Майлз Рут
3

Почему бы не сравнить ваши потребности с чем-то, что существует в стандартной библиотеке Python, а затем посмотреть, как это реализовано?

Обратите внимание, что если вам не нужен объект, вы все равно можете определять функции внутри функций. В Python 3 есть новое nonlocalобъявление, позволяющее вам изменять переменные в вашей родительской функции.

Возможно, вам все еще будет полезно иметь несколько простых закрытых классов внутри вашей функции для реализации абстракций и операций по уборке.

meuh
источник
Спасибо. А знаете ли вы что-нибудь в стандартной библиотеке, что звучит так, как у меня в вопросе?
iCanLearn
Мне было бы трудно что-то цитировать, используя вложенные функции, на самом деле я nonlocalнигде не нахожусь в моих установленных в настоящее время библиотеках Python. Возможно, вы можете позаботиться о textwrap.pyтом, чтобы иметь TextWrapperкласс, но также def wrap(text)функцию, которая просто создает TextWrapper экземпляр, вызывает .wrap()метод и возвращает его. Так что используйте класс, но добавьте несколько удобных функций.
meuh