Flow Statyczne metody. Dla początkującego programisty z krótkim stażem pracy są one cudowne. W sumie to kiedyś ten blog napisany w .NET miał wiele klas statycznych i dużo pomocniczych metod jak np. skróć mi URL, skróć mi treść wpisu, wstaw mi obrazek, usuń mi Polskie znaki, wygeneruj mi tabelkę HTML i tak dalej i tak dalej.

Jaka jest najważniejsza zaleta metody statycznej.

Możesz wywołać metody statyczne wszędzie. W każdej klasie, widoku Razor cshtml. Jest to wygodne. Nie musisz robić wstrzykiwania zależności. Nie musisz tworzyć instancji klasy, aby skorzystać z danej metody.

Kiedy metoda statyczna ma sens ? Na pewno, gdy masz pomocniczą metodę, z której wiesz, że skorzystasz w wielu miejscach.

public static string ColorToHexString(System.Drawing.Color color)
{
    byte[] bytes = new byte[3];
    bytes[0] = color.R;
    bytes[1] = color.G;
    bytes[2] = color.B;
    char[] chars = new char[bytes.Length * 2];
    for (int i = 0; i < bytes.Length; i++)
    {
        int b = bytes[i];
        chars[i * 2] = hexDigits[b >> 4];
        chars[i * 2 + 1] = hexDigits[b & 0xF];
    }
    return new string(chars);
}

Mając swoją stronę internetową, chciałbym w wielu miejscach zmienić obiekt Color na string Hex. Klasa statyczna ma swoje miejsce

Problemy jednak zaczynają się tutaj:

public static class Tools
{
    public static string GetImageVariable(Post p)
    {
        if (p == null || p.Variables == null)
            return "";
    
        return p.Variables.ValueForKey("bg");
    }

    public static string SplitTitle(Post p, int i)
    {
        if (p.Title.Contains("-"))
        {
            var spli = p.Title.Split('-');
            if (spli.Length > i)
            {
                return (spli[i]);
            }
            return (p.Title);
        }
        else
        {
            return (p.Title);
        }
    }
}

Zamiast tego:

public class Post
{
   public string Title { get; set; }
   public IList<PostVariables> Variables { get; set; } = new List<PostVariables>();

    public string GetImageVariable()
    {
        if (this == null || this.Variables == null)
            return "";
    
        return this.Variables.ValueForKey("bg");
    }

    public string SplitTitle(int i)
    {
        if (this.Title.Contains("-"))
        {
            var spli = p.Title.Split('-');
            if (spli.Length > i)
            {
                return (spli[i]);
            }
            return (p.Title);
        }
        else
        {
            return (p.Title);
        }
    }
}

Czy jest jakiś powód, aby mieć te metody w klasie statycznej? W końcu te metody odwołują się do obiektu Post to dlaczego te metody nie powinny być w nim?

Istnieje jeden dobry powód.

Metody statyczne są odrobinę szybsze niż metody, które są w instancji danego obiektu.

Z drugiej strony chciałbyś mieć wszystkie metody i funkcje biznesowe na danym obiekcie. Kto normalny by szukał logiki w klasach statycznych.

Dependency Injection pokazuje kolejny problem z klasami statycznymi

Gdy twój projekt zaczyna korzystać z Dependency Injection, wtedy zaczynasz rozumieć gdzie leży problem ze statycznymi metodami. Od razu nasuwa się pytanie, jak te metody testować.

  • Kod jest ściśle powiązany ze sobą za każdym razem, gdy wykonujesz jakąś metodę statyczną. Nie ma interfejsów, które mógłby wydzielić oddzielne zadania do innych przypadków testowych.
  • Mając mnóstwo klas statycznych i metod ciężko jest zrozumieć zależności pomiędzy jedną klasą a drugą. Trzeba wczytać się w kod, aby zobaczyć z czego dana klasa korzysta.

Wstrzykiwanie zależności rozwiązuje te problemy. W testach jednostkowych testujesz wybraną implementację danego interfejsu, a nie przy okazji 10 różnych metod statycznych.

To mi przypomina jak kiedyś moje klasy statyczne, generowały mi strony HTML na tym blogu. Było to możliwe, ponieważ lista wpisów znajdowała się w statycznej właściwości w statycznej klasie Blog.GetCategoriesWithPosts();.

public static class Archive
{
    public static HtmlString CreateArchive2()
    {
        var categories = Blog.GetCategoriesWithPosts();

        var sb = new StringBuilder();

        foreach (var cat in categories)
        {
            if (cat.Number> 0)
            {
                sb.Append(CreateRowHeader2(cat.Title, cat.Title, cat.Number));

                sb.AppendFormat("<table summary='{0}'>", cat.Title);
                sb.AppendFormat("<tbody>");
                sb.Append("<tr>" +
                          "<th>" + "Data" +
                          "</th>" +
                          "<th>" + "Tytuł"+
                          "</th>" +
                          "</tr>");
  

                foreach (Post post in cat.Posts)
                {
                    sb.Append(CreateTableRow2(post));
                }


                sb.Append("</tbody>");
                
                sb.Append("</table>");
            }

        }

        return new HtmlString(sb.ToString());
    }



    private static string CreateTableRow2(Post post)
    {
        string s = string.Format("<tr>" +
                                 "<td class='date'>{0}</td>" +
                                 "<td class='title'><a href='{1}'>{2}</a></td>" +
                                 "</tr>", post.PubDate.ToString("yyyy-MM-dd"), post.Url, WebUtility.HtmlEncode(post.Title));

        return s;
    }

    private static HtmlGenericControl CreateRowHeader(string cat, string name, int count)
    {
        var h2 = new HtmlGenericControl("h2");
        h2.Attributes["id"] = "cat-" + HttpUtility.HtmlEncode(cat);

        Control header = new LiteralControl(name + " (" + count + ")");
        h2.Controls.Add(header);
        return h2;
    }



    private static string CreateRowHeader2(string cat, string name, int count)
    {
        var s = string.Format("<h2 id='cat-{2}'>{0} ({1})</h2>", name, count, HttpUtility.HtmlEncode(cat));

        return s;
    }



}

Nagle, gdy zmigrowałem kod do ASP.NET Core gdzie wstrzykiwanie zależności istnieje normalnie. Musiałem zmienić swoją klasę statyczną, która generowała stronę HTML, ponieważ lista wszystkich wpisów już nie znajdowała się w klasie statycznej tylko w singletonie IQueryBlogService, który był wstrzykiwany.

Teraz klasa do generowania Archiwum wygląda tak:

public class Archive
{
    private IQueryBlogService _blogService;

    public Archive(IQueryBlogService blogService)
    {
        _blogService = blogService;
    }
    public async Task<HtmlString> CreateArchive2()
    {
        var categories = await _blogService.GetCategoriesWithPosts(1);

        var sb = new StringBuilder();

        foreach (var cat in categories)
        {
            if (cat.Number > 0)
            {
                sb.Append(CreateRowHeader2(cat.Title, cat.Title, cat.Number));

                sb.AppendFormat("<table summary='{0}'>", cat.Title);
                sb.AppendFormat("<tbody>");
                sb.Append("<tr>" +
                          "<th>" + "Data" +
                          "</th>" +
                          "<th>" + "Tytuł" +
                          "</th>" +
                          "</tr>");


                foreach (Post post in cat.Posts)
                {
                    sb.Append(CreateTableRow2(post));
                }


                sb.Append("</tbody>");

                sb.Append("</table>");
            }

        }

        return new HtmlString(sb.ToString());
    }
private static string CreateRowHeader2(string cat, string name, int count) { var s = string.Format("<h2 id='cat-{2}'>{0} ({1})</h2>", name, count, HttpUtility.HtmlEncode(cat)); return s; } private static string CreateTableRow2(Post post) { string s = string.Format("<tr>" + "<td class='date'>{0}</td>" + "<td class='title'><a href='{1}'>{2}</a></td>" + "</tr>", post.PubDate.ToString("yyyy-MM-dd"), post.AbsoluteUrl(), WebUtility.HtmlEncode(post.Title)); return s; } }

Możesz porównać sobie te dwie klasy. Jedno jest pewne zależności każdej teraz są z góry widoczne

Pisałem taki kod, to wiem, że na dłuższą metę łatwo potem się zgubić i uświadomić sobie co wywołuje co.

Aby zrozumieć bardziej, kiedy metody statyczne są złe. Warto spojrzeć na innym paradygmat programowania. Mówimy tutaj o programowaniu funkcyjnym

Programowanie funkcyjne 

Funkcyjne programowanie polega na uniknięciu ukrytych wejść i wyjść jak:

  • Używanie Wyjątków, aby kontrolować przepływ programu
  • Funkcja nie modyfikuje stanu
  • Funkcja nie trzyma referencji do obiektu, który miałby być zmieniony
  • Funkcja nie zapisuje czegokolwiek w polach statycznych

W programowaniu funkcyjnym metoda, czyli funkcja jawnie przyjmuje parametry do działania i jawnie zwraca wynik. Wewnątrz niej nie powinno być żadnej i jakiegoś stanu współdzielonego.

Skoro już mówi o stanie współdzielonym, to zobacz ten przykład.

public class SpeechService
{
    private Speech _speech;
    private Person _person;
 
    public void Process(string speakerName,
        ,string speech)
    {
        CreatePerson(speakerName);
        CreateSpeech(speech)
        SaveSpeech();
    }
 
    private void CreatePerson(string name)
    {
        _person = new Person(name);
    }

    private void  (string speech)
    {
        _speech= new Speech(speech, _person);
    }
 
    private void SaveSpeech()
    {
        var repository = new Repository();
        repository.Save(_speech);
    }
}

Jak widzisz metoda CreateSpeech, aby utworzyć mowę, potrzebuje obiektu osoby. Chociaż nie widać tego patrząc na parametry metody CreateSpeech. Zakładamy, że programista będzie pamiętał, w jakiej kolejność ma wykonać te metody, ponieważ mowy nie można utworzyć przed osobą.

Ten kod, jak i metody statyczne tworzą pewien problem. Jak widzisz metody statyczne, stają się wielką dziurą Behemota, gdy współdzielą stan między sobą.

Dlatego taka metoda statyczna może być zaakceptowana, zwłaszcza że spełnia zasady programowania funkcyjnego.

public static class SlugTools
{
    public static string CreateSlug(string title)
    {
        title = title.ToLowerInvariant().Replace(" ", "-");
        title = RemoveDiacritics(title);
        title = RemoveReservedUrlCharacters(title);

        return title.ToLowerInvariant();
    }
}

Taka już nie:

public class static Blog
{
   public static List<Posts> Posts {get;set;}
}

public class Tool
{
    public static Post ReadPost(string name)
    {
        return Blog.Posts.First(k => k.Name == name);
    }
}

Tak z ciekawości poprawny przykład powinien wyglądać tak:

public class SpeechService
{ 
    public void Process(string speakerName,
        ,string speech)
    {
        var person = CreatePerson(speakerName);
        var speech = CreateSpeech(speech,person)
        SaveSpeech(speech );
    }
 
    private void CreatePerson(string name)
    {
        _person = new Person(name);
    }

    private void  (string speech, Person person)
    {
        _speech= new Speech(speech, person);
    }
 
    private void SaveSpeech(Speech speech)
    {
        var repository = new Repository();
        repository.Save(speech);
    }
}

Czyli jeśli chcesz mieć metodę statyczną, to warto najpierw zobaczyć czy spełnia one zasady programowania funkcyjnego. 

Nie daje wyjątków. Nie zmienia jakiegoś stanu współdzielonego. Nie zapisuje czegoś do jeszcze inne klasy statycznej.

Dodatkowo pamiętaj, że metody dla danej klasy powinny być w tej klasie, a nie w jakieś klasie statycznej. 

Staraj się unikać klas statycznych, ponieważ ten kod ciężko się testuje. Możesz zamienić metody klasy statycznej na klasę, którą można potem wstrzykiwać przy użyciu Dependency Injection. Tutaj jednak radzę się też zastanowić, bo jeśli dana klasa ma mieć statyczny stan lub stanu nie ma, to może nie ma co się bawić Dependency Injection.

Gdzie się tego nauczyłem? Na jakim błędzie?

Nauczyło mnie tego przepisanie mojego bloga z ASP.NET MVC .NET 4.5, który korzystał w swoim przepływie aplikacji cały czas z klas statycznych do aplikacji ASP.NET CORE 3.2, która korzysta ze wstrzykiwania zależności.

Kod ze wstrzykiwania zależności jest definitywnie czytelniejszy w swoim przepływie.