Чи слід створювати клас, якщо моя функція є складною і має багато змінних?


40

Це питання є дещо мовно-агностичним, але не повністю, оскільки об'єктно-орієнтоване програмування (OOP) відрізняється, наприклад, 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
"Проблема, яку я бачу при такому підході, полягає в тому, що стан цього класу має сенс лише внутрішньо, і нічого не можна робити зі своїми екземплярами, крім виклику їх єдиного публічного методу". Ви сформулювали це як проблему, але не чому ви вважаєте, що це так.
Патрік Моупін

@Patrick Maupin - Ти маєш рацію. І я насправді не знаю, в цьому проблема. Просто відчувається, що я використовую заняття для тієї речі, для якої слід використовувати щось інше, і в Python є багато речей, які я ще не досліджував, тому я подумав, що, можливо, хтось запропонує щось більш підходяще.
iCanLearn

Можливо, це про те, щоб зрозуміти, що я намагаюся зробити. Мовляв, я не бачу нічого поганого у використанні звичайних класів замість перерахунків на Java, але все ж є речі, для яких переліки є більш природними. Отже, це питання справді стосується того, чи існує більш природний підхід до того, що я намагаюся зробити.
iCanLearn


1
Якщо ви просто розпорошите свій існуючий код у методах та змінних екземплярів і нічого не зміните щодо його структури, то ви нічого не втратите, окрім втрати ясності. (1) - погана ідея.
usr

Відповіді:


47
  1. Створіть клас, який має лише один загальнодоступний метод і зберігає внутрішній стан, необхідний для субоперацій, у своїх змінних екземплярів.

Проблема, яку я бачу при такому підході, полягає в тому, що стан цього класу має сенс лише внутрішньо, і нічого не можна робити зі своїми екземплярами, крім виклику їх єдиного публічного методу.

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

Якщо це означає, що ваш клас має лише один публічний метод, то так і нехай буде. Це буде набагато простіше в обслуговуванні.

Якщо у вас є клас, який виконує рівно одну річ, має невелику публічну поверхню і зберігає весь свій внутрішній стан прихованим, то ви (як би сказав Чарлі Шин) ПЕРЕМОЖЕНО .

  1. Складіть купу функцій без класу та передайте між ними різні внутрішньо необхідні змінні (як аргументи).

Проблема, яку я бачу в цьому, полягає в тому, що мені потрібно передати багато змінних між функціями. Також функції були б тісно пов'язані між собою, але не згрупувалися б.

Варіант 2 страждає від низької згуртованості . Це ускладнить обслуговування.

  1. Як і 2. але зробіть змінні стану глобальними, а не передавати їх.

Варіант 3, як і варіант 2, страждає від низької згуртованості, але набагато серйозніше!

Історія показала, що зручність глобальних змінних переважає жорстокі витрати на технічне обслуговування. Ось чому ви чуєте, як старі пукали, як я, постійно скачуть про інкапсуляцію.


Виграшний варіант - №1 .


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 трохи красивішим.
Joe Postbut

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()

Які фрагменти даних використовує подоперація1? Чи збирає він дані, які використовує подоперація2? Коли ви передаєте дані, зберігаючи їх самостійно, я не можу сказати, як пов’язані фрагменти функціональності. Коли я дивлюся на себе, деякі атрибути є від конструктора, деякі - від виклику до_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)

З цим у вас є найменший можливий інтерфейс до вашого коду, і клас, який ви використовуєте внутрішньо, справді стає лише деталізацією реалізації вашої публічної функції. Код виклику ніколи не повинен знати про ваш внутрішній клас.


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 дизайн природно управляє складністю OOP природно вводить зайву складність.
Майлз Руут

"Непотрібна складність" нагадує мені безцінні коментарі колег, такі як "о, це занадто багато занять". Досвід підказує мені, що сік варто вичавити. У кращому випадку я бачу практично цілі класи з методами довжиною 1-3 рядки, і з кожним класом це є частиною, складність будь-якого методу зводиться до мінімуму: Один короткий LOC, що порівнює дві колекції та повертає дублікати - звичайно, за цим є код. Не плутайте "багато коду" зі складним кодом. У будь-якому випадку нічого безкоштовного.
radarbob

1
Багато "простого" коду, який взаємодіє складним способом, ШЛУХО гірше, ніж невелика кількість складного коду.
Міль маршруту

1
Невелика кількість складного коду містила складність . Складність є, але тільки там. Це не витікає. Коли у вас є ціла маса індивідуально простих творів, які працюють разом по-справжньому складний і важкий для розуміння спосіб, це набагато заплутано, тому що немає меж і стін, щоб їх складнощі.
Майлз Рут

3

Чому б не порівняти свої потреби з чимось, що існує у стандартній бібліотеці Python, а потім подивитися, як це реалізується?

Зауважте, якщо об’єкт не потрібен, ви все одно можете визначати функції в межах функцій. З Python 3 є нове nonlocalоголошення, яке дозволяє вам змінювати змінні в батьківській функції.

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


Спасибі. А чи знаєте ви що-небудь у стандартній бібліотеці, що звучить як те, що у мене в питанні?
iCanLearn

Мені буде складно процитувати щось за допомогою вкладених функцій, адже я не знаходжу nonlocalніде в моїх встановлених в даний час пітонів. Можливо, ви можете прийняти серце, з textwrap.pyякого є TextWrapperклас, але і def wrap(text)функцію, яка просто створює TextWrapper екземпляр, викликає .wrap()метод на ньому і повертається. Тому використовуйте клас, але додайте деякі функції зручності.
meuh
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.