Encapsulation, Abstraction, Inheritance and Polymorphism are four fundamental concepts of object oriented programming. While inheritance and polymorphism distinct themselves during early stages of the project, there is a chance of misconception about the other two.

This article aims to explain in simple terms encapsulation and abstraction concepts, illustrated with a small example.

Encapsulation

"It refers to the bundling of data with the methods that operate on that data. Encapsulation is used to hide the values or state of a structured data object inside a class, preventing unauthorized parties' direct access to them. Publicly accessible methods are generally provided in the class (so-called getters and setters) to access the values, and other client classes call these methods to retrieve and modify the values within the object."

Encapsulation is the process of restricting access to some object components.

Consider the following class:

    public class UserBankAccount
    {
        public Guid Id { get; set; }
        public decimal Amount { get; set; }
        public string Currency { get; set; }
    }

While the previous code compiles successfully, is the worst way of logic implementation for such a purpose. That class is written like a DTO, any client code would be able to easily break the state of the object and nothing would happen to inform for such undesired behavior.

Suppose the following client code:

    var bankAccount = new UserBankAccount
    {
        Id = Guid.NewGuid(),
        Amount = 1234,
        Currency = "USD"
    };

    // add* money, multiply ammount instead of adding, use a negative value :/
    bankAccount.Amount *= -1000m;
    // even update currency
    bankAccount.Currency = "EUR";

What one could do to remedy the situation is:

  • Prevent direct write operations to specific components (id and currency) by using another mechanism for initialization, such as a constructor injection or a factory method
  • Introduce a custom type for currency property
  • Provide specific methods to update specific components of the object

Here's a slightly better implementation of the previous version:

    public class UserBankAccount
    {
        public UserBankAccount(Guid id, decimal amount, Currency currency)
        {
            if (amount <= 0)
                throw new ArgumentOutOfRangeException("Please provide a positive amount");

            Id = id;
            Amount = amount;
            AccountCurrency = currency;
        }

        public Guid Id { get; private set; }
        public decimal Amount { get; private set; }
        public Currency AccountCurrency { get; private set; }

        public void Withdraw(decimal withdrawAmount)
        {
            if (withdrawAmount <= 0 || withdrawAmount > Amount)
                throw new ArgumentOutOfRangeException("Please provide a valid amount for withdraw");

            Amount -= withdrawAmount;
        }

        public void Deposit(decimal depositAmount)
        {
            if (depositAmount <= 0)
                throw new ArgumentOutOfRangeException("Please provide a valid amount for deposit");

            Amount += depositAmount;
        }
    }

With this version now we are ensured that the calling code can't:

  • Change the account identifier or currency
  • Issue a withdraw operation with invalid amount
  • Deposit an invalid amount

So, it's not possible for the callers of this class to put it in an invalid state. In other words, we have achived an acceptable level of encapsulation, at least until another requirement is added for this class: Allow withdrawal into other currencies!

What one could (but shouldn't) do is, implement an overload of Withdraw method that takes the currency into considerations, something like this:

    public void Withdraw(decimal withdrawAmount, Currency withdrawCurrency)
    {
        // check for negative amount, prevent continuation if that's the case

        if(withdrawCurrency != AccountCurrency)
        {
            // get the convertion rates
            // find the right amount to decrease based on the convertion rates
            // check if the converted amount is bigger than current amount
            // decrease amount
        }
        else
        {
            Amount -= withdrawAmount;
        }
    }

This implementation can solve out our problem, right? Well sort of... The problem with this implementation is that we have charged this class with logic that doesn't belong to it, which is "Conversion rates", and Encapsulation can't help here, but Abstraction can :)

Abstraction

The essence of abstractions is preserving information that is relevant in a given context, and forgetting information that is irrelevant in that context.

– John V. Guttag

I like to think of abstraction as a form of encapsulation, but in a design level instead of implementation detail (through you apply this with code too). So if encapsulation is doing anything you can to prevent an object from outside callers putting it in an invalid state, abstraction focuses on exposing only what an object does instead of showing how it does it.

Though we can (and usually should) apply abstraction to other types as well, I find it naturally explained with the use of interfaces, which is the case I will follow on the example, by providing an interface, implementation of which takes care of conversion rates between currencies:

    public class UserBankAccount
    {
        private readonly IConvertionRates _convertionRates;
        public UserBankAccount()
        {
            _convertionRates = new ConvertionRates(); // suppose we got this from IoC
        }

        //
        // some code removed for brevity...
        //

        public void Withdraw(decimal withdrawAmount, Currency withdrawCurrency)
        {
            if (withdrawAmount <= 0)
                throw new ArgumentOutOfRangeException("Please provide a valid withdrawal amount");

            var amountInAccountCurrency = withdrawAmount;

            if (withdrawCurrency != AccountCurrency)
            {
                var convertedAmount = _convertionRates.Convert(withdrawAmount, withdrawCurrency, AccountCurrency);
                if (convertedAmount > Amount)
                    throw new ArgumentOutOfRangeException("Please provide a valid withdrawal amount");

                amountInAccountCurrency = convertedAmount;
            }

            Amount -= amountInAccountCurrency;
        }
    }

So with abstraction applied, an object (IConvertionRates in this case) exposes only a high level mechanism from which can be consumed, and hides every detail of how it works internally.

Where to apply these concepts

While you should keep principles and best practices in mind, real world projects are complicated. They grow large, and focus should be to satisfy business requirements. While applying these concepts may introduce some small delay for the initial demo, you will see the benefits as soon as testing starts and you won't regret when maintenance comes into calculation. So encapsulation is a "yes sir" for domain logic, and abstraction helps with services by keeping your focus to what really matters without worrying about implementation details.

You can find this small example in Encapsulation and Abstraction git repo