Polerowanie interfejsu IRandomization

W poprzednim poście, w którym analizowałem wydajność implementacji generatorów liczb pseudolosowych odkryłem, że w ostatecznym rozrachunku planuję używać systemowego typu Random w celu generowania serii liczb losowych – ma znakomitą wydajność i bardzo dobry rozkład – a przynajmniej dla wywołań zakresowych – GetNext(min, max). W międzyczasie, odkryłem jeszcze istnienie kilku innych opcji, które zamierzam sprawdzić w wolnej chwili. (Będzie ciężko, bo The Division…) Spodziewam się znaleźć coś ciekawego, stąd niniejsza część kodu. Planuję:

  • Opakować generator ustandaryzowanym interfejsem, który w miarę potrzeb będę rozbudowaywał
  • Dorobić klasę proxy, która zapewni te funkcje:
    • Umożliwi serializację stanu generatora, w celu odtworzenia jego stanu
    • Umożliwi śledzenie zdarzeń związanych z generowaniem liczb, przydatne zwłaszcza w projektach growych
  • Przy okazji naprawię odziedziczoną ze starych plików klasę Rnd, która jest napisana dość słabo w stosunku do obecnych standardów :-/


Zacznijmy od końca – krok po kroku zmienię istniejącą implementację i wyodrębnię potrzebne mechanizmy i interfejsy.

public enum RandomizationEngine { System, MersenneTwister };

public sealed class Rnd //: IDebugLog
{
    #region Singleton Implementation

    private static object _lockObj = new object(); // lock object
    private static volatile Rnd _instance; // instance

    /// <summary>
    /// The one instance of Rnd singleton 
    /// </summary>
    public static Rnd Instance
    {
        get
        {
            // do safe create singleton instance of the object
            if (_instance == null)
                lock (_lockObj)
                {
                    if (_instance == null)
                        _instance = new Rnd();
                }

            return _instance;
        }
    }

    // Private hidden ctor
    private Rnd()
    {
        InitializeInstance(); // this is done to put init code outside singleton region
    }

    #endregion

    private void InitializeInstance()
    {
        // Please provide here Singleton initialization code
        DateTime dt = DateTime.Now;

        systemEngine = new SystemRandomWrapper(dt.Millisecond);
        twister = new MersenneTwister(dt.Millisecond);


        // by default
        selectedEngine = twister;
    }

    SystemRandomWrapper systemEngine = null;
    MersenneTwister twister = null;

    IRandomizer selectedEngine = null;
    
    // ...
}
  1. Bardzo złym pomysłem była decyzja projektowa by obiekt Rand był singletonem, nie pamiętam czym podyktowana, prawdopodobnie chęcią udostępnienia jednej tylko instancji konkretnego wybranego typu generatora liczb w całej aplikacji. Głupie.
  2. W pierwszej kolejności zupełnie usunę enumerację RandomizationEngine – nie będzie już do niczego potrzebna.
  3. Następnie klasa Rnd zostanie odpieczętowana i stanie się szablonową klasą proxy dla wybranych implementacji generatorów.
  4. W dalszej kolejności dodam fabrykę abstrakcyjną, w założeniu 3 implementacje:
    1. “Zwykła” – do używania w typowej aplikacji
    2. “Deserializująca” – do odczytu generatora z wcześniej zachowanego stanu
    3. “Testowa” – do podstawiania fejkowej instancji na ewentualne potrzeby testów (chociaż mam z tym jeszcze spory zgryz – z jednej strony bezsensowność testowania losowych zachowań a w z drugiej strony – mocno abstrakcyjna i trudna koncepcja planowej psuedolosowości z użyciem Mocków czy czego tam…)
  5. Zaimplementuję podstawową klasę dla generatora systemowego
  6. Dodam kod związany z EventSourcingiem

Początkowo zastawiałem się co będzie lepsze – opakowanie generatorów w dodatkową warstwę proxy z oddzielnym interfejsem, czy dodanie w szablonie abstrakcyjnym tylko delegatów do konkretnych metod. Ale SOLID dość jasno rozwiązuje mój dylemat – jedna przyczyna dla zmiany, i separacja funkcjonalności / interfejsów. A ponieważ klasa Rnd sama już musi implementować interfejs IRandomizer dodatkowe wprowadzanie go niżej, bliżej samych generatorów zapewnia dodatkowe ryzyko. (I dodatkowy narzut na wirtualne wywołania… Chociaż w sumie przy abstraktach jest w zasadzie to samo…) Stąd:

public abstract class Rnd<T> : IRandomizerEventSource, IRandomizer
{
    private readonly T _randomEngine;

    protected Rnd(T randomEngine)
    {
        _randomEngine = randomEngine;
    }
    
    // ...
}

Gdzie IRandomizer dostarcza główne metody losowości (Póki co “stara” deklaracja, będzie rozwijana w miarę potrzeb.):

public interface IRandomizer
{
    /// <summary>
    /// Randomize an integer value
    /// </summary>
    /// <returns></returns>
    int GetNext();

    /// <summary>
    /// Randomize integer value not greater than <paramref name="max"/>
    /// </summary>
    /// <param name="max"></param>
    /// <returns></returns>
    int GetNext(int max);

    /// <summary>
    /// Randomize integer value not greater than <paramref name="max"/> and greater than <paramref name="min"/>
    /// </summary>
    /// <param name="min"></param>
    /// <param name="max"></param>
    /// <returns></returns>
    int GetNext(int min, int max);

    /// <summary>
    /// Randomize new real value
    /// </summary>
    /// <returns></returns>
    double GetNextDouble();
}

Początkowo miałem jeszcze wstrzykiwać obiekt Serializer<T>, który będzie odpowiedzialny za operacje serializacji i deserializacji konkretnego generatora (implementacja w jednym z kolejnych artykułów.), ale to bez sensu i tym będzie się zajmował stosowny builder. IRandomizerEventSource służy do podpięcia obserwatorów / loggerów w miarę potrzeb:

namespace ObscureWare.Randomization
{
    public interface IRandomizerEventSource
    {
        event NextIntEventHandler OnNextInt;

        event NextMaxIntEventHandler OnNextMaxInt;

        event NextIntRangeEventHandler OnNextIntRange;

        event NextDoubleEventHandler OnNextDouble;
    }

    public delegate void NextIntRangeEventHandler(object sender, NextIntRangeEventHandlerArgs args);

    public class NextIntRangeEventHandlerArgs
    {
        public NextIntRangeEventHandlerArgs(int minRange, int maxRange, int generatedValue)
        {
            MaxRange = maxRange;
            GeneratedValue = generatedValue;
            MinRange = minRange;
        }

        public int MinRange { get; private set; }

        public int MaxRange { get; private set; }

        public int GeneratedValue { get; private set; }
    }

    public delegate void NextDoubleEventHandler(object sender, double args);

    public delegate void NextMaxIntEventHandler(object sender, NextMaxIntEventHandlerArgs args);

    public struct NextMaxIntEventHandlerArgs
    {
        public NextMaxIntEventHandlerArgs(int maxRange, int generatedValue)
        {
            MaxRange = maxRange;
            GeneratedValue = generatedValue;
        }

        public int MaxRange { get; private set; }

        public int GeneratedValue { get; private set; }
    }

    public delegate void NextIntEventHandler(object sender, int args);
}

Jak widać wcześniejsze rozwiązanie było dość podobne, ale mocno ogólne. Za to o wiele prostsze :-/

#region IDebugLog Members

//internal void LogDebug(string errMsg) // these are internal, because can be used by Textlibrary also...
//{
//    if (LogDebugMessage != null)
//        LogDebugMessage(errMsg);
//}

//internal void LogError(string errMsg, ErrorSeverity severity) // these are internal, because can be used by Textlibrary also...
//{
//    if (LogErrorMessage != null)
//        LogErrorMessage(errMsg, severity);
//}

//public event SharpDevs.Debugging.InvocationOfString LogDebugMessage;

//public event SharpDevs.Debugging.InvocationOfSeverityString LogErrorMessage;

#endregion

Tym razem wbudowuję je w samą osnowę implementacji:

namespace ObscureWare.Randomization
{
    public abstract class Rnd<T> : IRandomizerEventSource, IRandomizer
    {
        protected readonly T _randomEngine;

        protected Rnd(T randomEngine)
        {
            _randomEngine = randomEngine;
        }

        public event NextIntEventHandler OnNextInt;
        public event NextMaxIntEventHandler OnNextMaxInt;
        public event NextIntRangeEventHandler OnNextIntRange;
        public event NextDoubleEventHandler OnNextDouble;

        public int GetNext()
        {
            int value = InnerGetNext();
            OnNextInt?.Invoke(this, value);
            return value;
        }

        public int GetNext(int max)
        {
            int value = InnerGetNext(max);
            OnNextMaxInt?.Invoke(this, new NextMaxIntEventHandlerArgs(max, value));
            return value;
        }

        public int GetNext(int min, int max)
        {
            int value = InnerGetNext(min, max);
            OnNextIntRange?.Invoke(this, new NextIntRangeEventHandlerArgs(min, max, value));
            return value;
        }

        public double GetNextDouble()
        {
            double value = InnerGetDouble();
            OnNextDouble?.Invoke(this, value);
            return value;
        }
        
        protected abstract int InnerGetNext();

        protected abstract int InnerGetNext(int max);

        protected abstract int InnerGetNext(int min, int max);

        protected abstract double InnerGetDouble();
    }
}

Oraz implementacja dla systemowego generatora:

using System;

namespace ObscureWare.Randomization
{
    internal class SystemRnd : Rnd<Random>
    {
        public SystemRnd(Random randomEngine) : base(randomEngine)
        {
        }

        protected override int InnerGetNext()
        {
            return _randomEngine.Next();
        }

        protected override int InnerGetNext(int max)
        {
            return _randomEngine.Next(max);
        }

        protected override int InnerGetNext(int min, int max)
        {
            return _randomEngine.Next(min, max);
        }

        protected override double InnerGetDouble()
        {
            return _randomEngine.NextDouble();
        }
    }
}

Co przy okazji inwaliduje dotychczasową implementację (SystemRandomWrapper), której kod niniejszym zostaje skasowany.

Oczywiście zmienia to dość drastycznie implementacje klas zależnych od IRandomizer (np. Cards, Dice), które do tej pory korzystały z singletona a obecnie mają go wstrzykiwanego w metody (zmiana robocza, docelowo wstrzyknięcie nastąpi w konstruktorze…). Ale o tym kiedy indziej.

W następnym odcinku – implementacja buildera i serializatora. Oraz testowanie całości.