Autor Wątek: Projektowanie frameworka  (Przeczytany 11278 razy)

Offline Esidar

  • Użytkownik

# Listopad 08, 2011, 23:47:26
uchwycenie wszystkeigo, raczej o to ze jednak jakis wstepny zamysl tego jak to ma wygladac powinien byc, na "code-refactor-loop" mozna sobie pozwolic jezeli piszesz cos w miare samodzielnego i czeste zmiany nie dotykaja osob ktore ewentualnie z tego kodu korzystaja
I tutaj masz 2 sprzeczności. Nie masz ogólnego zamysłu jak to powinno wygląć, jeśli nad kodem pracuje N osób. Ogólny zamysł możesz mieć jak pracujesz sam. Chyba że jesteś Leadem i narzucisz styl tworzenia kodu i sposobu łączenia elementów. Dlatego tym bardziej, jeśli pracujesz w kilka osób, kod powinien być elastyczny i odporny na code-refactor-loop i na wprowadzanie zmian. W przeciwnym razie problemy się zaczną nawarstwiać. Jeżeli masz mało powiązań, to łatwiej ci jest wprowadzać zmiany. A ich nie unikniesz.

sure, takie rady sa najlepsze, nie waznie czemu, po co i czy ma to sens - bo nie przedstawiles zadnego,
Ponieważ ta zasada dotyczy ogólnego sposobu pisania. Nie ma dowodu bezpośredniego że "dziedziczenie spowoduje miganie UI". Tu chodzi o kulturę pracy i opiera się głównie o doświadczenie. Tak jak Immilewski, miałem chwilową fazę na projektowanie i UML. Podkreślę chwilową. Nie sprawdza się, jest nie efektywna, powoduje więcej pracy itd. Od pewnego czasu stosuje też zasadę "unikaj dziedziczenia". Dzięki tej zasadzie oszczędziłem sobie wielu problemów, a kod stał się bardziej przejrzysty i uniwersalny.
Najbardziej namacalny dowód, że dziedziczenie jest niebezpieczne, jest jego uproszczenie w takich językach jak C# czy Java, w stosunku do C++. Uproszczenie ma za zadanie zminimalizować występowanie bad design zwłaszcza wśród początkujących. I porady typu "unikaj dziedziczenia" albo "przesadnego projektowania" dotyczy właśnie początkujących (stąd ten wątek). Możesz przekonywać że "dziedziczenie umiejętnie stosowane jest dobre". Ok, tyle że to nie jest ogólna zasada tylko warunkowa. I nie masz co się spierać, że "żółty śnieg czasami może być pyszny". Ogólna zasada jest żeby go nie jeść.

Offline Mr. Spam

  • Miłośnik przetworów mięsnych

Offline Kos

  • Użytkownik
    • kos.gd

# Listopad 10, 2011, 17:41:28
Przykładową pułapką, w którą wpadają domorośli entuzjaści OOP, jest sytuacja: "Mam klasę X i teraz chcę zrobić klasę Y która robi praktycznie to samo, co X, ale trochę inaczej - wydziedziczę i przeładuję sobie metodę wirtualną!". Super... nie? Nie! Bo jak taki kod się rozrasta, to rozrasta się wzwyż, zamiast wszerz, czego efektem jest urocze bagno.
Jeśli zamiast dziedziczenia wstawi się tu kompozycję, sytuacja robi się o niebo lepsza, bo wystarczy wyodrębnić część funkcjonalności klasy X do klasy A, a następnie zrobić klasę B (równorzędną klasie A i o wspólnym interfejsie) robiącą tę funkcjonalność inaczej - wtedy można robić obiekty X z członem A albo X z członem B.
Rozwiązanie to ma wielką przewagę, że skaluje się już tylko wszerz: przykładowo jeśli klasa X będzie potrzebowała takiego rozgraniczenia dla innej części funkcjonalności (np. P, Q, R), to będziemy wtedy dowolnie mogli mnożyć kombinacje X+A+P, X+B+R, itd.

Widzimy tutaj:
- jedna klasa robi jedną rzecz
- loose coupling (klasy powiązane formą asocjacji, a nie dziedziczeniem)

I to jest doskonałe wyjaśnienie, dlaczego kod staje się reusable, jeśli stosujemy kompozycję, a nie dziedziczenie.

Dziedziczenie natomiast stosuje się do wprowadzania abstrakcji, tak jak u nas: A oraz B realizują w różny sposób tę samą funkcjonalność (potrzebną X), więc mają wspólny intefejs (w C++ zaimplementowany w formie np. abstrakcyjnej klasy bazowej), a X korzysta jedynie z interfejsu. All clear.

Rzadko kiedy jest przydatne dziedziczenie po czym innym, niż suche interfejsy, podobnie jak dziedziczenie wielopoziomowe (nie mówiąc już o wielokrotnym). Nie wspominając już o tym, jak łatwo w wyższych hierarchiach złamać LSP.

Przykładowym zastosowaniem dla dziedziczenia wielopoziomowego jest takie małe ułatwienie: mając złożony interfejs ICośtam i wiedząc, że klasy dziedziczące będą prawdopodobnie implementowały część tego interfejsu w sposób podobny, "standardowy", można wprowadzić abstrakcyjną klasę AbstractCośtam i zawrzeć w niej "standardową" implementację tego interfejsu, do dowolnej modyfikacji przez klienta. Widziałem to wiele razy w kodzie Eclipsa i generalnie nie gryzło po palcach.
Ale z drugiej strony, krytycznym okiem: jeśli chcielibyśmy być gorliwi i zapewnić klientowi wiele generycznych rozwiązań, to zaraz można byłoby mnożyć: AbstractCośtam, AbstractCośtamWithFoobars, AbstractAwesomerCośtam, itd, itp... Tutaj już zdecydowanie powinna się zaświecić developerowi czerwona lampka i następna rundka refaktoringu polegałaby na przebudowaniu tej choinki na coś mniej dziedziczącego i bardziej komponującego się - w taki sposób, by całość skalowała się wszerz, a nie wzwyż.



Często mam wrażenie, że ludzie psioczą na OOP, bo widzą, jak inni ludzie go stosują w okropnie zdegenerowany sposób :(.

Offline quaikohc

  • Użytkownik

# Listopad 11, 2011, 21:09:02
domorośli entuzjaści OOP,

lol

Często mam wrażenie, że ludzie psioczą na OOP, bo widzą, jak inni ludzie go stosują w okropnie zdegenerowany sposób :(.

ostatnio - czesciej chyba dlatego ze przeczytali jakie DOD jest trendy i cool i "the only right way"


Offline rm-f

  • Użytkownik
    • Tu trolluje

# Listopad 11, 2011, 21:16:09
Mam dla was propozycje, po prostu wybierzmy jakiś mały kawałek kodu z gry i zaimplementujmy (znaczy implementacja interfejsu) je po swojemu. Porównamy, nastepnie wybierze się jakąś zmianę do wprowadzenia, wprowadzimy i zobaczymy kto się bardziej napracował.

To jak? :)


Proponuje, interfejs do rysowania w 2D.
Wymagania do ustalenia.
« Ostatnia zmiana: Listopad 11, 2011, 21:19:03 wysłana przez świrus »

Offline Kos

  • Użytkownik
    • kos.gd

# Listopad 11, 2011, 22:06:13
ostatnio - czesciej chyba dlatego ze przeczytali jakie DOD jest trendy i cool i "the only right way"

A mieli gdzie, bo dopiero co pół Warsztatu robiło prezentacje/artykuły/blogposty o DOD :) Fajne były zwykle, ale jak komuś się tam z rozpędu załączył "porównawczy" rant o OOP, to aż się zwijałem na krześle, patrząc na te abominacje, które były tam przedstawione. Coś jak kapitalizm w czerwonych książkach :)

DOD fajna rzecz, ale imho nie jest wcale czymś przeciwnym OOPowi.. Zaryzykowałbym stwierdzenie, że prostopadły. Po prostu podczas designu myśli się o sprzęcie, przez co stosuje mniej abstrakcji. Tyle.

Offline yarpen

  • Użytkownik

# Listopad 11, 2011, 22:12:01

Offline mihu

  • Użytkownik
    • mihu

# Listopad 11, 2011, 23:48:10
Random thought: ludzie z branży mający w niej już jakieś poważanie strasznie psioczą na twitterze na OOD.

Random thought 2: przeglądając pobieżnie źródła CryEngine byłem zaskoczony widząc wielkie klasy z dziesiątkami funkcji wirtualnych. Choć lowlevelowego renderera zapewne to już nie dotyczy.

Offline mihu

  • Użytkownik
    • mihu

# Listopad 12, 2011, 00:10:32
I jeszcze pytanko:
Sporo ostatnio jest podejść do pisania mniejszych gier w mniej klasycznych środowiskach, np C# + XNA. Mimo tego, że jeśli zależy nam na maksymalnej wydajności, należałoby się pewnie zwrócić z powrotem do C++, można zapewne znaleźć sytuację przy pisaniu w C#, kiedy idee pochodzące z DOD warto by zastosować. Nie znam się na bebechach i runtimie .NET i ogólnie interpretowanych/JIT-owanych języków i zastanawia mnie, jak w nich należałoby podejść do "DOD-owania"? Z jednej strony narzuca się od razu C#-owy struct, tak aby tablica obiektów była ciągła w pamięci. Z drugiej, czytałem kiedyś tekst Shawna Hargreavesa (nie mogę go teraz znaleźć), w którym zalecał programistom przesiadającym się z C++ na C# zapomnieć na pewien czas w ogóle o struct, bo mają tendencję do nadużywania go z pokutującym z C++ mindsetem, który jednak dla C# jest zupełnie nieodpowiedni. Szczegółów nie pamiętam.
Co wy na to? Na co jeszcze należałoby zwrócić uwagę, aby pisząc w takim języku jak C# zaczerpnąć chociaż po części z korzyści jakie może przynieść DOD?

Offline yarpen

  • Użytkownik

# Listopad 12, 2011, 01:33:08

Random thought 2: przeglądając pobieżnie źródła CryEngine byłem zaskoczony widząc wielkie klasy z dziesiątkami funkcji wirtualnych. Choć lowlevelowego renderera zapewne to już nie dotyczy.
Zauwaz, ze wiekszosc fkcji wirtualnych jest opakowana w jakies makro. Podejrzewam, ze zapuszczaja jakies narzedzie, ktore generuje finalne naglowki w zaleznosci od platformy (bez virtual)

Offline Kos

  • Użytkownik
    • kos.gd

# Listopad 12, 2011, 17:37:26
PKE sie nie zgadza :)
https://plus.google.com/115332336196774795982/posts/9L1HaA3NuZS
Uch. Yarpen, znowu mam ochotę wynająć ninja, by Ci się włamali do domowego komputera i wykradli listę bookmarków z przeglądarki :D Co wrzucisz, to dobre. Powinno się oznaczać linki "znaczkiem jakości Yarpen". +1

Offline Esidar

  • Użytkownik

# Listopad 12, 2011, 17:53:02
w którym zalecał programistom przesiadającym się z C++ na C# zapomnieć na pewien czas w ogóle o struct, bo mają tendencję do nadużywania go z pokutującym z C++ mindsetem
Struct w C# działa jak wartość. To znaczy że mając macierz "struct Matrix" w której jest 16 floatów (64 bajtów), i chcąc ją przekazać do funkcji, następuje kopiowanie całej macierzy na stos. Łatwo wtedy o problemy z wydajnością oraz z błędami. Class i struct nie wolno stosować zamiennie, trzeba wcześniej o tym pomyśleć. Class jest przekazywany przez referencję, a w przypadku struct najlepiej wtedy tą referencję ręcznie ustawiać ("ref").  Do tego struct jest nie wygodny w używaniu przy genericach. Łatwo o boxowanie - dodatkową alokację przy niewinnej operacji. Niektóre operacje są zabronione przy struct.

C# nie ma zbytnich ułatwień jeśli chodzi o DOD (tzn. w tej chwili nic mi nie przychodzi do głowy), ale ułatwia zmniejszać OOP (przesadne dziedziczenie) co z kolei powoduje, że kod jest bardziej przejrzysty, mniejszy i ma strukturę bardziej zbliżoną do DOD. Po prostu tak jak C++/native, tak i C# trzeba się nauczyć.

Offline Esidar

  • Użytkownik

# Listopad 12, 2011, 18:30:12
Proponuje, interfejs do rysowania w 2D.
Nie przesadzajmy nikomu się nie będzie chciało ;) Ale moje UI to akurat przykład zmniejszenia OOP. Mam klasę bazową Primitive, która służy do obsługi większości kontrolek. Dziedziczę z niej różne prymitywy w stylu RectanglePrimitive, TextPrimitive, ImagePrimitive itd. Ale nie tworzę klas w stylu Button, List, TextBox itd, zamiast tego mam osobne klasy dziedziczące z Action. Podstawowa funkcjonalność wygląda tak:
class Primitive
{
public:
     Action    _action;
     virtual void Draw()
     {
           if( _action != null )
                _action.OnDraw();
     }
     virtual void OnSelect() // wywołane gdy user wciśnie LButton myszki na primitive.
     {
           if( _action != null )
                _action.OnSelect();
     }
};

class Action
{
     virtual void OnDraw()
     {
     }
     virtual void OnSelect()
     {
     }
}
Oczywiście jest to bardziej rozbudowane, ale to pokazuje prosty podział. Zalety tego są duże. Jeśli chcę zrobić np. Button, to wygląda to tak (C#):

    Primitive window;
    window.AddChild( new TemplatePrimitive() { _template = UISkin._button, _action = new SetText( "New Game" ) { _nextAction = new Action() { _select = OnNewGameButton } } } );
Tworzę TemplatePrimitive (to jest primitive który rysuje drzewo innych prymitywów z podanego template'u, a następnie jest chain-of-responsibility. Jeden obiekt ustawia napis na przycisku, drugi wywołuje metodę, na wciśnięcie.
Kwestia wyjaśnienia. SetText jest wywoływane co ramkę, czyli tekst jest ustawiany co ramkę. Wydajnościowo praktycznie nie wpływa na kod, za to pozwala na dynamiczne zmiany UI. Zamiast generycznego SetText mogę wtedy napisać specjalną metodę:
    window.AddChild( new TemplatePrimitive() { _template = UISkin._button, _action = new Action() { _draw = OnDrawNewGameButton } } } );

public void OnDrawNewGameButton()
{
     if( saveExists == true )
         _dynamicText.Set( "Continue" );
     else
         _dynamicText.Set( "New Game" );
}
Unikam wtedy dodatkowego kodu który zmienia stan kontrolek w zależności od akcji które się dzieją w silniku.

Druga kwestia, to zamiast eventów które są używane w klasycznych UI, stosuję obiekty. Daje to dodatkową możliwość bo w klasie można zapamiętać dodatkowe dane, jak w przypadku SetText, który zapamiętuje tekst który ma przypisać do kontrolki.

Trochę mi czasu zajęło dopracowanie tego UI i przystosowanie się do jego filozofii... Nie mniej, nie żałuję i jest ona znacznie wygodniejsza niż klasyczne UI oparte o OOP i eventy. Mniej kodu, mniej klas, mniej specjalizowanego kodu.

Offline rm-f

  • Użytkownik
    • Tu trolluje

# Listopad 12, 2011, 18:44:54
Dziwnie to zabrzmi, ale ni cholery tego nie zrozumiałem.

Offline vashpan

  • Użytkownik
    • Strona

# Listopad 12, 2011, 19:49:27
Hmm, czy ja wiem. Tez nie widze jakiegos wielkiego zysku. Za to bardzo niejasny kod. Ale moze sie nie znam....

window.AddChild( new TemplatePrimitive() { _template = UISkin._button, _action = new SetText( "New Game" ) { _nextAction = new Action() { _select = OnNewGameButton } } } );

Hmmm, ja wole chyba naprawde zrobic
Button button = new Button();

A napis tez mozna zmieniac co klatke, nie widze w sumie przeciwskazan ?

Offline Esidar

  • Użytkownik

# Listopad 12, 2011, 20:51:08
Hmmm, ja wole chyba naprawde zrobic
Button button = new Button();

Jeśli chcesz mieć równoważny kod, to musisz:
Button button = new Button();
button.Text = "New Game";
button.Skin = UISkin._button;
button.OnLButton = OnNewGameButton;
_window.Add( button );

A napis tez mozna zmieniac co klatke, nie widze w sumie przeciwskazan ?
Czyli musimy zmodyfikować kod:
Button button = new Button();
button.Text = OnChangeDynamicText;
button.Skin = UISkin._button;
button.OnLButton = OnNewGameButton;
_window.Add( button );

Coś więc zyskałeś pisząc to "po staremu" ? Czy po prostu uzyskasz to samo ale z innymi nazwami ? A co z rozszerzalnością ? W tym przykładzie musiałem zmienić typ zmiennej Text ze "string" na "method". No ale przecież nie zawsze chcesz mieć metodę, czasami chcesz mieć sam string. Jak to rozwiążesz ?

Jak napisałem, mi również zajęło trochę czasu aby się dostosować do tej filozofii :)  Różnica jest w konstrukcji całego modelu, a nie w tym że "1 linijka wygląda inaczej". To co zyskałeś w tej linijce "stracisz" w pozostałych.
Ja spędziłem sporo czasu na pisaniu w systemach typu MFC, wxWidgets, XAML i tym podobnych. Wszystkie powielają te same schematy i te same problemy. Albo się godzisz na słabe rozwiązania, albo stwierdzasz że "da się to zrobić lepiej".