Roast me! Czyli co byście zmienili w mojej solucji?

Roast me! Czyli co byście zmienili w mojej solucji?
bakunet
  • Rejestracja:prawie 8 lat
  • Ostatnio:około 9 godzin
  • Lokalizacja:Polska
  • Postów:1595
1

Hej, napisałem sobie taki pół-żartem, pół-serio program do typowania koni w wyścigach. Nie chodzi mi tu o ocenę funkcjonalności czy skutecznośi, a architektury i błędów w sztuce. Ciekaw jestem ile uda Wam się ich znaleźć. Na chwilę obecną głównie się obawiam, że mam zbyt wiele logiki w warstwie serwisowej i jeszcze nie wiem jak ugryźć testowanie serwisów. TUTAJ też znajduje się cała logika dla pobierania danych. Do tego mogłem z niektórych metod utworzyć oddzielne serwisy, ale tutaj ciekaw jestem czy Wy wskażecie z których.

Solucja wykorzystuje m.in. Autofac, HtmlAgilityPack, Wpf.Toolkit, Newtonsoft.Json, xUnit, Moq. Jest też sporo asynchronicznych metod.

Dopiero zacząłem pisać testy jednostkowe do niej, więc jeszcze wszystkiego nimi nie pokryłem. Sporo czasu minie nim je skończę, bo 1) do tej pory rzadko pisałem testy, pora się nauczyć je pisać 2) testowanie asynchronicznego kodu nie jest łatwe, a tego mam tu sporo. Też pewnie będę ciągle refaktoryzował SUT pod testowanie, ale jak coś dziwnego zauważycie w kodzie to piszcie śmiało. Z góry dzięki za poświęcony czas i wszelkie uwagi.

GitHub link: https://github.com/przemyslawbak/Horse_Picker

A poniżej screen UI:
Horse_Picker

edytowany 6x, ostatnio: bakunet
kzkzg
  • Rejestracja:ponad 8 lat
  • Ostatnio:2 minuty
  • Postów:924
2

Ubogie modele i ViewModel na 2k linii? No nie wiem czy w dobrą stronę idziesz.


Keep calm and blame frontend.
Tell your cat I said pspsps.
bakunet
Przyznaję, 2k linii na klasę to sporo, z drugiej strony praktycznie jeden widok -> jeden model widoku. Nie chciałem się jakoś specjalnie rozdrabniać, ale teraz zastanawiam się jeszcze nad rozbiciem go trochę. Co byś zmienił w moich modelach? Nie wiem jak można je bardziej... wzbogacić?
flowCRANE
Moderator Delphi/Pascal
  • Rejestracja:ponad 13 lat
  • Ostatnio:minuta
  • Lokalizacja:Tuchów
  • Postów:12156
2

Pierwsze co widzę do poprawy to UI – paskudna kolorystyka, ten zielony nie pasuje do niczego. Najlepiej to ustaw domyślne, systemowe kolory, tak aby Twoja plikacja wyglądała tak jak reszta systemu.

A jeśli już koniecznie chcesz mieć własną kolorystykę to skorzystaj z narzędzi do doboru kolorów i odcieni, tak aby interfejs nie wypalał oczu. Ewentualnie zamiast jednokolorowego tła, skorzystaj z czegoś ciekawszego – może gradient (ale nie taki prosty wertykalny), może tekstura (byle nie samopowtarzająca).

Użytego języka nie znam, więc nie skomentuję.


Pracuję nad własną, arcade'ową, docelowo komercyjną grą z gatunku action/adventure w stylu retro (pixel art), programując silnik i powłokę gry od zupełnych podstaw, przy użyciu Free Pascala i SDL3. Więcej informacji znajdziesz na moim mikroblogu.
edytowany 2x, ostatnio: flowCRANE
SO
  • Rejestracja:ponad 10 lat
  • Ostatnio:12 miesięcy
1

No cóż, nigdy nie pisałem w WPF, ale jak na moje oko tutaj żadnej architektury nie ma i większość jest do poprawy.

3 serwisy na krzyż z czego jeden na ponad 1k linii i metody na 200+ linii i dodatkowo 1 viewmodel który ma 2k linii. Tam siedzi cała logika.

A co do tego użycia async to owszem, używasz, ale w sposób jaki się nie powinno używać bo to co robisz to anti-pattern. W FileDataServices i ScrapDataServices masz metody, które korzystają z I/O (odczyt/zapis z dysku i wysyłanie requestów http), a zamiast użyć asynchronicznych text readerów, czy httpclienta to wszystko robisz synchronicznie i wrapujesz to w Task.Run.

https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-dont-use.html

SO
Wygląda spoko, tylko znowu nie rozumiem po co tam jest wstawiony Task.Run. Ja kiedyś używałem sposobu podobnego do pierwszego w tej odpowiedzi (https://stackoverflow.com/a/25877042) i wydaje się że działał :P
bakunet
Widzę, że zastosowanie jest całkiem podobne do SemaphoreSlim.
bakunet
  • Rejestracja:prawie 8 lat
  • Ostatnio:około 9 godzin
  • Lokalizacja:Polska
  • Postów:1595
0
furious programming napisał(a):

Pierwsze co widzę do poprawy to UI (...)

Poprawiłem trochę kolory, myślisz że już lepiej?
obraz

bakunet
  • Rejestracja:prawie 8 lat
  • Ostatnio:około 9 godzin
  • Lokalizacja:Polska
  • Postów:1595
0

Dzięki za ciekawe uwagi.

some_ONE napisał(a):

W FileDataServices i ScrapDataServices masz metody, które korzystają z I/O (odczyt/zapis z dysku i wysyłanie requestów http), a zamiast użyć asynchronicznych text readerów (...)

Co do FileDataServices, odczyt z plików nie trwa długo, nie stanowi to najmniejszego problemu. Rozważałem asynchroniczny zapis, ale ostatecznie zdecydowałem się zostać przy synchronicznym. Czy to jakiś problem? Choć może dla dobrej praktyki zrobię asynchroniczny odczyt/zapis. Choć Newtonsoft.Json nie wspiera asynchronicznej (de)serializacji, może zastosuję wspominany wcześniej antypattern żeby nie blokować UI: https://stackoverflow.com/a/15648126 .

A w TextWriter zastosowałem już asynchroniczny WriteLineAsync:

Kopiuj
async Task SaveTestResultLine(string line)
        {
            using (TextWriter csvLineBuilder = new StreamWriter(_testsFileName, true))
            {
                await csvLineBuilder.WriteLineAsync(line);
            }
        }
some_ONE napisał(a):

wszystko robisz synchronicznie i wrapujesz to w Task.Run.

https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-dont-use.html

Wczytałem się w ten artykuł. Jak dobrze rozumiem, konkluzja jest taka, że UI powinno dbać o nieblokowanie wątku UI i nie powinno się tworzyć nowych wątków które mogą wpłynąć na pogorszenie wydajności. Jestem skłonny się zgodzić, ale w moim przypadku Task task = Task.Run(async () => ma na celu właśnie utworzenie przez pętlę for jak największej ilości Task-ów i wątków, które będą pracować równolegle dla przyspieszenia pracy z racji długiego trwania wykonywania każdego z zadania. Mi właśnie zależy na utworzeniu wielu nowych wątków i nie znalazłem lepszego rozwiązania. Ewentualnie Parallel.For mogłoby być alternatywą.

some_ONE napisał(a):

3 serwisy na krzyż z czego jeden na ponad 1k linii i metody na 200+ linii i dodatkowo 1 viewmodel który ma 2k linii. Tam siedzi cała logika.

Tutaj się zgodzę w 100%. Narobiłem trochę ciężkich metod i klas, pomyślę o ich odchudzeniu i zrobieniu z niektórych metod nowych serwisów.

edytowany 1x, ostatnio: flowCRANE
SO
  • Rejestracja:ponad 10 lat
  • Ostatnio:12 miesięcy
1
bakunet napisał(a):

Co do FileDataServices, odczyt z plików nie trwa długo, nie stanowi to najmniejszego problemu. Rozważałem asynchroniczny zapis, ale ostatecznie zdecydowałem się zostać przy synchronicznym. Czy to jakiś problem?

No nie trwa zbyt długo i w aplikacji desktopowej to raczej nie ma dużego znaczenia, ale dla przyzwoitości można napisać tak jak poprawnie powinno być :P
W apce webowej teoretycznie mogłoby to mieć znaczenie, bo tam możesz dostać nagle 10k requestów na sekundę i przy synchronicznym zapisie mogą się skończyć dostępne wątki.

Choć może dla dobrej praktyki zrobię asynchroniczny odczyt/zapis. Choć Newtonsoft.Json nie wspiera asynchronicznej (de)serializacji.

A nie możesz serializować w pamięci przez JsonConvert, a później asynchronicznie zapisać do pliku?

Wczytałem się w ten artykuł. Jak dobrze rozumiem, konkluzja jest taka, że UI powinno dbać o nieblokowanie wątku UI i nie powinno się tworzyć nowych wątków które mogą wpłynąć na pogorszenie wydajności. Jestem skłonny się zgodzić, ale w moim przypadku Task task = Task.Run(async () => ma na celu właśnie utworzenie przez pętlę for jak największej ilości Task-ów i wątków, które będą pracować równolegle dla przyspieszenia pracy z racji długiego trwania wykonywania każdego z zadania. Mi właśnie zależy na utworzeniu wielu nowych wątków i nie znalazłem lepszego rozwiązania. Ewentualnie Parallel.For mogłoby być alternatywą.

W poprzednim poście pisałem tylko o ScrapDataServices i FileDataServices, tam też w kilku miejscach wrzucasz operacje synchroniczne w Task.Run, a nie powinno to tam być.

A co do przykładu o którym mówisz z MainViewModel to też wcale Task.Run tam nie jest potrzebne, bo każdą operację asynchroniczną opakowujesz w Task.Run, co znowu nie jest dobrą praktyką. Ten kod powinien wyglądać mniej więcej tak (pisane z palca, więc pewnie są jakieś błędy, ale ogólną ideę powinno być widać):

Kopiuj
[...]
List<Task> tasks = Enumerable.Range(startIndex, stopIndex - startIndex)
   .Select(i => ScrapData(i))
   .ToList();

await Task.WhenAll(tasks); //albo WhenAny w pętli i tutaj aktualizujesz ProgressBara

[...]

private async Task ScrapData(int index)
{
    if (dataType == "racesPl") race = await _scrapServices.ScrapSingleRacePlAsync(index);

    //if the race is from 2018
    if (race.RaceDate.Year == 2018)
    {
        lock (((ICollection)Races).SyncRoot)
        {
            Races.Add(race);
        }
    }
}

Tutaj się zgodzę w 100%. Narobiłem trochę ciężkich metod i klas, pomyślę o ich odchudzeniu i zrobieniu z niektórych metod nowych serwisów.

Ja bym zaczął od wydzielenia klasy, która będzie odpowiadała za komunikację z tym serwisem z którego scrapujesz dane (teraz ładujesz stronę przez htmlagilitypack, a z tego co widziałem on nie obsługuje asynca). Ja bym spróbował zrobić klasę, która pobierze zawartość strony jako HTML przez HttpClienta (ma asynca) i dopiero później ładował gotowego stringa do HtmlAgilityPack (chyba się da?).

Dodatkowo przydałoby się wydzielić to całe parsowanie przez HtmlAgilityPack do jakiejś oddzielnej klasy.

No i jeszcze w FileDataServices masz kilka metod, które robią to samo (czytanie z pliku -> deserializacja, albo serializacja -> zapis do pliku). I teraz w każdej metodzie masz zrobiony copy-paste. Zrób sobie na początek jakiś generyczny helper wewnątrz tej klasy i te 3 metody GetAllHorses/GetAllJockeys/GetAllRaces to będą tylko wywołania helpera.

edytowany 3x, ostatnio: some_ONE
czysteskarpety
czysteskarpety
  • Rejestracja:prawie 10 lat
  • Ostatnio:ponad 4 lata
  • Lokalizacja:Piwnica
  • Postów:7697
bakunet
  • Rejestracja:prawie 8 lat
  • Ostatnio:około 9 godzin
  • Lokalizacja:Polska
  • Postów:1595
0
some_ONE napisał(a):

A co do przykładu o którym mówisz z MainViewModel to też wcale Task.Run tam nie jest potrzebne, bo każdą operację asynchroniczną opakowujesz w Task.Run, co znowu nie jest dobrą praktyką.

Tutaj się nie zgodzę, bo w razie potrzeby potrzebuję uśmiercić wszystkie zadania w poprawny sposób. Tak więc Przez Task.Run mogę wykorzystać CancellationToken jak w ODPOWIEDZI NA TO ZAPYTANIE.

some_ONE napisał(a):

Ja bym zaczął od wydzielenia klasy, która będzie odpowiadała za komunikację z tym serwisem z którego scrapujesz dane (teraz ładujesz stronę przez htmlagilitypack, a z tego co widziałem on nie obsługuje asynca). Ja bym spróbował zrobić klasę, która pobierze zawartość strony jako HTML przez HttpClienta (ma asynca) i dopiero później ładował gotowego stringa do HtmlAgilityPack (chyba się da?).

Dodatkowo przydałoby się wydzielić to całe parsowanie przez HtmlAgilityPack do jakiejś oddzielnej klasy.

No i jeszcze w FileDataServices masz kilka metod, które robią to samo (czytanie z pliku -> deserializacja, albo serializacja -> zapis do pliku). I teraz w każdej metodzie masz zrobiony copy-paste. Zrób sobie na początek jakiś generyczny helper wewnątrz tej klasy i te 3 metody GetAllHorses/GetAllJockeys/GetAllRaces to będą tylko wywołania helpera.

Zastosuję się do Twoich rad. Czy do tych klas robić interfejsy, żeby łatwiej było testować? Ciągle też się zastanawiam jak wygląda testowanie serwisu, więc nie wykluczam, że będę miał dalsze pytania, jak nie uda mi się znaleźć ciekawych przykładów ;p

Na szczeście da się parsować dokument pobrany przez HttpClienta z HtmlAgilityPack, już kiedyś miałem okazję to robić.

Co do FileDataServices to się zgadza, myslałem o optymalizacji tych metod, dokładałem je wraz z rozwojem VM i zmianą wykorzystanych bibliotek.

some_ONE napisał(a):

W poprzednim poście pisałem tylko o ScrapDataServices i FileDataServices, tam też w kilku miejscach wrzucasz operacje synchroniczne w Task.Run, a nie powinno to tam być.

Co do ScrapDataServices to się nie zgodzę z Tobą. Nawiązując do TEGO ARTYKUŁU i TEJ ODPOWIEDZI wydaje mi się, że wykonałem wszystko zgodnie ze sztuką, ponieważ:

  1. metoda wywołująca (ScrapHorses w VM) blokujące zadanie (ScrapSingleHorsePlAsync) jest async;
  2. wywołanie jest await
  3. blokujące zadanie (ScrapSingleHorsePlAsync) jest async

Przykład z artykułu:

Kopiuj
class MyService
{
  public async Task<int> RetrieveValueAsync(int id)
  {
    // Converted to nonblocking work.
    // DB access, web request, etc.
    await Task.Delay(500);
    return 42;
  }
}
edytowany 1x, ostatnio: bakunet
bakunet
  • Rejestracja:prawie 8 lat
  • Ostatnio:około 9 godzin
  • Lokalizacja:Polska
  • Postów:1595
0
some_ONE napisał(a):

(WSZYSTKO)

Ok, muszę przyznać Ci całkowitą rację co do mojego wykorzystania Task.Run :) Na SO zdmuchnęli resztkę moich nadziei na to, że może jednak jest OK. Potrzebuję teraz przebudować swoje serwisy i VM, chwilę pewnie mi to zajmie. Ale przynajmniej długo nie zapomnę tej lekcji o Taskach i Runach :)

Tak więc dzięki raz jeszcze za zwrócenie uwagi!

DA
  • Rejestracja:około 6 lat
  • Ostatnio:około 3 godziny
  • Postów:141
0

Liczby masz przygotowane z trzema miejscami po przecinku. Nie lepiej wyrównać je do prawej strony?

bakunet
Masz na myśli metodę z TextWriter? Jeśli tak, to nie zależało mi na jakimś specjalnym formatowaniu, chciałem mieć jedynie własność Score na końcu stringu, a resztę poglądowo. Swoją drogą, że pewnie wydajniej by było użyć StringBuilder
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)