Czy powinienem utworzyć klasę, jeśli moja funkcja jest złożona i zawiera wiele zmiennych?

40

To pytanie jest nieco niezależne od języka, ale nie do końca, ponieważ programowanie obiektowe (OOP) różni się na przykład w Javie , która nie ma funkcji pierwszej klasy, niż w Pythonie .

Innymi słowy, czuję się mniej winny z powodu tworzenia niepotrzebnych klas w języku takim jak Java, ale wydaje mi się, że może istnieć lepszy sposób w mniej popularnych językach, takich jak Python.

Mój program musi kilkakrotnie wykonać stosunkowo złożoną operację. Ta operacja wymaga dużo „księgowości”, musi tworzyć i usuwać niektóre pliki tymczasowe itp.

Dlatego też musi wywoływać wiele innych „podoperacji” - umieszczenie wszystkiego w jednej ogromnej metodzie nie jest zbyt ładne, modułowe, czytelne itp.

Teraz przychodzą mi na myśl podejścia:

1. Stwórz klasę, która ma tylko jedną metodę publiczną i zachowuje wewnętrzny stan wymagany dla podoperacji w zmiennych instancji.

Wyglądałoby to tak:

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.

Problem, jaki widzę w tym podejściu, polega na tym, że stan tej klasy ma sens tylko wewnętrznie i nie może nic zrobić z jej instancjami, z wyjątkiem wywołania ich jedynej publicznej metody.

# 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. Utwórz kilka funkcji bez klasy i przekaż między nimi różne wewnętrznie potrzebne zmienne (jako argumenty).

Problem, jaki tu widzę, polega na tym, że muszę przekazywać wiele zmiennych między funkcjami. Ponadto funkcje byłyby ściśle ze sobą powiązane, ale nie byłyby zgrupowane razem.

3. Podobnie jak 2., ale zmienne globalne należy wprowadzać zamiast przekazywać.

To nie byłoby w ogóle dobre, ponieważ muszę wykonać operację więcej niż raz, z różnymi danymi wejściowymi.

Czy istnieje czwarte, lepsze podejście? Jeśli nie, to które z tych podejść byłoby lepsze i dlaczego? Czy czegoś brakuje?

mogę się nauczyć
źródło
9
„Problem, jaki dostrzegam w tym podejściu, polega na tym, że stan tej klasy ma sens tylko wewnętrznie i nie może nic zrobić z jej instancjami poza wywołaniem ich jedynej publicznej metody”. Wyraziłeś to jako problem, ale nie dlatego, dlaczego tak myślisz.
Patrick Maupin,
@Patrick Maupin - Masz rację. I tak naprawdę nie wiem, to jest problem. Po prostu wydaje mi się, że używam klas do rzeczy, do których należy użyć czegoś innego. W Pythonie jest wiele rzeczy, których jeszcze nie zbadałem, więc pomyślałem, że może ktoś zaproponuje coś bardziej odpowiedniego.
iCanLearn,
Może chodzi o to, aby mieć jasność co do tego, co próbuję zrobić. Na przykład nie widzę nic szczególnie złego w używaniu zwykłych klas zamiast wyliczeń w Javie, ale wciąż istnieją rzeczy, dla których wyliczenia są bardziej naturalne. To pytanie naprawdę dotyczy tego, czy istnieje bardziej naturalne podejście do tego, co próbuję zrobić.
iCanLearn,
1
Jeśli tylko rozproszysz istniejący kod na metody i zmienne instancji i nie zmienisz nic o jego strukturze, nie zyskasz nic, tylko stracisz przejrzystość. (1) to zły pomysł.
usr

Odpowiedzi:

47
  1. Utwórz klasę, która ma tylko jedną metodę publiczną i zachowuje wewnętrzny stan wymagany dla podoperacji w zmiennych instancji.

Problem, jaki widzę w tym podejściu, polega na tym, że stan tej klasy ma sens tylko wewnętrznie i nie może nic zrobić z jej instancjami, z wyjątkiem wywołania ich jedynej publicznej metody.

Opcja 1 jest dobrym przykładem prawidłowego zastosowania kapsułkowania . Chcesz, aby stan wewnętrzny był ukryty przed kodem zewnętrznym.

Jeśli to oznacza, że ​​twoja klasa ma tylko jedną metodę publiczną, niech tak będzie. Będzie to o wiele łatwiejsze w utrzymaniu.

W OOP, jeśli masz klasę, która robi dokładnie jedną rzecz, ma małą powierzchnię publiczną i ukrywa cały swój stan wewnętrzny, to jesteś (jak powiedziałby Charlie Sheen) ZWYCIĘZCĄ .

  1. Utwórz kilka funkcji bez klasy i przekaż między nimi różne wewnętrznie potrzebne zmienne (jako argumenty).

Problem, jaki tu widzę, polega na tym, że muszę przekazywać wiele zmiennych między funkcjami. Ponadto funkcje byłyby ściśle ze sobą powiązane, ale nie byłyby zgrupowane razem.

Wariant 2 ma niską spójność . Utrudni to konserwację.

  1. Podobnie jak 2., ale zmienne globalne stanowe zamiast przekazywać.

Opcja 3, podobnie jak opcja 2, cierpi z powodu niskiej spójności, ale znacznie poważniej!

Historia pokazała, że ​​wygoda zmiennych globalnych jest przeważona przez związane z tym brutalne koszty utrzymania. Właśnie dlatego słyszysz takie stare bąki jak ja, które cały czas gadają o enkapsulacji.


Zwycięską opcją jest nr 1 .

MetaFight
źródło
6
# 1 jest dość brzydkim API. Gdybym zdecydował się stworzyć do tego klasę, prawdopodobnie stworzyłbym jedną funkcję publiczną, która delegowałaby się na klasę i uczyniłaby całą klasę prywatną. ThingDoer(var1, var2).do_it()vs do_thing(var1, var2).
user2357112 obsługuje Monikę
7
Chociaż opcja 1 jest wyraźnym zwycięzcą, poszedłbym o krok dalej. Zamiast używać wewnętrznego stanu obiektu, używaj zmiennych lokalnych i parametrów metody. Powoduje to ponowne otwarcie funkcji publicznej, co jest bezpieczniejsze.
4
Jedną dodatkową rzeczą, którą możesz zrobić w rozszerzeniu opcji 1: uczyń klasę wynikową chronioną (dodając znak podkreślenia przed nazwą), a następnie zdefiniuj funkcję na poziomie modułu def do_thing(var1, var2): return _ThingDoer(var1, var2).run(), aby zewnętrzny interfejs API był nieco ładniejszy.
Sjoerd Job Postmus
4
Nie podążam za twoim uzasadnieniem dla 1. Stan wewnętrzny jest już ukryty. Dodanie klasy tego nie zmienia. Dlatego nie rozumiem, dlaczego polecasz (1). W rzeczywistości nie jest wcale konieczne ujawnianie istnienia klasy.
usr
3
Każda klasa, którą tworzysz, a następnie natychmiast wywołujesz jedną metodę, a potem nigdy więcej nie używasz, jest dla mnie głównym zapachem kodu. Jest to izomorficzne w stosunku do prostej funkcji, tylko z zaciemnionym przepływem danych (ponieważ podzielony fragment implementacji komunikuje się, przypisując zmienne do instancji typu throwaway zamiast przekazywać parametry). Jeśli twoje funkcje wewnętrzne przyjmują tak wiele parametrów, że wywołania są złożone i nieporęczne, kod nie staje się mniej skomplikowany, gdy ukrywasz ten przepływ danych!
Ben
23

Myślę, że nr 1 to właściwie zła opcja.

Rozważmy twoją funkcję:

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

Z jakich danych korzysta podoperacja1? Czy zbiera dane wykorzystywane przez podoperację2? Gdy przekazujesz dane, przechowując je na sobie, nie mogę powiedzieć, w jaki sposób powiązane są funkcje. Kiedy patrzę na siebie, niektóre atrybuty pochodzą z konstruktora, niektóre z wywołania metody_publicznej, a niektóre losowo dodawane w innych miejscach. Moim zdaniem to bałagan.

A co z numerem 2? Po pierwsze, spójrzmy na drugi problem z tym:

Ponadto funkcje byłyby ściśle ze sobą powiązane, ale nie byłyby zgrupowane razem.

Byliby razem w module, więc byliby całkowicie zgrupowani.

Problem, jaki tu widzę, polega na tym, że muszę przekazywać wiele zmiennych między funkcjami.

Moim zdaniem to dobrze. To wyraźnie określa zależności danych w algorytmie. Przechowując je w zmiennych globalnych lub na sobie, ukrywasz zależności i sprawiasz, że wydają się mniej złe, ale wciąż tam są.

Zwykle, gdy pojawia się taka sytuacja, oznacza to, że nie znalazłeś właściwego sposobu na rozłożenie problemu. Dzielenie się na wiele funkcji jest niewygodne, ponieważ próbujesz podzielić go w niewłaściwy sposób.

Oczywiście, nie widząc rzeczywistej funkcji, trudno zgadnąć, co byłoby dobrą sugestią. Podajesz jednak odrobinę tego, z czym masz do czynienia:

Mój program musi kilkakrotnie wykonać stosunkowo złożoną operację. Ta operacja wymaga dużo „księgowości”, musi tworzyć i usuwać niektóre pliki tymczasowe itp.

Pozwól, że wybiorę przykład czegoś, co pasuje do twojego opisu, instalatora. Instalator musi skopiować kilka plików, ale jeśli anulujesz go częściowo, musisz przewinąć cały proces, łącznie z cofnięciem wszystkich zastąpionych plików. Algorytm do tego wygląda mniej więcej tak:

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)

Teraz pomnóż tę logikę na potrzeby wykonywania ustawień rejestru, ikon programów itp., A masz dość bałaganu.

Myślę, że twoje rozwiązanie nr 1 wygląda następująco:

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

To sprawia, że ​​ogólny algorytm jest bardziej przejrzysty, ale ukrywa sposób komunikowania się różnych elementów.

Podejście nr 2 wygląda mniej więcej tak:

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)

Teraz jest jasne, jak fragmenty danych przemieszczają się między funkcjami, ale jest to bardzo niewygodne.

Jak więc napisać tę funkcję?

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

Obiekt FileTransactionLog jest menedżerem kontekstu. Kiedy copy_new_files kopiuje plik, robi to poprzez FileTransactionLog, który obsługuje tworzenie kopii tymczasowej i śledzi, które pliki zostały skopiowane. W przypadku wyjątku kopiuje oryginalne pliki z powrotem, aw przypadku powodzenia usuwa kopie tymczasowe.

Działa to, ponieważ odkryliśmy bardziej naturalny rozkład zadania. Wcześniej mieszaliśmy logikę o tym, jak zainstalować aplikację z logiką o tym, jak odzyskać anulowaną instalację. Teraz dziennik transakcji obsługuje wszystkie szczegóły dotyczące plików tymczasowych i księgowości, a funkcja może skupić się na podstawowym algorytmie.

Podejrzewam, że twoja sprawa jest na tej samej łodzi. Musisz wyodrębnić elementy księgowości do jakiegoś obiektu, aby złożone zadanie mogło być wyrażone w bardziej prosty i elegancki sposób.

Winston Ewert
źródło
9

Ponieważ jedyną widoczną wadą metody 1 jest jej nieoptymalny wzorzec użycia, myślę, że najlepszym rozwiązaniem jest zwiększenie enkapsulacji o krok dalej: użyj klasy, ale także zapewnij funkcję wolnostojącą, która konstruuje tylko obiekt, wywołuje metodę i zwraca :

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

Dzięki temu masz najmniejszy możliwy interfejs do swojego kodu, a klasa, której używasz wewnętrznie, naprawdę staje się tylko szczegółem implementacji twojej funkcji publicznej. Kod telefoniczny nigdy nie musi wiedzieć o twojej klasie wewnętrznej.

cmaster
źródło
4

Z jednej strony pytanie jest w jakiś sposób agnostyczne wobec języka; ale z drugiej strony implementacja zależy od języka i jego paradygmatów. W tym przypadku jest to Python, który obsługuje wiele paradygmatów.

Oprócz twoich rozwiązań istnieje również możliwość wykonania operacji bezstanowych w bardziej funkcjonalny sposób, np

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

Wszystko sprowadza się do

  • czytelność
  • konsystencja
  • łatwość utrzymania

Ale -Jeśli twoja baza kodu to OOP, to narusza spójność, jeśli nagle niektóre części są pisane w (bardziej) funkcjonalnym stylu.

Thomas Junk
źródło
4

Po co projektować, kiedy mogę kodować?

Oferuję sprzeczne spojrzenie na odpowiedzi, które przeczytałem. Z tej perspektywy wszystkie odpowiedzi, a nawet samo pytanie, koncentrują się na mechanice kodowania. Ale to jest kwestia projektu.

Czy powinienem utworzyć klasę, jeśli moja funkcja jest złożona i zawiera wiele zmiennych?

Tak, ponieważ ma to sens dla twojego projektu. Może to być klasa sama w sobie lub część innej klasy lub jej zachowanie może być rozdzielone między klasy.


Projektowanie obiektowe polega na złożoności

Celem OO jest skuteczne budowanie i utrzymywanie dużych, złożonych systemów poprzez enkapsulację kodu pod względem samego systemu. „Właściwa” konstrukcja mówi, że wszystko jest w jakiejś klasie.

Projektowanie OO naturalnie zarządza złożonością przede wszystkim poprzez skoncentrowane klasy, które przestrzegają zasady pojedynczej odpowiedzialności. Klasy te nadają strukturę i funkcjonalność podczas oddechu i głębokości systemu, współdziałają i integrują te wymiary.

Biorąc to pod uwagę, często mówi się, że funkcje wiszące wokół systemu - zbyt wszechobecna ogólna klasa użyteczności - to zapach kodu sugerujący niewystarczający projekt. Zwykle się zgadzam.

radarbob
źródło
Projekt OO w naturalny sposób zarządza złożonością OOP w naturalny sposób wprowadza niepotrzebną złożoność.
Miles Rout
„Niepotrzebna złożoność” przypomina mi bezcenne komentarze współpracowników, takie jak „och, to za dużo klas”. Doświadczenie mówi mi, że sok jest wart ściśnięcia. W najlepszym wypadku widzę praktycznie całe klasy z metodami o długości 1-3 linii, a przy każdej wykonywanej klasie złożoność każdej metody jest zminimalizowana: Pojedynczy krótki LOC porównujący dwie kolekcje i zwracający duplikaty - oczywiście kryje się za tym kod. Nie myl „dużej ilości kodu” ze złożonym kodem. W każdym razie nic nie jest za darmo.
radarbob
1
Dużo „prostego” kodu, który oddziałuje w skomplikowany sposób, jest ZNACZNIE gorsze niż niewielka ilość złożonego kodu.
Miles Rout
1
Niewielka ilość złożonego kodu zawiera złożoność . Jest złożoność, ale tylko tam. Nie wyciek. Gdy masz wiele indywidualnie prostych elementów, które działają razem w naprawdę złożony i trudny do zrozumienia sposób, jest to znacznie bardziej mylące, ponieważ złożoność nie ma granic ani ścian.
Miles Rout
3

Dlaczego nie porównać swoich potrzeb z czymś, co istnieje w standardowej bibliotece Pythona, a następnie zobaczyć, jak to jest zaimplementowane?

Uwaga: jeśli nie potrzebujesz obiektu, nadal możesz definiować funkcje w ramach funkcji. W Pythonie 3 pojawiła się nowa nonlocaldeklaracja umożliwiająca zmianę zmiennych w funkcji nadrzędnej.

Nadal może się przydać kilka prostych klas prywatnych wewnątrz funkcji do implementowania abstrakcji i porządkowania operacji.

meuh
źródło
Dzięki. A czy wiesz coś w standardowej bibliotece, co brzmi jak to, co mam w pytaniu?
iCanLearn,
Trudno byłoby mi zacytować coś za pomocą funkcji zagnieżdżonych, w rzeczywistości nie znajduję nonlocalnigdzie w moich obecnie zainstalowanych bibliotekach Pythona. Być może możesz wziąć serce, z textwrap.pyktórego ma TextWrapperklasę, ale także def wrap(text)funkcję, która po prostu tworzy TextWrapper instancję, wywołuje na niej .wrap()metodę i zwraca. Więc skorzystaj z klasy, ale dodaj kilka funkcji wygody.
Meuh