Złożoność cyklomatyczna

Jednym z wcześniejszych postów przedstawiłem aplikację nDepend obliczającą przeróżne metryki kodu. Większość metryk obliczanych przez program jest oczywista (liczba linii kodu, procent komentarzy, liczba klas itp). Na solidny komentarz zasługuje jednak złożoność cyklomatyczna (w skrócie CC).

Aby obliczyć CC, należy najpierw należy narysować graf przepływu informacji dla badanego kodu. Złożoność można obliczać zarówno dla konkretnej metody jak i całego systemu. Rozpatrzmy następujący fragment kodu:

private int Method1()
{
  while (exit_condition == false)
  {
      if (condition1 == true)
      {
          if (condition2 == false)
          {
              //do something
          }
          else
          {
              //do something
          }
      }
      else
      {
          //do something
      }
  }
  return result;
}

Kod zawiera jedną pętle oraz kilka rozgałęzień. Graf przepływu informacji pokazuje w jakiej kolejności poszczególne instrukcję są wykonywane:

image

Na grafie zaznacza się miejsca rozgałęzień. Jeśli kilka operacji jest wykonywanych sekwencyjnie (jedna po drugiej) wystarczy zaznaczyć tylko jeden węzeł. Mając narysowany graf można przejść do obliczenia CC za pomocą:

M = E − N + 2P,

gdzie:

M – złożoność cyklomatyczna,

E – liczba krawędzi grafu,

N – liczba węzłów grafu,

P – liczba grafów spójnych.

Graf spójny to taki graf w którym dla dowolnych dwóch węzłów istnieje ścieżka. Jeśli badamy kompletnie niezależne od siebie  3 metody, wtedy będą występować 3 grafy spójne. Dla pojedynczej metody zawsze P jest równe 1.

W przypadku powyższego grafu CC wynosi więc:

M = 10 – 8 + 2 = 4

CC jest o tyle przydatna, że określa dokładnie dozwolone przedziały wartości:

Przedział

Skutek

1-10

Kod bardzo prosty, nie stwarza ryzyka związanego z dalszymi rozszerzeniami systemu.

11-20

Kod dość złożony jednak wszelkie modyfikacje wciąż możliwe.

21-50

Kod wysoce skomplikowany.

powyżej 50

Ekstremalnie źle napisany kod. Wszelkie modyfikacje oraz poprawy błędów ponoszą ze sobą ogromne koszty.

 

Na koniec wróćmy jeszcze do grafu. Powiedziałem, że liczba operacji sekwencyjnych nie ma znaczenia. Dopiszmy więc kilka węzłów:

image

Wartość wynosi wciąż 4, M=13 – 11 + 2 = 4. Oznacza to, że kod napisany w mało elegancki sposób może mieć “dobrą” wartość CC. Wniosek z tego jest taki, że w celu oceny kodu należy obliczyć różne metryki i podejmować decyzje o refaktoryzacji na podstawie wielu kryteriów a nie tylko CC.

Własna baza danych oraz ASP .NET Membership

Domyślnie dane o użytkownikach przetrzymywane się w osobnej bazie danych aspnetdb (SQL Server Express). Czasami jednak warto aby wpisy o użytkownikach były zapisywane w naszej bazie danych. Wyobraźmy sobie portal społecznościowy, w którym użytkownicy mogą umieszczać zdjęcia. W takim przypadku w bazie danych z pewnością będzie tabela łącząca wysłane fotografie z użytkownikami. Jeśli użytkownicy są w jednej bazie danych mamy łatwą kontrolę nad relacjami – wystarczy użyć klucza obcego wskazującego na klucz główny użytkownika. Ponadto często hosting ASP .NET nie wspiera lokalnych baz aspnetdb i jedynym sposobem jest umieszczenie danych membership w głównej bazie:

  1. Pierwszym krokiem jest wygenerowanie odpowiednich tabel oraz procedur (tych, które znajdują się w aspnetdb). Służy do tego narzędzie Aspnet_regsql.exe, które możecie znaleźć w katalogu c :\%windir%\Microsoft.NET\Framework\<versionNumber>. Po odpaleniu programu należy wskazać docelową bazę danych, typ uwierzytelnia itp. Kreator większość zrobi za nas – na koniec tego etapu struktura bazy danych powinna zostać wygenerowana.

    image

  2. W celu upewnienia się, można otworzyć bazę danych i obejrzeć jej strukturę. Widać, że zostały wygenerowane tabele:

    image

    1. Kolejnym krokiem jest dodanie odpowiedniego connectionString’a w pliku web.config:

      <connectionStrings>
          <add name="MembershipTestDB" connectionString="Data Source=PIOTR-PC;Initial Catalog=MembershipTest;Integrated Security=True;MultipleActiveResultSets=True" providerName="System.Data.SqlClient" />
      </connectionStrings>
      

    2. Mamy już skonfigurowaną bazę danych. Pozostało już tylko skonfigurować aplikację tak aby korzystała z tej bazy danych:
      <membership>
              <providers>
                  <clear/>
                  <add name="AspNetSqlMembershipProvider" 
                      connectionStringName="MembershipTestDB" 
                      applicationName="/"
      type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
              </providers>
          </membership>
      
      <roleManager enabled="true">
        <providers>
          <clear/>
          <add name="AspNetSqlRoleProvider"
              connectionStringName="MembershipTestDB"
              applicationName="/"
              type="System.Web.Security.SqlRoleProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"/>
        </providers>
      </roleManager>
      
      

    Walidacja kodu źródłowego, nDepend

    Poprawność kodu powinna być sprawdzana na każdym etapie produkcji oprogramowania. Do dyspozycji są wszelkie typy testów (jednostkowe, integracyjne obciążenia itp.). Wszystkie wymienione testy sprawdzają jednak czy kod wykonuje swoje zadanie w oczekiwany sposób. Nie sprawdzają jakości napisanego kodu. Testy nie wykażą, że dany kod jest napisany w sposób mało elegancki. Jednym z najskuteczniejszych rozwiązań jest tzw. code review (recenzjonowanie kodu). Polega to na tym, że dany fragment kodu jest przeglądany i analizowany przez różnych programistów. Każdy z nich ocenia czy kod jest napisany w sposób poprawny, umożliwiający dalsze rozszerzenia. Moi zdaniem jest to najlepsze rozwiązanie jednak nie zawsze możliwe i praktykowane. Dlatego w poście przedstawię rozwiązanie czysto matematyczne – metryki oprogramowania.

    Doskonałym narzędziem obliczającym przeróżne metryki kodu jest nDepend. Za darmo można ściągać wersję akademicką, trial lub przeznaczoną wyłącznie do projektów typu Open-Source. Ponadto program integruje się z Visual Studio (również VS 2010), znacząco ułatwiając stałą kontrolę nad kodem. W tym poście nie będę zajmować się opisem konkretnych metryk. Wszystkie metryki opisane są w dokumentacji dołączonej do nDepend. W przyszłych postach planuje opisać tylko kilka, według mnie najciekawszych metryk (m.in. złożoność cyklomatyczna).

    Aby zintegrować nDepend z Visual Studio należy odpalić plik NDepend.Install.VisualStudioAddin.exe:

    image

    Po instalacji w VS pojawi się dodatkowe menu:

    image

     

    Na początek przeprowadźmy analizę kodu – wybieramy nDepend->Run Analysis. Po chwili pojawi się wygenerowany raport w formie pliku html. Raport przedstawia wiele metryk, podzielonych na kategorie:

    1. Metryki aplikacji – bardzo ogólne, głównie informacyjne. Określają ile w projekcie jest bibliotek, klas, linii kodu itp.
    2. Metryki bibliotek – wartości liczone dla każdej bibliotek w projekcie.
    3. Visual NDepend View – wizualne przedstawienie bibliotek oraz metod zawartych w projekcie. Im większe pole, tym większa biblioteka:image
    4. Abstrakcyjność i niestabilnośc – wizualne przedstawienie metryki “Distance from main sequence”.
    5. Zależności między bibliotekami.
    6. Lista złamanych metryk oraz zasad (np. gdy metoda zawiera za mało komentarzy).
    7. Metryki typów (klasy, typy enumeryczne).

    Ponadto program umożliwia łatwe znalezienie konkretnej metody za pomocą języka zapytań. Przykładowo aby znaleźć metodę o nazwie SubmitOrder wystarczy wpisać:

    SELECT TYPES WHERE (NameLike "SubmitOrder\i") AND !IsInFrameworkAssembly

    Za pomocą zapytać CQL można wyszukać metody dla których dana metryka posiada konkretną wartość lub określony zbiór wartości.

    nDepend umożliwia również wygenerowanie macierzy zależności:

    image

    Polecam zainstalowanie programu i przejrzenie jego możliwości. Oczywiście żadne metryki nie zastąpią ciągłej kontroli kodu przez człowieka ale mogą znacząco wpłynąć na jakość pisanego kodu.

    Windows Workflow Foundation – persystencja stanu

    Czasami istnieje potrzeba zapisania stanu aktualnie wykonywanego workflow’a. Domyślnie wszelkie dane zapisywane są w pamięci ulotnej. W przypadku awarii komputera odtworzenie ostatnio wykonywanego stanu jest niemożliwe. Nie jest to problem w przypadku gdy wykonanie workflow’a zajmuje tylko kilka sekund. W sytuacji w której zakończenie workflow’a może potrwać kilka godzin lub tygodni, niezbędne jest zapisywanie informacji w pamięci trwałej np. w bazie danych SQL Server.

    WWF dostarcza specjalną usługę, SqlWorkflowPersistenceService, która większość brudnej roboty wykona za nas. Dzięki usłudze nie musimy kłopotać się z ręcznym projektowaniem struktury bazy danych i zapisem stanu. Wszystko wykona za nas gotowa usługa. Naszym zadaniem jest jedynie odpalenie skryptów generujących bazę danych oraz podpięcie usługi do środowiska uruchomieniowego.

    Pierwszym etapem jest zatem utworzenie bazy danych:

    1. Otwieramy SQL Server Management Studio. W okienku wykonywania skryptów wpisujemy zapytanie generujące pustą bazę danych:
      CREATE DATABASE WorkflowPersistenceStore
      

    2. Następnie musimy stworzyć niezbędne tabele, procedury oraz schema. Wystarczy odpalić skrypt SqlPersistenceService_Logic.sql oraz SqlPersistenceService_Schema.sql. Oba znajdują się w folderze “%WINDIR%\Microsoft.NET\Framework\v3.0\Windows Workflow Foundation\SQL\<language>\SqlPersistence_Schema”. W zależności od konfiguracji systemowej może to być np. ścieżka “C:\Windows\Microsoft.NET\Framework\v3.0\Windows Workflow Foundation\SQL\en”.

    Kolejnym krokiem jest konfiguracja aplikacji:

    1. Otwieramy plik app.config  lub web.config i wstawiamy nową sekcję dla workflow:
      <configSections>
          <section
           name="PersistenceRuntimeExample"
            type="System.Workflow.Runtime.Configuration.WorkflowRuntimeSection, 
                  System.Workflow.Runtime, Version=3.0.00000.0, 
                 Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
       </configSections>

    2. Dodajemy usługę persystencji oraz parametr zawierający connectionstring. Plik konfiguracyjny powinien zatem zawierać poniższy kod:

      <?xmlversion="1.0"encoding="utf-8" ?>
      <configuration>
       
       <configSections>
          <section
           name="PersistenceRuntimeExample"
            type="System.Workflow.Runtime.Configuration.WorkflowRuntimeSection, 
                  System.Workflow.Runtime, Version=3.0.00000.0, 
                 Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
       </configSections>
       
       <PersistenceRuntimeExample>    
          <CommonParameters>
            <add name="ConnectionString"value="Initial Catalog=WorkflowPersistenceStore;Data Source=localhost;Integrated Security=SSPI;" />
          </CommonParameters>
          
          <Services>
            <add
              type="System.Workflow.Runtime.Hosting.SqlWorkflowPersistenceService,
                    System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral,
                    PublicKeyToken=31bf3856ad364e35"
                    UnloadOnIdle="true"/>
          </Services>
       </PersistenceRuntimeExample>
       
      </configuration>

    3. Środowisko uruchomieniowe będzie skonfigurowane automatycznie po przekazaniu nazwy sekcji:
      WorkflowRuntime runtime = new WorkflowRuntime("PersistenceRuntimeExample");

    4. Od tej chwili stan workflow’a jest zapisywany w bazie danych SQL Server.

    Alternatywnym ale mniej elastycznym podejściem jest dołączenie usługi persystencji za pomocą kodu:

    WorkflowRuntime runtime = new WorkflowRuntime();
    SqlWorkflowPersistenceService persistenceService = new SqlWorkflowPersistenceService("Initial Catalog=WorkflowPersistenceStore;Data Source=localhost;Integrated Security=SSPI;");
    runtime.AddService(persistenceService);

    Odradzam jednak powyższy sposób. Dołączenie usługi za pomocą pliku konfiguracyjnego pozwala na modyfikacje connectionstring’a bez potrzeby ponownej kompilacji co stanowi ogromną zaletę.

    ASP .NET Membership w WinForms.

    ASP .NET Membership jest bardzo dobrym rozwiązaniem dla uwierzytelnienia oraz autoryzacji użytkowników w aplikacji webowej. Umożliwia m.in. zarządzanie kontami użytkowników czy tworzenie ról. Często jednak system składa się również z aplikacji typu desktop. W rozbudowanych systemach proponuję zrezygnować z czystego ASP .NET Membership na rzecz Windows Identity Framework. Jeśli jednak tworzymy małą aplikacje z pewnością przyda nam się wsparcie ASP .NET Membership w WinForms.

    Rozwiązaniem problemu są tzw. client application services. Zacznijmy jednak od samego początku:

    1. Najpierw tworzymy oczywiście aplikację web ASP .NET.
    2. Oprócz autoryzacji istnieje również możliwość udostępniania właściwości aplikacji WinForms. Dodajemy przykładową właściwość:
      <profile>
        <properties>
            <add name="Color" type="string" defaultValue="Blue"/>
          </properties>
      </profile>
      ...
      </system.web>

    3. W celu udostępnienia właściwości Color wystarczy w w web.config dodać następujący wpis:
      <system.web.extensions>
          <scripting>
              <webServices>
                  <authenticationService enabled="true" />
                  <roleService enabled="true"/>
                  <profileService enabled="true" readAccessProperties="Color" writeAccessProperties="Color"/>
              </webServices>        
          </scripting>
      </system.web.extensions>

    Powyższy kod również udostępnia usługę uwierzytelnienia oraz zarządzania rolami.

    Następnym etapem jest konfiguracja aplikacji WinForms:

    1. Tworzymy aplikacje typu WindowsForms.
    2. Wchodzimy we właściwości projektu a konkretnie w zakładkę Services. Następnie ustawiamy prawidłowy adres. Jeśli zakłada nie jest aktywna to prawdopodobnie  został ustawiony .NET Framework Client Profile i należy ustawić np. NET Framework 4.0.

      image 

    3. Wchodzimy w zakładkę Settings oraz klikamy w przycisk Load Web Settings. Po wpisaniu loginu i hasła zostaną zwrócone udostępnione właściwości:

    image 

    Od tej pory w kodzie aplikacji WinForms możemy odwoływać się do właściwości jak do zwykłych zasobów, np:

    public Form1()
    {
      InitializeComponent();
      if(Login())
        this.BackColor = Color.FromName(Properties.Settings.Default.Color) ;
    }
    private bool Login()
    {
      bool result = System.Web.Security.Membership.ValidateUser("admin", "password");
      if (result)
          MessageBox.Show("Zalogowano.", "Autoryzacja", MessageBoxButtons.OK,MessageBoxIcon.Information);
      else
          MessageBox.Show("Nieprawidłowy login lub hasło.", "Autoryzacja", MessageBoxButtons.OK,MessageBoxIcon.Error);
    
      return result;
    }

    Podobnie można zapisywać właściwość:

    Properties.Settings.Default.Color = Color.Red.ToString();
    Properties.Settings.Default.Save();

    W celu przekonania się, że to naprawdę działa ustawmy kolor również w aplikacji web:

    protected void Page_Load(object sender, EventArgs e)
    {                                        
      Button1.BackColor = System.Drawing.Color.FromName(Context.Profile.GetPropertyValue("Color").ToString());
    }

    Oczywiście aby kod zadziałał użytkownik musi być zalogowany (np. za pomocą gotowej kontrolki Login).

    W podobny sposób można używać metody InRole (sprawdzanie czy użytkownik należy do danej grupy). Warto podkreślić, że za pomocą client application service można korzystać z Membership również w aplikacjach WPF, AJAX czy nawet Silverlight.

    Aplikacja kliencka – Windows Workflow Foundation

    W jednym z ostatnich postów pokazałem jak stworzyć aplikację WWF na przykładzie prostego Sequential Workflow. Dzisiaj zajmiemy się wykorzystaniem stworzonej biblioteki w aplikacji klienckiej. Warto najpierw ściągnąć kompletny kod źródłowy ponieważ nastąpiła jedna drobna zmiana w implementacji workflow.

    Pierwszym etapem jest oczywiście stworzenie aplikacji np. WPF lub Connsole. Następnie należy dodać referencje do skompilowanej biblioteki zawierającej WWF. Ponadto jest jeszcze potrzebna biblioteka System.Workflow.Runtime, System.Workflow.ComponentModel oraz System.Workflow.Activities.

    W celu odpalenia workflow trzeba najpierw stworzyć tzw. środowisko uruchomieniowe:

    System.Workflow.Runtime.WorkflowRuntime runtime = new System.Workflow.Runtime.WorkflowRuntime();
    runtime.StartRuntime();

    W poprzednim poście został zdefiniowany specjalny interfejs dla usługi:

    [ExternalDataExchange]
    public interface IService
    {
       void NotifyUser(string message);
       void Approve();        
    }

    Przykładowa implementacja usługi może wyglądać następująco:

    class Service:SimpleWorkflowLibrary.IService
    {
       private Guid m_InstanceId;
    
       public Service(Guid instanceId)
       {
           m_InstanceId = instanceId;
       }                 
    
       #region IService Members
    
       void SimpleWorkflowLibrary.IService.NotifyUser(string message)
       {
           Console.WriteLine(message);
       }
    
       void SimpleWorkflowLibrary.IService.Approve()
       {
           // tutaj powinien znajdować się kod walidacyjny
       }
    
       #endregion
    }

     

    Usługa służy do komunikacji między workflow a aplikacją kliencką. Przykładowo stworzony workflow wywołuje metodę NotifyUser (powiadomienie użytkownika) oraz Approve (przeprowadzenie walidacji).

    Kolejnym krokiem jest inicjalizacja workflow, przekazanie niezbędnych parametrów oraz dodanie stworzonej usługi:

    Dictionary<string, object> parameters = new Dictionary<string, object>();
    parameters.Add("Number", number);
    
    System.Workflow.Runtime.WorkflowInstance instance= runtime.CreateWorkflow(typeof(SimpleWorkflowLibrary.Workflow1),parameters);
    System.Workflow.Activities.ExternalDataExchangeService externalService = new System.Workflow.Activities.ExternalDataExchangeService();            
    runtime.AddService(externalService);
    Service service = new Service(instance.InstanceId);
    
    externalService.AddService(service);
    instance.Start();            

    Parametry workflow przekazuje się w metodzie CreateWorkflow za pomocą słownika. Następnie należy dodać usługę Service, zaimplementowaną w poprzednim kroku. Po wywołaniu Start, workflow jest przetwarzany.

    Jeśli coś jest niezrozumiałe to najlepiej przejrzeć kod, który znajduje się tutaj lub zapytać (za pomocą komentarza na blogu):).

    Mapowania między DTO a obiektami biznesowymi

    Klasycznym sposobem mapowania DTO na obiekt biznesowy jest użycie wzorca projektowego adapter. Przykładowo aby zmapować Order do OrderDto możemy napisać następującą klasę:

    class OrderDtoAdapter
    {
       private Order m_Order = null;
       public OrderDtoAdapter(Order order)
       {
           m_Order = order;
       }
       public void Initialize(Order orderDto)
       {
           orderDto.CreationDate = m_Order.CreationDate;
           orderDto.Client = m_Order.Client;
           orderDto.Id = m_Order.Id;
       }
    }
    

    Rozwiązanie całkowicie poprawne jednak bardzo czasochłonne. Napisanie osobnej klasy dla każdej pary DTO-obiekt biznesowy wymaga sporo czasu. Na szczęście istnieje biblioteka AutoMapper, którą można wykorzystać. Generalnie przepisuje ona każdą właściwość z jednej klasy do drugiej. Bibliotekę możecie ściągnąć  z Codeplex’a.

    Spróbujmy zmapować jakiś obiekt. Załóżmy, że obiekt biznesowy wygląda następująco:

    public class Order : BaseDomainModel.BusinessObjects.DomainModel, EnterpriseStateMachine.Orders.IOrderService
    {
       private BaseDomainModel.Workflow.StateMachineStarter m_StateMachineStarter = null;
       
       public Order()
       {
           OrderItems = new List<OrderItem>();
           CreationDate = DateTime.Now;
           m_StateMachineStarter = new BaseDomainModel.Workflow.StateMachineStarter(this);
       }
       virtual public DateTime CreationDate { get; set; }
       virtual public PaymentType PaymentType { get; set; }
       virtual public IList<OrderItem> OrderItems { get; set; }
       virtual public string Status { get; set; }
       virtual public CrmDomainModel.Clients.Client Client { get; set; }
    
       
       private Dictionary<string, object> GetStateMachinePars()
       {
           Dictionary<string, object> parameters = new Dictionary<string, object>() { { "OrderId", Id }, { "ClientId", Client.Id } };
           parameters.Add("OnDeliveryPayment", PaymentType == PaymentType.OnDelivery);
           return parameters;
       }
    
       virtual public void Submit()
       {
           m_StateMachineStarter.CallWorkflowSync(Status, GetStateMachinePars(), ref OrderSubmitted, (instanceId) => OrderSubmit(null, new OrderEventArgs(instanceId)));
       }
       virtual public void Approve()
       {
           m_StateMachineStarter.CallWorkflowSync(Status, GetStateMachinePars(), ref OrderApproved, (instanceId) => OrderApprove(null, new ApproveEventArgs(instanceId)));
    
       }   
       virtual public void SetOrderAsSent()
       {
           m_StateMachineStarter.CallWorkflowSync(Status, GetStateMachinePars(), ref OrderPackageSent, (x) => OrderPackageSend(null, new OrderEventArgs(x)));
       }   
       virtual public void SetOrderAsReceived()
       {
           m_StateMachineStarter.CallWorkflowSync(Status, GetStateMachinePars(), ref OrderPackageReceived, (x) => OrderPacakgeReceive(null, new OrderEventArgs(x)));
       }   
       virtual public void SetOrderAsPacked()
       {
           m_StateMachineStarter.CallWorkflowSync(Status, GetStateMachinePars(), ref OrderPacked, (x) => OrderPack(null, new OrderEventArgs(x)));
       }
       virtual public void SetOrderAsPaid()
       {
           m_StateMachineStarter.CallWorkflowSync(Status, GetStateMachinePars(), ref OrderPaid, (x) => OrderPay(null, new OrderEventArgs(x)));
       }   
       virtual public void Disapprove()
       {
           m_StateMachineStarter.CallWorkflowSync(Status, GetStateMachinePars(), ref OrderDisapproved, (x) => OrderDisapprove(null, new DisapproveEventArgs(x)));
       }
       
       void EnterpriseStateMachine.Orders.IOrderService.AddOrderToDB(string defaultStatus)
       {
           this.Status = defaultStatus;
           BaseDomainModel.BusinessObjects.Repositories.IDMRepository repository = null;
    
           BaseDomainModel.BusinessObjects.Repositories.IRepositoryLocalizer localizer = BaseDomainModel.BusinessObjects.Repositories.RepositoryLocalizerPlugin.Instance.GetLocalizer();
           repository = localizer.GetForBusinessObject(typeof(Order));
    
           repository.Create<Order>(this);
       }
       void EnterpriseStateMachine.Orders.IOrderService.UpdateOrderStatus(string status)
       {
           BaseDomainModel.BusinessObjects.Repositories.IDMRepository repository = null;
           repository =
               BaseDomainModel.BusinessObjects.Repositories.RepositoryLocalizerPlugin.Instance.GetLocalizer().
                   GetForBusinessObject(this.GetType());
           this.Status = status;
           repository.Update<Order>(this);
       }
       virtual public event EventHandler<OrderEventArgs> OrderSubmit;
       virtual public event EventHandler OrderSubmitted;
    
    
       virtual public event EventHandler<ApproveEventArgs> OrderApprove;
       virtual public event EventHandler OrderApproved;
    
       virtual public event EventHandler<DisapproveEventArgs> OrderDisapprove;
       virtual public event EventHandler OrderDisapproved;
    
       virtual public event EventHandler<OrderEventArgs> OrderPack;
       virtual public event EventHandler OrderPacked;
    
       virtual public event EventHandler<OrderEventArgs> OrderPackageSend;
       virtual public event EventHandler OrderPackageSent;
    
       virtual public event EventHandler<OrderEventArgs> OrderPacakgeReceive;
       virtual public event EventHandler OrderPackageReceived;
    
       virtual public event EventHandler<OrderEventArgs> OrderPay;
       virtual public event EventHandler OrderPaid;
       
       void EnterpriseStateMachine.Orders.IOrderService.OnDisapproved()
       {
           this.OrderDisapproved.RaiseEvent(this, null);
       }   
       void EnterpriseStateMachine.Orders.IOrderService.OnPaid()
       {
           this.OrderPaid.RaiseEvent(this, null);
       }
       void EnterpriseStateMachine.Orders.IOrderService.OnSubmitted()
       {
           this.OrderSubmitted.RaiseEvent(this, null);
       }   
       void EnterpriseStateMachine.Orders.IOrderService.OnApproved()
       {
           this.OrderApproved.RaiseEvent(this, null);
       }
    
       void EnterpriseStateMachine.Orders.IOrderService.OnPackageSent()
       {
           this.OrderPackageSent.RaiseEvent(this, null);
       }
       void EnterpriseStateMachine.Orders.IOrderService.OnPackageReceived()
       {
           OrderPackageReceived.RaiseEvent(this, null);
       }
       void EnterpriseStateMachine.Orders.IOrderService.OnPacked()
       {
           OrderPacked.RaiseEvent(this, null);
       }
    }

    Co dokładnie powyższa klasa  zawiera nie ma większego znaczenia ;). Ważne jest tylko to, że występują w niej właściwości oraz metody (logika). Odpowiadający DTO zawiera oczywiście wyłącznie właściwości:

    [DataContract()]
    public class OrderDto:DtoBase.DtoBase
    {
       public OrderDto()
       {
           OrderItems = new List<OrderItemDto>();
       }
       [DataMember]
       public string Status { get; set; }
       
       [DataMember]
       public DateTime CreationDate { get; set; }
       
       [DataMember]
       public List<OrderItemDto> OrderItems { get; set; }
       
       [DataMember]
       public CrmDto.Client.ClientDto Client { get; set; }
       
       [DataMember]
       public PaymentType PaymentType { get; set; }
    }

    Chcemy zatem zmapować Order do OrderDto. Innymi słowy przepisać wartości wszystkich właściwości Order do OrderDto. W AutoMapper wystarczy najpierw stworzyć takie mapowanie za pomocą:

    AutoMapper.Mapper.CreateMap<Order, OrderDto>();

    Następnie wystarczy tylko wywołać:

    OrderDto ordersDto = AutoMapper.Mapper.Map<Order, OrderDto>(order);

    W analogiczny sposób można mapować całe tablice:

    OrderDto[] ordersDto = AutoMapper.Mapper.Map<Order[], OrderDto[]>(orders);

    Można również wywoływać jakąś metodę po lub przed mapowaniem (callback):

    private void Init()
    {
        Mapper.CreateMap<OrderDto, Order>().AfterMap(AfterOrderMap);
    }
    private void AfterOrderMap(OrderDto orderDto, Order order)
    {  
      foreach (OrderItem item in order.OrderItems)
          item.Order = order;
          
      int paymentType=(int)orderDto.PaymentType;
      order.PaymentType = (EnterpriseDomainModel.Orders.PaymentType)paymentType;
    }

    Jeśli w trakcie mapowania chcemy pominąć jakieś właściwości, ponieważ są one np. generowane dynamiczne można to zrobić za pomocą metody Ignore:

    Mapper.CreateMap<OrderDto, Order>().AfterMap(AfterOrderMap).ForMember("wlasciwosc", (x) => x.Ignore());

    Interfejs biblioteki jest przyjazny i naprawdę łatwo zrealizować wszystko to co się chce. Szczegółowe informacje znajdziecie na stronie biblioteki. Dodam tylko, że istnieje również możliwość pisania własnych konwerterów.

    Prosty Sequential Workflow

    Dzisiaj spróbujemy stworzyć pierwszy workflow. Co prawda, nic praktycznego nie będzie wykonywał, ale pokaże kilka mechanizmów, które można wykorzystać w workflow. Zasada działania przykładowego workflow wzorowana jest na przykładzie z MSDN:

    1. Jako parametr wejściowy podajemy liczbę całkowitą,
    2. Workflow sprawdza wartość liczby. Jeśli jest mniejsza niż 2000, kończy działanie wysyłając e-mail. W przeciwnym razie, wymagana jest akceptacja wprowadzonej wartości. Użytkownik musi po prostu wywołać pewnie zdarzenie – wtedy workflow będzie kontynuował pracę.

    Do dzieła wiec:

    1. Zaczynamy od stworzenia odpowiedniego projektu. Wybieramy “Sequential Workflow Library”.

     

    image

     

    Po utworzeniu mamy do dyspozycji pusty workflow:

    image

    Ponadto oprócz wizualnego wsparcia, możemy wszystkie operacje wykonywać bezpośrednio w kodzie. Wygląda to tak jak w przypadku aplikacji WindowsForms – użytkownik może dodawać kontrolki zarówno z poziomu IDE jak i czystego kodu.

    2. Przed opracowaniem workflow’a, zdefiniujmy niezbędne parametry wejściowe. W naszym przypadku jest to po prostu liczba całkowita. Wszelkie parametry wejściowe i wyjściowe w WWF definiujemy jako właściwości:

    public sealed partial class Workflow1 : SequentialWorkflowActivity
    {
       public Workflow1()
       {
           InitializeComponent();
       }
       private int m_Number = 0;
       public int Number
       {
           set { m_Number = value; }
       }
    }

    Użytkownik będzie mógł w aplikacji hostującej przekazać wartości za pomocą słownika (Dictionary<string,object>). Dokładnie jak to się robi przedstawię w następnym poście kiedy będzie mowa o kliencie workflow.

    3. Ponadto potrzebujemy pewnego interfejsu, który będzie stanowił medium komunikacyjne między klientem a workflow. W naszym przypadku będzie on zawierał m.in. metodę odpowiedzialną za wysłanie e-mail’a oraz zdarzenie, które poiwnno przychodzić po zatwierdzeniu przez użytkownika podanej liczby. Co prawda wysłanie e-mail’a może odbywać się bezpośrednio w workflow, jednak uważam, że workflow powinien zawierać wyłącznie logikę a nie szczegóły techniczne.

    [ExternalDataExchange]
    public interface IService
    {
       void NotifyUser(string message);
       void Approve();
       event EventHandler<ExternalDataEventArgs> OnApproved;
    }

    Interfejs musi być opatrzony również atrybutem ExternalDataExchange. Workflow wywołuje NotifyUser kiedy chce powiadomić użytkownika (np. za pomocą e-mail’a). Jeśli liczba wymaga akceptacji, worklflow wywołuje metodę Approve. Wtedy klient może je zaakceptować poprzez wysłanie zdarzenia OnApproved.

    4. Przyszedł czas na implementacje samego workfow’a. Na początku musimy sprawdzić wartość Number, zatem wrzucamy aktywność IfElse:

    image Klikamy na pierwszą gałąź (ifElseBranchActivity1) i w oknie properties rozwijamy węzeł Condition (który musi być ustawiony na Declarative Rule Condition). Wybieramy ConditionName. Powinno pojawić się następujące okienko:

    image

    Tworzymy nowy warunek, wpisując w pole edycyjne:

    this.m_Number<2000

    Podobne operacje wykonujemy dla drugiej gałęzi, z tym, że jako warunek podajemy m_Number>=2000. Po zakończeniu powinniśmy mieć  utworzone dwa warunki (np. Condition1, Condition2).

    5. Gdy liczba jest mniejsza niż 2000, chcemy powiadomić użytkownika za pomocą metody NotifyUser. W tym celu do lewej gałęzi wrzucamy aktywność CallExternalActivity:

    image

    Ustawiamy właściwość InterfaceType na IService, MethodName na NotifyUser. Z kolei parametr wejściowy message ustawiamy na jakiś komunikat np. “Liczba mniejsza niż 2000”.

    image

    6. W drugiej gałęzi wywołujemy metodę Approve a następnie oczekujemy na zdarzenie zwrotne OnApproved. Wrzucamy zatem aktywności CallExternalMethod oraz HandleExternalMethod:

    image

    CallExternalMethod wypełniamy w analogiczny  sposób jak w poprzednim kroku (InterfaceType – IService, MethodName– Approve).

    HandleExternalMethod blokuje wykonywanie workflow aż do momentu przyjścia konkretnego zdarzenia. W naszym przypadku chcemy czekać aż przyjdzie OnApproved więc ustawiamy EventName na OnApproved oraz oczywiście InterfaceType na IService.

    image

    7. Na zakończenie przy wejściu z IF można umieścić jeszcze aktywność CallExternalMethod, która wywołuje metodę NotifyUser przekazując jakiś komunikat.

    image

    Po kompilacji zostanie utworzona biblioteka dll. W kolejnym poście pokaże jak wykorzystać WWF w aplikacji klienckiej (np. w WPF).

    Kompletny kod źródłowy.

    Warstwa usług

    W poprzednich postach przedstawiłem wzorce projektowe warstwy biznesowej: skrypt transakcji (transaction script), moduł tabeli (table module), aktywny rekord (active record) oraz model domeny (domain model). Napisałem, że dwa ostatnie wzorce posiadają bardzo rozdrobniony interfejs i nie nadają się bezpośrednio do użycia w rozproszonej aplikacji. Dla przypomnienia, AR oraz DM polegają na stworzeniu klasy dla każdej (lub prawie każdej w przypadku DM) tabeli w bazie danych. Przykładowo system sprzedaży posiada obiekty biznesowe takie jak np. Product, Order, ProductParameter, Client czy Invoice.

    Załóżmy, że chcemy stworzyć system rozproszony czyli taki, który pracuje na kilku komputerach. Sprzedawcy będą korzystać z aplikacji mobilnej (składanie zamówień, przeglądanie produktów), a centrala będzie znajdowała się na jakimś zdalnym serwerze. Musi zatem istnieć sposób na przekazywanie poleceń z aplikacji mobilnej do centrali. W tym celu centrala będzie eksponować warstwę biznesową za pomocą usługi sieciowej np. WCF:

    image

    Wszelkie obliczenia oraz przetwarzanie danych będzie odbywało się na serwerze głównym. Aplikacja mobilna po prostu wysyła żądania – nie zawiera żadnej zaawansowanej logiki. Gdybyśmy chcieli wyeksponować wcześniej wspomniane obiekty biznesowe (Product, Order, Client itp),  klient (aplikacja mobilna) otrzymałaby bardzo niewygodny interfejs. Klient nie powinien zagłębiać się w szczegóły techniczne. Kolejną wadą takiego podejścia jest fakt, że obiekty biznesowe zawierają przeważnie bardzo krótkie, atomowe operacje.  My jako klient chcielibyśmy jednak aby metody implementowały całe przypadki użycia a nie tylko pojedyncze kroki. Z pomocą oczywiście przychodzi warstwa usług…

    Warstwa usług mieści się w architekturze między warstwą biznesową a warstwą prezentacji. Może być zaimplementowana za pomocą wzorca “zdalna fasada”. Innymi słowy, jest to po prostu klasa, która łączy funkcjonalność zawartą w obiektach biznesowych.Zamiast kilku klas (Product, Order, Client), warstwa usług zawiera np. tylko jedną, eksponującą zbiór funkcjonalności:

    public class SalesSystemService:ISalesSystemContract
    {
        public void AddOrder(OrderDto orderDto)
        {
            //...
        }
        public void ApproveOrder(Guid id)
        {
            //...
        }
        public void DisapproveOrder(Guid id)
        {
            //...
        }
        public void UpdateOrder(OrderDto orderDto)
        {
            //...
        }
        public void SetOrderFeedback(FeedbackDto feedbackDto)
        {
            //...
        }
        public void AddClient(ClientDto clientDto)
        {
            //...
        }
        public void UpdateClient(ClientDto clientDto)
        {
            //...
        }
        public void RemoveClient(Guid id)
        {
            //...
        }
        public void AddProduct(ProductDto productDto)
        {
            //...
        }
        public void UpdateProduct(ProductDto productDto)
        {
            //...
        }
    }

     

    Klasa powinna udostępniać metody, które realizują najczęstsze przypadki użycia. Ze względu, że warstwę usług projektuje się pod kątem zdalnego wywoływania, powinna być tak napisana, aby klient mógł wykonać swoje operacje z jak najmniejszą liczbą wywołań.

    W aplikacji mobilnej programista zatem dodaje referencje wyłącznie do warstwy usług. Klient nie powinien mieć dostępu do sposobu wykonywania wszelkich operacji (m.in dlatego, że jest to niebezpieczne). Aplikacja mobilna wie co i gdzie może wykonać ale nie ma wiedzy o tym jak to dokładnie jest przetwarzane. Ponadto ze względu, że klient nie ma referencji do obiektów biznesowych (Product, Client, ParameterEntry) musi istnieć jakiś zamiennik. Tym zamiennikiem jest tzw. obiekt DTO (Data Transfer Object).

    image

    DTO to obiekty wspierające serializację (muszą być zdolne do przekazywania przez sieć) oraz zawierające wyłącznie właściwości. Stanowią czyste kontenery na dane. Przykładowo obiekt biznesowy Order mógłby wyglądać następująco:

    public class Order
    {
        public void Approve()
        {
            // logika biznesowa
        }
        public void Disapprove()
        {
            // logika biznesowa
        }
        public void Submit()
        {
            // logika biznesowa
        }
        public Guid Id{get;set;}
        public DateTime Date{get;set;}
        public IList<OrderItem> OrderItems{get;set;}
    }

    Z kolei DTO dla tego obiektu biznesowego, mógłby wyglądać:

    public class OrderDto
    {
        public Guid Id{get;set;}
        public DateTime Date{get;set;}
        public IList<OrderItemDto> OrderItems{get;set;}
    }

    Obiekt DTO jest pozbawiony logiki ponieważ służy wyłącznie do przesłania danych do warstwy usług. Warstwa usług po otrzymaniu takiego pakietu, mapuje go na obiekt biznesowy (OrderDto –> Order) a następnie wykonuje wszelką logikę biznesowa poprzez wywołanie odpowiednich metod na obiekcie biznesowym. Opcjonalnie warstwa usług może zwrócić wynik klientowi, również w postaci jakiegoś obiektu DTO.

    Warto podkreślić, że nie musi występować relacja jeden do jednego między obiektami biznesowymi a DTO. Przeważnie DTO zawierają mniej informacji (właściwości) niż obiekty biznesowe. DTO może różnić się w zależności od metody warstwy usług. Projektując te obiekty powinniśmy zawrzeć w nich jak najmniejsza liczbę informacji wymaganą do przeprowadzenia operacji.

    Na początku wspomniałem o systemie rozproszonym. Czy zatem dla aplikacji pracujących wyłącznie na pojedynczym komputerze powinniśmy kłopotać się z implementacją warstwy usług? Odpowiedź brzmi: przeważnie tak :). Dla małych systemów jest to oczywiście zbędne. Jednak w przypadku średnich nigdy nie wiemy czy za parę miesięcy nie będzie potrzeby trybu pracy rozproszonej. Nie musi być to od razu praca przez Internet ale np. przez LAN. W przypadku systemów sprzedaży, często kilka komputerów pracuje w sieci w ramach tego samego sklepu (kilka kas). Warstwa usług daje możliwość skalowalności , od pojedynczego komputera, pracy w LAN po systemy działające w całkowicie różnych miejscach geograficznych.

    Pozostało jeszcze wyjaśnić jak wykonywać mapowanie między DTO a obiektem biznesowym. Tym zajmiemy się w następnym poście – będzie to jeden wzorzec projektowy oraz pewna biblioteka wykonująca większość pracy za nas ;).

    Jak pobrać adres IP serwera RIA Services z poziomu aplikacji klienckiej Silverlight?

    Dzisiaj tylko króciutka notka ale mam nadzieję, że komuś się przyda. Ostatnio pisząc aplikację w Silverlight + RIA Services musiałem pobrać (w aplikacji SL) adres IP serwera na którym znajduje się RIA Service. Zrealizowałem to następująco:

    System.Windows.Ria.Services.WebDomainClient<Services.DataContext.IDataServiceContract> domain;
    domain = m_DataContext.DomainClient as System.Windows.Ria.Services.WebDomainClient<Services.DataContext.IDataServiceContract>;
    string url = string.Format("http://{0}:{1}/",domain.ServiceUri.Host,domain.ServiceUri.Port));

    Z okazji Wielkanocy –  życzę wszystkiego najlepszego ! 🙂