Programując w C# często natrafiamy na klasy implementujące interfejs IDisposable, często też o tym nie wiedząc (nasza wina) lub nie mając o tym świadomości (kto stworzył taką bibliotekę?) – wystarczy popatrzeć na API od SharePointa i prawie od razu możemy natrafić na takie piękne kruczki, utworzenie obiektu listy powoduje przypisanie do niej obiektu witryny itp. itd. tego jest sporo.

Może dlatego też większość z nas z rzadka korzysta z IDisposable a jak już korzysta to w celu pozbycia się zasobów. Jednak mała część korzysta z IDisposable jako sposobu na wykonanie kodu przed jak i po zakończeniu działania pewnego fragmentu programu. Dla przykładu, ostatnio w firmie musiałem do swoich ViewModeli dodać indykator mówiący, że coś się dzieje w tle – dane są ładowane, trwa przetwarzanie, cokolwiek. Mogłem to zrobić dość klasycznie:

public class TestViewModel : BaseViewModel
{
    public bool IsBusy { get; set; }

    public void DoAction()
    {
        IsBusy = true;

        // some actions
        Thread.Sleep(1000);

        IsBusy = false;
    }
}

Przeważnie ma to ręce i nogi, potrzebujemy takie coś napisać raz to nie ma problem, dwa… też nie, trzy i więcej – może warto się zastanowić nad czymś uniwersalnym? Albo czymś czym można zarządzać w jednym miejscu. Jeżeli byśmy teraz chcieli dodać, BusyText to nagle w trzech i większej liczbie miejsc trzeba wstawić nową własność z nowym tekstem. Jak przyjdzie kolejna zmiana to znów trzeba wszystko modyfikować. Kodem jest coraz ciężej zarządzać a do tego cały czas widzimy ten sam powtarzający się fragment (trochę wkurzające to potrafi być).

W moim przypadku chodziło jedynie o ustawienie IsBusy i nic więcej jednak zamiast pisać rozwiązanie na miarę stwierdziłem, że nic mnie nie kosztuje dopisanie do swojej klasy bazowej, klasy zagnieżdżonej BusyWorker:

public abstract class BaseViewModel
{
    // for simplicity, INotifyPropertyChanged and 
    // [put here whatever you like] is omitted 

    public bool IsBusy { get; set; }
    protected BusyWorker BusyContext
    {
        get { return new BusyWorker(this); }
    }

    protected class BusyWorker : IDisposable
    {
        private readonly BaseViewModel _model;
        public BusyWorker(BaseViewModel model)
        {
            _model = model;
            _model.IsBusy = true;
            // for test purpose only
            Console.WriteLine(DateTime.Now.ToLongTimeString());
        }

        public void Dispose()
        {
            // for test purpose only
            Console.WriteLine(DateTime.Now.ToLongTimeString());
            _model.IsBusy = false;
        }
    }
}

Dzięki takiemy podejściu w klasach dziedziczących wystarczyło, że napisałem:

public class TestViewModel : BaseViewModel
{
    public void DoAction()
    {
        using(BusyContext)
        {
            // some action that can take time
            Thread.Sleep(1000);
        }
    }
}

Teraz jeżeli nastąpi zmiana w mojej logice to mam jedynie jedno miejsce w którym muszę zmodyfikować zachowanie swojego kodu – dodać kolejne własności, zmodyfikować ich parametry itp.

A co w przypadku kiedy chcemy wywołać jakąś funkcję, która w zależności od typu metody jak i klasy może być inna? Nic prostszego:

public class DisposableAction : IDisposable
{
    private readonly Action _action;

    public DisposableAction(Action action)
    {
        _action = action;
    }

    public void Dispose()
    {
        _action();
    }
}

Przykład wykorzystania:

using(new DisposableAction(() => Console.WriteLine(DateTime.Now.ToLongTimeString())))
{
    Console.WriteLine(DateTime.Now.ToLongTimeString());
    // some long action
    Thread.Sleep(1000);
}

int count = 0;
Console.WriteLine("Before enter: {0}", count);
using(new DisposableAction(() => count = 0))
{
    for(int i = 1; i < 11; i++)
    {
        count += i * i;
    }
    Console.WriteLine("Before exit: {0}", count);
}
Console.WriteLine("After exit: {0}", count);

Co nam daje takie podejście? Na przykład wykonujemy jakieś skomplikowane obliczenia i musimy się upewnić, że po wykonaniu jakiejś część tych obliczeń pewne wartości zostaną wyzerowane, albo log zostanie dodany, czy też nasz obiekt zostanie na nowo zainicjalizowany by na ekranie użytkownika móc odzwierciedlić zmiany na obiekcie. Problem polega na tym iż mało z nas programistów zdaje sobie sprawę, że IDisposable może być naszym przyjacielem a nie tylko wrogiem.

Tak jak obiecałem w komentarzach, skrót i wnioski z dyskusji z Procentem podzielone na tematy o jakich gadaliśmy.

Niewłaściwy przykład

Wszystko zaczęło się od podania niewłaściwego przypadku użycia Disposable Action. Zgadza się, nie jest to z życia wzięty przypadek a stworzony na szybko by pokazać jak można DA wykorzystać w normalnym kodzie. Jest on prosty i banalny, może także nie przemawiać do wszystkich, ale hej, ktoś widział przykład unit testu, który oddaje skomplikowanie w rzeczywistej aplikacji a nie beznadziejnie proste przykłady stworzone pod post? Z przykładami zawsze jest problem, nawet Ayendowi zarzucają iż nie pokazuje przykładów z życia wziętych (choć moim zdaniem pokazuje). Ciężko jest też zawsze znaleźć czas byśmy my blogerzy mogli w spokoju napisać odpowiedni przypadek użycia.

Dlatego z góry zgadzam się z procentem, że przykład jest zły ale nie jest on na tyle zły by go nie publikować – IMO, pokazuje w prosty sposób jak można z tego skorzytać.

Tak wiem, winny się tłumaczy. Czuję się winny z tego powodu, że przykład jest zły bo to moja wina :)

Wykorzystanie using

Po kliku wymianach zdań i podania innych przykładów użycia zacięliśmy się na tym, iż według Procenta można było by to tak zrobić i kod byłby przejrzysty:

public void SomeMethod()
{
    // whatever
}

public void MainProgram()
{
    // some code
    SomeMethod();

    // some code2
    SomeMethod();

    // OR
    int count;
    Action reset = () => count = 0;

    // some code
    reset();

    // some code
    reset();
}

Zgadza się można, dla mnie osobiście jest on mniej przejrzysty gdyż tracę scope, który daje mi using i mogę pomylić się w wprowadzaniu jakiś poprawek i wykonać akcje później. Ale jednocześnie sam nie raz niego korzystam. Jest to najprostszy przypadek i jeżeli nie muszę z niego korzystać w wielu miejscach to też idę tak na skróty :)

Dlatego przeszliśmy do takiego przypadku, dzięki czemu mam swój scope:

public void MainWithScope()
{
    int count;
    Action reset = () => count = 0;

    {
        // some code
    }
    reset();

    {
        // some code
    }
    reset();
}

Tutaj nie znam opinii Procenta, dla mnie czyta się ten kod tak jakby programista o czymś zapomniał. Nie dodał using? Może tworzy delegat? Albo skasował if lub while? Takie fragmenty kodu widziałem jedynie w aplikacjach gdzie pierwsza linijka była zakomentowana – na przykład if zakomentowany albo wzięty dyrektywę kompilatora #if.

Zdefiniowanie metody najpierw wykonującej się na końcu

Chodzi o to, że przy using od razu mówimy co ma się na końcu wykonać. Zmieniamy trochę przebieg działania. Ale czym to się różni od zdefiniowania Action wcześniej w kodzie a następnie w ciele metody korzystania z niej? Też definiujemy ją wcześniej i wykorzystujemy później. A co jeżeli metoda która chcemy wywołać jest w innym pliku albo deklarowana na początku/końcu danego pliku, czy to też psuje czytelność kodu?

Dla mnie psuje to tak samo jak using. Jak się człowiek przyzwyczaił do tego by deklarować Action w metodzie zamiast tworzyć osobą metodę, to nie widzę problemu z określeniem metody w using która wykonuje się na końcu metody. Plusem using jest to iż jak wcześniej wspomniałem mamy scope, mamy zdefiniowane co robi using, na co jest przez kompilator zamieniany i wiemy co jest wykonywane w bloku finally.

A kto z was korzysta z AOP? Albo z interceptor z projektu Castle czy też NH? Albo kto polega na wstrzykiwaniu własności? Czy osobie która nie zna IoC i DI będzie łatwo przeczytać wasz kod? Czy zakładacie od razu, że tylko guru będą go czytać? Dla mnie zdefiniowanie metody najpierw wykonującej się na końcu jest jak najbardziej w porządku i nie zaburza czytelności kodu. Ale jest to moje zdanie.

Generyczność rozwiązania

W końcu doszło do tego, że jak już mniej więcej się prawie w niczym nie zgadzaliśmy :) tylko w tym, że fajnie jest dyskutować poprzez chat kiedy każdy z nas pisze jakiś inny soft i co chwilę ktoś coś dodaje i w końcu powstaje mętlik a konkluzji nie ma :) W ostateczności nagle stanęło na tym iż zgadza się takie podejście ma ręce i nogi ale nie w postaci generycznej, ale już bezpośrednio zaimplementowanej klasy dla przykładu:

Tutaj oczywiście zgadzam się z Procentem, wykorzystanie w ten sposób może zwiększyć czytelność naszego kodu (patrz przykład IsBusy), jednak czasami moim zdaniem nie ma sensu pisać całej klasy. Zmodyfikowany przykład IsBusy:

public abstract class BaseViewModel
{
    // for simplicity, INotifyPropertyChanged and 
    // [put here whatever you like] is omitted 

    public bool IsBusy { get; set; }
    protected IDisposable BusyContext
    {
        get
        {
            IsBusy = true;
            return new DisposableAction(() => IsBusy = false);
        }
    }
}

public class TestViewModel : BaseViewModel
{
    public void DoAction()
    {
        using(BusyContext)
        {
            // some actions
            Thread.Sleep(1000);
        }
    }
}

Tutaj moim zdaniem wszystko zależy od was, co waszym zdaniem będzie lepsze – dla innych może być przydatna wersja generyczna, dla innych bezpośrednia implementacja. Tutaj nie ma reguły. W końcu każdy z nas ma swoje przyzwyczajenia i nawyki, które mogą pomagać lub przeszkadzać. Najważniejsze jednak by zespół (jeżeli z ów pracujemy) podzielał takie samo zdanie lub rozumiał nasz zapis.

Podsumowanie

Jak widać mamy dwa różne podejścia do tego problemu. I tutaj nie ma lepszego/gorszego, słusznego/niesłusznego. Jest podeście, które nam odpowiada. Jeżeli wolimy pisać samemu funkcje na końcu to nie ma problemu – możemy, przecież nikt nas po łapach bić nie będzie. Czy zwiększy to czytelność czy zmniejszy to zależy od was i waszych kumpli. W tworzeniu oprgramowania fajne jest to, że do tego samego celu można dojść na kilka sposobów, a który z nich będzie efektywniejszy, czytelniejszy to zależy od innych czynników, na które nie zawsze mamy wpływ – patrz zadufany w sobie menadżer po kursie Komputerowe Prawo jazdy.

A wy jak, zgadzacie się z którymś konkretnym podejściem? Czy to zależy – dla jednego z projektów widzicie zastosowanie, do innego wolicie jednak pójść klasycznie? Jakie macie odczucia co do czytelności kodu? Który waszym zdaniem przypadek będzie lepszy, wyraźniejszy?

PS.: Procent, jeżeli coś przekręciłem z naszej dyskusji, popraw mnie. dzięki.

PS2.: Fajnie jest móc czasami z kimś na temat prog. porozmawiać :)

6 KOMENTARZE

  1. Ciekawe wykorzystanie interfejsu. Rzeczywiście do tej pory wykorzystywałem go raczej "wprost", bardzo podoba mi się pomysł przedstawiony w ostatnim fragmencie kodu.

  2. O ile zwrócenie uwagi na możliwość zastosowania IDisposable do celów innych niż proste zwalnianie zasobów jest jak najbardziej spoko, to już przykłady takiego zastosowania średnio mi się podobają. A już szczególnie przykład II. Nie wiem skąd pomysł, że takie coś:

    using (new DisposableAction(() => count = 0))
    {
    // some work
    // …..
    // some more work
    }

    jest lepsze niż takie coś:

    // some work
    // …..
    // some more work
    count = 0;

    W czym pomóc ma umieszczenie NA POCZĄTKU instrukcji, która wykonuje się NA KOŃCU? Jak dla mnie to wprowadza niepotrzebne zamieszanie. Mniej czytelne, trudniejsze do debuggowania, trudniejsze do zrozumienia, zaburzające cały wizualny flow programu…

    Rozwiń jak możesz:)

  3. Podstawowa różnica jest taka, że delegat uruchomi się w bloku finally (czyli zawsze).
    Co do wykorzystanie IDisposable – osobiście tego typu problemy rozwiązuje w oparciu o delegaty, wtedy mamy możliwość otoczenia delegata przez kod a nie tylko umieszczenia go na końcu.

  4. Chyba nic nie przekręciłeś.
    A że fajnie na temat programowania porozmawiać to jasna sprawa, dlatego się cieszę że mogę to robić prawie 24/7:).

Comments are closed.