Wzorce projektowe w źródłach .NET: 1/3 wzorce kreacyjne

O wzorcach projektowych napisano już chyba wszystko, co można było napisać. Jedni ich nadużywają, inni je ignorują, jeszcze inni używają ich nieświadomie, nie wiedząc nawet, że te czasami proste rozwiązania mają swoje nazwy.

Z drugiej strony żyjemy w świecie open source i bardzo wiele można się nauczyć przeglądając kod innych. Ponieważ od jakiegoś czasu mamy na githubie dostęp do kodu .NET core, postanowiłem sprawdzić, jak i czy w ogóle autorzy frameworka korzystają z wzorców.

Ponieważ wzorce klasyfikuje się najczęściej w trzech kategoriach to post ten rozpoczyna cykl trzech wpisów. Na początek szukamy w kodzie coreclr wzorców kreacyjnych. Zamieszczone poniżej klasy nie są pełnymi definicjami. Zawierają one jedynie to, co konieczne do pokazania mechanizmu danego wzorca.

Prototype

Umożliwia stworzenie nowego obiektu z zainicjalizowanej już instancji innego. 

namespace System.Net.Http.Headers
{
    public class EntityTagHeaderValue : ICloneable
    {
        private static EntityTagHeaderValue s_any;

        private string _tag;
        private bool _isWeak;

        private EntityTagHeaderValue(EntityTagHeaderValue source)
        {
            Contract.Requires(source != null);

            _tag = source._tag;
            _isWeak = source._isWeak;
        }

        object ICloneable.Clone()
        {
            if (this == s_any)
            {
                return s_any;
            }
            else
            {
                return new EntityTagHeaderValue(this);
            }
        }
    }
}

Znalezienie tego wzorca jest najprostsze z uwagi na interfejs ICloneable. Klasa implementuje metodę Clone (jako explicit interface function implementation), metoda ta zaś wywoła prywatny kontruktor klasy lub zwróci statyczną instancję.

Singleton

Typ, który może być zinstancjonowany tylko raz.

namespace System.Data.SqlClient
{
    public sealed class SqlClientFactory : DbProviderFactory
    {
        public static readonly SqlClientFactory Instance = new SqlClientFactory();

        private SqlClientFactory()
        {
        }
    }
}

Słowo kluczowe sealed blokuje dalsze dziedziczenie, prywatny konstruktor uniemożliwia instancjonowanie poza samą klasą. Jedyna instancja dostępna jest pod publicznym statycznym polem Instance. Jest to najprostsza implementacja Singletonu.

Object Pool

Używamy, gdy ze względów wydajnościowych chcemy reużywać pewnej grupy obiektów i nie instancjonować kolejnych.

namespace System.Reflection.Internal
{
    internal sealed class ObjectPool where T : class
    {
        private struct Element
        {
            internal T Value;
        }

        private readonly Element[] _items;

        private readonly Func _factory;

        internal ObjectPool(Func factory) : this(factory, Environment.ProcessorCount * 2)
        { }

        internal ObjectPool(Func factory, int size)
        {
            _factory = factory;
            _items = new Element[size];
        }

        private T CreateInstance()
        {
            var inst = _factory();
            return inst;
        }

        internal T Allocate()
        {
            var items = _items;
            T inst;

            for (int i = 0; i < items.Length; i++)
            {
                inst = items[i].Value;
                if (inst != null)
                {
                    if (inst == Interlocked.CompareExchange(ref items[i].Value, null, inst))
                    {
                        goto gotInstance;
                    }
                }
            }

            inst = CreateInstance();
        gotInstance:
            return inst;
        }

        internal void Free(T obj)
        {
            var items = _items;
            for (int i = 0; i < items.Length; i++)
            {
                if (items[i].Value == null)
                {
                    items[i].Value = obj;
                    break;
                }
            }
        }
    }
}

Jest to wewnętrzna implementacja generycznej puli obiektów używanej przez reflection. Pula jest reprezentowana przez tablicę elementów o określonym rozmiarze. Elementy tworzone są za pomocą delegata przekazywanego w konstruktorze (factory). Pobranie elementu przez klienta (Allocate) polega na znalezieniu pierwszego elementu różnego od null i atomowej próby wstawienia w jego miejsce w tablicy wartości null. Atomową operację (porównaj i podstaw jeśli porównanie zwraca true) zapewnia metoda CompareExchange z klasy Interlocked. Klient oddając obiekt do puli wywoła metodę Free, a pula wstawi ten obiekt w pierwsze wolne miejsce. Całość zrealizowana jest zatem w sposób lock -free. Na potrzeby wpisu wyciąłem komentarze autorów. Gdyby ktoś był zainteresowany, to są na githubie ObjectPool`1.cs

Factory method

Jest statyczną metodą zwracającą obiekt danego typu lub typów po nim dziedziczących w zależności od parametrów tej metody. 

namespace System.Linq.Expressions
{
    [DebuggerTypeProxy(typeof(Expression.ParameterExpressionProxy))]
    public class ParameterExpression : Expression
    {
        private readonly string _name;

        internal ParameterExpression(string name)
        {
            _name = name;
        }

        internal static ParameterExpression Make(Type type, string name, bool isByRef)
        {
            if (isByRef)
            {
                return new ByRefParameterExpression(type, name);
            }
            else
            {
                if (!type.GetTypeInfo().IsEnum)
                {
                    switch (type.GetTypeCode())
                    {
                        case TypeCode.Boolean: return new PrimitiveParameterExpression(name);
                        //...
                        case TypeCode.Int64: return new PrimitiveParameterExpression(name);
                        case TypeCode.Object:
                            if (type == typeof(object))
                            {
                                return new ParameterExpression(name);
                            }
                            else if (type == typeof(Exception))
                            {
                                return new PrimitiveParameterExpression(name);
                            }
                            else if (type == typeof(object[]))
                            {
                                return new PrimitiveParameterExpression<object[]>(name);
                            }
                            break;
                        case TypeCode.SByte: return new PrimitiveParameterExpression(name);
                        //...
                        case TypeCode.UInt64: return new PrimitiveParameterExpression(name);
                    }
                }
            }

            return new TypedParameterExpression(type, name);
        }        
    }
}

Klasa Expression w .NET udostępnia nam statyczną Factory Method o nazwie Parameter, która z kolei jest przelotką do metody Make zamieszczonej powyżej. Tworzymy obiekty typu ParameterExpression i pochodne. Sprawdzenie odbywa się głównie na podstawie tego, co udostępnia klasa Type

Builder

Umożliwia oddzielenie logiki tworzenia obiektów na wiele etapów.

namespace System
{
    public class UriBuilder
    {
        private string _host = "localhost";
        private int _port = -1;
        private string _query = string.Empty;
        //....

        public UriBuilder()
        {
        }

        public UriBuilder(string uri)
        {
            // setting allowRelative=true for a string like www.acme.org
            Uri tryUri = new Uri(uri, UriKind.RelativeOrAbsolute);

            if (tryUri.IsAbsoluteUri)
            {
                Init(tryUri);
            }
            else
            {
                uri = Uri.UriSchemeHttp + Uri.SchemeDelimiter + uri;
                Init(new Uri(uri));
            }
        }

        public int Port
        {
            get
            {
                return _port;
            }
            set
            {
                if (value < -1 || value > 0xFFFF)
                {
                    throw new ArgumentOutOfRangeException(nameof(value));
                }
                _port = value;
                _changed = true;
            }
        }
    }
}

Klasa ta jest bardzo duża. Uri składa się z wielu części, które albo możemy od razu podać w jednym z kilku konstruktorów, albo ustawiać później poprzez setery, tak, jak port w kodzie powyżej. Ostatnim etapem budowy Uri będzie przeważnie wywołanie metody ToString, zwracającej ostateczną postać.

Reasumując, w źródłach .NET wcale nie tak trudno znaleźć praktyczne zastosowania wzorców projektowych. Kolejne przykłady w następnych odcinkach.

Advertisements

4 thoughts on “Wzorce projektowe w źródłach .NET: 1/3 wzorce kreacyjne

  1. Zastanawia mnie czemu w ObjectPoolu zwalnianie obiektów jest wykonane bez synchronizacji. W przypadku gdy wiele wątków zwalnia wiele obiektów część z nich może uciec nam z puli. W przypadku ciężkich obiektów takich jak wątki może to mieć widoczne dla aplikacji skutki w postaci degradacji wydajności.

    I jeszcze to goto…

    Like

    • Ja celowo usunąłem komentarze z tego kodu, żeby ten post nie rozrósł się do ogromnych rozmiarów. Akurat przy metodzie Free autor wydaje się, że pragmatycznie uznał, że nie ma takiej potrzeby, być może znając use -case’y tej puli (jest internalowa). Oto oryginalny komentarz:
      // Intentionally not using interlocked here.
      // In a worst case scenario two objects may be stored into same slot.
      // It is very unlikely to happen and will only mean that one of the objects will get collected.

      Like

    • I tak i nie 🙂

      Lepiej, bo nie trzeba później rzutować, ale z drugiej strony implementujemy tutaj dotnetowy interfejs ICloneable, który służy też trochę jako marker, że dany typ umie stworzyć swój klon (zasada ISP).

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s