using System;
public class BankAccount
{
private readonly object _lock = new();
private decimal _balance;
private bool _isOpen;
public void Open() => _isOpen = true;
public void Close() => _isOpen = false;
public decimal Balance => _isOpen ? _balance : throw new InvalidOperationException("Account is closed");
public void UpdateBalance(decimal change)
{
if (!_isOpen)
throw new InvalidOperationException("Account is closed");
lock (_lock)
_balance += change;
}
}
First, we define to field to keep track of our account's balance and whether the account is opened or closed:
private decimal _balance;
private bool _isOpen;
When dealing with monetary amount, always use decimal
instead of float
or double
, as the later suffer from rounding errors.
The Open()
and Close()
methods change the open state:
public void Open()
{
_isOpen = true;
}
public void Close()
{
_isOpen = false;
}
The Balance
property either returns the current balance if the account has been opened, or throws an InvalidOperationException
if not:
public decimal Balance
{
get
{
if (!_isOpen)
throw new InvalidOperationException("Account is closed");
return _balance;
}
}
We then get to the core of the exercise: updating the balance in a safe way, without any concurrency issues.
First, we check if the account is not actually closed, in which case we throw an InvalidOperationException
:
public void UpdateBalance(decimal change)
{
if (!_isOpen)
throw new InvalidOperationException("Account is closed");
...
}
For that, we can use a lock
statement, which takes an object to lock on and ensures that there is never any concurrent execution of the code within the lock's scope.
Any other process wanting to execute the same code is halted until the currently executing process is done executing the lock's code block.
As said, you take a lock on an object.
It is good practice to define a separate field for that, of type object
:
private readonly object _lock = new object();
We can then lock on this object to safely update our balance:
lock (_lock)
{
_balance += change;
}
As our lock object is unique per instance of the BankAccount
class, we won't have any issues with locking other bank accounts when we lock on that object.
The lock
statement is syntactic sugar for calls to Monitor.Enter()
and Monitor.Exit()
, using a try/finally
block.
lock (_lock)
{
_balance += change;
}
gets compiled to:
Monitor.Enter(_lock)
try
{
_balance += change;
}
finally
{
Monitor.Exit(_lock);
}
Shortening
The lock
statement has only one statement in its scope, so we can omit the braces:
lock (_lock)
_balance += change;
The Open()
and Close()
methods have just one single statement and thus can be written as expression-bodied methods:
public void Open() => _isOpen = true;
public void Close() => _isOpen = false;
The same can be done for the Balance
property, using the ternary operator (? :
):
public decimal Balance => _isOpen ? _balance : throw new InvalidOperationException();
Finally, we can use a target-typed new expression to replace new object
with just new
for creating the lock (the compiler can figure out the type from the method's return type):
private readonly object _lock = new();