When you don't have an aspect-oriented framework, you can get some of the same functionality with dependency inversion and the decorator pattern.
As you will see with the dependency inversion principle (Chapter 7 ), dependencies should be on abstractions (or interfaces). We can define what we want to be decorated by defining an interface. In the context of auditability, let's look at another pattern—the repository pattern. We could use the repository pattern to model data access and create an interface for the repository, for example IBankAccountRepository in Listing 4-1 .
Listing 4-1. Using the Repository Pattern to Model Data Access and Create an Interface for the Repository
public interface IBankAccountRepository {
decimal Deposit(decimal amount); decimal Withdraw(decimal amount); }
Suppose another class used an instance of that interface, like BankAccountCompounder shown in Listing 4-2 , for example.
Listing 4-2. Another Class Using an Instance of the Preceding Interface
using System;
public sealed class BankAccountCompounder {
private IBankAccountRepository repository;
public BankAccountCompounder(IBankAccountRepository repository) {
if(repository == null)
throw new ArgumentNullException(nameof(repository)); this.repository = repository;
} //... }
A new class that implements IBankAccountRepository could be created, wrapping the standard repository (like BankAccountRepository ) and could be implemented to perform auditing functions. See the example in Listing 4-3 .
83
Listing 4-3. A New Class that Implements IBankAccountRepository using System;
public sealed class AuditingBankAccountRepository : IBankAccountRepository {
private IBankAccountRepository repository; private IAuditor auditor;
public AuditingBankAccountRepository(
IBankAccountRepository repository, IAuditor auditor) {
if (repository == null)
throw new ArgumentNullException(nameof(repository)); if (auditor == null)
throw new ArgumentNullException(nameof(auditor)); this.repository = repository;
this.auditor = auditor; }
public decimal Deposit(decimal amount) { auditor.AuditActionStart( Actions.Deposit, System.Security.Principal.WindowsIdentity.GetCurrent()); try { return repository.Deposit(amount); } finally { auditor.AuditActionEnd( Actions.Deposit, System.Security.Principal.WindowsIdentity.GetCurrent()); } }
public decimal Withdraw(decimal amount) { auditor.AuditActionStart( Actions.Withdraw, System.Security.Principal.WindowsIdentity.GetCurrent()); try { return repository.Withdraw(amount); } finally { auditor.AuditActionEnd( Actions.Withdraw,
84
System.Security.Principal.WindowsIdentity.GetCurrent()); }
} }
Now AuditingBankAccountRepository can be passed to the existing BankAccountCompounder and auditing will occur when BankAccountCompounder is executed without any changes to BankAccountCompounder .
Since AuditingBankAccountRepository uses the dependency inversion and utilizes an
IBankAccountRepository , any number of IBankAccountRepository implementations could be used to perform more than auditing, including logging, authorization, security, etc.
Availability
Availability is the time a system is running and able to be used. This is sometimes considered uptime. Many systems do not have a 100% availability guarantee, but have a certain percentage of time where availability is guaranteed. This availability is generally above 99% and sometimes called "x 9s" of availability, where x is the number of 9s in the guarantee. For example, if there is a 99.99% availability guarantee, that's four 9s.
Architecting for availability can be very complex. The root of the solution is to be able to provide resources to users almost all the time. For this to occur, you need to know how many users your system will have and know what resources the users will need while using it. In terms of web-based applications, this is generally a number of users per second and knowing how many resources are required to service each user. You also have to take into consideration spikes —when an above average number of users simultaneously use the system. Handling average user load can be easier than handling spikes. You can architect a system to always have the resources online to handle the largest expected spike, but that is problematic. This can be very expensive, for one. A spike could take much more resources on average for a small amount of time. If these resources were online all the time, this could cost the organization much more money than necessary.
Practices
Typically, availability is architected so that the system is available 100% of the time; the downtime is a portion of time the system is taken down for servicing or updates. A system can be architected so that it can be serviced or updated in situ, or without taking it down. But providing a realistic uptime guarantee that is between 99 and 100% allows for more realistic planning in the cases where the system must be down. It's better to give users the right expectations rather than have surprise downtime.
So, how do you architect for near 100% availability? Well, tomes have been written on the topic. Although it depends on your circumstances, there are basic traits of highly available systems and patterns that can be used to achieve high availability (HA).
For one, highly available systems are scalable . Although availability is its own non-functional requirement, let's jump from availability right into scalability to continue addressing availability.
Scalability
Scalable systems respond to extra load by distributing that load across elastically available (or statically available, normally dormant) resources. Scalability requires that the workload be able to be divided among the resources. This means that each item of work is well defined and is independent at some level from all the other work that needs to be performed. There may be dependencies between the items of work, but their execution must be mostly independent. This means that work can be distributed to any number of resources for execution and something manages the collecting of the results of that work into a response or passes it along as another unit of work.