📜 ⬆️ ⬇️

Composition and interfaces

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


There are a lot of arguments:



An alternative to inheritance is the use of interfaces and composition. (Interfaces have long been used as an alternative to multiple inheritance, even if inheritance is actively used in the class hierarchy.)

The declared advantage of inheritance is the lack of code duplication. In the subject area, there may be objects with a similar or identical set of properties and methods, but with partially or completely different behavior or mechanisms for implementing this behavior. And in this case, other mechanisms will have to be used to eliminate duplication of code.

And how can you solve this problem (no duplication of code) in the case of composition and interfaces?
This topic is devoted to this article.

Let some interface be declared, and two or more classes that implement this interface. Some of the code implements the interface, each class is different, and some - the same.
')
To simplify, we consider a particular variant, when the MethodA method of the interface is implemented for each class differently, and the MethodB method is the same.

The first option to eliminate duplication of code that comes to mind is most likely to be a variant of the helper class with static methods that take the necessary variables as arguments, and these methods are called in methodB implementation of different classes, where the necessary values ​​are passed to the helper method .

The helper class does not need to be implemented as static; you can also make it an instance strategy class — in this case, it is better to pass the input data to the strategy constructor, rather than to its methods.

I propose to consider how this approach can be implemented on a specific example, using the means of modern languages. This article will use the C # language. In the future I plan to write a sequel with examples in Java and Ruby.

So, let us in the project need to implement a set of classes that allow us to authorize the user in the system. The authorization methods will return instances of the entities, which we will call AuthCredentials, and which will contain the authorization / authentication information about the user. These entities must have “bool IsValid ()” type methods that allow you to check the validity of each AuthCredentials instance.

Step 1


The main idea of ​​the proposed approach to solving the problem discussed above is that we create a set of atomic interfaces — various versions of the representation of the AuthCredentials entity, as well as interfaces that are a composition of atomic interfaces. For each of the interfaces, we create the necessary extension methods for working with interfaces. Thus, for the implementation of each of their interfaces, a single code will be defined that allows working with any implementation of these interfaces. The peculiarity of this approach is that extension methods can work only with the properties and methods defined in the interfaces, but not with the internal implementation of the interfaces.

Let's create in the Visual Studio Community 2015 a solution (Solution) for the Windows Console Application consisting of four projects:

  1. HelloExtensions - the console application itself, in which the main code of the example rendered to the library (Class Library) will be called;
  2. HelloExtensions.Auth is the main library containing interfaces that allow you to demonstrate the solution of the problem discussed in this article;
  3. HelloExtensions.ProjectA.Auth - a library with the implementation of interfaces defined in HelloExtensions.Auth;
  4. HelloExtensions.ProjectB.Auth is a library with alternative implementations of the interfaces defined in HelloExtensions.Auth.

Step 2


We define the following interfaces in the HelloExtensions.Auth project. (Hereinafter, the proposed interfaces are of a demonstration nature; in real projects, the contents of the interfaces are determined by business logic.)

ICredentialUser interface - for the case when the user can log in to the system by his login or other identifier (without the possibility of anonymous authorization) and without creating a user session; in case of successful authorization, the user ID in the database is returned (UserId), otherwise null is returned.

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

ICredentialToken interface - for the case when the user can log in to the system anonymously; in case of successful authorization, the session identifier (token) is returned, otherwise null is returned.

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

ICredentialInfo interface - for the case of traditional user authorization in the system by login (or other identifier), with the creation of a user session; The interface is a composition of the ICredentialUser and ICredentialToken interfaces.

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

IEncryptionKey interface - for the case when, upon successful authorization, an encryption key is returned to the system with the help of which the user can encrypt the data before sending it to the system.

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

Interface ICredentialInfoEx is a composition of the ICredentialInfo and IEncryptionKey interfaces.

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

Step 2.1


We define helper classes and other data types in the HelloExtensions.Auth project. (Hereinafter, the declarations and logic of auxiliary classes have demonstration character, the logic is implemented in the form of stubs. In real projects, auxiliary classes are defined by business logic.)

The TokenValidator class provides logic for the validation of a token identifier (for example, validity checks, internal consistency, and the existence of active tokens registered in the system).

class TokenValidator
 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 logic for identifier validation (for example, checking for the validity of a value and the existence of an identifier in the system database).

class IdentifierValidator
 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); } } 

Enumeration KeySize is a list of permissible sizes (in bits) of encryption keys, with the definition of an internal value in the form of a key length in bytes.

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

The KeySizes class is a list of acceptable sizes of encryption keys in the form of a list.

class KeySizes
 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 logic for encryption key validity.

class KeyValidator
 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 define the CredentialsExtensions class, which provides extension methods for the interfaces defined above that declare different AuthCredentials structures, depending on the authorization method in the system.

class CredentialsExtensions
 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, depending on which interface the variable implements, one or another IsValid method will be selected to check the structure of the AuthCredentials: at the compilation stage, the most “complete” method will always be selected - for example, for a variable implementing the ICredentialInfo interface, the IsValid method will be selected (this ICredentialInfo info), not IsValid (this ICredentialUser user) or IsValid (this ICredentialToken token).

However, not everything is so good yet, and there are nuances:


Thus, in the current version of the implementation of extension methods, there is no “polymorphism” of interfaces (let's call it so).

Therefore, it appears that the interfaces of various variants of the structures AuthCredentials and the class CredentialsExtensions with extension methods need to be rewritten as follows.

We implement the empty IAuthCredentials interface, from which the atomic interfaces will inherit (the “root” interface for all variants of the AuthCredentials structures).

(You do not need to redefine the composition interfaces — they will automatically inherit IAuthCredentials, nor do you need to redefine such atomic interfaces for which you do not intend to create standalone implementations — in our case, this is IEncryptionKey.)

interface IAuthCredentials
 namespace HelloExtensions.Auth.Interfaces { public interface IAuthCredentials { } } 

interface ICredentialUser
 using System; namespace HelloExtensions.Auth.Interfaces { public interface ICredentialUser : IAuthCredentials { Guid? UserId { get; } } } 

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

In the CredentialsExtensions class, we will leave only one open (public) extension method that works with the IAuthCredentials:

class CredentialsExtensions
 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; } } } 

As you can see, when calling the IsValid method, checks on which interface the variable implements are now performed not at the compilation stage, but at runtime.

Therefore, when implementing the IsValid method (this IAuthCredentials credentials), it is important to perform checks on the implementation of interfaces in the correct sequence (from the most “complete” interface to the least “complete”) to ensure the validity of the result of checking the AuthCredentials structure.

Step 3


Let's fill in the HelloExtensions.ProjectA.Auth and HelloExtensions.ProjectB.Auth projects with logic that implements the AuthCredentials interfaces from the HelloExtensions.Auth project and the tools for working with the implementations of these interfaces.

The general principle of filling projects:

  1. we define the necessary interfaces that inherit the interfaces from HelloExtensions.Auth and add declarations specific to each of the projects;
  2. we create stub implementations of these interfaces;
  3. we create an auxiliary infrastructure with stubs, providing the authorization API in a certain system (the infrastructure is created according to the principle - interface, implementation, factory).

Project "A"

Interfaces:

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

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

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

Interface implementations:

class UserCredentials
 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: Entity element names may differ from the names defined in the interface; in this case, it is necessary to implement interface elements explicitly (explicitly), wrapping inside the call to the corresponding element of the entity.

class UserCredentialsEx
 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(); } } } 

API infrastructure:

interface IAuthProvider
 namespace HelloExtensions.ProjectA.Auth { using Interfaces; public interface IAuthProvider { IUserCredentials AuthorizeUser(string login, string password); IUserCredentialsEx AuthorizeUserEx(string login, string password); } } 

class AuthProvider
 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(); } } 

class AuthProviderFactory
 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:

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

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

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

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

Interface implementations:

class SimpleUserCredentials
 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(); } } } 

class UserCredentials
 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(); } } } 

class NonRegistrationSessionCredentials
 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(); } } } 

API infrastructure:

interface IAuthProvider
 namespace HelloExtensions.ProjectB.Auth { using Interfaces; public interface IAuthProvider { INonRegistrationSessionCredentials AuthorizeSession(); ISimpleUserCredentials AuthorizeSimpleUser(string login, string password); IUserCredentials AuthorizeUser(string login, string password); } } 

class AuthProvide
 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(); } } 

class AuthProviderFactory
 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


Fill the console application with calls to the authorization provider methods from Project “A” and Project “B” projects. Each of the methods will return variables of some interface inheriting from IAuthCredentials. For each of the variables, call the IsValid verification method. Is done.

class program
 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, we have achieved the goal, when for different entities implementing similar functionality (as well as having different functionality from each other), we can implement a single set of methods without copy-paste, which allows us to work with these entities in the same way.

This method of using extension methods is suitable both for designing an application from scratch and for refactoring existing code.

Separately, it is worth noting why the task in this example is not implemented through classical inheritance: entities in projects “A” and “B” implement functionality specific to each project - in the first case, entities can be (un) serialized from / to XML, in the second - from / to JSON.

This is a demonstration difference, although it may occur in real projects (in which the differences in the entities may be even greater).

In other words, if there is a certain set of entities that intersect in functionality only partially, and this very intersection has a “fuzzy character” (UserId and SessionToken are used somewhere, and EncryptionKey is used somewhere else), then in creating a unified API that works with these entities in the intersection of their functionality, will help extensions methods.

The methodology for working with extension methods is proposed in this article.

To be continued.

Update:


Initially, the article did not mention one thing - the proposed approach is more applicable for cases where the project in different assemblies already have entities (classes) that are similar in functionality, and in top-level assemblies (in client code) work with these entities way, by referring to their properties and carrying out some checks (copy-paste with all the consequences).

How is it better to implement the uniformity of work with these entities without refining and refactoring them?

In this case, it seems appropriate to declare a set of interfaces (the properties and methods of the interfaces will repeat the existing properties and methods of the classes), and declare that the classes implement these interfaces.
It is possible that some elements of the interface implementation of the classes will need to be declared explicitly by referring to the corresponding elements of the classes if the elements of the classes are called “out of tune”, and class refactoring is impractical.

Then we implement a class with extension methods for new interfaces, and in all places of accessing classes, we replace the copy-paste of some work with these classes with a call to one extension method.

Thus, the proposed approach is applicable for working with legacy code, when it is necessary to quickly “fix” and implement uniformity of work with a certain set of classes with similar (in a certain section) declarations and functionality.

(The question of how such a set of classes could be in the project will be taken out of the brackets.)

When developing the project API and class hierarchy from scratch, other approaches should be used.
How can you implement the code without copy-paste, if two or more classes have the same method, but with a slightly different logic, this is a topic for another conversation.
Perhaps this is the topic of a new article.

Source: https://habr.com/ru/post/314258/


All Articles