26.02.20204 min

Guillaume BonnotSoftware Architect, Senior C# Engineer / Founder

Memento jako antywzorzec - część pierwsza

Poznaj Memento i zobacz, jak można go zaimplementować w rzeczywistym projekcie.

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>

Dziel się wiedzą ze 160 tysiącami naszych czytelników

Zostań autorem Readme

Altkom Software & Consulting

Team Lead Frontend Developer

senior

20 000 - 25 000 PLN

Kontrakt B2B

Praca zdalna 100%

Ważna do 25.02.2022

Bardzo dobrze
AngularReactJavaScript
Dobrze
TypeScriptjQuery

Mixort sp. zo. o.

.Net Tech Lead

medium

14 000 - 17 000 PLN

Kontrakt B2BUmowa o pracę

Praca zdalna 100%

Ważna do 25.02.2022

Dobrze
.NETC#SQL

T-Mobile Polska S. A.

SAP PM Consultant

medium

Brak widełek

Kontrakt B2BUmowa o pracę

Warszawa

Ważna do 25.02.2022

T-Mobile Polska S. A.

Specjalista/-ka ds. Wsparcia Technicznego B2B

medium

Brak widełek

Kontrakt B2B

T-Mobile Polska S. A.

SAP Incident Solver / SAP Consultant MM/SD

medium

Brak widełek

Kontrakt B2BUmowa o pracę

Warszawa

Ważna do 25.02.2022

T-Mobile Polska S. A.

SAP EWM Consultant

medium

Brak widełek

Kontrakt B2BUmowa o pracę

Warszawa

Ważna do 25.02.2022

dotLinkers - IT Recruitment Agency

Senior iOS Developer

medium

Do 6 500 USD

Kontrakt B2B

Praca zdalna 100%

Ważna do 12.03.2022

Dobrze
iOSSQLJSON

InPost

Specjalista/tka ds. bezpieczeństwa IT

medium

Brak widełek

Kontrakt B2BUmowa o pracę

Ważna do 25.02.2022

Dobrze
Azure

InPost

Scrum Master / Kierownik Zespołu

medium

Brak widełek

Kontrakt B2BUmowa o pracę

Praca zdalna 100%

Ważna do 25.02.2022

Bardzo dobrze
Scrum

UniqSoft

Tech Lead

senior

Brak widełek

Kontrakt B2B

Bydgoszcz

Ważna do 25.02.2022

Bardzo dobrze
Node.jsReact