Object Oriented Programming Concepts – Abstraction

Abstraction can be simply stated as "exposing only which is required for the context". In other words, it means to only provide access to those features or contents of it which are needed for the moment.

Abstraction can be simply stated as "exposing only which is required for the context". In other words, it means to only provide access to those features or contents of it 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.

This is a real-world principle which applies to all state machines and applications we use in our daily life. For example, everyone would love to listen to music and we use a typical music player to access and play music from some music source (say a library on the disk or from the Internet). A person accessing the music player would generally have buttons to search, play, pause or stop playing music from the music player. But seldom one knows about what happens when he presses a play button or pause button – its only natural because its irrelevant for the purpose of his using the music player. One would want to just play music, knowing what happens internally it completely out of context. And so we just provide an abstract music client, which is a music player UI to the user who just thinks about how to use it but not about the internals of it.

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

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, ie which just expose what is require but not everything. We call these as abstract classes.

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

First, let’s define a template for what this MusicPlayer component does. This is done by an interface; which specifies rules about how any obeying derivatives must be constructed. This interface serves as a "face" for the component at the requesting component end, so that the components in the outside can never know which derivative they’re accessing in the name of this "face".

The "face" IMusicPlayer is as below:


using System.Collections.Generic;

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

We’ve declared all the functionalities which a "MusicPlayer" would offer, 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. And they call the respective functionality on top of IMusicPlayer without having to know what is being actually called under the hood.

Now, we’d create an implementation of IMusicPlayer in the form of MusicPlayer class as below:


using System.Collections.Generic;
using System.Linq;
using OopDemoApp.Abstractions;

namespace OopDemoApp.Implementations
{
    public class MusicPlayer : IMusicPlayer
    {
        private List<string> songsLibrary;
        bool isPlaying;
        private List<string> playQueue;

        public MusicPlayer(ILibraryService service)
        {
            this.songsLibrary = service.GetSongsList();
            this.playQueue = new List<string>();
        }

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

        public string resume()
        {
            if (!isPlaying)
            {
                isPlaying = true;
            }
            return this.playQueue.FirstOrDefault();
        }

        public void play(string songName)
        {
            this.playQueue.Add(songName);
            this.isPlaying = true;
        }

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

        public void stop()
        {
            this.playQueue.Clear();
            if (isPlaying)
            {
                isPlaying = false;
            }
        }
    }
}

Observe that we’ve addressed all the functions declared in the IMusicPlayer interface with an implementation in the MusicPlayer, since the MusicPlayer class "implements" the interface "IMusicPlayer".

"An implementation class must provide definitions to all the methods declared by an interface 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. And it is using a method GetSongsList() on the instance of type ILibraryService which returns the songs list.

This is a classic example of how abstraction is useful. The MusicPlayer class doesn’t know the whereabouts of ILibraryService or it has no idea of what it is doing inside to return the songs list. All the MusicPlayer knows is that the ILibraryService has one method GetSongsList() 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 GetSongsList().

This ILibraryService is defined as:


using System.Collections.Generic;

namespace OopDemoApp.Abstractions
{
    public interface ILibraryService
    {
        List<string> GetSongsList();
    }
}

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


using System.Collections.Generic;
using OopDemoApp.Abstractions;

namespace OopDemoApp.Implementations
{
    public class LibraryService : ILibraryService
    {
        public List<string> GetSongsList()
        {
            return FetchFromSource();    
        }

        private List<string> FetchFromSource() 
        {
            // Fetch the List from an unknown functionality

            return new List<string>()
            {
                "Let it Out - Miho Fukara",
                "Melissa - PornoGraffiti",
                "Fight Together - Namie",
                "Rewrite - AKG",
                "Re:Re - AKG",
                "USO - SID",
                "Again - Yui"
            };
        }
    }
}

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 Abstraction – The songsLibrary collection in the LibraryService is being fetched up from a non-descriptive source which is a functionality under the function FetchFromSource(). Now 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"

Finally, the client component which is "out of context" for the MusicPlayer component, makes use of all this design as below:


using System;
using OopDemoApp.Abstractions;
using OopDemoApp.Implementations;

namespace OopDemoApp
{
    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());

            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

Summary:

  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.
Sriram Mannava
Sriram Mannava

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity.

Leave a Reply

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