Jak zmienia się myślenie o wzorcach projektowych i praktykach OOP w dynamicznych i słabo typowanych językach?

11

Jest już dość pomocne pytanie w tym zakresie („ Wzory projektowe inne niż OOP? ”), Ale jestem bardziej ciekawy przejściowego punktu widzenia dla kogoś, kto dopiero zaczyna pracę z dynamicznymi i słabo typowanymi językami.

To znaczy: załóżmy, że programuję w C ++, C # lub Javie od wielu lat i pochłonąłem wiele mądrości zgodnie z wzorcami projektowymi GoF, wzorcami architektury aplikacji korporacyjnych Fowlera , zasadami SOLID itp. Teraz ” Zajmuję się językiem Ruby, Python, JavaScript itp. i zastanawiam się, do czego służy moja wiedza. Prawdopodobnie w wielu przypadkach mógłbym dokonywać bezpośrednich tłumaczeń, ale prawie na pewno nie skorzystałoby to w pełni z mojego nowego ustawienia. Samo pisanie kaczek wywraca na myśl wiele moich myśli opartych na interfejsie.

Co pozostaje takie samo? Jakie zmiany? Czy istnieją zasady przewodnie, takie jak SOLID, czy wzorce kanoniczne (być może zupełnie nowe), które powinien znać początkujący język dynamiczny?

Domenic
źródło

Odpowiedzi:

7

Co pozostaje takie samo? Jakie zmiany?

Wzory są takie same. Techniki językowe się zmieniają.

Czy istnieją zasady przewodnie, takie jak SOLID,

Tak. Rzeczywiście pozostają one zasadami przewodnimi. Nic się nie zmienia.

czy wzorce kanoniczne (być może zupełnie nowe), które powinien znać początkujący język dynamiczny?

Niektóre rzeczy są wyjątkowe. Głównie wpływa to na zmianę technik wdrażania.

Wzór jest - cóż - wzorem . To nie jest prawo. Nie podprogram. Nie makro. To tylko dobry pomysł, który się powtarza, ponieważ to dobry pomysł.

Dobre pomysły nie wychodzą z mody ani nie zmieniają się dramatycznie.

Inne notatki. Python nie jest „słabo napisany”. Jest silniej napisany niż Java lub C ++, ponieważ nie ma operacji rzutowania. [Tak, istnieje sposób, aby sfałszować klasę związaną z przedmiotem, ale nie jest to coś, co się robi, z wyjątkiem udowodnienia dziwnego, legalistycznego punktu.]

Również. Większość wzorców projektowych opiera się na różnych sposobach wykorzystania polimorfizmu.

Spójrz na państwa lub Dowództwa lub Memento jako przykłady. Mają hierarchie klas do tworzenia stanów polimorficznych, poleceń lub pamiątek zmian stanu. Nic nie zmienia się znacząco, gdy robisz to w Pythonie. Niewielkie zmiany obejmują złagodzenie dokładnej hierarchii klas, ponieważ polimorfizm w Pythonie zależy od powszechnych metod, a nie wspólnych przodków.

Ponadto niektóre wzory są po prostu próbą osiągnięcia późnego wiązania. Większość wzorców związanych z fabryką jest próbą umożliwienia łatwej zmiany hierarchii klas bez ponownej kompilacji każdego modułu C ++ w aplikacji. To nie jest tak interesująca optymalizacja w dynamicznym języku. Jednak Fabryka jako sposób na ukrycie szczegółów implementacji wciąż ma ogromną wartość.

Niektóre wzorce są próbą sterowania kompilatorem i konsolidatorem. Na przykład Singleton istnieje, aby tworzyć mylące globale, ale przynajmniej je kapsułkować. Singletonowe klasy Python nie są przyjemną perspektywą. Ale moduły Pythona są już singletonami, więc wielu z nas po prostu używa modułu i nie próbuje próbować zadzierać z klasą Singleton .

S.Lott
źródło
Nie powiedziałbym, że „nic się nie zmienia” w SOLID. W zależności od języka i modelu obiektowego zarówno zasada otwartego-zamkniętego, jak i zasada podstawienia Liskowa mogą być bez znaczenia. (JavaScript i Go przychodzą mi na myśl.)
Mason Wheeler
@Mason Wheeler. Z mojego doświadczenia wynika, że ​​Open-Closed jest niezależny od języka. Będziesz musiał podać bardziej konkretne przykłady tego, jak otwarta konstrukcja jest „bez znaczenia” w JavaScript lub Go. Być może podstawienie Liskowa nie dotyczy JavaScript, ale wydaje się, że nadal stosuje się podstawowy wzorzec - polimorfizm.
S.Lott,
@ S.Lott: Ładne aktualizacje w edycji; były o wiele bardziej interesujące niż pierwotna odpowiedź: P. Dzięki za poprawienie mojego błędu w Pythonie. Ogólnie rzecz biorąc, konkretne przykłady wzorców i ich powiązanie z dynamicznymi językami, polimorfizmem, późnym wiązaniem itp. Są idealne.
Domenic,
@ S.Lott: Ponieważ Open / Closed dotyczy dziedziczenia, którego te języki nie mają. (Również pomysł, że obiekt jest „zamknięty dla modyfikacji” nie pasowałby do wielu koderów Ruby ...)
Mason Wheeler
@Mason Wheeler: Dziękuję za wyjaśnienie dotyczące Open / Closed. Myślę, że wyjątek JavaScript jest ważny, ale ponieważ pytanie jest tak otwarte (wymieniając JavaScript, Python i Ruby, a także język ETC), nie jestem pewien, jak rozwiązać ten szczególny przypadek.
S.Lott,
8

Peter Norvig podjął się tego samego pytania w 1998 r. Przeczytaj http://norvig.com/design-patterns/ppframe.htm, aby uzyskać zestaw szczegółowych rzeczy, które zauważył, oraz http://c2.com/cgi/wiki?AreDesignPatternsMissingLanguageFeatures dla dalsza dyskusja wokół tego punktu.

Krótka wersja jest taka, że ​​gdy Twój język ma więcej funkcji, powtarzalne wzorce projektowe stają się prostsze - często nawet niewidoczne. Odkrył, że dotyczy to większości wzorców projektowych zidentyfikowanych przez GoF.

btilly
źródło
8

Programowanie w dynamicznym języku obiektowym wykorzystuje wiele takich samych wzorców i zasad, ale istnieją pewne poprawki i różnice związane z otoczeniem:

Zamień interfejsy na pisanie kaczek - tam, gdzie Gang of Four powiedziałby ci, abyś używał abstrakcyjnej klasy bazowej z czystymi funkcjami wirtualnymi, a ty używałbyś interfejsu w Javie, w dynamicznym języku, potrzebujesz tylko zrozumienia. Ponieważ możesz użyć dowolnego obiektu w dowolnym miejscu i będzie on działał dobrze, jeśli zaimplementuje metody, które są faktycznie wywoływane, nie musisz definiować formalnego interfejsu. Warto je udokumentować , aby było jasne, co jest rzeczywiście wymagane.

Funkcje to także obiekty - istnieje wiele wzorców dotyczących oddzielania decyzji od akcji; Dowodzenie, strategia, łańcuch odpowiedzialności itp. W języku z pierwszorzędnymi funkcjami często rozsądne jest po prostu przekazywanie funkcji zamiast tworzenia obiektów .doIt()metodami. Wzorce te przekształcają się w „użyj funkcji wyższego rzędu”.

SPRZEDANY - Zasada segregacji interfejsu ma tutaj największe znaczenie, ponieważ nie ma interfejsów. Nadal powinieneś wziąć pod uwagę tę zasadę, ale nie możesz jej zweryfikować w swoim kodzie. Tylko osobista czujność ochroni cię tutaj. Z drugiej strony, ból spowodowany naruszeniem tej zasady jest znacznie zmniejszony w typowych środowiskach dynamicznych.

„... w moim szczególnym ... Idiomie!” - Każdy język ma dobre i złe praktyki, więc musisz się ich nauczyć i postępować zgodnie z nimi, jeśli chcesz mieć najlepszy kod w tych językach. Idealnie napisany wzorzec iteratora można na przykład wyśmiewać w języku z wbudowanym rozumieniem list.

Sean McMillan
źródło
3

Z mojego doświadczenia wynika, że ​​niektóre wzorce są nadal przydatne w Pythonie, a nawet łatwiejsze w konfiguracji niż w bardziej statycznych językach. Niektóre Wzory OTOH są po prostu niepotrzebne, a nawet marszczone, jak Wzorzec Singleton. Zamiast tego użyj zmiennej lub funkcji na poziomie modułu. Lub użyj Wzoru Borga.

Zamiast konfigurować Wzorzec Kreatywny, często wystarczy przekazać dookoła wywołanie, które tworzy obiekty. Może to być funkcja, obiekt z __call__metodą lub nawet klasą, ponieważ new()w Pythonie nie ma , tylko wywołanie samej klasy:

def make_da_thing(maker, other, stuff):
    da_thing = maker(other + 1, stuff + 2)
    # ... do sth
    return da_thing

def maker_func(x, y):
     return x * y

class MakerClass(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
...
a = make_da_thing(maker_func, 5, 8)
b = make_da_thing(MakerClass, 6, 7)

Wzorzec stanu i strategii ma bardzo podobną strukturę w językach takich jak C ++ i Java. Mniej w Pythonie. Wzorzec strategii pozostaje mniej więcej taki sam, ale Wzorzec stanu staje się w większości niepotrzebny. Wzorzec stanu w językach statycznych symuluje zmianę klasy w czasie wykonywania. W Pythonie możesz to zrobić: zmienić klasę obiektu w czasie wykonywania. Tak długo, jak robisz to w kontrolowany, zamknięty sposób, wszystko powinno być w porządku:

class On(object):
    is_on = True
    def switch(self):
        self.__class__ = Off

class Off(object):
    is_on = False
    def switch(self):
        self.__class__ = On
...

my_switch = On()
assert my_switch.is_on
my_switch.switch()
assert not my_switch.is_on

Wzory oparte na statycznej wysyłce typów nie będą działać lub będą działać zupełnie inaczej. Nie musisz pisać zbyt dużo kodu płyty kotłowej, np. Wzorzec gościa: w Javie i C ++ musisz napisać metodę akceptacji w każdej odwiedzalnej klasie, natomiast w Pythonie możesz odziedziczyć tę funkcjonalność poprzez klasę mixin, np. Visitable:

class Visitable(object):
    def accept(self, visitor):
        visit = getattr(visitor, 'visit' + self.__class__.__name__)
        return visit(self)
...

class On(Visitable):
    ''' exactly like above '''

class Off(Visitable):
    ''' exactly like above '''

class SwitchStatePrinter(object): # Visitor
    def visitOn(self, switch):
         print 'the switch is on'
    def visitOff(self, switch):
         print 'the switch is off'

class SwitchAllOff(object): # Visitor
    def visitOn(self, switch):
         switch.switch()
    def visitOff(self, switch):
         pass
...
print_state = SwitchStatePrinter()
turn_em_off = SwitchAllOff()
for each in my_switches:
    each.accept(print_state)
    each.accept(turn_em_off)

Wiele sytuacji, które wymagają zastosowania Wzorca w Języku Statycznym, nie robi tego tak często w Pythonie. Wiele rzeczy można rozwiązać za pomocą innych technik, takich jak funkcje wyższego rzędu (dekoratorzy, fabryki funkcji) lub meta-klasy.

pillmuncher
źródło
Rozumiem teraz, że twoja odpowiedź dotyczy pytania, które właśnie zadałem: Czy nadpisywanie w __class__celu zaimplementowania fabryki w Pythonie jest dobrym pomysłem?
rds