Wiele podklas, czy jedna z nullowalnymi właściwościami?

Wiele podklas, czy jedna z nullowalnymi właściwościami?
MA
  • Rejestracja:ponad 5 lat
  • Ostatnio:około 10 godzin
  • Postów:20
0

Hej, taki problem..
Mam do zamodelowania różne rodzaje zagrywek (z koszykówki)- rozkład ich właściwości wygląda mniej więcej tak:
klasy.png
W sumie jest 11 'podtypów' zagrywek, pięć z nich nie potrzebują innych właściwości, reszta tak - z czego 4 używają jednej wspólnej (ilość pkt), a żadna nie potrzebuje wielu.
Waham się pomiędzy dwoma rozwiązaniami, z których żadne mi się nie podoba:

  1. Klasa Play - mająca wszystkie propsy oraz Enuma z rodzajem zagrywki- tutaj problemem (?) jest duża ilość nulli.
  2. Abstrakcyjna klasa Play, mająca 11(!) podklas - przy tym rozwiązaniu będę się pewnie musiał potem paprać z rozpoznawaniem dynamicznym jaka klasa akurat przyszła (do wyliczania statystyk itp.) (EDIT: albo i tak zostawić propsa z typem klasy do rozpoznawania ale to też jakieś takie brzydkie? )
    Skłaniam się ku opcji nr. 1, ale bolą mnie te nulle. Może jakieś inne (lepsze) podejście?
edytowany 2x, ostatnio: Magiczny
AF
To jest bardzo podobne do problemu bazodanowego. Szukaj "table per hierarchy", "table per class", "class per hierarchy".
maszrum
  • Rejestracja:około 5 lat
  • Ostatnio:12 miesięcy
  • Lokalizacja:Kraków
  • Postów:219
2

Podejrzem, że tego problemu nie da się rozwiązać bez odpowiedzi na pytanie:
Po co ci właściwie ta abstrakcja nad klasami zagrywek? W jaki sposób będziesz na nich operował? Czy na pewno potrzebujesz klasy ZagrywkaBase która w przyszłości będzie sprawiała tylko problemy, nie przynosząc żadnej korzyści?

SZ
  • Rejestracja:prawie 11 lat
  • Ostatnio:około 3 godziny
  • Postów:1474
2

Jest jeszcze opcja 3.
Robisz jedną klasę, która ma w sobie Dictionary<string,object> co stanowi listę twoich właściwości co w sumie stanowiło by taką kompozycje (Composition Over Inheritance).

Zobacz pozostałe 2 komentarze
S9
@WeiXiao: teraz rozumiem czemu @somekind z Ciebie się nabija.
WeiXiao
tak, też uważam że zazdrość jest czymś okropnym :P
AK
@Miang: jak nie zgadzam się z religijnym przymusem wzorców, tak i z bezwarunkowym szafowaniem "antywzorcami".
AK
@szydlak: albo Dictionary<MyEnum,object>, lub zamiast object typ liczbowy
somekind
EAV to nie jest żaden antywzorzec tylko sposób na przechowywanie dynamicznych struktur danych w bazie relacyjnej. Alternatywą jest walenie xml/json w kolumny, co niekoniecznie zawsze będzie lepsze.
somekind
Moderator
  • Rejestracja:około 17 lat
  • Ostatnio:42 minuty
  • Lokalizacja:Wrocław
1
szydlak napisał(a):

Jest jeszcze opcja 3.

Robisz jedną klasę, która ma w sobie Dictionary<string,object> co stanowi listę twoich właściwości co w sumie stanowiło by taką kompozycje (Composition Over Inheritance).

No to by było dobre rozwiązanie, gdyby istniała potrzeba dynamicznej zmiany listy właściwości podczas pracy programu. Jeśli takiej nie ma, to lepiej wybrać rozwiązanie, które się kompiluje.

@Magiczny - co to za klasy i gdzie ich używasz? Jakieś DTO jakiegoś API? Przynajmniej tak brzmi to, co napisałeś: przy tym rozwiązaniu będę się pewnie musiał potem paprać z rozpoznawaniem dynamicznym jaka klasa akurat przyszła.

QU
  • Rejestracja:prawie 11 lat
  • Ostatnio:ponad rok
  • Postów:15
0

Możesz zrobić jedną klasę bazową (nie abstrakcyjną), która będzie zawierać pola z kolumny Universal oraz właściwość (Enum) jaki to rodzaj rozgrywki. Następnie tworzysz nowe klasy (dziedziczące po bazowej) tylko dla rozgrywek z dodatkowymi polami (według tabelki będą to: FG, Assist, Rebound, Block, Ft). Kolejnym krokiem może być wyrzucenie powtarzających się właściwości w nowych klasach do osobnych interfejsów(widzę tu raptem 3 interfejsy - ale musisz sobie odpowiedzieć czy będziesz potrzebować tego).
W ten sposób ograniczasz liczbę klas do max 6, każdy obiekt ma przypisany rodzaj rozgrywki. Jeżeli nie wyrzucisz powtarzających się właściwości do interfejsów to masz tylko mały nadmiar powtarzających się właściwości (na problemy akademickie do przełknięcia).

edytowany 2x, ostatnio: Quedin
maszrum
  • Rejestracja:około 5 lat
  • Ostatnio:12 miesięcy
  • Lokalizacja:Kraków
  • Postów:219
0

@Quedin:
To tak nie działa. Ja wiem, że kiedy zna się wszystkie te elementy OOP to wszystko wygląda jak klasa bazowa. Abstrakcję wydzielamy tylko w momencie kiedy jest to niezbędne, to znaczy: inne części kodu polegają na tych abstrakcjach. Dziedziczenie to jest konieczność, a nie zachcianka. Niepotrzebna abstrakcja zwiększa złożoność kodu i sprawia, że staje się mniej rozbudowywalny i utrzymywalny. Nigdy nie powinno się podchodzić do tematu na zasadzie: "te klasy mają takie same właściwości więc wydzielę interfejs/klasę bazową". Dziedziczenie to jest raczej coś czego chcemy unikać i stosujemy tylko wtedy kiedy jest taka potrzeba.

Gdy twoim jedynym narzędziem jest młotek, wszystko zaczyna ci przypominać gwoździe.

@Magiczny:
Według mnie najlepszym podejściem będzie zrobienie wszystkich zagrywek jako osobne klasy. Bez żadnych interfejsów ani klas bazowych. Bez enuma, najprostsze klasy z właściwościami. Dopiero kiedy w trakcie pisania kolejnych części kodu zajdzie taka potrzeba wydziel abstrakcję do interfejsu/klasy bazowej. Nie zwiększaj sobie złożoności kodu już na samym początku.

Aventus
  • Rejestracja:około 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
3

A czemu nie klasa Play która będzie zawierać pola uniwersalne, enum z rodzajem zagrywki oraz opcjonalny obiekt z parametrami? Zależnie od tego jak to musisz później użyć, obiekt ten może dziedziczyć po jakiejś klasie bazowej (np. BasePlayParameters) albo możesz do tego po prostu użyć marker interfejs (czyli pusty interfejs).

Później do wyciągnięcia konkretnego typu tego obiektu możesz gdzieś użyć pattern matching.

EDIT: Coś takiego

Kopiuj
class Play
{
  public int Id { get; }
  // Inne wspólne pola
  public PlayType PlayType { get; }
  public IPlayParameters { get; }
}

class FGPlayParameters : IPlayParameters
{
  public int Points { get; }
  // Reszta parametrów
}

class SomeProcessingLogic
{
  public static int? GetPoints(IPlayParameters parameters) =>
    parameters switch
    {
      FGPlayParameters p => p.Points,
      _ => null
    };
}

Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
edytowany 1x, ostatnio: Aventus
somekind
Myślę, że możemy tak zgadywać w nieskończoność, ale dopóki autor nie powie co to za klasy, gdzie ich używa i na czym polega problem z rozpoznawaniem przychodzących klas, każda odpowiedź może być równie dobra i zła.
maszrum
  • Rejestracja:około 5 lat
  • Ostatnio:12 miesięcy
  • Lokalizacja:Kraków
  • Postów:219
1

@Aventus:
Jest to jedno z rozwiązań. Ja na przykład od razu pomyślałem o wzorcu odwiedzający (nie wizytator).

Kopiuj
public class Match
{
    public int FirstTeamScore { get; set; }
    public int SecondTeamScore { get; set; }
    public List<Player> FirstTeam { get; }
    public List<Player> SecondTeam { get; }
    
    public bool IsPlayerInFirstTeam(Player player) => 
        FirstTeam.Contains(player);
    
    public bool IsPlayerInSecondTeam(Player player) => 
        SecondTeam.Contains(player);
    
    public void DoPlay(IPlay play)
    {
        play.Make(this);
    }
}

public interface IPlay
{
    void Make(Match match);
}

public class FieldGoal : IPlay
{
    public int Points { get; }
    public Player Player { get; }
    public bool Blocked { get; }

    public FieldGoal(/*...*/)
    {
        // ... initialize properties
    }
    
    public void Make(Match match)
    {
        if (!Blocked)
        {
            if (match.IsPlayerInFirstTeam(Player))
            {
                match.FirstTeamScore += Points;
            }
            else if (match.IsPlayerInSecondTeam(Player))
            {
                match.SecondTeamScore += Points;
            }
        }
    }
}

I do tego use-case:

Kopiuj
var match = new Match(); // zero-state match

var plays = new[]
{
    new FieldGoal(/*...*/),
    new FieldGoal(/*...*/),
    new FieldGoal(/*...*/)
};

foreach (var play in plays)
{
    match.DoPlay(play);
}
edytowany 1x, ostatnio: maszrum
Riddle
Administrator
  • Rejestracja:prawie 15 lat
  • Ostatnio:około 4 godziny
  • Lokalizacja:Laska, z Polski
  • Postów:10058
2
maszrum napisał(a):

@Aventus:

Jest to jedno z rozwiązań. Ja na przykład od razu pomyślałem o wzorcu wizytator.

Nie ma czegoś takiego jak "wzorzec wizytator". Jest "visitor", co tłumaczy się na "odwiedzający".

(Wizytator to jest członek komisji która wykonuje wizytację).

edytowany 1x, ostatnio: Riddle
maszrum
Szczerze to zanim napisałem tamtego posta musiałem sprawdzić w google jak się to tłumaczy i na wikipedii stosują zamiennie "wizytator" i "odwiedzający"
Riddle
@maszrum: Możliwe że stosują, niemniej "wizyta" i "wizytacja" to dwie różne rzeczy, podobnie jak "wizytacja" i "odwiedziny", a później "wizytator" i "odwiedzający".
maszrum
Okej być może masz rację, "odwiedzający" bardziej pasuje :)
Riddle
Najlepiej używać angielskich odpowiedników, czyli "visitor".
MA
  • Rejestracja:ponad 5 lat
  • Ostatnio:około 10 godzin
  • Postów:20
0

@wszyscy: przede wszystkim dzięki za odpowiedzi i sorry, że tyle mi zeszło z tym postem..

@Magiczny - co to za klasy i gdzie ich używasz? Jakieś DTO jakiegoś API? Przynajmniej tak brzmi to, co napisałeś: przy tym rozwiązaniu będę się pewnie musiał potem paprać z rozpoznawaniem dynamicznym jaka klasa akurat przyszła.

To jest mój twór - webapka do trackowania statystyk graczy na bieżąco.
Zagrywki ma wprowadzać użytkownik (najpewniej będą przychodzić jsonem, zazwyczaj w paczkach po kilka stanowiących jedną, większą akcję - np. nietrafiony rzut A -> zbiórka B). Przy wprowadzaniu to właściwie nic ciekawego się nie dzieje - tylko przerobić i wrzucić do bazy. (edit: wyniki "na bieżąco" to front sobie sam powinien dać radę obsłużyć)
Będą one potem używane w innych miejscach (tylko do odczytu) do generowania mniej lub bardziej złożonych statystyk graczy (np. lifetime, z danego dnia, vs. inny zawodnik itp.).

Jak na razie to chyba visitor mi faktycznie najbardziej przypadł do gustu - coś mi podobnego po głowie świtało, ale nie umiałem tego "ubrać".

edytowany 3x, ostatnio: Magiczny
somekind
Moderator
  • Rejestracja:około 17 lat
  • Ostatnio:42 minuty
  • Lokalizacja:Wrocław
1
maszrum napisał(a):

To tak nie działa. Ja wiem, że kiedy zna się wszystkie te elementy OOP to wszystko wygląda jak klasa bazowa.

Widocznie za mało elementów OOP znam, skoro nie zauważam takich objawów u siebie.

Abstrakcję wydzielamy tylko w momencie kiedy jest to niezbędne, to znaczy: inne części kodu polegają na tych abstrakcjach.

To taka trochę rekurencyjna definicja. :) Gdy wydzielisz abstrakcję, stanie się ona niezbędna, bo bez niej nie będzie działała reszta kodu. Gdy nie wydzielisz abstrakcji, to nie jest ona niezbędna, więc nie ma sensu jej nigdy wydzielać. :P

Dziedziczenie to jest konieczność, a nie zachcianka. Niepotrzebna abstrakcja zwiększa złożoność kodu i sprawia, że staje się mniej rozbudowywalny i utrzymywalny. Nigdy nie powinno się podchodzić do tematu na zasadzie: "te klasy mają takie same właściwości więc wydzielę interfejs/klasę bazową". Dziedziczenie to jest raczej coś czego chcemy unikać i stosujemy tylko wtedy kiedy jest taka potrzeba.

To w dużej mierze racja, tylko ja np. wciąż nie wiem, co tu tak naprawdę jest potrzebne, więc tworzenia hierarchii klas bym nie skreślał.
No i abstrakcja to nie są klasy bazowe/interfejsy, to nie ma ze sobą wiele wspólnego.

Dopiero kiedy w trakcie pisania kolejnych części kodu zajdzie taka potrzeba wydziel abstrakcję do interfejsu/klasy bazowej. Nie zwiększaj sobie złożoności kodu już na samym początku.

Istnienie klas nie zwiększa złożoności. Istnienie ifów owszem.

Magiczny napisał(a):

To jest mój twór - webapka do trackowania statystyk graczy na bieżąco.
Zagrywki ma wprowadzać użytkownik (najpewniej będą przychodzić jsonem, zazwyczaj w paczkach po kilka stanowiących jedną, większą akcję - np. nietrafiony rzut A -> zbiórka B). Przy wprowadzaniu to właściwie nic ciekawego się nie dzieje - tylko przerobić i wrzucić do bazy. (edit: wyniki "na bieżąco" to front sobie sam powinien dać radę obsłużyć)

Ok, czyli jeśli mówimy o DTO wpadającym do jakiegoś API, to zrobiłbym to jedną klasą z polem informującym o tym, co to za typ zagrywki. Na bazie tego pola można zrobić walidację wejścia i sprawdzić, czy wszystkie potrzebne rzeczy zostały przysłane z zewnątrz (a także, czy nie zostały przysłane żadne niepotrzebne).
Hierarchii tu się i tak nie da zrobić, bo na podstawie samych tylko ustawionych właściwości nie można określić typu klasy.

Pytanie, czy ten jeden DTO może być przetwarzany dalej w aplikacji, czy należy go najpierw przetworzyć w jakąś hierarchę klas. Na to pytanie nie da się odpowiedzieć nie znając szczegółów przetwarzania. Im bardziej ma to być tylko proste zapisanie do bazy, tym bardziej można zostawić DTO. Im więcej ma być logiki, tym więcej zysku będzie ze zróżnicowania na klasy... Prawodpodobnie.

Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)