Prywatny konstruktor - pozwolenie na stworzenie obiektu klasy tylko w obrębie innej klasy

0

Witam.
Potrzebuje nakierowania czy gra warta świeczki. Piszę sobie bibliotekę pod mój ukochany system ERP. Mam podział na moduły, które odpowiadają za konkretne moduły w systemie ERP.

PRZYKŁAD

MODUŁ BANK - BankEntry - zapis kasowo/bankowy (KP, KW itp, itd)

public sealed class BankEntry
{
  private BankEntry()
  {

  }  
}
public sealed class BankModule
{
  readonly IDbConnection? _db;
  static BankModule? _instance;

  public BankModule(IDbConnection db)
  {
    _db = db;
  }

  public static BankModule Init(IDbConnection db)
  {
    if(_instance is null)
      _instance = new BankModule(db);

    return _instance;
  }

  public BankEntry CreateBankEntry()
  {
    if (_db is null)
      throw new Exception("");

    var entry = Activator.CreateInstance<BankEntry>();
    // default data

    return entry;
}

Co chce takim zabiegiem uzyskać? Chce mieć pewność, że obiekt BankEntry jest poprawnie stworzony. Stąd CreateBankEntry(), który ma dostęp do bazy i tworzy obiekt z domyślnymi wartościami. Nie chce, aby korzystający z biblioteki mógł stworzyć obiekt w tradycyjny sposób new BankEntry(). Tak jak widać w kodzie, znalazłem w internetach taki sposób Activator.CreateInstance<BankEntry>(); na utworzenie obiektu z prywatnym konstruktorem i doszło do mnie, że użytkownik biblioteki też będzie mógł tak zrobić 🤔

Jest w ogólne opcja, aby utworzenie obiektu BankEntry była możliwa tylko w obrębie BankModule?

2

Tylko po co? To samo pytanie można zadać w takim przypadku:

public class MyClass
{
  private readonly string _myField;

  public MyClass()
  {
    _myField = "Default Value";
  }
}

public class Program
{
  public static void Main()
  {
    var myObj = new MyClass(); // pole _myField posiada domyślną wartość ustawioną przez konstruktor

    // użyj refleksji aby dostać się do pola _myField
    var fieldInfo = typeof(MyClass).GetField("_myField", BindingFlags.NonPublic | BindingFlags.Instance);

    // nadpisz złośliwie domyślną wartość pola _myField w obiekcie typu MyClass
    fieldInfo.SetValue(myObj, "new value");
  }
}

Jak widać udało mi się nadpisać domyślną wartość pola mimo, że jawny dostęp do niego był zabroniony (visibility private)
Przed pewnymi hackami nie ma możliwości się zabezpieczyć i zwyczajnie szkoda na to czasu, bo jak ktoś się uprze to i tak obejdzie zabezpieczenie i tak.

0

No właśnie to jest moje pierwsze, najważniejsze pytanie, bo mam wrażenie, że nie warto. Jak ktoś nie potrafi korzystać to sobie narobi bałaganu w bazie i głównie tego chciałbym uniknąć. Najwyraźniej niepotrzebnie się tym przejmuje. Jak ktoś będzie postępował zgodnie z dokumentacją to wszystko będzie dobrze. Choć, mimo wszystko, daje do myślenia po co klepiemy te wszystkie "prywaty" skoro i tak od innej strony idzie się do tego dostać 🤔

3
AdamWox napisał(a):

Choć, mimo wszystko, daje do myślenia po co klepiemy te wszystkie "prywaty" skoro i tak od innej strony idzie się do tego dostać 🤔

Bo ukrywanie takich rzeczy nie ma chronić przed nadgorliwością, tylko wpływać na jakość kodu i jego czytelność. Gdyby wszystko było publiczne to byłby syf i bałagan. Nie mówiąc o tym, że grzebanie przy atrybutach prywatnych i internalach obiektu czy biblioteki to proszenie się o kłopoty w przypadku jej aktualizacji. Po to moje obiekty mają atrybuty publiczne (kontrakt), żebym sobie mógł bebechy (internale) modyfikować ile wlezie i jak chcę tak długo dopóki nie zmieniam kontraktu (atrybutów publicznych).

1

A nie szukasz przypadkiem "delete" ?

BankEntry() = delete ;

albo

friend class BankModule

?

0
AdamWox napisał(a):
public sealed class BankModule
{
  readonly IDbConnection? _db;
  static BankModule? _instance;

  public BankModule(IDbConnection db)
  {
    _db = db;
  }

  public static BankModule Init(IDbConnection db)
  {
    if(_instance is null)
      _instance = new BankModule(db);

    return _instance;
  }

  public BankEntry CreateBankEntry()
  {
    if (_db is null)
      throw new Exception("");

    var entry = Activator.CreateInstance<BankEntry>();
    // default data

    return entry;
}

Szczerze to ten kod jest nieczytelny. Wymaga myślenia. A dziś mam dzień bezmyślny. Czyli tego kodu nie rozumiem.

2

Jeśli BankModule masz w tym samym projekcie co BankEntry to zamiast kombinować z refleksją możesz zrobić konstruktor tej klasy jako internal zamiast private. A co do drugiej części pytania to wydaje mi się, że nie ma zabezpieczenia przed niechcianym wykorzystaniem refleksji.

0

@Azarien: Pomyślałem, że podkradnę trochę sposobów od konkurencji i to z modułami zerżnąłem żywcem z Enova365. Zaznaczam, że ja piszę bibliotekę do istniejącego systemu ERP, muszę jakoś zaincjalizować obiekty z domyślnymi wartościami. Tam gdzie jest to prostę to zrobię to w modelu, ale są też takie, które trzeba pobrać z bazy danych. Ze względu na to, że to taki trochę reverse enginering to muszę sobie jakoś radzić. Chętnie zrobię to mądrzej, jeśli jest taka opcja.

@maszrum: Nie mogę zrobić internal, bo to ma być biblioteka, którą podpinam pod projekty. Zamysł ogólny jest taki, że chciałbym ominąć biblioteki Comarchu i zrobić bezpośrednio, większość operacji na bazie. Dlatego pomyślałem, że stworzę sobie bibliotekę, w której będę miał całą logikę i tylko będę podpinał do projektów. BTW... To już jest chyba piąte podejście do takiej biblioteki, więc miejmy nadzieje, że nie będzie szóstego 🙈

1

A konstruktor prywatny + builder który waliduje?

0

Zależy mi na tym, aby obiekty były tworzone za pomocą modułu. Jeśli zrobię prywatny konstruktor, to nie będę w stanie stworzyć obiektu w module, a tworząc builder do tego obiektu, będzie można pominąć moduł. To co zasugerował @maszrum jest dobre. Dzięki temu będę też zmuszony trzymać to wszystko w osobnych projektach. Jeszcze nie wiem czy takie podejście jest dobre 😅

1

Czyli nie można utworzyć obiektu, który nie będzie istniał w bazie?
Dla mnie to mocno wtfowe jest. Jeśli obiekt musi mieć jakieś wartości, to załatwia się to:

  1. konstruktorem z parametrami;
  2. statyczną metodą tworzącą obiekt - przydatne, gdy jest wiele sposobów/źródeł tworzenia obiektu;
  3. wzorcem builder - przydatne, gdy jest wiele kombinacji utworzenia obiektu, który ma wiele opcjonalnych wartości.

Nie widzę za bardzo sensu z próbami wymuszenia przywiązania obiektu do bazy danych na poziomie kompilacji. Czemu martwi Cię, że ktoś będzie mógł sobie tak po prostu utworzyć ten obiekt?

0

Jeżeli chcesz coś wczytać z bazy to czemu nie zrobić po ludzku:

public sealed class BankEntry
{
  public BankEntry(IDbConnection db=null)
  {
    if(db==null)
    {
      // read db data
    }
    else
    {
      // set default data
    }
  }  
}

Albo niech sam sobie będzie builderem:

public class BankEntry
{
  private int PropA { get; set; }
  private string PropB { get; set; }
  public BankEntry()
  {
    PropA=defaultValueA;
    PropB=defaultValueB;
  }
  public BankEntry SetA(int A)
  {
    PropA=A;
    return this;
  }
  public BankEntry SetB(string B)
  {
    PropB=B;
    return this;
  }
  public BankEntry ReadDb(IDbConnection db)
  {
    PropA=readAFromDb(db);
    PropB=readBFromDb(db);
    return this;
  }
}

...
{
  BankEntry be=new BankEntry().ReadDb(db).SetA(7).SetB("Akuku");
}
0

@somekind: Bo to ma być reprezentacja bazy. Przekazuje do modułu IDbConnection, ponieważ to ma służyć tylko do udostępnienia modelu oraz operacji na bazie, wraz z wczytaniem domyślnych wartości dla danego obiektu. Pobieranie danych byłoby już po stronie aplikacji, która z tych bilbiotek by korzystała. Cały problem polega na tym, że muszę jakoś ten obiekt zainicjować na podstawie konfiguracji całego systemu. Robiąc new BankEntry() musiałbym przekazać IDbConnection i klepać zapytania w klasie za pomocą get, set. Już raz tak zrobiłem i kiepsko to działało.

Przykład jak to robi Comarch w swoich bibliotekach

var kontrahenci = (Kontrahenci)session.CreateObject("CDN.Kontrahenci");
var kontrahent = (Kontrahent)kontrahenci.AddNew(); // <-- gotowy do zapisania do bazy z domyślnymi danymi
session.Save();

// nie da się
var kontrahent = new Kontrahent();

Przykład jak to robi Enova365

using (ITransaction trans = session.Logout(true))
{
  CRMModule crm = CRMModule.GetInstance(session);
  var kontrahenci = crm.Kontrahenci;

  Kontrahent k = new Kontrahent();
  kontrahenci.AddRow(k);

  trans.Commit();
}

@_13th_Dragon: Oj... nie ma szans. Takie coś nie przejdzie kompletnie. Tabela dokumentów ma ponad 200 kolumn, kontrahenci to samo. To będzie straszna klepanka i mam wrażenie, że ten sposób jest zdeczko przeinżynierowany, jeśli chodzi o ten projekt 🤔

2

Moim skromnym zdaniem, to co chcesz zrobić to proszenie się o problemy.
Piszesz bibliotekę w której zamykasz implementację tworzenia obiektu jakiegoś tam typu przed użytkownikiem i żądasz przy tym dostępu do bazy bo chcesz żeby biblioteka sama sobie pobrała odpowiednie dane i zbudowała ten obiekt.

A co się stanie jeśli baza klienta będzie odbiegać od tego co sobie założyłeś? Klient nie będzie mógł w żaden normalny sposób utworzyć danego obiektu i będzie musiał pisać jakieś diabelskie haki żeby to obejść. Twoja biblioteka powinna wystawiać API, które pozwala jej użyć w dowolny sposób. Dlaczego uważasz, że wystawienie odpowiednio sparametryzowanego konstruktora jest gorszym rozwiązaniem niż pchanie tam lDbConnection?
To raczej bardzo nierozsądne bo klient który będzie używać tej biblioteki będzie od niej bardzo zależny i to w najgorszy możliwy sposób. Dlaczego? Dlatego że jakakolwiek zmiana będzie wymagać grzebania w dokumentacji, dekompilacji lub kontaktu z autorem. I mnóstwa pracy po obu stronach.
Co zrobisz w sytuacji jeśli tej biblioteki będzie używać kilkadziesiąt różnych firm i wszystkie na pewnym etapie rozwoju swojego softu będą modyfikować strukturę bazy? Będziesz utrzymywać kilkadziesiąt wersji?

Oprócz tego brak jawnego kontraktu nie pozwala na sprawdzenie poprawności na etapie kompilacji, co również wydaje się być bardzo nieprzyjazne użytkownikowi.

edit: weryfikację czy stan obiektu odpowiada oczekiwanemu załatwia się walidacją a nie powiązaniem do źródła danych.
Wg mnie dużo lepszym rozwiązaniem będzie wystawienie tego co proponuje @somekind i dodanie odpowiedniej walidacji po stronie biblioteki.

0

To skąd mam wziąć połączenie do bazy, aby stworzyć domyślne wartości dla obiektu? Jak mam stworzyć ten obiekt poprawnie bez dostępu do bazy? Przy tworzeniu kontrahenta może nie będzie to takie trudne, ale takie płatności lub dokument, to już potrzebuje domyślną definicje dokumentu, w której jest numeracja (konfigurowalna, takich definicji może być kilka, konfigurowalna domyślna). Klient w każdej chwili może pozmieniać sobie numerację i ja muszę to pobrać z bazy, aby nowe dokumenty były z nową definicją. Domyślna kategoria sprzedaży, domyślne konto przeciwstawne itp itd... Tego jest masa i nie jestem w stanie tego zrobić po stronie samego obiektu.

Jak mam wymusić na użytkowniku podania/stworzenia obiektu jakieś właściwości, np ta definicja dokumentu

public class Document
{
  public string NumberString {get;set;} // FS-1/01/2018/@numerS
  public string FullNumber {get;set;} // FS-1/01/2018/3
  public int DocumentType {get;set;} // np.: 302 - brane z DocumentDefinition.Class
  public int DocumentDefinitionId {get;set;} // DocumentDefinition.Id
  public DocumentDefinition DocumentDefinition {get;set;} // ustawienie tego, przeliczałoby NumberString, FullNumber, DocumentType oraz DocumentDefinitionId
}

public class DocumentDefinition 
{
  public int Id {get;set;}
  public string Numeration {get;set;} // PRZYKŁAD @symbol/@miesiac/@rok_kal/@numerS/@brak <-- można konfigurować systemie ERP
  public int Class {get;set;}
  public string Name {get;set;}
  public bool isActive {get;set;}
}

Ja wiem jak to "policzyć" podpiąć pod obiekt, dlatego mam zrzucać to na użytkownika i zwiększać prawdopodobieństwo błędu?

Co masz na myśli mówiąc API? REST? Jak to ma mi niby pomóc jak ja chce tej biblioteki używać w usługach windows, winforms czy WebAPI + Angular?

1

API to z angielskiego application programming interface i nie zawsze wiąże się z REST.
Biblioteki też wystawiają jakieś publiczne API, po to właśnie aby klient który ich używał mógł z nich skorzystać.

To skąd mam wziąć połączenie do bazy, aby stworzyć domyślne wartości dla obiektu? Jak mam stworzyć ten obiekt poprawnie bez dostępu do bazy?

Nie wiem po co w ogóle biblioteka ma się łączyć z bazą. Obiekt możesz stworzyć bazując na tym co zostanie przekazane do konstruktora, fabryki, buildera czy czego tam sobie użyjesz. Ale niech to klient decyduje skąd pobiera dane potrzebne do utworzenia tego obiektu. Biblioteka, jako zewnętrzna zależność nie powinna definiować jak ma wyglądać baza klienta. Dlatego, że wtedy to biblioteka staje się szeroko pojętym driverem wymuszającym jakąś konfigurację która może zupełnie nie pasować do realiów danego projektu.
Wyobraź sobie że jesteś użytkownikiem tej biblioteki i chcesz zmienić nazwę kolumny w tabeli lub przeorganizować zupełnie bazę bo przestało ci się podobać trzymanie 200 kolumn w jednej tabeli. Albo w ogóle chcesz dostarczyć dane z innego miejsca. Używając twojego designu skazujesz użytkownika na to co sobie wymyśliłeś. A powinno być inaczej - to biblioteka powinna być elastyczna i nie interesować się tym skąd pochodzą dane.

Klient w każdej chwili może pozmieniać sobie numerację i ja muszę to pobrać z bazy, aby nowe dokumenty były z nową definicją.

Skoro to jest zmienne to dlaczego robisz to właśnie ty? Dlaczego klient nie może przekazać już gotowego numeru? Jeśli to z jakiegoś powodu niemożliwe to możesz w ramach interfejsu przyjąć fabrykę numerów w postaci delegatu np Func<T, string> gdzie klient przekaże wszystko co jest potrzebne do wyliczenia numeru włącznie z implementacją a biblioteka wygeneruje go w najbardziej odpowiednim momencie. Jeśli ERP dostarcza konfigurację którą biblioteka potrafi obsłużyć to dlaczego nie przekazać na wejściu tej konfiguracji zamiast pobierać jej samemu?

Jak mam wymusić na użytkowniku podania/stworzenia obiektu jakieś właściwości, np ta definicja dokumentu

Odpowiednio sparametryzowana metoda czy konstruktor oraz walidacja. Jeśli trzeba to przecież nic nie stoi na przeszkodzie abyś utworzył sobie całkiem rozbudowany typ, którego instancja zostanie przekazana do biblioteki a ty wtedy będziesz mógł zwalidować czy masz wszystko czego potrzeba, włącznie z obsługą walidacji warunkowej bazującej na stanie obiektu.

Ja wiem jak to "policzyć" podpiąć pod obiekt, dlatego mam zrzucać to na użytkownika i zwiększać prawdopodobieństwo błędu?

Ja nie do końca rozumiem dlaczego to właśnie ta biblioteka ma zapewniać implementację rzeczy które są zmienne.

0

Nie rozumiesz najważniejszego. Nikt nie zmieni żadnej kolumny, to jest Comarch Optima. Tworzę bibliotekę do pominięcia obiektów COM (.dll Optimy) i jednocześnie pominięcia dokupowania licencji, aby zrobić jakieś operacje w systemie. Biblioteka nie ma być elastyczna i nigdy nie powinna być. Biblioteka ma zastąpić .dll Comarchu, tak, aby nie klepać po raz setny tych samych modeli, serwisów, repozytoriów itp itd. Wszystko byłoby w jednym miejscu i tylko by się .dll podłączało do projektów. Na tę chwilę każdy mały, średni i duży projekt klepie od zera i to wszystko o czym teraz rozmawiamy (modele klas, serwisy, repozytoria, controllery itp itd) tworze/powielam z każdym nowym projektem.

Nie ma innej bazy, nie ma innych tabel, kolumn, jest i musi być 1:1 to co ma Comarch Optima. Może tytuł wątku nie jest odpowiedni co do "problemu" jaki próbuje rozwiązać. Z tego co widzę to najważniejszym punktem jest to, że robię to stricte pod Optimę i nic innego nie ma prawa działać z tą biblioteką (to też duże słowo moim zdaniem).

Dalszym krokiem będzie możliwość udostępnienia/sprzedania takiej biblioteki dzięki której inni partnerzy Comarch będą mogli tworzyć swoje dodatki i niestandardowe rozwiązania.

Na tę chwilę buduje to wszystko na podstawie swoich doświadczeń i tego jak ja piszę te wszystkie projekty. Szukam jeszcze jak taką bibliotekę stworzyć, aby było to w miarę mądrze ogarnięte. Jednym z elementów, na którym się zatrzymałem jest właśnie inicjalizacja obiektów, która wymaga bardziej skomplikowanej logiki. Oczywiście będzie można zmienić kluczowe wartości, to nie jest tak, że narzucam domyślne i koniec, zwyczajnie przygotowuje na start obiekt w taki sposób, aby szło go bezbłędnie zapisać do bazy.

1

W javie pewnie zrobiłbym dwa konstruktory jeden package protected dla fabryki, drugi publiczny i fabryke.
Wtedy jeżeli ktoś potrzebuje po swojemu utworzyć obiekt to musi zapewnić wszystko co potrzebuje obiekt do działania.
Jeśli używa standardowego flow przewidzianego przez ciebie to robi fabryka.utwórzMiObiekt(). I jeżeli fabryka w jakis inny sposob uzupełnia obiekt to zrobiłbym drugi konstruktor package protected.

Co do buildera. Zwykle buildery są straszne jeżeli jest więcej niż 5 propercji potrzebnych i tam imo sprawdzaja się genialnie wzorzec Step Builder który prowadzi cie za rączkę przez autocomplete :p. I jeżeli chcesz uzywac czesciej takiego obiektu to polecam :p.

0

No to mogę założyć, że moją "fabryką" jest moduł? Przecież zamysł jest ten sam 🤔 a builder odpada. Zdecydowanie za dużo tego jest żeby to w ten sposób obsłużyć

0

@AdamWox Czy dobrze rozumiem, że chcesz dodawać pozycje bezpośrednio do db z pominięciem obiektów COM?
Nie lepiej zrobić nakładkę na obiekty COM?

0

Mogę prosić o rozpiskę, co to jest "zrobić nakładkę na obiekty COM"? Bo chyba tak daleko nie zaszedłem w życiu...

0
AdamWox napisał(a):

Mogę prosić o rozpiskę, co to jest "zrobić nakładkę na obiekty COM"? Bo chyba tak daleko nie zaszedłem w życiu...

Wrapper, coś w stylu:
Masz klasę Optima, która w konstuktorze przyjmuje ścieżkę do programu ewentualnie inne rzeczy np. connectionstring oraz ma dostęp do sesji...

public class Optima
	{
	public Optima(string optima_path, string dbstring, ...){}
	
	public void Login(string login, string haslo, dictionary<Modul, bool> moduly){...}
	public void LogOut(){...}
	
	private AdoSession Sesja;
	
	public ModulKB KasaBank = new(Sesja, dbstring);
	public ModulKH Ksiegowosc = new(Sesja, dbstring);
	//...
	
	
	}

//Enums:
public enum Modul {KB, KH, KP, ...}
public enum TypPodmiotu {Kontrahent, Pracownik, Bank, Urzad}
 

Obiekt ModulKB to klasa, która ma dostęp do Rejestrów, Raportów, Preliminarza itd. Korzystanie z takiego wrappera wyglądałoby jakoś tak:

Optima optima = new(...);
optima.login(...);
optima.KasaBank.Raporty.Add(new Raport(...));
var raport = optima.KasaBank.Raporty.GetRaport(/*jakies warunki*/);
BankKp new_KP = new(123.12, TypPodmiotu.Kontrahent, "Akronim_podmiotu");
raport.AddKp(new_KP);

Przynajmniej ja coś takiego planowałem zrobić :). Korzystanie z takiego wrappera byłoby mega wygodne - dodawanie wszystkiego odbywałoby się w analogiczny sposób jak korzystanie z programu.

0

Czyli w dalszym ciągu ich COMy, ale opakowane we wrapper, który pewne kwestie upraszcza. Taki zabieg wymaga wolnej licencji, aby zrobić jakąkolwiek operację. Jeśli nie będzie wolnej licencji to program będzie działał w trybie do odczytu. I to rozumiem, bo pomysł ciekawy ale...

  1. Nie dla .NET Core (.NET 6, .NET 7) - działa to bardzo niestabilnie mimo, iż .NET wspiera COMy
  2. Jeśli masz kilka małych programów, które automatyzują parę rzeczy to nawet probranie danych będzie wymagało licencji, wole to pociągnąć z bazy i zapisać SQL'em
  3. Biblioteka będzie musiała pilnować wersji Optimy
0
AdamWox napisał(a):

Czyli w dalszym ciągu ich COMy, ale opakowane we wrapper, który pewne kwestie upraszcza. Taki zabieg wymaga wolnej licencji, aby zrobić jakąkolwiek operację. Jeśli nie będzie wolnej licencji to program będzie działał w trybie do odczytu. I to rozumiem, bo pomysł ciekawy ale...

No dobra, ale podobne rozwiązanie możesz zastosować bez obiektów COM. Wszystkie obiekty, które mogą wykonywać dowolne operacje na bazie danych muszą w jakiś sposób otrzymać connectionstring do tej bazy a konstruktorem wymuszasz, aby wszystkie wymagane pola były uzupełnione. Dodatkowo przed dodaniem czegoś do bazy danych robisz dodatkową weryfikację (np. czy kontrahent o podanym akronimie już nie istnieje).

0

Zgadza się. Jest jeszcze opcja wykorzystać rejestr, w którym zapisany jest aktualny "connectionString" do bazy konfiguracyjnej HKCU:\SOFTWARE\CDN\CDN Opt!ma\CDN Opt!ma\Login. Tam znajduje się wpis KonfigConnectStr z wartością NET:CDN_KNF_Konfiguracja,MOJE-PC\SQL,NT=1. Tylko problemem jest później dorwać dane do sa, bo jak parametr NT=0 to Optima gdzieś to zapisuje. Drugi minus jest taki, że jak ktoś ma kilka baz konfiguracyjnych, a mam takich klientów, to biblioteka będzie działać tylko w obrębie tego co w rejestrze. Jeśli wybranej firmy nie ma w CDN_KNF_Konfiguracja to rzuci błędem, że nie ma takiej firmy.

Jeśli ogarnęłoby się wpis z rejestru, to wtedy można tylko przekazać nazwę firmy, do której chcemy się połączyć. A nazwę bazy danych można pobrać z tabeli konfiguracyjnej CDN.Bazy i zbudować connectionString.

Co do walidacji, to też mam pewne przemyślenia, no bo i tak muszę tą walidację (przed zapisem do bazy) zrobić, więc ten wątek wstępnie nie miałby sensu, bo inicjalizacja obiektu w poprawny sposób jest tylko dodatkową opcją i ewidentnie stratą czasu. Nie ustawisz definicji dokumentu to rzuci błędem, podłączysz do dokumentu id kontrahenta 0, to rzuci błędem. Optima ma też masę triggerów, które w 50% są w stanie walidować to za mnie, ale i tak bym wolał wcześniej gdzieś poinformować o tym, jeszcze zanim poleci insert do bazy.

1
AdamWox napisał(a):

No to mogę założyć, że moją "fabryką" jest moduł? Przecież zamysł jest ten sam 🤔 a builder odpada. Zdecydowanie za dużo tego jest żeby to w ten sposób obsłużyć

Fabryka to klasa albo metoda, która umie wyprodukować jakiś obiekt z parametrów. Moduł to raczej zbiór klas. ;)

Ogólnie niezależnie od tego jakie rozwiązanie wybierzesz (tzn. jak opakujesz konstruktor), warto, aby konstruktor przyjmował wartości potrzebne dla klasy. Brak parametrów w konstruktorze sugeruje, że obiekt baz ustawionych wartości ma sens - a przecież nie ma.

0

Jeśli dobrze rozumiem, to konstruktorem miałbym załatwić kwestie wymuszenia na użytkowniku podania tego co potrzebne do zapisania obiektu? Czyli nie byłoby ani jednego "pustego" konstruktora? Jak to wpłynie na wyciąganie danych za pomocą jakiegoś ORM'a, na przykład Dapperem? Nie wymaga on pustego konstruktora? Będę zmuszony stworzyć jakieś dto do tych wszystkich encji tylko po to, aby móc pobrać dane z bazy?

1

No tak, do tego służy właśnie konstruktor. Jeśli nie będziesz miał nieprywatnego konstruktora niesparametryzowanego to za każdym razem będziesz musiał podać zestaw parametrów jakich oczekuje konstruktor mający zostać użyty.

Co do samego przypadku użycia: jeśli ta biblioteka ma współdziałać wyłącznie z Comarchem i jesteś pewien że zmiany na poziomie bazy danych to coś co nigdy nie nastąpi i użycie SqlConnection przekazanego od klienta jest w 100% bezpieczne to i tak lepiej całą czynność tworzenia obiektu rozdzielić na mniejsze składowe:

  1. pobranie odpowiednich danych z bazy
  2. przygotowanie tych danych w sposób jakiego oczekuje tworzenie instancji danego typu
  3. walidacja tego co zostało pobrane i przygotowane
  4. na poziomie samego typu którego obiekt ma zostać utworzony - dodanie odpowiednio sparametryzowanych konstruktorów czy metod statycznych

Walidacja i konstrukcja mogą być wykonane już na poziomie obiektu, ale czy warto robić to w ten sposób to już zależy. Jeśli walidacja jest złożona to warto wystawić wtedy osobny walidator. Taki podział bardzo ułatwia utrzymanie i testowanie.

0

Ok, to spróbuje to załatwić konstruktorami, ale mam jeszcze jedno pytanie. W przypadku zapisywania dokumentu, potrzebne jest kilka etapów. Osobne tabele są do VAT i do pozycji dokumentu. Obie tabele mają klucz obcy do dokumentu, czyli napierw muszę zrobić insert dla dokumentu, poźniej na podstawie towarów sprawdzić ile wspisów jest do tablei VAT (stawki VAT mogą być różne na pozycjach), wrzucić vat i pozycje.

Czy pozycje i vat też powinny być wrzucane przez konstruktor? Czy mogę zrobić sobię jakąś statyczną metodę AddProduct(), w której od razu wyliczyłbym to co ma iść do tabeli VAT? Jakoś mam dziwne wrażenie, że ten konstruktor będzie za bardzo napchany logiką. Czy pamięciowo/wydajnościowo wyjdzie na to samo - stworze obiekt i wpuszczę go przez konstruktor, czy stworze obiekt w klasie?

4

Ja bym podszedł do tego jeszcze inaczej.
Konstruktor + walidacja załatwia proces tworzenia obiektu. Czy musisz jednak strzelać do bazy już na tym etapie? Może lepszym wyjściem będzie trzymanie tego obiektu w pamięci. Wtedy, po jego utworzeniu będzie można dodawać do niego pozycje lub edytować go w inny sposób a dopiero na samym końcu, kiedy cały proces obsługi zostanie zakończony to cały ten dokument zostanie zapisany do bazy w transakcji.

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