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

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?
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?

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).

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.

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).

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.

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

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
    };
}
1

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

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:

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

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

foreach (var play in plays)
{
    match.DoPlay(play);
}
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ę).

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ć".

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.

1 użytkowników online, w tym zalogowanych: 0, gości: 1