Written by 12:15 Computer Environment, Languages & Coding

Composition and Interfaces in OOP World

In the object-oriented programming world, the concept of inheritance has been criticized for a long time.

There are quite a lot of arguments:

  • A derived class inherits all the parent’s data and behavior that is not always necessary. When modifying a parent class, some data and behavior that are not supposed to be in the derived class get into it;
  • Virtual methods are less productive. If a language allows declaring a non-virtual method, what would you do when you need to override one in an inherited method? You can mark the method with the new keyword, however, in this case, polymorphism will not work. The consequent use of this object may cause unexpected behavior, depending on the type the object is going to be converted to.
  • The vast majority of languages do not allow multiple inheritance.
  • There are tasks where inheritance is helpless. Consider the case, when you need to create a container (array, list, or set) and implement the same behavior for elements with different types. In addition, you need to provide static typing. In this case, generics will come to help.

An alternative to inheritance in OOP is using interfaces and composition in object-oriented programming. Interfaces have long been used as an alternative to multiple inheritance, even if inheritance is actively used in a class hierarchy.

Inheritance allows avoiding code duplication. There can be objects with the same or similar set of properties and methods, but having partially or completely different behavior or implementation mechanisms of this behavior. In this case, to eliminate code duplication you will have to use other mechanisms.

How can we avoid code duplication when using interfaces and composition in object-oriented programming? This is exactly what we are going to talk in this article!

Let’s declare an interface and two or more classes that implement this interface. Each class that implements the interface provides the individual implementation for MethodA and equal implementation for MethodB.

The easiest way to eliminate code duplication is to create a helper class with static methods, which take the required variables as arguments. Different classes would call these static methods inside MethodB and pass required values.

It is not necessary to implement the helper class as static. You can make it an instance strategy-class.  In this case, it is better to pass the input to the constructor of the strategy class, rather to its methods.

I am going to describe how to use this approach on a particular example.

Assume we need to implement a set of classes that allows authenticating a user in the system. Authentication methods will return instances of entities that we are going to call AuthCredentials. They will contain the authorization and authentication information about the user. These entities must have methods like bool IsValid () that allow verifying the validity of each AuthCredentials instance.

Step1

The main idea of the proposed approach is to create a number of atomic interfaces that would act as different ways of representing the AuthCredentials entity, as well as the interfaces representing the atomic interfaces composition. Additionally, we need to create extension methods for each interface. As a result, we will have a single code base that allows us to work with any interface implementation. It is important to note that the extension methods can only work with the properties and methods defined in interfaces, but not with internal data.

Additionally, we need to create extension methods for each interface. As a result, we will have a single code base that allows us to work with any interface implementation. It is important to note that the extension methods can only work with the properties and methods defined in interfaces, but not with internal data.

Let’s create a Windows Console Application solution in Visual Studio:

HelloExtensions is a console application where we are going to call the main code that is separated by class libraries.

HelloExtensions.Auth is the main library containing the interfaces that allow demonstrating the considered solution;

HelloExtensions.ProjectA.Auth is a library containing the code that implements interfaces defined in HelloExtensions.Auth;

HelloExtensions.ProjectB.Auth is a library containing the code for alternative implementation of the interfaces defined in HelloExtensions.Auth.

Step 2

Let’s define the following interfaces in the HelloExtensions.Auth project. Note that proposed interfaces serve for demo purposes only.

ICredentialUser – a user can authenticate with their login or another identifier without the possibility of anonymous authentication and creating a user session. In the case of successful authentication, we get a UserID, otherwise null.

using System;

namespace HelloExtensions.Auth.Interfaces
{
    public interface ICredentialUser
    {
        Guid? UserId { get; }
    }
}

The IcredentialToken – a user can authenticate anonymously. In the case of success, we get the identifier (session token), otherwise null.

namespace HelloExtensions.Auth.Interfaces
{
    public interface ICredentialToken
    {
        byte[] Token { get; }
    }
}

The IcredentialInfo – traditional authentication with creating a user session. The interface is a composition of the ICredentialUser and IcredentialToken interfaces.

namespace HelloExtensions.Auth.Interfaces
{
    public interface ICredentialInfo : ICredentialUser, ICredentialToken
    {
    }
}

The IencryptionKey – in the case of successful authentication, an encryption key is returned. A user can use this key to encrypt data before sending it to the system.

namespace HelloExtensions.Auth.Interfaces
{
    public interface IEncryptionKey
    {
        byte[] EncryptionKey { get; }
    }
}

The IcredentialInfoEx interface is a composition of the ICredentialInfo and IEncryptionKey interfaces.

namespace HelloExtensions.Auth.Interfaces
{
    public interface ICredentialInfoEx : ICredentialInfo, IEncryptionKey
    {
    }
}

Step 2.1.

Now, it is time to determine utility classes and other data types in the HelloExtensions.Auth project.

Note that all utility classes’ logic is for demo purposes only. It is implemented as stubs.

The TokenValidator class provides the logic that validates the token identifier. For example, it checks for

  • valid values
  • internal consistency
  • existence of an identifier in the system database
namespace HelloExtensions.Auth
{
    public static class TokenValidator
    {
        private static class TokenParams
        {
            public const int TokenHeaderSize = 8;
            public const int MinTokenSize = TokenHeaderSize + 32;
            public const int MaxTokenSize = TokenHeaderSize + 256;
        }

        private static int GetTokenBodySize(byte[] token)
        {
            int bodySize = 0;

            for (int i = 0; i < 2; i++)
                bodySize |= token[i] << i * 8;

            return bodySize;
        }

        private static bool IsValidTokenInternal(byte[] token)
        {
            if (GetTokenBodySize(token) != token.Length - TokenParams.TokenHeaderSize)
                return false;

            // TODO: Additional Token Validation,
            // for ex., searching token in a Session Cache Manager

            return true;
        }

        public static bool IsValidToken(byte[] token) =>
            token != null &&
            token.Length >= TokenParams.MinTokenSize &&
            token.Length <= TokenParams.MaxTokenSize &&
            IsValidTokenInternal(token);
    }
}

The IdentifierValidator class provides the logic that validates the identifier. For example, it checks for

  • valid values
  • existence of an identifier in the system database
using System;

namespace HelloExtensions.Auth
{
    public static class IdentifierValidator
    {
        // TODO: check id exists in database
        private static bool IsIdentidierExists(Guid id) => true;

        public static bool IsValidIdentifier(Guid id) =>
            id != Guid.Empty && IsIdentidierExists(id);

        public static bool IsValidIdentifier(Guid? id) =>
            id.HasValue && IsValidIdentifier(id.Value);
    }
}

The KeySize enumeration is the list of allowable encryption key length (in bits), with the definition of internal values as the key length in bytes.

namespace HelloExtensions.Auth
{
    public enum KeySize : int
    {
        KeySize256 = 32,
        KeySize512 = 64,
        KeySize1024 = 128
    }
}

The KeySizes class lists acceptable length of encryption keys.

using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace HelloExtensions.Auth
{
    public static class KeySizes
    {
        public static IReadOnlyList<KeySize> Items { get; }

        static KeySizes()
        {
            Items = new ReadOnlyCollection<KeySize>(
                (KeySize[])typeof(KeySize).GetEnumValues()
            );
        }
    }
}

The KeyValidator class provides a logic to check an encryption key validity.

using System.Linq;

namespace HelloExtensions.Auth
{
    public static class KeyValidator
    {
        private static bool IsValidKeyInternal(byte[] key)
        {
            if (key.All(item => item == byte.MinValue))
                return false;

            if (key.All(item => item == byte.MaxValue))
                return false;

            // TODO: Additional Key Validation, for ex., checking for known testings values

            return true;
        }

        public static bool IsValidKey(byte[] key) =>
            key != null &&
            key.Length > 0 &&
            KeySizes.Items.Contains((KeySize)key.Length) &&
            IsValidKeyInternal(key);
    }
}

Step 2.2.

In the HelloExtensions.Auth project, we will declare the CredentialsExtensions class, which provides extension methods for the above-mentioned interfaces that declare different AuthCredentials structures depending on the way a user authenticates to the system.

namespace HelloExtensions.Auth
{
    using Interfaces;

    public static class CredentialsExtensions
    {
        public static bool IsValid(this ICredentialUser user) =>
            IdentifierValidator.IsValidIdentifier(user.UserId);

        public static bool IsValid(this ICredentialToken token) =>
            TokenValidator.IsValidToken(token.Token);

        public static bool IsValid(this ICredentialInfo info) =>
            ((ICredentialUser)info).IsValid() &&
            ((ICredentialToken)info).IsValid();

        public static bool IsValid(this ICredentialInfoEx info) =>
            ((ICredentialInfo)info).IsValid();

        public static bool IsValidEx(this ICredentialInfoEx info) =>
            ((ICredentialInfo)info).IsValid() &&
            KeyValidator.IsValidKey(info.EncryptionKey);
    }
}

As you can see, a required IsValid method will be selected depending on the interface a variable implements. The fullest method will be selected at the compile time. For example, the IsValid(this ICredentialInfo info) is selected for the variable that implements the ICredentialInfo interface.

Still, there are some details we should keep in mind:

  • The fullest method is going to be selected in case a variable is casted to its original type or the fullest interface. What is going to happen if we cast a variable that implements the IcredentialInfo interface to the IcredentialUser interface in the code? It will lead to incorrect check of the AuthCredentials structure, because the IsValid method will call IsValid(this ICredentialUser user).
  • Is it correct to have the IsValid(this ICredentialInfoEx info)and IsValidEx(this ICredentialInfoEx info) methods to be simultaneously used? It turns out that incorrect and incomplete check is possible for the ICredentialInfoEx interface.

Thus, there is no interface polymorphism in the current implementation of extension methods.

To move on, we need to redesign the interfaces for different AuthCredentials structures and the CredentialsExtensions class in the following manner.

Let’s create the empty IAuthCredentials interface from which other interfaces will be inherited (a parent interface for all AuthCredentials structures).

We do not need to override composition interfaces, as they inherit IAuthCredentials automatically. In addition, it is not necessary to override atomic interfaces, which do not require creating individual implementations. IEncryptionKey in our case.

namespace HelloExtensions.Auth.Interfaces
{
    public interface IAuthCredentials
    {
    }
}
using System;

namespace HelloExtensions.Auth.Interfaces
{
    public interface ICredentialUser : IAuthCredentials
    {
        Guid? UserId { get; }
    }
}
namespace HelloExtensions.Auth.Interfaces
{
    public interface ICredentialToken : IAuthCredentials
    {
        byte[] Token { get; }
    }
}

In the CredentialsExtensions class, we will keep only one public extension method, which works with IAuthCredentials:

using System;

namespace HelloExtensions.Auth
{
    using Interfaces;

    public static class CredentialsExtensions
    {
        private static bool IsValid(this ICredentialUser user) =>
            IdentifierValidator.IsValidIdentifier(user.UserId);

        private static bool IsValid(this ICredentialToken token) =>
            TokenValidator.IsValidToken(token.Token);

        private static bool IsValid(this ICredentialInfo info) =>
            ((ICredentialUser)info).IsValid() &&
            ((ICredentialToken)info).IsValid();

        private static bool IsValid(this ICredentialInfoEx info) =>
            ((ICredentialInfo)info).IsValid() &&
            KeyValidator.IsValidKey(info.EncryptionKey);

        public static bool IsValid(this IAuthCredentials credentials)
        {
            if (credentials == null)
            {
                //throw new ArgumentNullException(nameof(credentials));
                return false;
            }

            {
                var credentialInfoEx = credentials as ICredentialInfoEx;
                if (credentialInfoEx != null)
                    return credentialInfoEx.IsValid();
            }

            {
                var credentialInfo = credentials as ICredentialInfo;
                if (credentialInfo != null)
                    return credentialInfo.IsValid();
            }

            {
                var credentialUser = credentials as ICredentialUser;
                if (credentialUser != null)
                    return credentialUser.IsValid();
            }

            {
                var credentialToken = credentials as ICredentialToken;
                if (credentialToken != null)
                    return credentialToken.IsValid();
            }

            //throw new ArgumentException(
            //    FormattableString.Invariant(
            //        $"Specified {nameof(IAuthCredentials)} implementation not supported."
            //    ),
            //    nameof(credentials)
            //);
            return false;
        }
    }
}

Now, all check occur at runtime. Therefore, when implementing the IsValid method (this IAuthCredentials credentials) it is important to check if the interface implementation is run in a correct order (from the fullest interface to the emptiest one) to provide the correct check of the AuthCredentials structure.

Step 3.

Now we can add logic that implements interfaces AuthCredentials from HelloExtensions.Auth to HelloExtensions.ProjectA.Auth and HelloExtensions.ProjectB.Auth projects.

The common way is as follows:

  1. Determine necessary interfaces that inherit interfaces from HelloExtensions.Authand add project-specific declarations.
  2. Create stubs for these interfaces;
  3. Create an additional infrastructure with stubs that provides authentication API (the infrastructure has the following sequence: interface, implementation, and factory).

Project A

Interfaces:

namespace HelloExtensions.ProjectA.Auth.Interfaces
{
    public interface IXmlSupport
    {
        void LoadFromXml(string xml);

        string SaveToXml();
    }
}
using HelloExtensions.Auth.Interfaces;

namespace HelloExtensions.ProjectA.Auth.Interfaces
{
    public interface IUserCredentials : ICredentialInfo, IXmlSupport
    {
    }
}
using HelloExtensions.Auth.Interfaces;

namespace HelloExtensions.ProjectA.Auth.Interfaces
{
    public interface IUserCredentialsEx : ICredentialInfoEx, IXmlSupport
    {
    }
}

Interface implementation:

using System;
using HelloExtensions.Auth.Interfaces;

namespace HelloExtensions.ProjectA.Auth.Entities
{
    using Interfaces;

    public class UserCredentials : IUserCredentials
    {
        public Guid? UserId { get; set; }

        public byte[] SessionToken { get; set; }

        byte[] ICredentialToken.Token => this.SessionToken;

        public virtual void LoadFromXml(string xml)
        {
            // TODO: Implement loading from XML
            throw new NotImplementedException();
        }

        public virtual string SaveToXml()
        {
            // TODO: Implement saving to XML
            throw new NotImplementedException();
        }
    }
}

Note: Names of entities may differ from the ones specified in the interface; in this case, it is necessary to implement the interface explicitly by wrapping the reference to the corresponding entity.

using System;

namespace HelloExtensions.ProjectA.Auth.Entities
{
    using Interfaces;

    public class UserCredentialsEx : UserCredentials, IUserCredentialsEx
    {
        public byte[] EncryptionKey { get; set; }

        public override void LoadFromXml(string xml)
        {
            // TODO: Implement loading from XML
            throw new NotImplementedException();
        }

        public override string SaveToXml()
        {
            // TODO: Implement saving to XML
            throw new NotImplementedException();
        }
    }
}

Infrastructure API:

namespace HelloExtensions.ProjectA.Auth
{
    using Interfaces;

    public interface IAuthProvider
    {
        IUserCredentials AuthorizeUser(string login, string password);

        IUserCredentialsEx AuthorizeUserEx(string login, string password);
    }
}
namespace HelloExtensions.ProjectA.Auth
{
    using Entities;
    using Interfaces;

    internal sealed class AuthProvider : IAuthProvider
    {
        // TODO: Implement User Authorization
        public IUserCredentials AuthorizeUser(string login, string password)
            => new UserCredentials();

        // TODO: Implement User Authorization
        public IUserCredentialsEx AuthorizeUserEx(string login, string password)
            => new UserCredentialsEx();
    }
}
using System;

namespace HelloExtensions.ProjectA.Auth
{
    public static class AuthProviderFactory
    {
        private static readonly Lazy<IAuthProvider> defaultInstance;

        static AuthProviderFactory()
        {
            defaultInstance = new Lazy<IAuthProvider>(Create);
        }

        public static IAuthProvider Create() => new AuthProvider();

        public static IAuthProvider Default => defaultInstance.Value;
    }
}

Project B

Interfaces:

namespace HelloExtensions.ProjectB.Auth.Interfaces
{
    public interface IJsonSupport
    {
        void LoadFromJson(string json);

        string SaveToJson();
    }
}
using HelloExtensions.Auth.Interfaces;

namespace HelloExtensions.ProjectB.Auth.Interfaces
{
    public interface ISimpleUserCredentials : ICredentialUser, IJsonSupport
    {
    }
}
using HelloExtensions.Auth.Interfaces;

namespace HelloExtensions.ProjectB.Auth.Interfaces
{
    public interface IUserCredentials : ICredentialInfo, IJsonSupport
    {
    }
}
using HelloExtensions.Auth.Interfaces;

namespace HelloExtensions.ProjectB.Auth.Interfaces
{
    public interface INonRegistrationSessionCredentials : ICredentialToken, IJsonSupport
    {
    }
}

Interface implementation

using System;

namespace HelloExtensions.ProjectB.Auth.Entities
{
    using Interfaces;

    public class SimpleUserCredentials : ISimpleUserCredentials
    {
        public Guid? UserId { get; set; }

        public virtual void LoadFromJson(string json)
        {
            // TODO: Implement loading from JSON
            throw new NotImplementedException();
        }

        public virtual string SaveToJson()
        {
            // TODO: Implement saving to JSON
            throw new NotImplementedException();
        }
    }
}
using System;

namespace HelloExtensions.ProjectB.Auth.Entities
{
    using Interfaces;

    public class UserCredentials : SimpleUserCredentials, IUserCredentials
    {
        public byte[] Token { get; set; }

        public override void LoadFromJson(string json)
        {
            // TODO: Implement loading from JSON
            throw new NotImplementedException();
        }

        public override string SaveToJson()
        {
            // TODO: Implement saving to JSON
            throw new NotImplementedException();
        }
    }
}
using System;

namespace HelloExtensions.ProjectB.Auth
{
    using Interfaces;

    public class NonRegistrationSessionCredentials : INonRegistrationSessionCredentials
    {
        public byte[] Token { get; set; }

        public virtual void LoadFromJson(string json)
        {
            // TODO: Implement loading from JSON
            throw new NotImplementedException();
        }

        public virtual string SaveToJson()
        {
            // TODO: Implement saving to JSON
            throw new NotImplementedException();
        }
    }
}

Infrastructure API

namespace HelloExtensions.ProjectB.Auth
{
    using Interfaces;

    public interface IAuthProvider
    {
        INonRegistrationSessionCredentials AuthorizeSession();

        ISimpleUserCredentials AuthorizeSimpleUser(string login, string password);

        IUserCredentials AuthorizeUser(string login, string password);
    }
}
using System.Security.Cryptography;

namespace HelloExtensions.ProjectB.Auth
{
    using Entities;
    using Interfaces;

    internal sealed class AuthProvider : IAuthProvider
    {
        private static class TokenParams
        {
            public const int TokenHeaderSize = 8;
            public const int TokenBodySize = 64;
            public const int TokenSize = TokenHeaderSize + TokenBodySize;
        }

        private static void FillTokenHeader(byte[] token)
        {
            for (int i = 0; i < 2; i++)
            {
                token[i] = unchecked(
                    (byte)((uint)TokenParams.TokenBodySize >> i * 8)
                );
            }

            // TODO: Put Additional Info into the Token Header
        }

        private static void FillTokenBody(byte[] token)
        {
            using (var rng = RandomNumberGenerator.Create())
            {
                rng.GetBytes(token, TokenParams.TokenHeaderSize, TokenParams.TokenBodySize);
            }
        }

        private static void StoreToken(byte[] token)
        {
            // TODO: Implement Token Storing in a Session Cache Manager
        }

        private static byte[] CreateToken()
        {
            byte[] token = new byte[TokenParams.TokenSize];
            FillTokenHeader(token);
            FillTokenBody(token);
            return token;
        }

        public INonRegistrationSessionCredentials AuthorizeSession()
        {
            var credentials = new NonRegistrationSessionCredentials() { Token = CreateToken() };

            StoreToken(credentials.Token);

            return credentials;
        }

        // TODO: Implement User Authorization
        public ISimpleUserCredentials AuthorizeSimpleUser(string login, string password)
            => new SimpleUserCredentials();

        // TODO: Implement User Authorization
        public IUserCredentials AuthorizeUser(string login, string password)
            => new UserCredentials();
    }
}
using System;

namespace HelloExtensions.ProjectB.Auth
{
    public static class AuthProviderFactory
    {
        private static readonly Lazy<IAuthProvider> defaultInstance;

        static AuthProviderFactory()
        {
            defaultInstance = new Lazy<IAuthProvider>(Create);
        }

        public static IAuthProvider Create() => new AuthProvider();

        public static IAuthProvider Default => defaultInstance.Value;
    }
}

Step 3.1.

Let’s add calls of providers’ authorization methods from Project A and В to a console application. Each method will return variables of an interface that inherits IAuthCredentials. To check each variable, we will call the IsValid method. Done.

using HelloExtensions.Auth;

namespace HelloExtensions
{
    static class Program
    {
        static void Main(string[] args)
        {
            var authCredentialsA = ProjectA.Auth.AuthProviderFactory.Default
                .AuthorizeUser("user", "password");
            bool authCredentialsAIsValid = authCredentialsA.IsValid();

            var authCredentialsAEx = ProjectA.Auth.AuthProviderFactory.Default
                .AuthorizeUserEx("user", "password");
            bool authCredentialsAExIsValid = authCredentialsAEx.IsValid();

            var authCredentialsBSimple = ProjectB.Auth.AuthProviderFactory.Default
                .AuthorizeSimpleUser("user", "password");
            bool authCredentialsBSimpleIsValid = authCredentialsBSimple.IsValid();

            var authCredentialsB = ProjectB.Auth.AuthProviderFactory.Default
                .AuthorizeUser("user", "password");
            bool authCredentialsBIsValid = authCredentialsB.IsValid();

            var sessionCredentials = ProjectB.Auth.AuthProviderFactory.Default
                .AuthorizeSession();
            bool sessionCredentialsIsValid = sessionCredentials.IsValid();
        }
    }
}

Thus, it is possible to implement a separate set of methods without using copy/paste for different entities having both similar and different functionality.

This method can be applied both for developing an application and for refactoring.

Additionally, I would like to point out why this task cannot be performed using classical inheritance: entities in the A and В projects implement functionality specified for each project – in the first case, entities can be serialized into/from XML, in the latter case – into/from JSON.

It is an illustrative difference. Still, we may face it in real projects, where entities may differ even more.

In other words, if there is a set of entities that similar in a functional part (some statements use UserId and SessionToken, whereas other statements have EncryptionKey), then extensions methods will help create a unified API that will work with these entities.

This article suggests ways of working with these extension methods.

Resume

I would like to add that the proposed approach is more acceptable for cases when there are entities (classes) similar to their functionality in different assemblies of the project, and the top-level assemblies (client code) work with these entities in a similar way.

How can we unify the work with these entities without refactoring? It is a good idea to declare a set of interfaces with exactly the same properties and methods as those defined in classes that are going to implement these interfaces.

Most probably, you will have to explicitly declare some interface elements in classes.

Then, we implement a class with extension methods for new interfaces. Then, we use one extension method instead of the “copy/paste”, everywhere we have a reference to the classes.

Thus, the proposed approach is suitable for the legacy code when it is necessary to quickly fix and implement a set of classes with similar declarations and functionality. We do not take into account the way such a set of classes can appear in the project.

When developing a project API and class hierarchy from scratch, other approaches should be applied.

How it is possible to build a code without copy/paste if two or more classes have the same method which differs in logic only is a separate topic. Perhaps, it is a topic for another article.

Tags: , Last modified: September 23, 2021
Close