Czy sprzęganie z łańcuchami jest „luźniejsze” niż przy użyciu metod klasowych?

18

Zaczynam projekt grupy szkolnej w Javie, używając Swinga. Jest to prosty GUI w aplikacji komputerowej Database.

Profesor dał nam kod zeszłorocznego projektu, abyśmy mogli zobaczyć, jak on to robi. Moje pierwsze wrażenie jest takie, że kod jest o wiele bardziej skomplikowany, niż powinien być, ale wyobrażam sobie, że programiści często myślą to, patrząc na kod, którego nie napisali.

Mam nadzieję znaleźć powody, dla których jego system jest dobry lub zły. (Zapytałem profesora, a on powiedział, że później zobaczę, dlaczego jest lepiej, co mnie nie satysfakcjonuje).

Zasadniczo, aby uniknąć sprzężenia jego trwałych obiektów, modeli (logiki biznesowej) i widoków, wszystko odbywa się za pomocą łańcuchów. Trwałymi obiektami, które są przechowywane w bazie danych, są tablice skrótów ciągów, a modele i widoki „subskrybują się”, zapewniając klucze łańcuchów dla „zdarzeń”, które subskrybują.

Po uruchomieniu zdarzenia widok lub model wysyła ciąg znaków do wszystkich swoich subskrybentów, którzy decydują, co zrobić dla tego zdarzenia. Na przykład w jednej z metod detektora akcji widoków (uważam, że ustawia to właściwość bicycleMakeField na obiekcie trwałym):

    else if(evt.getSource() == bicycleMakeField)
    {
        myRegistry.updateSubscribers("BicycleMake", bicycleMakeField.getText());
    }

To wywołanie ostatecznie trafia do tej metody w modelu pojazdu:

public void stateChangeRequest(String key, Object value) {

            ... bunch of else ifs ...

    else
    if (key.equals("BicycleMake") == true)
    {
                ... do stuff ...

Profesor mówi, że ten sposób robienia rzeczy jest bardziej rozszerzalny i łatwiejszy w utrzymaniu niż widok wywołujący metodę na obiekcie logiki biznesowej. Mówi, że nie ma sprzężenia między poglądami a modelami, ponieważ nie znają się nawzajem.

Myślę, że jest to gorszy rodzaj sprzężenia, ponieważ widok i model muszą używać tych samych ciągów, aby działać. Jeśli usuniesz widok lub model lub popełnisz literówkę w ciągu, nie wystąpią błędy kompilacji. Dzięki temu kod jest o wiele dłuższy niż myślę, że powinien.

Chcę z nim o tym porozmawiać, ale wykorzystuje swoje doświadczenie branżowe, aby odeprzeć wszelkie argumenty, które ja, niedoświadczony student, mogę wysunąć. Czy jego podejście nie ma żadnej przewagi?


Aby to wyjaśnić, chcę porównać powyższe podejście, mając widok oczywiście połączony z modelem. Na przykład możesz przekazać obiekt modelu pojazdu do widoku, a następnie zmienić „markę” pojazdu, wykonaj następujące czynności:

vehicle.make = bicycleMakeField.getText();

Zmniejszyłoby to 15 wierszy kodu, które są obecnie WYŁĄCZNIE używane do ustawienia marki pojazdu w jednym miejscu, do jednego czytelnego wiersza kodu. (A ponieważ tego rodzaju operacje są wykonywane setki razy w całej aplikacji, myślę, że byłoby to duże zwycięstwo zarówno pod względem czytelności, jak i bezpieczeństwa.)


Aktualizacja

Mój lider zespołu i ja zrestrukturyzowaliśmy ramy w taki sposób, w jaki chcieliśmy to zrobić, używając pisania statycznego, poinformowaliśmy profesora i ostatecznie daliśmy mu wersję demonstracyjną. Jest wystarczająco hojny, aby pozwolić nam korzystać z naszego frameworka, dopóki nie poprosimy go o pomoc, i jeśli uda nam się przyspieszyć resztę naszego zespołu - co wydaje nam się sprawiedliwe.

Philip
źródło
12
OT. Myślę, że twój profesor powinien się zaktualizować i nie używać starego kodu. W środowisku akademickim nie ma powodu, aby używać wersji JAVA z 2006 roku, kiedy jest rok 2012.
Farmor
3
Prosimy o informowanie nas, gdy w końcu otrzymasz jego odpowiedź.
JeffO
4
Bez względu na jakość kodu lub mądrość techniki, należy usunąć słowo „magia” z opisu ciągów znaków używanych jako identyfikatory. „Magiczne sznurki” oznaczają, że system nie jest logiczny i że pokoloruje twój widok i zatruć każdą dyskusję z instruktorem. Słowa takie jak „klucze”, „nazwy” lub „identyfikatory” są bardziej neutralne, być może bardziej opisowe i mogą prowadzić do pomocnej rozmowy.
Caleb
1
Prawdziwe. Podczas rozmowy z nim nie nazwałam ich magicznymi strunami, ale masz rację, nie ma potrzeby, aby brzmiało to gorzej niż w rzeczywistości.
Philip
2
Podejście, które opisuje twój profesor (powiadamianie słuchaczy poprzez zdarzenia o nazwie łańcuch) może być dobre. W rzeczywistości wyliczenia mogą mieć większy sens, ale nie jest to największy problem. Prawdziwy problem, jaki tu widzę, polega na tym, że obiektem odbierającym powiadomienie jest sam model, a następnie trzeba ręcznie wysłać w zależności od rodzaju zdarzenia. W języku, w którym funkcje są pierwszej klasy, zarejestrowałbyś funkcję , a nie obiekt zdarzenia. Wydaje mi się, że w Javie należy zastosować zawijanie obiektów w jednej funkcji
Andrea

Odpowiedzi:

32

Podejście zaproponowane przez profesora najlepiej opisać jako ściśle wpisane i jest błędne na prawie każdym poziomie.

Sprzężenie najlepiej redukuje się przez inwersję zależności , która robi to poprzez wysyłkę polimorficzną, a nie przez szereg przypadków.

back2dos
źródło
3
Piszesz „prawie na każdym poziomie”, ale nie napisałeś, dlaczego (Twoim zdaniem) nie jest to odpowiedni przypadek w tym przykładzie. Byłoby interesujące wiedzieć.
hakre
1
@hakre: Nie ma odpowiednich przypadków, przynajmniej nie w Javie. Powiedziałem, że „jest błędne na prawie każdym poziomie”, ponieważ jest lepsze niż w ogóle nie stosowanie pośrednictwa i rzucanie całego kodu w jednym miejscu. Jednak użycie (globalnych) stałych ciągów magicznych jako podstawy ręcznej wysyłki jest po prostu skomplikowanym sposobem użycia obiektów globalnych.
back2dos
Więc twój argument brzmi: nigdy nie ma odpowiednich przypadków, więc to też nie jest jeden? To raczej nie jest argument, a właściwie niezbyt pomocny.
hakre
1
@hakre: Moim argumentem jest to, że takie podejście jest złe z wielu powodów (akapit 1 - wystarczające powody, aby tego uniknąć podano w wyjaśnieniu ciągów znaków) i nie należy go stosować, ponieważ jest lepsze (akapit 2 ).
back2dos
3
@hakre Mogę sobie wyobrazić sytuację, która uzasadniałaby takie podejście, gdyby seryjny zabójca przykleił cię taśmą do krzesła i trzymał nad głową piłę łańcuchową, śmiejąc się maniakalnie i nakazując ci używanie literałów ciągowych wszędzie dla logiki wysyłki. Poza tym lepiej jest polegać na funkcjach językowych, aby robić takie rzeczy. Właściwie mogę wymyślić jeszcze jeden przypadek dotyczący krokodyli i ninja, ale to jest trochę bardziej zaangażowane.
Rob
19

Twój profesor robi to źle . Próbuje całkowicie oddzielić widok i model, zmuszając komunikację do podróżowania sznurkiem w obu kierunkach (widok nie zależy od modelu, a model nie zależy od widoku) , co całkowicie przeczy celowi projektowania obiektowego i MVC. Wygląda na to, że zamiast pisać klasy i projektować architekturę opartą na OO, pisze odmienne moduły, które po prostu reagują na wiadomości tekstowe.

Aby być nieco uczciwym wobec profesora, próbuje stworzyć w Javie coś, co wygląda bardzo podobnie do bardzo popularnego wywoływanego interfejsu .NET INotifyPropertyChanged, który powiadamia subskrybentów o zdarzeniach zmiany poprzez przekazywanie ciągów określających, która właściwość uległa zmianie. Przynajmniej w .NET interfejs ten służy przede wszystkim do komunikacji z modelu danych w celu przeglądania obiektów i elementów GUI (wiązanie).

Jeśli zostanie to właściwie wykonane , przyjęcie tego podejścia może przynieść pewne korzyści¹:

  1. Widok zależy od modelu, ale model nie musi zależeć od widoku. Jest to właściwe odwrócenie kontroli .
  2. W kodzie View masz jedno miejsce, które zarządza reagowaniem na powiadomienia o zmianie z modelu, i tylko jedno miejsce do subskrypcji / rezygnacji z subskrypcji, co zmniejsza prawdopodobieństwo wycieku pamięci odniesienia.
  3. Jest o wiele mniej narzutów związanych z kodowaniem, gdy chcesz dodać lub usunąć wydarzenie z wydawcy wydarzenia. W .NET istnieje wbudowany mechanizm publikowania / subskrybowania o nazwie, eventsale w Javie trzeba tworzyć klasy lub obiekty i pisać kod subskrypcji / anulowania subskrypcji dla każdego zdarzenia.
  4. Największą wadą tego podejścia jest to, że kod subskrybujący może nie być zsynchronizowany z kodem publikującym. Jest to jednak również zaletą w niektórych środowiskach programistycznych. Jeśli w modelu zostanie opracowane nowe zdarzenie, a widok nie zostanie jeszcze zaktualizowany, aby go zasubskrybować, widok prawdopodobnie nadal będzie działać. Podobnie, jeśli zdarzenie zostanie usunięte, masz martwy kod, ale nadal można go skompilować i uruchomić.
  5. Przynajmniej w .NET Reflection jest tutaj bardzo efektywnie wykorzystywane do automatycznego wywoływania modułów pobierających i ustawiających w zależności od łańcucha przekazanego subskrybentowi.

Krótko mówiąc (i aby odpowiedzieć na twoje pytanie), można to zrobić, ale podejście profesora jest winne za to, że nie pozwala się na przynajmniej jednokierunkową zależność między widokiem a modelem. To po prostu niepraktyczne, zbyt akademickie i nie jest dobrym przykładem tego, jak korzystać z dostępnych narzędzi.


¹ Wyszukaj w Google, INotifyPropertyChangedaby znaleźć milion sposobów, w jaki ludzie starają się unikać używania magicznego ciągu podczas jego wdrażania.

Kevin McCormick
źródło
Wygląda na to, że opisałeś tutaj SOAP. : D… (ale tak, rozumiem i masz rację)
Konrad Rudolph
11

enums (lub public static final Strings) powinny być używane zamiast magicznych strun.

Twój profesor nie rozumie OO . Na przykład mając konkretną klasę pojazdu?
I żeby ta klasa zrozumiała markę roweru?

Pojazd powinien być abstrakcyjny i powinna istnieć konkretna podklasa o nazwie Bike. Będę grał według jego zasad, aby uzyskać dobrą ocenę, a potem zapomnę o jego nauczaniu.

Farmor
źródło
3
Chyba że celem tego ćwiczenia jest poprawienie kodu przy użyciu lepszych zasad OO. W takim przypadku refaktoryzuj się.
joshin4colours
2
@ joshin4colours Jeśli StackExchange oceniało to, miałbyś rację; ale ponieważ równiarka jest tą samą osobą, która wpadła na pomysł, że dałby nam wszystkim złe oceny, ponieważ wie, że jego wdrożenie było lepsze.
Dan Neely
7

Myślę, że masz rację, magiczne sznurki są złe. Ktoś z mojego zespołu musiał kilka dni temu debugować problem spowodowany przez łańcuchy magiczne (ale napisali to w swoim własnym kodzie, więc trzymałem gębę na kłódkę). I stało się ... w przemyśle !!

Zaletą tego podejścia jest to, że kodowanie może być szybsze, szczególnie w przypadku małych projektów demonstracyjnych. Wady polegają na tym, że jest on podatny na literówki i im częściej ciąg znaków jest używany, tym większa szansa, że ​​kod literówki coś popsuje. A jeśli kiedykolwiek chcesz zmienić magiczny ciąg, refaktoryzacja ciągów jest trudniejsza niż zmiennych refaktoryzujących, ponieważ narzędzia refaktoryzujące mogą również dotykać ciągów zawierających magiczny ciąg (jako substring), ale same nie są magicznym ciągiem i nie należy ich dotykać. Ponadto może być konieczne prowadzenie słownika lub indeksu magicznych ciągów, aby nowi programiści nie zaczęli wymyślać nowych ciągów magicznych w tym samym celu i nie tracili czasu na wyszukiwanie istniejących ciągów magicznych.

W tym kontekście wygląda na to, że Enum może być lepszy niż ciągi magiczne lub nawet globalne stałe ciągów. Ponadto, IDE często daje ci jakąś pomoc w kodzie (autouzupełnianie, refaktoryzacja, znajdź wszystkie odniesienia itp.), Gdy używasz ciągów ciągłych lub Enums. IDE nie oferuje dużej pomocy, jeśli używasz magicznych ciągów.

FrustratedWithFormsDesigner
źródło
Zgadzam się, że wyliczenia są lepsze niż literały strunowe. Ale nawet wtedy, czy naprawdę lepiej jest wywołać „stateChangeRequest” za pomocą klucza enum, czy po prostu wywołać metodę bezpośrednio w modelu? Tak czy inaczej, model i widok są połączone w jednym miejscu.
Philip
@Philip: Cóż, więc sądzę, że aby oddzielić model i wyświetlić w tym momencie, możesz potrzebować jakiejś klasy kontrolera, aby to zrobić ...
FrustratedWithFormsDesigner
@FrustratedWithFormsDesigner - Tak. Architektura MVC jest najbardziej odsprzężona
cdeszaq
Racja, jeśli kolejna warstwa pomoże je oddzielić, nie będę się temu sprzeciwiał. Ale tę warstwę można nadal wpisać statycznie i użyć rzeczywistych metod zamiast ciągów lub komunikatów wyliczeniowych w celu ich połączenia.
Filip
6

Używanie „doświadczenia branżowego” jest słabym argumentem. Twój profesor musi wymyślić lepszy argument :-).

Jak powiedzieli inni, używanie magicznych strun jest z pewnością marne, używanie enum jest uważane za znacznie bezpieczniejsze. Wyliczenie ma znaczenie i zakres , magiczne ciągi nie. Poza tym sposób, w jaki twój profesor oddziela obawy, jest uważany za starszą i delikatniejszą technikę niż stosowana obecnie.

Widzę, że @backtodos to omówił, kiedy odpowiadałem na to pytanie, więc po prostu dodam swoją opinię, że używając Inversion of Control (którego forma jest zależna od Dependency Injection), już niedługo natkniesz się na środowisko Spring w branży. ..) jest uważany za bardziej nowoczesny sposób radzenia sobie z tego rodzaju oddzielaniem.

Martijn Verburg
źródło
2
W pełni zgadzają się, że środowisko akademickie powinno mieć znacznie wyższe wymagania niż przemysł. Środowisko akademickie == najlepszy teoretyczny sposób; Branża == najlepszy praktyczny sposób
Farmor
3
Niestety, niektórzy z tych, którzy uczą takich rzeczy w środowisku akademickim, robią to, ponieważ nie mogą tego zrobić w przemyśle. Pozytywne jest to, że niektórzy pracownicy akademiccy są błyskotliwi i nie należy ich ukrywać w jakimś biurze korporacyjnym.
FrustratedWithFormsDesigner
4

Powiedzmy sobie jasno; jest nieuchronnie pewne sprzężenie. Fakt, że istnieje taki subskrybowalny temat, jest punktem sprzęgającym, podobnie jak natura reszty wiadomości (takiej jak „pojedynczy ciąg”). Biorąc to pod uwagę, powstaje pytanie, czy sprzężenie jest większe, gdy wykonuje się je za pomocą łańcuchów lub typów. Odpowiedź zależy od tego, czy martwisz się sprzężeniem rozproszonym w czasie lub przestrzeni: czy wiadomości zostaną zachowane w pliku przed odczytaniem później, czy też zostaną wysłane do innego procesu (szczególnie jeśli to nie jest napisane w tym samym języku), użycie ciągów może znacznie uprościć sprawę. Minusem jest to, że nie ma sprawdzania typu; błędy zostaną wykryte dopiero w czasie wykonywania (a czasem nawet wtedy).

Strategią ograniczania zagrożeń / kompromisów w Javie może być użycie interfejsu maszynowego, ale ten interfejs maszynowy znajduje się w osobnym pakiecie. Powinien składać się tylko z Javy interfacei podstawowych typów wartości (np. Wyliczenia, wyjątki). W tym pakiecie nie powinno być żadnych implementacji interfejsów. Następnie wszyscy stosują się do tego, a jeśli konieczne jest kompleksowe przesyłanie wiadomości, określona implementacja delegata Java może w razie potrzeby używać magicznych ciągów. Ułatwia także migrację kodu do bardziej profesjonalnego środowiska, takiego jak JEE lub OSGi, i znacznie pomaga w drobiazgach, takich jak testowanie…

Donal Fellows
źródło
4

Twój profesor proponuje użycie „magicznych sznurków”. Chociaż „luźno łączy” klasy między sobą, umożliwiając łatwiejsze zmiany, jest anty-wzorcem; ciągi powinny być bardzo, BARDZO rzadko używane do przechowywania lub kontrolowania instrukcji kodu, a kiedy to absolutnie musi się zdarzyć, wartość ciągów, których używasz do sterowania logiką, powinna być centralnie i stale definiowana, więc JEDEN autorytet w zakresie oczekiwanej wartości dowolny konkretny ciąg.

Powód jest bardzo prosty; ciągi nie są sprawdzane przez kompilator pod kątem składni lub zgodności z innymi wartościami (w najlepszym wypadku sprawdzane są pod kątem prawidłowej formy dotyczącej znaków zmiany znaczenia i innego formatowania specyficznego dla języka w ciągu). Możesz umieścić w łańcuchu wszystko, co chcesz. Jako taki, możesz uzyskać beznadziejnie zepsuty algorytm / program do kompilacji, a błąd pojawia się dopiero po uruchomieniu. Rozważ następujący kod (C #):

private Dictionary<string, Func<string>> methods;

private void InitDictionary() //called elsewhere
{
   methods = new Dictionary<string, Func<string>> 
      {{"Method1", ()=>doSomething()},
       {"MEthod2", ()=>doSomethingElse()}} //oops, fat-fingered it
}

public string RunMethod(string methodName)
{
   //very naive, but even a check for the key would generate a runtime error, 
   //not a compile-time one.
   return methods[methodName](); 
}

...

//this is what we thought we entered back in InitDictionary for this method...
var result = RunMethod("Method2"); //error; no such key

... cały ten kod się kompiluje, za pierwszym razem, ale błąd jest oczywisty przy drugim spojrzeniu. Istnieje wiele przykładów tego rodzaju programowania i wszystkie one są obarczone tego rodzaju błędem, NAWET JEŚLI zdefiniujesz ciągi jako stałe (ponieważ stałe w .NET są zapisywane w manifeście każdej biblioteki, w której są używane, co musi następnie wszystkie zostaną ponownie skompilowane po zmianie zdefiniowanej wartości, aby wartość mogła zmienić się „globalnie”). Ponieważ błąd nie jest wychwytywany w czasie kompilacji, musi zostać wychwycony podczas testowania w czasie wykonywania, a jest to gwarantowane tylko przy 100% pokryciu kodu w testach jednostkowych z odpowiednim ćwiczeniem kodu, co zwykle można znaleźć tylko w najbardziej absolutnie bezpiecznym przypadku awarii , systemy czasu rzeczywistego.

KeithS
źródło
2

W rzeczywistości klasy te są nadal ściśle powiązane. Tyle że teraz są ściśle powiązane w taki sposób, że kompilator nie może powiedzieć Ci, kiedy coś się zepsuło!

Jeśli ktoś zmieni „BicycleMake” na „BicyclesMake”, to nikt nie dowie się, że wszystko jest zepsute do czasu uruchomienia.

Błędy czasu wykonania są znacznie bardziej kosztowne do naprawienia niż błędy czasu kompilacji - to tylko wszelkiego rodzaju zło.

17 z 26
źródło