Przejdź do treści

Przykłady użycia logiki biznesowej (od Neos 6.0)

Jak i kiedy pisać własne metody logiki biznesowej?

Własne metody logiki biznesowej piszemy po to aby, aby:

  • wyjść z logiki z bazy danych i przenieść ją na poziom C#. Dzięki czemu będziemy niezależni od silnika bazodanowego, gdyż w bazie danych zostaną jedynie struktury danych

  • uprościć pisanie logiki biznesowej. Chodzi o to aby logika biznesowa wysokiego poziomu wołała metody logiki biznesowej nieco niższego poziomu, a te jeszcze niższego, itp. Dzięki temu kod jest czytelniejszy i bardziej odporny na błędy.

Przykład

Podczas wdrożenia, we własnym projekcie neosowym chcemy programowo założyć dokument elektroniczny w module Ewidencji Dokumentów. Moduł Ewidencji Dokumentów jest częścią obszaru Workflow i Dokumenty reprezentowanym przez projekt neosowy WORKFLOW. W tym projekcie szukamy obiektów i metod fasady, gdyż to tych metod powinniśmy używać. Przeglądając obiekty biznesowe w folderze "api" projektu WORKFLOW, znajdziemy obiekty fasadowe DOCUMENT oraz ACTIVITY. Zatem nasz kod napiszemy następująco:

public void CreateOffer(int activitytype, string filename)
{
  //pobierz REF folderu lub go załóż, jeśli nie istnieje
  int folder = WORKFLOW.LOGIC.DOCUMENT.GetOrCreateFolder("Oferty");
  //załóż dokument w folderze
  ActivityCreateInfo aci = new ActivityCreateInfo() {
    Type = activitytype,
    Folder = folder,
    Name = filename,
  };
  ActivityInfo ai = LOGIC.ACTIVITY.Create(aci, false);
  if (ai != null)
  {
    //podłącz plik, który już jest wgrany do repozytorium
    LOGIC.ACTIVITY.AddFileAfterUpload(ai.Ref, filename, filename);
    //oznacz, że zakończono dodawanie aktywności
    LOGIC.ACTIVITY.AfterCreate(ai.Ref);
  }
}

Zauważmy, że pisząc kod w taki sposób jak w powyższym przykładzie, jest on prosty, czytelny i krótki. Każda linia kodu prezentuje jakąś czynność biznesową. Przyjrzyjmy się, wybranym metodom, które są tam użyte.

Przykład

W obiekcie fasadowym WORKFLOW.DOCUMENT istnieje poniższa metoda:

public static int GetOrCreateFolder(string name, int parentfolder = 0)
{
  return new LOGIC.WFFOLDER().GetOrCreateFolder(name,
  parentfolder).REF.Value;
}

Jak widać ona sama niewiele robi, jedynie wystawia do obiektu fasady metodę logiki innego obiektu - WFFOLDER. Jest to poprawne, gdyż:

  • programista-wdrożeniowiec przegląda jedynie obiekty fasadowe i nie musi przeszukiwać pozostałych kilkudziesięciu obiektów biznesowych w celu znalezienia odpowiednich metod

  • faktyczny kod logiki znajdzie się u źródła, czyli w obiekcie WFFOLDER, gdyż to ten obiekt biznesowy zna i steruje zachowaniem folderów modułu Ewidencji Dokumentów

Zajrzyjmy zatem głębiej do obiektu WFFOLDER.

Przykład

Metoda GetOrCreateFolder jest niestatyczną metodą logiki biznesowej obiektu WFFOLDER. Dekomponuje ona problem biznesowy na mniejsze, a więc sprawdza czy folder istnieje i należy go zwrócić, czy nie istnieje i należy go założyć.

public MODEL.WFFOLDER GetOrCreateFolder(string name, int parentfolder = 0)
{
  var folder = GetFolder(name,parentfolder);
  if(folder==null)
    folder = CreateFolder(name,parentfolder);
  return folder;
}

Wszystkie robocze metody logiki biznesowej, czyli takie, które rzeczywiście coś robią konkretnego, powinny być pisane jako niestatyczne metody logiki w tych obiektach, które merytorycznie są odpowiedzialne za dany obszar. Dzięki temu:

  • podobne metody gromadzimy w jednym obiekcie

  • unikamy dublowania tej samej funkcjonalności biznesowej w różnych obiektach

  • jeśli zmienimy struktury danych, to w jednym miejscu mamy zgromadzone wszystkie metody, w których może być wymagana refaktoryzacja.

Przykład

W obiekcie WFFOLDER istnieją metody robocze najniższego poziomu zajmujące się pobraniem istniejącego folderu z bazy danych (GetFolder) i założeniem nowego folderu (CreateFolder).

public MODEL.WFFOLDER GetFolder(string name, int parentfolder = 0)
{
  return new LOGIC.WFFOLDER.Get()
  .Where(x => x.NAME == name && (x.PARENT==parentfolder || x.PARENT==null))
  .FirstOrDefault();
}

public MODEL.WFFOLDER CreateFolder(string name, int parentfolder = 0)
{
  var wffolder = new TModel()
  {
    NAME = name,
    PARENT = parentfolder
  };
  Create(wffolder);
  return wffolder;
}

W powyższych metodach wykorzystano techniki opisane w poprzednich przykładach. W metodzie GetFolder() wykorzystano wyrażenie LINQ, a w metodzie CreateFolder powołano nowy model i wewnętrzną metodę Create. Warto zauważyć, że metoda logiki biznesowej, która powołuje model własnego obiektu może użyć klasy TModel, która w każdym obiekcie biznesowym reprezentuje klasę modelu danych tego obiektu. Z tego względu typu TModel używamy jedynie w obrębie obiektu. Nie przekazujemy tej klasy między obiektami, bo dla każdego obiektu, to jest inna klasa.

Warto zauważyć ponadto, że w metodzie GetFolder na nowo powołano obiekt logiki LOGIC.WFFOLDER mimo, że ta metoda jest napisana wewnątrz obiektu WFFOLDER i wywołanie this.Get() także się poprawnie skompiluje. W poniższej tabeli zebrano rozróżnienie, kiedy zaleca się powoływać nowy obiekt logiki, a kiedy działać na bieżącym.

new LOGIC.OBIEKT().Get() this.Get()
Piszemy tak wtedy, kiedy obiekt może być użyty interfejsowo, czyli może być pokazane okno tego obiektu z jakąś zawężoną dziedziną danych, a wywoływana metoda logiki biznesowej powinna działać niezależnie od kontekstu tych danych. Czyli w oknie widzimy 10 rekordów z tabeli, w której jest 1000 rekordów, bo taka dziedzina wynika np z zawężenia metodą na filtrację. Następnie wywołujemy metodę logiki: Logic.Metoda() i oczekujemy, że wywołana metoda przeiteruje po wszystkich 1000 rekordach. Piszemy tak wtedy, kiedy obiekt może być użyty interfejsowo, czyli może być pokazane okno tego obiektu z jakąś zawężoną dziedziną danych, a metoda logiki powinna przeiterować właśnie po tej dziedzinie widocznej na oknie, lub po pewnym podzbiorze tych danych. Piszemy tak także wtedy, kiedy obiekt biznesowy nigdy nie będzie używany interfejsowo, bo nie ma i nie będzie żadnego okna zdefiniowanego na tym obiekcie, a metoda logiki ma działać na pełnej dziedzinie danych.

Jak korzystać z dostępu do bazy danych w warstwie logiki biznesowej?

Obiekty logiki biznesowej oraz źródła danych (np. WORKFLOW.LOGIC.WFACTIVITY, LOGIC.WFFOLDER, WORKFLOW.WFACTIVITY) korzystają z zasobów, które powinny być zwolnione zaraz po użyciu. Zaleca się zatem tworzenie tych obiektów w bloku using, aby były one automatycznie zamykane i nie powodowały narastania zużycia pamięci przez aplikację. Jest to szczególnie istotne w metodach wystawianych jako webserwisowe (WebAPI) — wywoływanych często z zewnątrz — gdzie brak poprawnego zwalniania zasobów może w krótkim czasie prowadzić do nadmiernego zużycia pamięci.

Jeśli pracujesz na oknach stworzonych w neosie, to korzystanie z logiki biznesowej odbywa się automatycznie. Dodawanie, poprawianie i usuwanie rekordów z poziomu okien zawsze przechodzi przez warstwę logiki biznesowej, a więc uruchomi opisane wyżej "triggery" w C#. Ponadto jeśli powołasz obiekt biznesowy programowo i dokonasz na nim zapisu przez wywołanie metody PostRecord(), on także przejdzie przez warstwę logiki biznesowej, a więc wykona kod "triggerów" w C#.

Przykład

pokazuje programowy sposób operowania na danych dla Neosa starszego niż 6.0.

public void SetDescription4Activity(int activityref, string descript)
{
  using (var activities = new WORKFLOW.WFACTIVITY())
  {
    activities.FilterAndSort("REF=" + activityref.ToString());
    if(activities.FirstRecord())
    {
      activities.EditRecord();
      activities.DESCRIPT = descript;
      activities.PostRecord();
    }
  }
}

Od wersji 6.0 ten sposób także działa i kod się kompiluje, natomiast nie jest to zalecana metoda programowego operowania na źródłach danych. Poniżej napisano kilka przykładów pisania kodu w metodach logiki biznesowej dla neosa 6.0.

Przykład

To kopia powyższej metody, ale z wykorzystaniem logiki biznesowej.

public void SetDescription4Activity(int activityref, string descript)
{
  using (var activity_logic = new WORKFLOW.LOGIC.WFACTIVITY())
  {
    var activity = activity_logic.Get(activityref);
    if(activity!=null)
    {
      activity.DESCRIPT = descript;
      activity_logic.Update(activity);
    }
  }
}

Główne różnice są następujące:

  • zamiast powoływać nowy obiekt źródła danych powołujemy nowy obiekt logiki biznesowej tego samego obiektu

  • aby pobrać rekord o zadanej wartości pól klucza głównego, wykorzystujemy prostą metodę Get(int), która zwraca obiekt modelu (TModel). Klasa modelu jest generowana automatycznie i zawiera wszystkie pola modelu danych.

  • zamiast pary EditRecord() / PostRecord() wywołujemy metodę Update(TModel) na obiekcie logiki

Przykład

To programowy sposób na dodanie nowego rekordu w kodzie logiki biznesowej:

public int CreateActivity(string name, int activitytype, string descript="")
{
  using (var activity_logic = new WORKFLOW.LOGIC.WFACTIVITY())
  {
    var activity = new WORKFLOW.MODEL.WFACTIVITY()
    {
      NAME = name,
      DESCRIPT = descript,
      WFACTIVITYTYPE = activitytype //REF typu aktywności
    };
    activity_logic.Create(activity);
    return activity.REF.Value;
  }
}

Jak widać, kod jest podobny do przykładu nr 2. Przy czym aby dodać nowy rekord powoływany jest nowy obiekt modelu klasy modelu. Dodanie tego rekordu do bazy danych odbywa się poprzez wywołanie metody Create(TModel) na obiekcie logiki.

Przykład

To programowy sposób na usunięcie rekordu w kodzie logiki biznesowej:

public void DeleteActivity(int activityref)
{
  using (var activity_logic = new WORKFLOW.LOGIC.WFACTIVITY())
  {
    var activity = activity_logic.Get(activityref);
    if(activity!=null)
    {
      activity_logic.Delete(activity);
    }
  }
}

Przykład

Usunięcie rekordu można zrobić prościej. Jeśli znamy wartość pola klucza głównego, to obiektu modelu nie trzeba w ogóle pobierać funkcją Get().

public void DeleteActivity(int activityref)
{
  using (var activity_logic = new WORKFLOW.LOGIC.WFACTIVITY())
  {
    var activity = new WORKFLOW.MODEL.WFACTIVITY();
    activity.REF = activityref;
    activity_logic.Delete(activity);
  }
}

Przykład

Pobranie i przetwarzanie zbioru rekordów. Metoda bezparametryczna Get() wywołana na obiekcie logiki pozwala zwrócić kolekcję wszystkich rekordów ze źródła danych zadanego obiektu biznesowego, czyli z tabeli lub zapytania SQL.

public List<MODEL.WFFOLDER> GetAllFolders()
{
  var list = new List<MODEL.WFFOLDER>();
  using (var wffolder = new LOGIC.WFFOLDER())
  {
    foreach(var f in wffolder.Get())
    {
      list.Add(f);
    }
  }
  return list;
}

Przykład

Powyższą metodę można napisać prościej, bez iteracji.

public List<MODEL.WFFOLDER> GetAllFolders()
{
  using (var wffolder = new LOGIC.WFFOLDER())
  {
    return wffolder.Get().ToList();
  }
}

Oba przykłady (6 i 7) dokonują materializacji wyniku. To znaczy, że zapytanie SQL jest zadawane wewnątrz metody GetAllFolders() w momencie jej wywołania i jako wynik otrzymujemy listę rekordów otrzymanych z bazy danych w momencie wywołania tej funkcji.

Przykład

Listę rekordów można ograniczyć i posortować. Nie trzeba w tym celu pisać wyrażenia SQL. Stosujemy wyrażenia LINQ, które są automatycznie zamieniane na odpowiednie frazy zapytania SQL. W wyniku takiego wyrażenia, do bazy danych dociera zapytanie z klauzulami WHERE i ORDER BY, a więc filtrowanie i sortowanie danych dzieje się po stronie bazy danych.

public List<MODEL.WFFOLDER> GetChildrenOf(int wffolderref)
{
  using (var wffolder = new LOGIC.WFFOLDER())
  {
    return wffolder.Get()
      .Where(x => x.PARENT == wffolderref)
      .OrderBy(x => x.NAME)
      .ToList();
  }
}

Jeśli nie jesteśmy pewni, czy utworzone zapytanie SQL na pewno jest poprawne, możemy na wyniku wyrażenia LINQ wykonać metodę ToString() i skierować taki wynik np do DEBUG.Log(). Metoda ToString() zwraca właśnie wyrażenie SQL wygenerowane przez sumę wszystkich wyrażeń LINQ.

Przykład

Powyższą funkcję można zapisać nieco prościej, bez budowania lokalnej listy wyników. W tym celu zamieniamy zwracany typ danych na IEnumerable oraz stosujemy wyrażenie "yield return" działające podobnie jak "suspend" w SQL.

public IEnumerable<MODEL.WFFOLDER> GetChildrenOf(int wffolderref)
{
  using (var wffolder = new LOGIC.WFFOLDER())
  {
    foreach(var f in wffolder.Get()
      .Where(x => x.PARENT == wffolderref)
      .OrderBy(x => x.NAME))
    {
      yield return f;
    }
  }
}

Taka zmiana wpływa też na moment materializacji wyniku. Jeśli wewnątrz nie zwracamy listy (List), tylko IEnumerable, to zapytanie SQL nie jest wykonane w metodzie GetChildrenOf, lecz dopiero na zewnątrz, podczas iteracji po zwróconej kolekcji. Jeżeli nie będzie żadnej iteracji, żadne zapytanie nie zostanie wysłane do bazy danych.

Przykład

Niezależnie od tego, w jaki sposób została napisana metoda GetChildrenOf (przykłady 8,9,10) korzystamy z niej w taki sam sposób, poprzez iterację po kolekcji zwracanej przez tą metodę, tak jak poniżej. Jeśli piszemy kod metody interfejsowej wewnątrz obiektu WFFOLDER, to wywołujemy ją tak:

...
foreach(var f in Logic.GetChildrenOf(this.REF.AsInteger))
{
  ...
}

Jeśli piszemy taki kod w innym obiekcie, a metoda GetChildrenOf jest publiczna, to piszemy następująco:

...
using (var wffolder = new LOGIC.WFFOLDER())
{
  foreach(var f in wffolder.GetChildrenOf(this.REF.AsInteger))
  {
    ...
  }
}

Nie musimy się przejmować częstym i licznym powoływaniem nowych obiektów logiki biznesowej. Narzut czasowy i pamięciowy jest niewielki, gdyż sam obiekt logiki jest tworzony szybko i nie przechowuje danych. Same zapytania do bazy danych są generowane najpóźniej, jak się da, czyli dopiero podczas iteracji po wyniku.

Warning

W przypadku wykorzystania logiki biznesowej do dostępu do danych z obiektu opartego o zapytanie SQL, należy pamiętać o aliasowaniu pól modelu danych niepochodzących z głównej tabeli. Więcej informacji tutaj

Tworzenie logiki biznesowej w kodzie na przykładzie modułu Planowania Produkcji

Moduł Planowania Produkcji w Teneum jest przykładem projektu w technologii Neos, w którym znaczna część logiki biznesowej została napisana w C#. Warto się z tym zapoznać oglądając film ze szkolenia wewnętrznego.