Guillaume Bonnot
Guillaume BonnotSoftware Architect, Senior C# Engineer / Founder @ Helios Services

Memento jako antywzorzec - część pierwsza

Poznaj Memento i zobacz, jak można go zaimplementować w rzeczywistym projekcie.
26.02.20204 min
Memento jako antywzorzec - część pierwsza

W tym artykule przyjrzymy się formalnej definicji wzorca projektowego Memento i napiszemy jego rzeczywistą implementację.

Memento

Schemat memento jest wzorcem projektowym oprogramowania, który umożliwia przywrócenie danego obiektu do jego wcześniejszej wersji (przez rollback). 

Schemat ten ma zastosowanie tylko gdy mamy do czynienia z mutowalnymi obiektami.

Aplikacja

    class Program
    {
        private const string ALICE = "alice";
        private const string BOB = "bob";

        private const string BTC = "BTC";
        private const string USD = "USD";

        static void Main(string[] args)
        {
            var service = CreateAccountService();
            
            service.CreateAccount(ALICE);
            service.CreateAccount(BOB);

            service.Deposit(ALICE, BTC, 2);
            service.Deposit(BOB, USD, 9000);

            // ok
            service.Exchange(ALICE, BTC, 1, BOB, USD, 5000);
            // bob has not enough funds
            service.Exchange(ALICE, BTC, 1, BOB, USD, 5000);

            Console.ReadKey();
        }

        private static IAccountService CreateAccountService()
        {
            return new AccountService();
        }
    }


Przyjrzyjmy się poniższemu scenariuszowi:

Alicja i Robert chcą wymienić 1 bitcoin na 5000 dolarów. Niestety, komenda została wysłana dwa razy i druga wymiana się nie powiedzie, bo Robert nie ma wystarczających środków. 

Pro tip: Aby uniknąć tego problemu, spraw, aby Twoja komenda była idempotentna 

    internal interface IAccountService
    {
        void CreateAccount(string username);
        void Deposit(string username, string currency, int amount);
        void Exchange(string username1, string currency1, int amount1, string username2, string currency2, int amount2);
    }


Używamy interfejsu IAccountService, aby oddzielić implementację usługi od jej użycia. Możemy wtedy spokojnie przełączać między implementacją używającą Memento, a jakąkolwiek inną. 

Account Service

    class AccountService : IAccountService
    {
        private readonly Dictionary<string, Account> accounts = new Dictionary<string, Account>();

        public void CreateAccount(string username)
        {
            if(accounts.ContainsKey(username))
            {
                Console.WriteLine($"Account {username} already exists !");
                return;
            }
            accounts.Add(username, new Account(username));
            Console.WriteLine($"Account {username} created.");
        }

        public void Deposit(string username, string currency, int amount)
        {
            if(!accounts.TryGetValue(username, out var account))
            {
                Console.WriteLine($"Account {username} does not exists !");
                return;
            }

            Console.WriteLine("Deposit");
            UpdateBalance(account, currency, amount);
        }

        private bool UpdateBalance(Account account, string currency, int delta)
        {
            var username = account.Name;
            var expected = account.GetBalance(currency) + delta;
            if(expected < 0)
            {
                Console.WriteLine($"Account {username} Balance cannot be negative : {expected} !");
                return false;
            }
            account.SetBalance(currency, expected);
            return true;
        }

        public void Exchange(string username1, string currency1, int amount1, string username2, string currency2, int amount2)
        {
            if (!accounts.TryGetValue(username1, out var account1))
            {
                Console.WriteLine($"Account {username1} does not exists !");
                return;
            }

            if (!accounts.TryGetValue(username2, out var account2))
            {
                Console.WriteLine($"Account {username2} does not exists !");
                return;
            }

            Console.WriteLine("Exchange");

            // we create a memento of account before we modify it
            var memento1 = new Memento(account1);
            var memento2 = new Memento(account2);

            // if error
            if (!(UpdateBalance(account1, currency1, -amount1)
                && UpdateBalance(account2, currency1, amount1)
                && UpdateBalance(account1, currency2, amount2)
                && UpdateBalance(account2, currency2, -amount2)))
            {
                // restore previous account state
                memento1.Undo();
                memento2.Undo();
            }
        }
    }


AccountService obsługuje kolekcję kont i używa wzorca memento do wycofania zmian na koncie, jeżeli operacja się nie powiedzie.

    // mutable
    // not thread safe
    internal class Account
    {
        public readonly string Name;
        private Dictionary<string, int> balances;

        public Account(string name) : this(name, new Dictionary<string, int>()) { }

        public Account(string name, Dictionary<string, int> balances)
        {
            Name = name;
            this.balances = balances;
        }

        internal int GetBalance(string currency)
        {
            if(balances.TryGetValue(currency, out var balance))
            {
                return balance;
            }
            return 0;
        }

        internal void SetBalance(string currency, int balance)
        {
            balances[currency] = balance;
            Console.WriteLine($"Account {Name} Balance {balance} {currency}.");
        }

        public Dictionary<string, int> GetBalances()
        {
            return new Dictionary<string, int>(balances);
        }

        internal void Restore(Dictionary<string, int> balances)
        {
            // not optimized but we want the balance to written in the console :)
            this.balances = new Dictionary<string, int>(balances.Count);
            foreach (var balance in balances)
            {
                SetBalance(balance.Key, balance.Value);
            }
        }
    }


Klasa Account jest mutowalna, balans w każdej instancji może być modyfikowany.

AccountService bezpośrednio modyfikuje instancje konta podczas przetwarzania logiki biznesowej funkcji.

Metoda Exchange wymaga modyfikacji wielu kont, zatem tworzy się memento każdego z nich. Ich pierwotny stan może zostać przywrócony jeżeli jakiekolwiek wywołanie UpdateBalance się nie powiedzie. 

Memento

    class Memento
    {
        private readonly Account account;
        private readonly Dictionary<string, int> balances;

        public Memento(Account account)
        {
            this.account = account;
            balances = account.GetBalances();
        }

        internal void Undo()
        {
            Console.WriteLine($"Restore {account.Name}.");
            account.Restore(balances);
        }
    }


Obiekt Memento tworzy kopię obecnego stanu konta. Poprzedni stan jest zatem przywracany przez wywołanie funkcji Undo

Problem

Zmienne obiekty nie są “thread-safe”. Aby zapobiec problemom z współbieżnością, obiekt, którym zajmuje się inny wątek powinien być niemutowalny.

Kiedy projektuje się architekturę oprogramowania o wysokiej wydajności, powinno się myśleć o Memento jak o antywzorcu.


Wkrótce na blogu kolejna część cyklu "Memento jako antywzorzec" - Post State.

<p>Loading...</p>