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.