Understanding Abstraction in OOP made Easy

In this detailed article, let's explore what is abstraction in OOP with an illustrated example and understand how it works

Introduction

Abstraction is one of the key characteristics of an Object Oriented Programming Language. The other characteristics are Encapsulation, Inheritance and Polymorphism. Any Object Oriented Programming Language must have features that support these characteristics.

What is Abstraction?

Abstraction can be simply stated as exposing only which is required for the context.

It means to only provide access to those features or contents which are needed for the moment. The client or the requesting component need not know all the details of the component it is requesting, it just needs to be informed of only things it requires to get things done.

Abstraction is a real-world principle which applies to all state machines and applications we use in our daily life.

Understanding abstraction with an Example

For example, we may generally use a music player to access and play music from some music source (local storage, Spotify or some other streaming service).

A music player generally provides options to search, play, pause or stop playing. We use these options to apply our desired operation.

But nobody knows what happens when one presses a play button or pause button. It is irrelevant for the purpose of his using the music player. Knowing what happens internally is completely out of context for a normal user.

We just provide an abstraction for an underlying system that is a music service – a music player UI to the user who just thinks about how to use it but not about the internals of it.

This is a real-world application of the principle of abstraction.

Provide access to the functionality for only what is required in the scenario. Be it playing music or launching an application. Underlying details are not needed to be exposed.


How to implement abstraction in OOP?

In a programming language perspective, abstraction refers to the logic to be exposed to the requesting component.

We provide access to the data or the processing functionality to a requesting component via abstractions, i.e. which just expose what is required but not everything.

We call these as abstract classes.

How to implement abstraction in OOP with an example in C#

Let’s take the previous example we spoke about: a music player and try to build a component which delivers the functionalities of a music player – search library, play, pause, resume and stop.

We will first define a template for what this MusicPlayer component does. This is done by an interface.

What is an Interface?

In any Object Oriented Programming Language, an Interface is a structure that specifies rules about how any obeying derivatives must be constructed.

This interface serves as a blueprint or a menu card for the component at the requesting component end, so that the components on the outside can never know which derivative they’re accessing in the name of this face.

The interface that is IMusicPlayer is as below.

using System.Collections.Generic;

namespace MyMusicPlayer.Contracts
{
    public interface IMusicPlayer
    {
        void play(string songName);
        void pause();
        void stop();
        IEnumerable<string> search(string keyword);
        string resume();
    }
}

We declared all the functionalities this MusicPlayer service is expected to provide, but there are no implementation details here.

The components in the outside scope make use of this IMusicPlayer to declare a type and know what functionalities an IMusicPlayer has to offer.

How to Implement an Interface?

They call the respective functionality on top of IMusicPlayer without having to know what is being actually called under the hood.

We will create an implementation of IMusicPlayer in the form of MusicPlayer class as below.

using System.Collections.Generic;
using System.Linq;
using MyMusicPlayer.Contracts;

namespace MyMusicPlayer.Core.Functionalities
{
    public class MusicPlayer : IMusicPlayer
    {
        private List<string> _musicLibrary;
        private List<string> _isNowPlayingQueue;
        private bool _isNowPlaying;

        public MusicPlayer(ILibraryService libraryService)
        {
            _musicLibrary = libraryService.GetMusicLibrary();
            _isNowPlayingQueue = new List<string>();
        }

        public void pause()
        {
            if (_isNowPlaying)
            {
                _isNowPlaying = false;
            }
        }

        public string resume()
        {
            if (!_isNowPlaying)
            {
                _isNowPlaying = true;
            }
            return _isNowPlayingQueue.FirstOrDefault();
        }

        public void play(string songName)
        {
            _isNowPlayingQueue.Add(songName);
            _isNowPlaying = true;
        }

        public IEnumerable<string> search(string keyword)
        {
            var songs = _musicLibrary.Where(x => x.Contains(keyword));
            return songs;
        }

        public void stop()
        {
            _isNowPlayingQueue.Clear();
            if (_isNowPlaying)
            {
                _isNowPlaying = false;
            }
        }
    }
}

Observe that we have added all the functionalities declared by the IMusicPlayer interface inside the MusicPlayer.

The MusicPlayer class implements the interface IMusicPlayer.

Any class implementing an abstraction must provide definitions to all the methods declared by the abstraction that it implements.

There’s more to observe in the MusicPlayer component. It makes use of an instance of type ILibraryService to get the list of all songs available in the library.

It is using a method GetMusicLibrary() on the instance of type ILibraryService which returns the songs list.

This is a classic example of how abstraction works. The MusicPlayer class doesn’t need to know about the ILibraryService or how it works.

All the MusicPlayer knows is that the ILibraryService has one method GetMusicLibrary() and it returns a list of song names.

That’s all. Nothing more. Nothing less.

In this case, the ILibraryService is an abstraction that the MusicPlayer uses to access a functionality for GetMusicLibrary().

This ILibraryService is defined as below.

using System.Collections.Generic;

namespace MyMusicPlayer.Contracts
{
    public interface ILibraryService
    {
        List<string> GetMusicLibrary();
    }
}

And its implementation is provided by a class LibraryService as below.

using System.Collections.Generic;
using MyMusicPlayer.Contracts;

namespace MyMusicPlayer.Core.Services
{
    public class LibraryService : ILibraryService
    {
        private IMusicDatabase _db;

        public LibraryService(IMusicDatabase db)
        {
            _db = db;
        }

        public List<string> GetMusicLibrary()
        {
            return FetchFromSource();
        }

        private List<string> FetchFromSource()
        {
            // fetch the songs active in the database
            var songs = _db.Albums.Where(_ => _.IsActive == true)
                        .Select(_ => _.SongName)
                        .ToList();
        }
    }
}

What are the types of abstraction in OOP

There are two kinds of abstraction we’re using here –

  1. Data abstraction – The songs list in the MusicPlayer is not accessible to the outside, the other components don’t even know that there’s a list called playQueue which is being used to add or remove the songs. They can only access via the functionalities.
  2. Process/Feature abstraction – The songsLibrary collection in the LibraryService is being fetched up from a non-descriptive source which is a functionality under the function FetchFromSource().

The MusicPlayer doesn’t even know that there’s a method called FetchFromSource() inside of the LibraryService through which the list is being prepared and sent. This is because the ILibraryService doesn’t have a specification for it.

The components which interact by means of abstractions don’t even know what is actually inside of each other except the things that are specified in their abstractions.

How a client calls via abstractions

Finally, the client component which is “out of context” for the MusicPlayer component, makes use of all this design as below.

using System;
using MyMusicPlayer.Contracts;
using MyMusicPlayer.Core.Services;

namespace MyMusicPlayer
{
    class Program
    {
        static void Main(string[] args)
        {
            // create an instance of MusicPlayer
            // pass an instance of LibraryService
            // LibraryService is an implementation of ILibraryService
            IMusicPlayer musicPlayer = new MusicPlayer(
                new LibraryService(new MyMusicDatabase()));

            Console.WriteLine("*******Music Player*******");

            while (true)
            {
                Console.Write("n1.Searchn2.Playn3.Pausen4.Stopn0.Exit");
                Console.Write("nOption:");
                var option = Console.ReadLine();
                if (option == "1")
                {
                    Console.Write("Which song would you like to search for? ");
                    var songName = Console.ReadLine();

                    // call the functionality over the instance
                    var matchingSongs = musicPlayer.search(songName);

                    Console.WriteLine("Matching Songs:");
                    foreach (var s in matchingSongs)
                    {
                        Console.WriteLine(s);
                    }
                }
                else if (option == "2")
                {
                    Console.Write("Which song would you like to play? ");
                    var songName = Console.ReadLine();

                    // call the functionality over the instance
                    musicPlayer.play(songName);

                    Console.WriteLine("Playing {songName}");
                }
                else if (option == "3")
                {
                    musicPlayer.pause();
                    Console.WriteLine("Player Paused.");
                    Console.WriteLine("Resume? (y/n)");
                    var resume = Console.ReadLine();
                    if (resume == "y")
                    {
                        // call the functionality over the instance
                        var songName = musicPlayer.resume();
                        Console.WriteLine("Playing {songName}");
                    }
                }
                else if (option == "4")
                {
                    // call the functionality over the instance
                    musicPlayer.stop();
                    Console.WriteLine("Player Stopped.");
                }
                else
                {
                    break;
                }
            }
        }
    }
}
wp-content/uploads/2022/05/oop-abstraction.png

Conclusion

I hope this article gave you a clear understanding of what is meant by abstraction in oop, the types of abstraction and why it is useful. We have also looked at how we implement abstraction in OOP using abstract classes and interfaces.

The following are the key takeaways you need to remember –

  1. Abstraction specifies what others must know about a component
  2. The level of abstraction can be on the data or the functionality provided
  3. The other components interact to this component only via its abstraction which is a “face” of this component
  4. This helps in keeping components clean and decoupled from others.

abstraction and encapsulation difference – how is abstraction different from encapsulation

AbstractionEncapsulation
Abstraction specifies and exposes only what others must know about a componentEncapsulation provides information hiding from unwanted access
The level of abstraction can be on the data or the functionality providedIt provides a logical grouping for the data and functionality which serve a single responsibility. It
Other components interact to this component only via its abstractionA encapsulated grouping is done by a class and is accessed by an object
access modifiers provide necessary locks over a class contents from outside access in public, private or protected termsaccess modifiers provide necessary locks over a class contents from outside access in public, private or protected terms

Buy Me A Coffee

Found this article helpful? Please consider supporting!

Ram
Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity. You can connect with me on Medium, Twitter or LinkedIn.

Leave a Reply

Your email address will not be published. Required fields are marked *