Jaka jest najlepsza praktyka, jeśli chodzi o pisanie klas, które mogą wymagać wiedzy o interfejsie użytkownika. Czy klasa, która wie, jak się rysować, nie łamie niektórych dobrych praktyk, ponieważ zależy to od interfejsu użytkownika (konsola, GUI itp.)?
W wielu książkach o programowaniu natrafiłem na przykład „Shape”, który pokazuje dziedziczenie. Kształt klasy bazowej ma metodę draw (), która zastępuje każdy kształt, taki jak okrąg i kwadrat. Pozwala to na polimorfizm. Ale czy metoda draw () nie zależy w dużym stopniu od interfejsu użytkownika? Jeśli napiszemy tę klasę, na przykład Win Forms, nie będziemy mogli jej ponownie użyć w aplikacji na konsolę lub aplikacji internetowej. Czy to jest poprawne?
Powodem tego pytania jest to, że zawsze utknąłem i rozłączałem się, jak uogólniać klasy, aby były najbardziej przydatne. To działa przeciwko mnie i zastanawiam się, czy „nie staram się zbyt mocno”.
Shape
zajęcia, prawdopodobnie piszesz sam stos graficzny, a nie piszesz klienta do stosu graficznego.Odpowiedzi:
To zależy od klasy i przypadku użycia. Element wizualny, który wie, jak się rysować, niekoniecznie stanowi naruszenie zasady pojedynczej odpowiedzialności.
Znów niekoniecznie. Jeśli możesz stworzyć interfejs (drawPoint, drawLine, ustawić Kolor itp.), Możesz przekazać dowolny kontekst do rysowania rzeczy na czymś do kształtu, na przykład w konstruktorze kształtu. Umożliwiłoby to rysowanie kształtów na konsoli lub dowolnym podanym płótnie.
Cóż, to prawda. Jeśli napiszesz UserControl (ogólnie nie klasę) dla Windows Forms, nie będziesz mógł używać go z konsolą. Ale to nie jest problem. Dlaczego miałbyś oczekiwać, że UserControl for Windows Forms będzie działać z dowolną prezentacją? UserControl powinien zrobić jedną rzecz i zrobić to dobrze. Z definicji wiąże się to z pewną formą prezentacji. W końcu użytkownik potrzebuje czegoś konkretnego, a nie abstrakcji. Może to częściowo dotyczyć tylko frameworków, ale tak jest w przypadku aplikacji użytkowników końcowych.
Jednak logika, która się za tym kryje, powinna zostać oddzielona, aby można było użyć jej ponownie z innymi technologiami prezentacji. W razie potrzeby wprowadź interfejsy, aby zachować ortogonalność aplikacji. Ogólna zasada brzmi: konkretne rzeczy powinny być wymienialne na inne konkretne rzeczy.
Wiesz, ekstremalni programiści lubią swoje podejście do YAGNI . Nie próbuj pisać wszystkiego ogólnie i nie próbuj zbytnio robić wszystkiego ogólnego. Nazywa się to nadmierną inżynierią i ostatecznie doprowadzi do całkowicie zawiłego kodu. Daj każdemu komponentowi dokładnie jedno zadanie i upewnij się, że robi to dobrze. Umieszczaj abstrakcje tam, gdzie to konieczne, tam, gdzie oczekujesz, że coś się zmieni (np. Interfejs do rysowania kontekstu, jak podano powyżej).
Zasadniczo pisząc aplikacje biznesowe, należy zawsze starać się rozdzielić rzeczy. MVC i MVVM doskonale nadają się do oddzielenia logiki od prezentacji, dzięki czemu można jej ponownie użyć do prezentacji internetowej lub aplikacji konsolowej. Pamiętaj, że ostatecznie pewne rzeczy muszą być konkretne. Twoi użytkownicy nie mogą pracować z abstrakcją, potrzebują czegoś konkretnego. Abstrakcje są tylko dla ciebie, programisty, pomocą w utrzymywaniu kodu w rozszerzalności i utrzymywaniu. Musisz zastanowić się, gdzie potrzebujesz elastyczności kodu. Ostatecznie wszystkie abstrakcje muszą rodzić coś konkretnego.
Edycja: jeśli chcesz przeczytać więcej o architekturze i technikach projektowania, które mogą zapewnić najlepsze praktyki, sugeruję przeczytanie odpowiedzi @Catchops i przeczytanie o praktykach SOLID na Wikipedii.
Ponadto na początek zawsze polecam następującą książkę: Head First Design Patterns . Pomoże Ci zrozumieć techniki abstrakcyjne / praktyki projektowania OOP, bardziej niż książkę GoF (która jest doskonała, po prostu nie pasuje początkującym).
źródło
Masz zdecydowanie rację. I nie, „próbujesz dobrze” :)
Przeczytaj o zasadzie pojedynczej odpowiedzialności
Twoje wewnętrzne działanie klasy i sposób, w jaki te informacje powinny być prezentowane użytkownikowi, to dwa obowiązki.
Nie bój się rozdzielić klas. Rzadko problemem jest zbyt duża abstrakcja i oddzielenie :)
Dwa bardzo istotne wzorce to Model-widok-kontroler dla aplikacji internetowych i Model View ViewModel dla Silverlight / WPF.
źródło
Często korzystam z MVVM i moim zdaniem klasa obiektów biznesowych nigdy nie powinna wiedzieć o interfejsie użytkownika. Pewnie mogą potrzebować znać
SelectedItem
,,IsChecked
lubIsVisible
, itd., Ale te wartości nie muszą wiązać się z żadnym konkretnym interfejsem użytkownika i mogą być ogólnymi właściwościami w klasie.Jeśli musisz coś zrobić z interfejsem w kodzie z tyłu, na przykład ustawić Fokus, uruchomić animację, obsługi skrótów klawiszowych itp., Kod powinien być częścią kodowania interfejsu użytkownika, a nie klas logiki biznesowej.
Powiedziałbym więc, że nie przestawajcie rozdzielać interfejsu użytkownika i klas. Im bardziej są oddzielone, tym łatwiej jest je utrzymywać i testować.
źródło
Istnieje wiele wypróbowanych i prawdziwych wzorów projektowych, które zostały opracowane przez lata, aby dokładnie odpowiedzieć na to, o czym mówisz. Inne odpowiedzi na twoje pytanie odwoływały się do zasady pojedynczej odpowiedzialności - która jest absolutnie ważna - i tego, co wydaje się być motorem twojego pytania. Ta zasada mówi po prostu, że klasa musi zrobić jedną DOBRĄ. Innymi słowy, podnoszenie spójności i obniżanie sprzężenia, na tym właśnie polega dobry projekt obiektowy - czy klasa robi jedną rzecz dobrze i nie ma wielu zależności od innych.
Cóż ... masz rację, obserwując, że jeśli chcesz narysować okrąg na iPhonie, będzie inaczej niż narysowanie koła na komputerze z systemem Windows. MUSISZ mieć (w tym przypadku) konkretną klasę, która rysuje jedną studnię na iPhonie, a drugą, która rysuje jedną studnię na komputerze PC. To tutaj załamuje się podstawowa dziedziczenie OO, które rozkładają wszystkie przykłady kształtów. Po prostu nie można tego zrobić tylko z dziedziczeniem.
W tym miejscu pojawiają się interfejsy - jak stwierdza Gang of Four (MUSI CZYTAĆ) - Zawsze faworyzuj implementację zamiast dziedziczenia. Innymi słowy, użyj interfejsów, aby połączyć architekturę, która może wykonywać różne funkcje na wiele sposobów, bez polegania na zależnościach zakodowanych na stałe.
Widziałem odniesienie do zasad SOLID . Te są świetne. „S” to zasada pojedynczej odpowiedzialności. ALE „D” oznacza odwrócenie zależności. Można tu zastosować wzorzec Inwersji Kontroli (Wstrzykiwanie Zależności). Jest bardzo wydajny i może służyć do odpowiedzi na pytanie, jak zaprojektować system, który może narysować okrąg dla iPhone'a, a także dla PC.
Możliwe jest stworzenie architektury, która zawiera wspólne reguły biznesowe i dostęp do danych, ale ma różne implementacje interfejsów użytkownika za pomocą tych konstrukcji. Naprawdę jednak pomaga być w zespole, który go wdrożył i zobaczyć, jak działa, aby naprawdę to zrozumieć.
To jest tylko szybka odpowiedź wysokiego poziomu na pytanie, która zasługuje na bardziej szczegółową odpowiedź. Zachęcam do głębszego przyjrzenia się tym wzorom. Można znaleźć bardziej konkretną implementację tych wzorów, jak również znane nazwy MVC i MVVM.
Powodzenia!
źródło
W takim przypadku nadal możesz używać MVC / MVVM i wstrzykiwać różne implementacje interfejsu użytkownika za pomocą wspólnego interfejsu:
W ten sposób będziesz mógł ponownie użyć logiki kontrolera i modelu, jednocześnie dodając nowe typy GUI.
źródło
Można to zrobić na różne sposoby: MVP, MVC, MVVM itp.
Miły artykuł autorstwa Martina Fowlera (duża nazwa) do przeczytania to GUI Architectures: http://www.martinfowler.com/eaaDev/uiArchs.html
MVP nie zostało jeszcze wspomniane, ale zdecydowanie zasługuje na cytowanie: spójrz na to.
Jest to wzorzec sugerowany przez twórców Google Web Toolkit do użycia, jest naprawdę schludny.
Możesz znaleźć prawdziwy kod, prawdziwe przykłady i uzasadnienie, dlaczego takie podejście jest przydatne tutaj:
http://code.google.com/webtoolkit/articles/mvp-architecture.html
http://code.google.com/webtoolkit/articles/mvp-architecture-2.html
Jedną z zalet stosowania tego lub podobnych podejść, które nie zostało wystarczająco podkreślone, jest możliwość przetestowania! W wielu przypadkach powiedziałbym, że to główna zaleta!
źródło
Jest to jedno z miejsc, w których OOP nie wykonuje dobrej roboty przy abstrakcji. Polimorfizm OOP wykorzystuje dynamiczne wysyłanie do pojedynczej zmiennej („this”). Jeśli polimorfizm został zrootowany w Shape, nie można wysyłać polimorficznych sojuszników do renderera (konsoli, GUI itp.).
Rozważ system programowania, który mógłby wywołać dwie lub więcej zmiennych:
a także załóżmy, że system może dać ci sposób wyrażenia poly_draw dla różnych kombinacji typów kształtów i typów renderujących. W takim razie łatwo byłoby wymyślić klasyfikację kształtów i rendererów, prawda? Sprawdzanie typów w jakiś sposób pomoże ci dowiedzieć się, czy istnieją kombinacje kształtu i renderera, których mogłeś przegapić.
Większość języków OOP nie obsługuje niczego takiego jak wyżej (kilka obsługuje, ale nie są one głównym nurtem). Sugeruję, abyś obejrzał wzór Odwiedzającego, aby obejść to obejście.
źródło
Powyższe dźwięki są dla mnie poprawne. Za moim rozumieniem, można powiedzieć, to znaczy stosunkowo szczelne połączenie pomiędzy kontrolerem i widokiem w zakresie projektowania wzorca MVC. Oznacza to również, że aby przełączać się między aplikacją desktop-konsola-web, trzeba odpowiednio zmienić zarówno kontroler, jak i widok jako parę - tylko model pozostaje niezmieniony.
Cóż, moje obecne stanowisko jest takie, że to złącze View-Controller , o którym mówimy, jest OK, a co więcej, jest raczej modne w nowoczesnym designie.
Jednak rok lub dwa lata temu czułem się niepewnie. Zmieniłem zdanie po przestudiowaniu dyskusji na forum Sun na temat wzorów i projektowania OO.
Jeśli jesteś zainteresowany, wypróbuj to forum samodzielnie - migrowało ono teraz do Oracle ( link ). Jeśli tam dotrzesz, spróbuj pingować faceta Saisha - wtedy jego wyjaśnienia tych trudnych spraw okazały się dla mnie najbardziej pomocne. Nie wiem jednak, czy nadal uczestniczy - sam nie byłem tam od dłuższego czasu
źródło
Z pragmatycznego punktu widzenia, część kodu w twoim systemie musi wiedzieć, jak narysować coś takiego,
Rectangle
jeśli jest to wymóg użytkownika. A to w pewnym momencie sprowadza się do robienia naprawdę niskich poziomów, takich jak rasteryzacja pikseli lub wyświetlanie czegoś w konsoli.Pytanie z punktu widzenia sprzężenia brzmi: kto / co powinien zależeć od tego rodzaju informacji i do jakiego stopnia szczegółowości (na przykład jak abstrakcyjny)?
Wyodrębnianie możliwości rysowania / renderowania
Ponieważ jeśli kod rysunkowy wyższego poziomu zależy tylko od czegoś bardzo abstrakcyjnego, abstrakcja ta może być w stanie działać (poprzez podstawienie konkretnych implementacji) na wszystkich platformach, na które zamierzasz celować. Jako wymyślony przykład, niektóre bardzo abstrakcyjne
IDrawer
interfejsy mogą być zaimplementowane zarówno w interfejsach API konsoli, jak i GUI, aby wykonywać takie czynności, jak kształty wydruku (implementacja konsoli może traktować konsolę jak jakiś „obraz” 80xN ze sztuką ASCII). Oczywiście jest to wymyślony przykład, ponieważ zwykle nie jest to, co chcesz zrobić, to traktować wyjście konsoli jak bufor obrazu / ramki; zazwyczaj większość użytkowników końcowych potrzebuje więcej interakcji tekstowych w konsolach.Innym zagadnieniem jest to, jak łatwo zaprojektować stabilną abstrakcję? Ponieważ może być łatwe, jeśli wszystkim, na co celujesz, są nowoczesne interfejsy API GUI, aby wyodrębnić podstawowe możliwości rysowania kształtów, takie jak kreślenie linii, prostokątów, ścieżek, tekstu, tego rodzaju rzeczy (po prostu prosta rasteryzacja 2D ograniczonego zestawu prymitywów) , z jednym abstrakcyjnym interfejsem, który można łatwo zaimplementować za pomocą różnych podtypów przy niewielkich kosztach. Jeśli potrafisz skutecznie zaprojektować taką abstrakcję i wdrożyć ją na wszystkich platformach docelowych, to powiedziałbym, że jest to znacznie mniejsze zło, a nawet w ogóle zło, dla kształtu lub kontroli GUI, lub czegokolwiek, aby wiedzieć, jak narysować się za pomocą takiego abstrakcja.
Ale powiedz, że próbujesz pozbyć się krwawych szczegółów, które różnią się między Playstation Portable, iPhone'em, XBox One i potężnym komputerem do gier, podczas gdy twoje potrzeby to wykorzystanie najnowocześniejszych technik renderowania / cieniowania 3D w czasie rzeczywistym . W takim przypadku próba opracowania jednego abstrakcyjnego interfejsu w celu wyodrębnienia szczegółów renderowania, gdy podstawowe możliwości sprzętowe i interfejsy API różnią się tak bardzo, że niemal na pewno doprowadzi do ogromnego czasu projektowania i przeprojektowania, wysokie prawdopodobieństwo powtarzających się zmian w projekcie z nieoczekiwanymi odkrycia, a także rozwiązanie o najniższym wspólnym mianowniku, które nie wykorzystuje pełnej wyjątkowości i mocy podstawowego sprzętu.
Dokonywanie zależności w kierunku stabilnych, „łatwych” projektów
W mojej dziedzinie jestem w tym ostatnim scenariuszu. Celujemy w wiele różnych urządzeń o radykalnie różnych podstawowych możliwościach i interfejsach API, a próba wymyślenia jednej abstrakcji renderowania / rysowania, aby rządzić nimi wszystkimi, jest beznadziejna bez granic (możemy stać się sławni na całym świecie, robiąc to skutecznie, ponieważ byłaby to gra zmieniacz w branży). Więc ostatnią rzeczą, chcę w moim przypadku jest podobnie jak analogiczne
Shape
lubModel
lubParticle Emitter
, że nie wie, jak zwrócić się, nawet jeśli to jest wyrażanie że rysunek na najwyższym poziomie i najbardziej abstrakcyjny sposób możliwe ...... ponieważ te abstrakcje są zbyt trudne do prawidłowego zaprojektowania, a kiedy projekt jest trudny do poprawienia, a wszystko zależy od niego, jest to przepis na najbardziej kosztowne centralne zmiany projektu, które marszczą i niszczą wszystko w zależności od niego. Ostatnią rzeczą, jakiej chcesz, jest to, aby zależności w twoich systemach płynęły w kierunku abstrakcyjnych projektów zbyt trudnych do poprawienia (zbyt trudnych do ustabilizowania bez ingerencji w zmiany).
Trudne zależy od łatwego, a nie łatwe zależy od trudnego
Zamiast tego sprawiamy, że zależności płyną w kierunku rzeczy, które można łatwo zaprojektować. O wiele łatwiej jest zaprojektować abstrakcyjny „Model”, który koncentruje się na przechowywaniu rzeczy takich jak wielokąty i materiały i uzyskać poprawność tego projektu, niż zaprojektować abstrakcyjny „Renderer”, który można skutecznie wdrożyć (poprzez zastępowalne podtypy betonu) do rysowania usługi żądania jednolicie dla sprzętu tak odmiennego jak PSP z komputera.
Dlatego odwracamy zależności od rzeczy, które są trudne do zaprojektowania. Zamiast sprawić, by modele abstrakcyjne wiedzieli, jak przyciągnąć się do projektu mechanizmu renderującego abstrakcję, od którego wszystkie są zależne (i przerwać ich implementacje, jeśli ten projekt się zmieni), zamiast tego mamy mechanizm renderujący abstrakcję, który wie, jak narysować każdy obiekt abstrakcyjny w naszej scenie ( modeli, emiterów cząstek itp.), dzięki czemu możemy następnie zaimplementować podtyp renderujący OpenGL dla komputerów typu PC
RendererGl
, inny dla systemów PSPRendererPsp
, inny dla telefonów komórkowych itp. W takim przypadku zależności płyną w kierunku stabilnych projektów, łatwych do poprawienia, od renderera do różnego rodzaju bytów (modeli, cząstek, tekstur itp.) w naszej scenie, a nie na odwrót.Jeśli próbujesz wyodrębnić coś na centralnym poziomie bazy kodu i jest to zbyt trudne do zaprojektowania, zamiast uparcie uderzać głową o ściany i ciągle wprowadzać w tym uciążliwe zmiany każdego miesiąca / roku, co wymaga aktualizacji 8000 plików źródłowych, ponieważ jest to rozbijając wszystko, co od tego zależy, moją pierwszą sugestią jest rozważenie odwrócenia zależności. Sprawdź, czy możesz napisać kod w taki sposób, że rzecz, która jest tak trudna do zaprojektowania, zależy od wszystkiego, co jest łatwiejsze do zaprojektowania, bez posiadania rzeczy, które są łatwiejsze do zaprojektowania, w zależności od rzeczy, które są tak trudne do zaprojektowania. Pamiętaj, że mówię o projektach (w szczególności projektach interfejsów), a nie o implementacjach: czasami rzeczy są łatwe do zaprojektowania i trudne do wdrożenia, a czasem rzeczy są trudne do zaprojektowania, ale łatwe do wdrożenia. Zależności płyną w kierunku projektów, więc należy skupić się tylko na tym, jak trudno jest tutaj zaprojektować coś, aby określić kierunek przepływu zależności.
Zasada pojedynczej odpowiedzialności
Dla mnie SRP nie jest tu zwykle tak interesujące (choć w zależności od kontekstu). Mam na myśli balansowanie na linie podczas projektowania rzeczy, które są jasne w celu i łatwe do utrzymania, ale twoje
Shape
obiekty mogą musieć ujawnić bardziej szczegółowe informacje, jeśli na przykład nie wiedzą, jak się rysować, i może nie być wiele znaczących rzeczy do zrobić z kształtem w określonym kontekście użytkowania, niż skonstruować go i narysować. Występują kompromisy z prawie wszystkim i nie jest to związane z SRP, które mogą uświadomić, jak wyciągnąć umiejętność stania się koszmarem konserwacyjnym z mojego doświadczenia w pewnych kontekstach.Ma on znacznie więcej wspólnego ze sprzężeniem i kierunkiem, w którym zależności płyną w twoim systemie. Jeśli próbujesz przenieść abstrakcyjny interfejs renderowania, od którego wszystko zależy (ponieważ używają go do rysowania), do nowego docelowego interfejsu API / sprzętu i zdajesz sobie sprawę, że musisz znacznie zmienić jego projekt, aby działał tam efektywnie, to jest to bardzo kosztowna zmiana, która wymaga zastąpienia implementacji wszystkiego w twoim systemie, który umie się rysować. I to jest najbardziej praktyczny problem konserwacyjny, jaki napotykam, mając świadomość tego, jak się rysować, jeśli przekłada się to na mnóstwo zależności płynących w kierunku abstrakcji, które są zbyt trudne do prawidłowego zaprojektowania z góry.
Duma programisty
Wspominam o tym jednym punkcie, ponieważ z mojego doświadczenia wynika, że często jest to największa przeszkoda w kierowaniu zależności w kierunku rzeczy łatwiejszych do zaprojektowania. Deweloperom bardzo łatwo jest tutaj podchodzić nieco bardziej ambitnie i powiedzieć: „Zaprojektuję abstrakcyjną platformę renderowania, by rządzić nimi wszystkimi, rozwiążę to, co inni programiści spędzają miesiącami na portowaniu, i dostanę to dobrze i będzie działać jak magia na każdej obsługiwanej przez nas platformie i wykorzystywać najnowocześniejsze techniki renderowania na każdej z nich; już sobie to wyobrażałem ”.W takim przypadku opierają się praktycznemu rozwiązaniu, które polega na uniknięciu tego i po prostu odwracają kierunek zależności i tłumaczą to, co może być niezwykle kosztowne i powtarzające się zmiany w projekcie centralnym, na tanie i lokalne powtarzające się zmiany we wdrażaniu. W programistach musi istnieć instynkt „białej flagi”, aby się poddać, gdy coś jest zbyt trudne do zaprojektowania na tak abstrakcyjnym poziomie i ponownie przemyśleć całą swoją strategię, w przeciwnym razie czeka ich wiele cierpienia i bólu. Sugerowałbym przeniesienie takich ambicji i ducha walki na najnowocześniejsze implementacje łatwiejszej do zaprojektowania niż przeniesienie takich ambicji podbijających świat na poziom projektowania interfejsu.
źródło
OpenGLBillboard
, zrobiłbyś taki,OpenGLRenderer
który umie narysować dowolny typIBillBoard
? Ale czy zrobiłby to poprzez przekazanie logikiIBillBoard
, czy też miałby ogromne przełączniki lubIBillBoard
typy z warunkami warunkowymi? Trudno mi to pojąć, ponieważ nie wydaje się to w ogóle możliwe do utrzymania!RendererPsp
punkt, który zna abstrakcje na wysokim poziomie Twojej sceny gry, może wykonać całą magię i wykonać backflipy potrzebne do renderowania takich rzeczy w sposób, który wygląda przekonująco na PSP ....BillBoard
od nich, byłoby naprawdę trudne? Podczas gdyIRenderer
poziom a jest już wysoki i może zależeć od tych problemów o wiele mniej problemów?