Architektura onion/clean - kiedy "ogólna" abstrakcja, a kiedy bardziej "konkretna"

0

Tytuł chyba trochę z czapy, ale nic lepszego nie wymyśliłem :D
Robimy apkę w stylu clean/onion.

Taki hipotetyczny case - zaczynamy od tego, że użytkownik ma możliwość wyeksportowania eksportu jakichś biznesowych danych w postaci CSV.
No to w warstwie aplikacyjnej definiujemy model na dane do eksportu i interfejs do eksportera, coś w stylu:

public record ExportData(Employee employee, IEnumerable<SalesDetails> salesDetails);

public interface ICsvReportExporter 
{
   public Stream Export(ExportData data);
}

W warstwie infrastruktury implementujemy ten interfejs:

internal CsvReportExporter : ICsvReportExporter
{
   public Stream Export(ExportData data) { //korzystajac z jakiejs libki robimy mapowania i eksport do CSV }
}

Handler aplikacyjny najpierw zbiera potrzebne dane, tworzy model do eksportu (ExportData) i wywołuje interfejs. Wszystko wygląda git.

I tutaj już pierwsza rzecz która przychodzi na myśl to czy ten interfejs w takiej postaci to dobry poziom abstrakcji i czy na pewno powinien się nazywać ICsvReportExporter. Czy nie jest to zbyt mało ogólna abstrakcja, bo od razu sugeruje, że to potrafi eksport tylko do CSV - może eksporter powinien być interfejsem IReportExporter, a to czy CSV, czy nie z punktu widzenia warstwy aplikacyjjnej powinno być przezroczyste.
Z drugiej strony w tym przypadku to żaden szczegół implementacyjny tylko raczej wymaganie biznesowe - format jest z góry ustalony i to nie jest coś co podmienimy jako zależność czysto techniczną bez wpływu na funkcjonalność aplikacji (za zależność czysto techniczną rozumiem np. wewnętrzny storage czyli słynne repozytorium, albo broker wiadomości - Rabbit/Azure Service Bus, w tych przypadkach to rzeczywiście szczegół czysto implementacyjny zazwyczaj przezroczysty dla użytkownika końcowego/funkcjonalności aplikacji).

Później sytuacja rozwija się tak, że użytkownik ma możliwość wyeksportowania tych samych danych, ale w innej formie - Excel - dane dokładnie te same jak w CSV, a wstawione po prostu jako kolumny w Excelu.
I albo teraz tworzymy nowy interfejs (IExcelReportExporter), albo jednak stwierdzamy, że przykrywamy to jednym interfejsem:

public enum ExportType
{
   CSV
   Excel,
}

public interface IReportExporter
{
   public Stream Export(ExportData data, ExportType type)
}

Sam interfejs dalej przyjmuje parametr dotyczący typu eksportu, bo jak pisałem wyżej - w takim przypadku obsługiwanie kilku typów eksportów to funkcjonalność aplikacyjna (to użytkownik w aplikacji wybiera w jakim formacie chce wyeksportować dane), a nie szczegół czysto implementacyjny.

Ogólnie nowy interfejs wygląda ok.
Ale dostajemy wymaganie eksportu do kolejnego formatu, niech to będzie PDF. Jako, że PDF umożliwia eksport w bardziej przystępnej postaci - z jakiegoś biznesowego powodu eksport do PDF wymaga trochę więcej danych/danych w innym formacie.

I co robimy teraz?
Albo znowu wracamy do tego, że tworzymy nowy interfejs pod eksport PDF - IPdfExporter i warstwa aplikacyjna sobie jakoś tam ifuje na podstawie inputu od użytkownika i wyciąga inne dane dla eksportów CSV/Excel, a inne dla PDF i następnie wywołuje odpowiedni interfejs, albo wprowadzamy jakieś kolejne (chyba trochę pokraczne) abstrakcje tak żeby mieć jeden interfejs do eksportu, który w zależności od typu musi przyjąć trochę inne dane - albo rzeźbienie na słownikach, albo jakieś hierarchie klas i rzutowanie w konkretnych implementacjach.

No więc wracając do pytania z wątku - byście to chowali za jednym interfejsem wprowadzając dodatkowe abstrakcje (może jakiś IExportDataProvider, który następnie jest przekazywany do metody Export z interfejsu), czy jednak zrobili oddzielny interfejs?

1

Ja pewnie bym szedł w opcje jeden interface a manipulował klasą ExportData. A po co Ci w metodzie

public Stream Export(ExportData data, ExportType type)

ExportType type ?

0
szydlak napisał(a):

A po co Ci w metodzie

public Stream Export(ExportData data, ExportType type)

ExportType type ?

Chwilę po napisaniu postu o tym samym myślałem. Generalnie, ten parametr jest bez sensu xD Bo jeden eksporter raczej nie potrafi eksportować do różnych formatów.
Zamysł był taki, żeby w warstwie aplikacyjnej uniknąć jakichś fabryk/providerów eksporterów, które wybiorą odpowiedni eksporter (w zamyśle miała to robić infra), ale lepsze to niż parametr w interfejsie, który do niczego nie jest używany.

szydlak napisał(a):

Ja pewnie bym szedł w opcje jeden interface a manipulował plikiem ExportData.

Spoko, dopóki nie ma tam 10 różnych przypadków, że różne eksporty wymagają różnych danych pewnie może tak być - a nawet i wtedy, w sumie można zrobić ExportData, który przechowuje jako obiekty definicje danych dla konkretnego typu eksportu, warstwa aplikacyjna to składa, a konkretny eksporter wie z którego obiektu w ExportData ma skorzystać.
Zastanawiałem się czy znajdzie się jakieś fajne czystsze rozwiązanie.

0
some_ONE napisał(a):

Chwilę po napisaniu postu o tym samym myślałem. Generalnie, ten parametr jest bez sensu xD Bo jeden eksporter raczej nie potrafi eksportować do różnych formatów.
Zamysł był taki, żeby w warstwie aplikacyjnej uniknąć jakichś fabryk/providerów eksporterów, które wybiorą odpowiedni eksporter (w zamyśle miała to robić infra), ale lepsze to niż parametr w interfejsie, który do niczego nie jest używany.

Pytanie czy generowanie tych rzeczy ma być w infrastructure?
Bo oprócz generowania stream/byte[] to nic innego nie robi. Nie uderza do żadanych zewnętrznych zasobów.

1

No to jeszcze inna kwestia.
Stwierdziłem, że samo generowanie konkretnych formatów dokumentów dam do infry, bo korzystam tam z zewnętrznych libek, a ewentualna podmiana libki niekoniecznie powinna wymagać jakichkolwiek zmian w warstwach wewnętrznych.

2
some_ONE napisał(a):

No więc wracając do pytania z wątku - byście to chowali za jednym interfejsem wprowadzając dodatkowe abstrakcje (może jakiś IExportDataProvider, który następnie jest przekazywany do metody Export z interfejsu), czy jednak zrobili oddzielny interfejs?

Myślę, że mamy tu sensowny przypadek zastosowania interfejsów (wiele implementacji), a nie typowego korporacyjnego smellu w postaci jeden interfejs do jednej klasy.
Dla czytelników o mocnych nerwach - to jest nawet wzorzec projektowy, który nazywa się strategia.

some_ONE napisał(a):

Zamysł był taki, żeby w warstwie aplikacyjnej uniknąć jakichś fabryk/providerów eksporterów, które wybiorą odpowiedni eksporter (w zamyśle miała to robić infra), ale lepsze to niż parametr w interfejsie, który do niczego nie jest używany.

Infrastruktura powinna być głupia i nie decydować o przepływie. Takie decyzje powinny odbywać się w warstwie aplikacji, jak cały przepływ.

some_ONE napisał(a):

Zastanawiałem się czy znajdzie się jakieś fajne czystsze rozwiązanie.

A na czym polega nieczystość tutaj?

0
somekind napisał(a):

Myślę, że mamy tu sensowny przypadek zastosowania interfejsów (wiele implementacji), a nie typowego korporacyjnego smellu w postaci jeden interfejs do jednej klasy.

Tylko w sumie nie odpowiedziałeś na pytanie :P
Ale jak rozumiem chowałbyś to za jednym interfejsem.

No i zakładając, że różne formaty potrzebują trochę różnych zestawów danych mamy m.in. takie opcje:

  • upychamy wszystko do jednej zbiorczej klasy ExportData, która musi być zdefiniowana tak żeby pasowała każdemu formatowi, warstwa aplikacyjna wie jaki typ generuje, więc wyciąga odpowiednie dane, pakuje je do ExportData i przekazuje do generatora
  • definiujemy jakąś hierarchię klas z klasą bazową i dzidziczącymi po niej, m.in. ExcelExportData i PdfExportData, znowu warstwa aplikacyjna to składa tak jak trzeba, interfejs eksportera przyjmuje klasę bazową, a konkretne eksportery rzutują ją sobie na swój typ i pracują już na tych konkretnych modelach zdefiniowanych pod ich potrzeby
  • interfejs eksportera zamiast przyjmować dane bezpośrednio, przyjmuje jakąś fabrykę (np. ten IExportDataProvider), ale w sumie nie dużo różni się od opcji powyższych, bo albo znów mamy jedną zbiorczą definicję modelu dla eksportera, albo hierarchię klas i eksporter to rzutuje na konkretny typ. Przy czym znowu przepychamy tutaj trochę operacje za które powinna być odpowiedzialna warstwa aplikacyjna do infrastruktury
  • zawsze można to upychać do słownika i bazować na stringly-typed, ale nie brzmi to jak najlepsze rozwiązanie :D

A na czym polega nieczystość tutaj?

Mamy jedną definicję modelu eksportu, która w zależności od typu eksportu zawsze będzie miała część parametrów ustawionych jako nulle/puste wartości - obojętnie czy zrobimy to jako bardziej płaską listę parametrów, czy jako zagnieżdżone struktury:

public class ExportData
{
   public PdfExportData PdfExportData { get; set; }
   public CsvExportData CsvExportData { get; set; }
}

Jak hipotetycznie dojdą kolejne typy eksportów (HTML, Docx), które znowu wymagają innych danych to ciągle rozbudowujemy tą klasę i robi się coraz większym workiem na dane, gdzie co raz więcej właściwości jest null/pustych bo dotyczy innych typów eksportów.

0
some_ONE napisał(a):

No i zakładając, że różne formaty potrzebują trochę różnych zestawów danych

Jakiego zestawu danych? Na wejściu czy wyjściu eksportera? Czy chodzi ci o model danych czy argumenty na podstawie których dane są filtrowane i przekazywane do eksportera?

Ja bym zakładał że jeżeli eksportuję listę klientów to eksporter dostaje już gotowy zestaw danych i jedyne co robi to składa to w plik Excel czy CSV czy jeszcze inny.

Uproszczony flow by wyglądał tak:

public class ExportCustomersHandler
{
  public Result Handle(ExportCustomers command)
  {
    var customers = _dbcontext
      .Customers
      .Where(command.Filter.ToQuery()) // filtrujemy klientów na podstawie parametrów przekazanych z jakiegoś UI
      .ToList(); 
    
    var exporter = exporterFactory.GetExporter(command.ExportType);

    var result = exporter.Export(customers);

    // 
  }
}
1
markone_dev napisał(a):

Jakiego zestawu danych? Na wejściu czy wyjściu eksportera? Czy chodzi ci o model danych czy argumenty na podstawie których dane są filtrowane i przekazywane do eksportera?

Na wejściu, chodzi o model danych, który dostaje eksporter i na jego podstawie robi eksport, a nie argumenty filtrowania.

To jest przykład hipotetyczny (i im dłużej o nim myślę to wydaje się coraz dziwniejszy), ale powiedzmy, że to co pokazałeś jest wystarczające do eksportu do CSV, ale do PDF dla każdego klienta jeszcze musimy podociągać jakieś dane z innych tabelek, bo PDF ma być bogatszym eksportem.

1
some_ONE napisał(a):

No to jeszcze inna kwestia.
Stwierdziłem, że samo generowanie konkretnych formatów dokumentów dam do infry, bo korzystam tam z zewnętrznych libek, a ewentualna podmiana libki niekoniecznie powinna wymagać jakichkolwiek zmian w warstwach wewnętrznych.

To nie jest dobry powód, konwersja na pdfa to nie jest infra. Jak chcesz większej separacji to wprowadź osobny moduł (nie wiem jak to się robi w CSV)

some_ONE napisał(a):

I tutaj już pierwsza rzecz która przychodzi na myśl to czy ten interfejs w takiej postaci to dobry poziom abstrakcji i czy na pewno powinien się nazywać ICsvReportExporter. Czy nie jest to zbyt mało ogólna abstrakcja, bo od razu sugeruje, że to potrafi eksport tylko do CSV - może eksporter powinien być interfejsem IReportExporter, a to czy CSV, czy nie z punktu widzenia warstwy aplikacyjjnej powinno być przezroczyste.

Ja bym nie szedł w ICsvReportExporter, bo zarówno z perspektywy kodu jak i interfejsu widać, że formaty mogą być różne.

Z drugiej strony w tym przypadku to żaden szczegół implementacyjny tylko raczej wymaganie biznesowe - format jest z góry ustalony i to nie jest coś co podmienimy jako zależność czysto techniczną bez wpływu na funkcjonalność aplikacji (za zależność czysto techniczną rozumiem np. wewnętrzny storage czyli słynne repozytorium, albo broker wiadomości - Rabbit/Azure Service Bus, w tych przypadkach to rzeczywiście szczegół czysto implementacyjny zazwyczaj przezroczysty dla użytkownika końcowego/funkcjonalności aplikacji).

Czy klocki Lego interesują się tym w co zostaną złożone? Nie, twóje klasy i metody też powinny być zaprojektowane tak, że są maksymalnie reużywalne i przy minimalnej liczbie klocków można stworzyć coś złożonego.

0
some_ONE napisał(a):

To jest przykład hipotetyczny (i im dłużej o nim myślę to wydaje się coraz dziwniejszy), ale powiedzmy, że to co pokazałeś jest wystarczające do eksportu do CSV, ale do PDF dla każdego klienta jeszcze musimy podociągać jakieś dane z innych tabelek, bo PDF ma być bogatszym eksportem.

Ja robiłem w ten sposób (nic związanego z exportem plikami), że robiłem sobię fabrykę commandów. I w zależności od enuma tworzyłem inny command i wiadomo odpalał się inny handler który miał specyficzną logikę

2
some_ONE napisał(a):

To jest przykład hipotetyczny (i im dłużej o nim myślę to wydaje się coraz dziwniejszy), ale powiedzmy, że to co pokazałeś jest wystarczające do eksportu do CSV, ale do PDF dla każdego klienta jeszcze musimy podociągać jakieś dane z innych tabelek, bo PDF ma być bogatszym eksportem.

Moim zdaniem, źle do tego podchodzisz. To o czym piszesz że PDF ma bogatszy model danych niż CSV to nie zależy od typu eksportera, który jest szczegółem technicznym, tylko od typu raportu. Więc w tej hipotetycznej sytuacji załóżmy że raport A to CSV a bogaty PDF to raport B. Teraz po wyborze rodzaju raportu A lub B przez użytkownika frontend uderza do API raportowego i na podstawie requestu wybierany jest odpowiedni handler GenerateRaportAHandler lub GenerateReportBHandler. W zależności od wybranego raportu handler pobiera odpowiednią ilość danych i woła pod spodem odpowiedni eksporter przekazując mu gotowe dane i eksporter dla podanych danych generuje plik w oparciu o jakąś prawdopodobnie zahardkodowaną strukturę.

Jak już na siłę chcesz to upchać w jednym handlerze to robisz obiekt w stylu ReportGenerator ktory przyjmuje na wejściu parametry i ifologią lub lepiej wzorcem strategii, decyduje jakie dane pobrać w zależności od typu raportu i którego eksportera użyć.

Generalnie to o czym piszę to to aby eksporter był niezależny od danych. W moim odczuciu taki eksporter ma odpowiadać za utworzenie pliku w danym formacie z danymi o podanej strukturze a nie ogarniać do tego jeszcze logikę biznesową, chociaż i to można tam upchać jak się ktoś uprze :P

Pisałem dużo w OpenXML, gdzie generowaliśmy dokumenty MS Word i Excel dynamicznie, z treścią i strukturą specyficzną dla każdego klienta i ten moduł miał normalnie wydzieloną część biznesową, która odpowiadała za pobranie i przygotowanie danych, wybranie szablonu dokumentu na podstawie identyfikatora klienta i przekazanie tego jako argumenty do części infrastruktury czyli właściwych "generatorów" które sobie składały to wszystko w całość w OpenXML i tworzyły wynikowy plik.

0
markone_dev napisał(a):

Moim zdaniem, źle do tego podchodzisz. To o czym piszesz że PDF ma bogatszy model danych niż CSV to nie zależy od typu eksportera, który jest szczegółem technicznym, tylko od typu raportu. Więc w tej hipotetycznej sytuacji załóżmy że raport A to CSV a bogaty PDF to raport B. Teraz po wyborze rodzaju raportu A lub B przez użytkownika frontend uderza do API raportowego i na podstawie requestu wybierany jest odpowiedni handler GenerateRaportAHandler lub GenerateReportBHandler. W zależności od wybranego raportu handler pobiera odpowiednią ilość danych i woła pod spodem odpowiedni eksporter przekazując mu gotowe dane i eksporter dla podanych danych generuje plik w oparciu o jakąś prawdopodobnie zahardkodowaną strukturę.

No i mamy wtedy IReportAExporter i IReportBExporter ze swoimi implementacjami, czy wciąż IReportExporter z wymaganymi dodatkowymi abstrakcjami, bo jednak pracują na różnych modelach danych? :)

1
some_ONE napisał(a):

No i mamy wtedy IReportAExporter i IReportBExporter ze swoimi implementacjami, czy wciąż IReportExporter z wymaganymi dodatkowymi abstrakcjami, bo jednak pracują na różnych modelach danych? :)

Jakimi dodatkowymi abstrakcjami?
IReportExporter ma metodę Export, która zwraca plik/stream/whatever, a strategia podrzuci odpowiednią implementacje zależnie od potrzeby dla typu A/B.

0
Veo napisał(a):

IReportExporter ma metodę Export, która zwraca plik/stream/whatever, a strategia podrzuci odpowiednią implementacje zależnie od potrzeby dla typu A/B.

Co taki interfejs przyjmuje wtedy na wejściu?

0
some_ONE napisał(a):

Co taki interfejs przyjmuje wtedy na wejściu?

Jeżeli logika pobierania danych i generowania struktury raportu i formatu pliku jest zakodowana w konkretnym eksporterze to typ raportu i informacje potrzebne do przefiltrowania danych w zapytaniu po dane.

Generalnie różnie można podejść do takiej implementacji w zależności od potrzeb biznesowych. Można wszystko trzymać w konkretnym eksporterze jak wyżej, można logikę przygotowania danych wynieść poza taki eksporter, zwłaszcza jeżeli ta sama logika może być używana w różnych eksporterach i tylko różnić się formatem pliku. Dodać to tego abstrakcję w postaci tworzenia formatu raportu w oparciu o generyczne szablony, itd Wtedy eksportery stają się właściwie generatorami i ich jedynym zadaniem jest zbudować cały raport od zera w oparciu o podany na wejściu szablon i dane

0

No to albo mi się wydaje, albo w ciągu kilku postów rozszerzając moje pytanie o różne typy eksportów (a nie tylko formatów) zatoczyliśmy koło, bo proponujesz alternatywy:

  1. Logika dla różnych typów eksportów zaszyta w eksporterach, które przyjmują parametry filtrowania i wtedy albo mamy dedykowane eksportery per typ raportu, albo musimy zgeneralizować obiekt definiujący parametry filtrowania, bo typ raportu A i typ raportu B mają inne pola i inne zestawy filtrów.
  2. Dodanie dodatkowych abstrakcji żeby eksporter był bardziej generyczny i generował praktycznie dowolny output na podstawie szablonu i znowu zgeneralizowanych danych wejściowych w jakimś formacie
1

Bo mam problem z opisanym wymaganiem. Zwykle aplikacje mają różne typy raportów w stylu CustomerSalesReport, ProductInventoryReport, itd. I każdy z tych raportów można sobie wyeksportować do dowolnego formatu (CSV, Excel, PDF). Co najważniejsze dane nie zależą od formatu pliku.

Jak twoje wymaganie miałoby działać jeśli oparte byłoby tylko o typ pliku? Czyli co? Masz wyłącznie dane odnośnie sprzedaży które chcesz zaraportować i nie będziesz nigdy raportować innych rzeczy czyli de facto masz jeden typ raportu który może być wyeksportowany do jednego z kilku formatów plików? To wtedy rozwiązanie 1 czyli eksporter per typ pliku.

A teraz wyobraź sobie że masz więcej rodzajów raportów jak w pierwszym akapicie, to wtedy co? Robiłbyś CustomerSalesReportPdf, CustomerSalesReportCsv, CustomerSalesReportExcel, ProductInventoryReportPdf, ProductInventoryReportCsv, ProductInventoryReportExcel?

Lepiej wtedy zrobić coś w stylu generatora raportów, który na podstawie informacji od użytkownika przygotowuje dane, czyli dane odnośnie sprzedaży lub dane odnośnie stanu magazynowego, pobiera z bazy szablon raportu w zależności od typu pliku i tak przygotowane dane przekazuje do eksportera, który generuje plik w oparciu o przekazane informacje czyli szablon i właściwe dane. Wtedy 2.

0
markone_dev napisał(a):

Bo mam problem z opisanym wymaganiem.

Nie dziwię się, bo sam z czasem życia tego wątku zauważyłem, że to generalnie jest dziwny case, no ale tak to wygląda jak się wymyśla jakieś pół-hipotetyczne przypadki na potrzeby pytania.

markone_dev napisał(a):

A teraz wyobraź sobie że masz więcej rodzajów raportów jak w pierwszym akapicie, to wtedy co? Robiłbyś CustomerSalesReportPdf, CustomerSalesReportCsv, CustomerSalesReportExcel, ProductInventoryReportPdf, ProductInventoryReportCsv, ProductInventoryReportExcel?

Przy dwóch typach to może i jeszcze bym to rozważył :P

markone_dev napisał(a):

Lepiej wtedy zrobić coś w stylu generatora raportów, który na podstawie informacji od użytkownika przygotowuje dane, czyli dane odnośnie sprzedaży lub dane odnośnie stanu magazynowego, pobiera z bazy szablon raportu w zależności od typu pliku i tak przygotowane dane przekazuje do eksportera, który generuje plik w oparciu o przekazane informacje czyli szablon i właściwe dane. Wtedy 2.

No generalnie brzmi lepiej, chociaż na razie na szybko nie widzę do końca jak to miałoby być zaimplementowane. Co rozumiemy np. przez szablon w przypadku CSV? ;)

Wydaje mi się, że w przypadku generycznych eksporterów (tzn. eksporter do PDF, który wspiera wiele różnych raportów, eksporter do CSV, który wspiera wiele różnych raportów itd.) przy bardziej skomplikowanych layoutach będzie masa rzeźbienia, żeby zrobić eksporter odpowiednio uniwersalny.
Bo w przypadku PDF/Excel od razu przychodzą mi do głowy takie rzeczy jak jakieś dynamicznie generowane powtarzalne tabelki (np. tabelka z wynikami sprzedaży w ujęciu miesięcznym, która w zależności od wybranego zakresu dat pojawia się X razy na eksporcie PDF), gdzie jest ona tak specyficzna dla danego typu raportu, że i tak pewnie skończyłoby się na tym, że eksporter ma zaimplementowaną customową obsługę tego typu parametru.

0
some_ONE napisał(a):

Tylko w sumie nie odpowiedziałeś na pytanie :P
Ale jak rozumiem chowałbyś to za jednym interfejsem.

No bo tak zrozumiałem pierwotnie pytanie, że mamy jakieś dane, które chcemy zaprezentować w różnych formatach, więc tak - jeden interfejs.

Jak hipotetycznie dojdą kolejne typy eksportów (HTML, Docx), które znowu wymagają innych danych to ciągle rozbudowujemy tą klasę i robi się coraz większym workiem na dane, gdzie co raz więcej właściwości jest null/pustych bo dotyczy innych typów eksportów.

No tak, w takim podejściu to niefajnie. Sugeruję zapomnieć, że takie rzeczy jak null i rzutowanie istnieją, i zacząć projektowanie od nowa.

To jest dość normalne, że chcemy eksportować dane w jakimś formacie, a ten format może mieć swoje ustawienia.
Po pierwsze może tak najpierw rozdzielmy dane od jakichś tam ustawień eksportu, żeby nam się nie merdało.

public interface IReportExporter 
{
   public Stream Export<TExportSettings>(ExportData data, TExportSettings settings);
}

Konkretne implementacje będą już wiedziały, co ze swoimi ustawieniami zrobić. Być może część ogarną same, a część przekażą do jakiejś 3rd party biblioteki odpowiedzialnej za faktyczny eksport. A konkretne typy TExportSettings będą miały właściwości potrzebne tylko w konkretnym typie eksportu.

No i teraz możemy się zastanawiać, czy tworzenie oddzielnych interfejsów ma tu sens. Moim zdaniem nie, bo jak mamy jeden, to przynajmniej możemy wszystkie implementacje w jednej kolekcji trzymać, ergo przy dodawaniu nowego formatu nie trzeba będzie zmieniać kodu odpowiedzialnego za wybieranie i wywoływanie konkretnego eksportera. A poza tym interfejs z jedną implementacją to zawsze code smell, więc trzeba mieć poważny powód, aby tak zrobić.

0
somekind napisał(a):

No i teraz możemy się zastanawiać, czy tworzenie oddzielnych interfejsów ma tu sens. Moim zdaniem nie, bo jak mamy jeden, to przynajmniej możemy wszystkie implementacje w jednej kolekcji trzymać, ergo przy dodawaniu nowego formatu nie trzeba będzie zmieniać kodu odpowiedzialnego za wybieranie i wywoływanie konkretnego eksportera.

Jak zakładamy, że każdy eksporter pracuje na tych samych danych wejściowych to oczywiście, nie ma nawet co z tym dyskutować.

Ale w moim przykładzie (trochę pokracznym i pewnie zazwyczaj nierealnym, co już opisywał @markone_dev) chodziło o case, gdzie eksporter dla jednego formatu danych wymaga wejścia X, a dla drugiego formatu danych wejścia Y, a nie o to, że eksporter PDF potrzebuje opcji typu - wstaw background albo i nie, zrób taki margin, a eksporter CSV opcji typu użyj średnika albo przecinka.

Ok, przykład dziwny, więc można się skupić na przykładzie który podał @markone_dev, oddzielne raporty typu A i raporty typu B - każdy raport jest do wyeksportowania w różnych formatach.
Jeśli z góry nie wiadomo, że tych raportów będzie zaraz nie 2, a 15, to wydaje mi się, że pisanie w pełni generycznych eksporterów, które obsłużą przyszłościowo dowolny format danych wejściowych to będzie straszna rzeźba - np. case który podawałem wyżej z jakimiś dynamicznymi tabelkami.

W przypadku gdy zrobię IReportExporterA i IReportExporterB, eksportery dostają proste worki z danymi i jako że wiedzą co konkretnie eksportują, mają w sobie zahardkodowaną logikę tworzenia raportu - czyli eksporter raportu sprzedaży, wie jeśli to raport w ujęciu miesięcznym to ma narysować X tabelek z podsumowaniem sprzedaży każda dla pojedynczego miesiąca.
Jakby to wyglądało w przypadku pełni generycznego eksportera? Taki eksporter musiałby dostać wejście w jakimś abstrakcyjnym formacie, gdzie dla każdego parametru wejściowego musiałby mieć określony typ, na podstawie którego stwierdza jak to renderować - czy ma to wstawić gdzieś jako tekst, czy jako sekcje powtarzającą się, i czy to tabelka, czy nie, i może jeszcze struktura tabelki też dynamicznie konfigurowalna? :D
No mi to brzmi jak mocne rzeźbienie.

1
some_ONE napisał(a):

Ale w moim przykładzie (trochę pokracznym i pewnie zazwyczaj nierealnym, co już opisywał @markone_dev) chodziło o case, gdzie eksporter dla jednego formatu danych wymaga wejścia X, a dla drugiego formatu danych wejścia Y, a nie o to, że eksporter PDF potrzebuje opcji typu - wstaw background albo i nie, zrób taki margin, a eksporter CSV opcji typu użyj średnika albo przecinka.

Tylko, że przekazywanie wielkości marginesów do PDFa czy separatora do CSV to są realistyczne wymagania.

W przypadku gdy zrobię IReportExporterA i IReportExporterB, eksportery dostają proste worki z danymi i jako że wiedzą co konkretnie eksportują, mają w sobie zahardkodowaną logikę tworzenia raportu - czyli eksporter raportu sprzedaży, wie jeśli to raport w ujęciu miesięcznym to ma narysować X tabelek z podsumowaniem sprzedaży każda dla pojedynczego miesiąca.
Jakby to wyglądało w przypadku pełni generycznego eksportera? Taki eksporter musiałby dostać wejście w jakimś abstrakcyjnym formacie, gdzie dla każdego parametru wejściowego musiałby mieć określony typ, na podstawie którego stwierdza jak to renderować - czy ma to wstawić gdzieś jako tekst, czy jako sekcje powtarzającą się, i czy to tabelka, czy nie, i może jeszcze struktura tabelki też dynamicznie konfigurowalna? :D
No mi to brzmi jak mocne rzeźbienie.

Jak dla mnie, to eksporter powinien się zajmować eksportowaniem, a nie przygotowaniem danych do wyeksportowania. To powinno dziać się w kodzie, który eksportera używa.

0
somekind napisał(a):

Tylko, że przekazywanie wielkości marginesów do PDFa czy separatora do CSV to są realistyczne wymagania.

Ale ja tego nie neguję przecież, tylko napisałem, że nie do końca to było przedmiotem pytania :)

somekind napisał(a):

Jak dla mnie, to eksporter powinien się zajmować eksportowaniem, a nie przygotowaniem danych do wyeksportowania. To powinno dziać się w kodzie, który eksportera używa.

Krążymy wokół mało istotnych elementów dookoła, a pomijamy sedno, czyli czy robimy jeden generyczny eksporter (tak jak zdaje się sugeruje @markone_dev), który wymaga jakichś powalonych abstrakcji żeby obsłużyć wiele potencjalnych eksportów różnych, zupełnie niezwiązanych ze sobą danych (typ raportu A i typ raportu B), czy tworzymy eksporter(y) per typ raportu :)

1
some_ONE napisał(a):

Krążymy wokół mało istotnych elementów dookoła, a pomijamy sedno, czyli czy robimy jeden generyczny eksporter (tak jak zdaje się sugeruje @markone_dev), który wymaga jakichś powalonych abstrakcji żeby obsłużyć wiele potencjalnych eksportów różnych, zupełnie niezwiązanych ze sobą danych (typ raportu A i typ raportu B), czy tworzymy eksporter(y) per typ raportu :)

To zależy. Jak masz jeden czy dwa rapoty to taki eksporter/generator per raport będzie ok. Jak piszesz bardziej zaawansowany moduł raportów, który może działać na różnych typach raportów i różnych danych, a nie daj boże klient powie że tenanci w aplikacji mają mieć możliwość customizowania raportów to bez "powalonych abstrakcji" jak to ująłeś się nie obędzie :P

Co do powalonych abstrakcji to fakt. Jak pisałem wyżej pracowałem kiedyś przy projekcie który miał takie pokręcone wymagania odnośnie raportów i mimo, że udawało nam się utrzymać kod relatywnie łatwy w rozszerzaniu i dodawaniu kolejnych wyjątków od klientów to był on dość mocno skomplikowany szczególnie te klasy które odpowiadały za składanie dokumentów.

1
some_ONE napisał(a):

Krążymy wokół mało istotnych elementów dookoła, a pomijamy sedno, czyli czy robimy jeden generyczny eksporter (tak jak zdaje się sugeruje @markone_dev), który wymaga jakichś powalonych abstrakcji żeby obsłużyć wiele potencjalnych eksportów różnych, zupełnie niezwiązanych ze sobą danych (typ raportu A i typ raportu B), czy tworzymy eksporter(y) per typ raportu :)

No problem polega na tym, że ja sugeruję zrobić eksporter per typ, a Ty chcesz, aby eksporter nie tylko eksportował, ale i organizował sobie dane do wyeksportowania, czyli zajmował się nie tylko eksportem.

Widziałeś kiedyś chleb, który sam siebie kroi nożem? Podejrzewam, że nie, takie rzeczy możliwe tylko w programowaniu. ;)

0
somekind napisał(a):

No problem polega na tym, że ja sugeruję zrobić eksporter per typ, a Ty chcesz, aby eksporter nie tylko eksportował, ale i organizował sobie dane do wyeksportowania, czyli zajmował się nie tylko eksportem.

Problem polega też na tym, że jak zawsze w takich dyskusjach rozmawiamy na wysokim poziomie nie mając konkretów :D

Bo teraz to rozumiem, że proponujesz eksporter per typ raportu - czyli oddzielne dla raportów A i B. No fajnie, ale jak już zahaczyliśmy o ten raport miesięczny to załóżmy, że typ raportu A można wyeksportować w ujęciu miesięcznym/kwartalnym/rocznym, i co, znowu do każdego oddzielny exporter (x3, bo mamy PDF/CSV/Excel), czy jednak exporter typu A przyjmuje parametr dotyczący typu exportu i sam sobie grupuje dane (ale nie fizycznie pobiera z zewnętrznego źródła dancyh) jak trzeba i ewentualnie agreguje - co już jednak wygląda trochę słabo, bo to logika aplikacyjna :/

0
some_ONE napisał(a):

Problem polega też na tym, że jak zawsze w takich dyskusjach rozmawiamy na wysokim poziomie nie mając konkretów :D

No bo wymagania też są niekonkretne i zmieniają się w czasie. :P
Ja nadal nie wiem, jak jest od początku flow tego wszystkiego. Np. skąd bierze się informacja o formacie raportu?

Bo teraz to rozumiem, że proponujesz eksporter per typ raportu - czyli oddzielne dla raportów A i B. No fajnie, ale jak już zahaczyliśmy o ten raport miesięczny to załóżmy, że typ raportu A można wyeksportować w ujęciu miesięcznym/kwartalnym/rocznym, i co, znowu do każdego oddzielny exporter (x3, bo mamy PDF/CSV/Excel), czy jednak exporter typu A przyjmuje parametr dotyczący typu exportu i sam sobie grupuje dane (ale nie fizycznie pobiera z zewnętrznego źródła dancyh) jak trzeba i ewentualnie agreguje - co już jednak wygląda trochę słabo, bo to logika aplikacyjna :/

Ja sugeruję eksporter zajmujący się tylko eksportem. Dane do wyeksportowania zbiera i buduje jakiś use case handler, który potem wywołuje eksporter.

0
public class ReportExporter
{
	public Result<...> Export(IExportProvider provider)
	{
        // ?? logs??
		return provider.GenerateReport();
	}
}

public class PdfExportProvider : IExportProvider
{
	public PdfExportProvider(PdfSpecificData pdf) // <------
	{
       this.Data = pdf;
	}
	
	public Result<...> GenerateReport()
	{
       // fancy stuff
	}
}

public class ExcelExportProvider : IExportProvider
{
	public ExcelExportProvider(ExcelSpecificData excel) // <------
	{
       this.Data = excel;
	}
	
	public Result<...> GenerateReport()
	{
       // fancy stuff
	}
}

albo dodatkowo w GenerateReport() dodać arg na dane ogólne, wspólne dla all

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