В мире объектно-ориентированного программирования уже достаточно давно подвергается критике концепция наследования.
Аргументов достаточно много:
- дочерний класс наследует все данные и поведение родительского, что нужно не всегда (а при доработке родительского в дочерний класс вообще попадают данные и поведение, которые не предполагались на момент разработки дочернего);
- виртуальные методы менее производительные, а в случае, если язык позволяет объявить невиртуальный метод, то как быть, если в наследнике нужно его перекрыть (можно пометить метод словом new, но при этом не будет работать полиморфизм, и использование такого объекта может привести к не ожидаемому поведению, в зависимости от того, к какому типу приведен объект в момент его использования);
- если возникает необходимость множественного наследования, то в большинстве языков оно отсутствует, а там, где есть (C++), считается трудоемким;
- есть задачи, где наследование как таковое не может помочь — если нужен контейнер элементов (множество, массив, список) с единым поведением для элементов разных типов, и при этом нужно обеспечить статическую типизацию, то здесь помогут обобщения (generics).
- и т.д., и т.п.
Альтернативой наследованию являются использование интерфейсов и композиция. (Интерфейсы давно используется в качестве альтернативы множественному наследованию, даже если в иерархии классов активно используется наследование.)
Декларируемым преимуществом наследование является отсутствие дублирования кода. В предметной области могут встречаться объекты с похожим или одинаковым набором свойств и методов, но имеющих частично или полностью разные поведение или механизмы реализации этого поведения. И в этом случае для устранения дублирование кода придется использовать иные механизмы.
А как можно решить эту задачу (отсутствие дублирования кода) в случае композиции и интерфейсов?
Этой теме и посвящена настоящая статья.
Пусть объявлен некоторый интерфейс, и два или более классов, реализующих этот интерфейс. Часть кода реализующая интерфейс, у каждого класса различная, а часть — одинаковая.
Для упрощение рассмотрим частный вариант, когда метод MethodA интерфейса реализован у каждого класса по разному, а метод MethodB — одинаково.
Первым вариантом устранения дублирования кода, который приходит в голову, скорее всего, окажется вариант класса-хелпера со статическими методами, которые в качестве аргументов принимают необходимые переменные, и эти методы вызываются в реализациях метода MethodB разных классов, где в метод-хелпер передаются необходимые значения.
Класс хелпер необязательно реализовывать статическим, можно сделать его и экземплярным классом-стратегией, в этом случае входные данные лучше передать в конструктор стратегии, а не в ее методы.
Предлагаю рассмотреть, как этот подход можно реализовать на конкретном примере, с использованием средств современных языков. В данной статье будет использоваться язык C#. В дальнейшем я планирую написать продолжение с примерами на Java и Ruby.
Итак, пусть нам в проекте необходимо реализовать набор классов позволяющих авторизовать пользователя в системе. Методы авторизации будут возвращать экземпляры сущностей, которые мы будем называть AuthCredentials, и которые будут содержать авторизационную/аутентификационную информацию о пользователе. Эти сущности должны иметь методы вида «bool IsValid()», которые позволяют проверять действительность каждого экземпляра AuthCredentials.
Шаг 1
Основная идея предлагаемого подхода решения задачи, рассмотренной выше, заключается в том, что мы создаем набор атомарных интерфейсов — различные варианты представления сущности AuthCredentials, а также интерфейсы, являющиеся композицией атомарных интерфейсов. Для каждого из интерфейсов создаем необходимые методы расширения (extension methods) работы с интерфейсов. Таким образом, для реализации каждого их интерфейсов будет определен единый код, позволяющий работать с любой реализацией этих интерфейсов. Особенностью данного подхода является то, что методы расширения могут работать только со свойствами и методами, определенными в интерфейсах, но не с внутренними данными реализацией интерфейсов.
Создадим в Visual Studio Community 2015 решение (Solution) Windows Console Application, состоящее из четырех проектов:
- HelloExtensions — непосредственно консольное приложение, в котором будет вызываться основной код примера, вынесенный в библиотеки (Class Library);
- HelloExtensions.Auth — основная библиотека, содержащая интерфейсы, позволяющие продемонстрировать решение задачи, рассматриваемой в данной статье;
- HelloExtensions.ProjectA.Auth — библиотека с реализацией интерфейсов, определенных в HelloExtensions.Auth;
- HelloExtensions.ProjectB.Auth — библиотека с альтернативной реализаций интерфейсов, определенных в HelloExtensions.Auth.
Шаг 2
Определим в проекте HelloExtensions.Auth следующие интерфейсы. (Здесь и далее — предлагаемые интерфейсы имеют демонстрационный характер, в реальных проектах содержимое интерфейсов определяется бизнес-логикой.)
Интерфейс ICredentialUser — для случая, когда пользователь может авторизоваться в системе по своему логину или иному идентификатору (без возможности анонимной авторизации) и без создания сессии пользователя; в случае успешной авторизации возвращается идентификатор пользователя в базе данных (UserId), в противном случае возвращается null.
using System; namespace HelloExtensions.Auth.Interfaces { public interface ICredentialUser { Guid? UserId { get; } } }
Интерфейс ICredentialToken — для случая, когда пользователь может авторизоваться в системе анонимно; в случае успешной авторизации возвращается идентификатор (токен) сессии, в противном случае возвращается null.
namespace HelloExtensions.Auth.Interfaces { public interface ICredentialToken { byte[] Token { get; } } }
Интерфейс ICredentialInfo — для случая традиционной авторизации пользователя в системе по логину (или иному идентификатору), с созданием сессии пользователя; интерфейс является композицией интерфейсов ICredentialUser и ICredentialToken.
namespace HelloExtensions.Auth.Interfaces { public interface ICredentialInfo : ICredentialUser, ICredentialToken { } }
Интерфейс IEncryptionKey — для случая, когда при успешной авторизации в системе возвращается ключ шифрования, с помощью которого пользователь может зашифровать данные перед отправкой их в систему.
namespace HelloExtensions.Auth.Interfaces { public interface IEncryptionKey { byte[] EncryptionKey { get; } } }
Интерфейс ICredentialInfoEx — композиция из интерфейсов ICredentialInfo и IEncryptionKey.
namespace HelloExtensions.Auth.Interfaces { public interface ICredentialInfoEx : ICredentialInfo, IEncryptionKey { } }
Шаг 2.1
Определим в проекте HelloExtensions.Auth вспомогательные классы и другие типы данных. (Здесь и далее — декларации и логика вспомогательных классов имеют демонстрационных характер, логика выполнена в виде заглушек (stubs). В реальных проектах вспомогательные классы определяются бизнес-логикой.)
Класс TokenValidator — предоставляет логику валидации идентификатора токена (например, проверки на допустимость значения, внутреннюю консистентность и существование в множестве зарегистрированных в системе активных токенов).
using System; namespace HelloExtensions.Auth { public static class TokenValidator { public const int TokenHeaderSize = 8; public const int MinTokenSize = TokenHeaderSize + 32; public const int MaxTokenSize = TokenHeaderSize + 256; private static bool IsValidTokenInternal(byte[] token) { int tokenBodySize = BitConverter.ToInt32(token, 0); if (tokenBodySize != token.Length - TokenHeaderSize) return false; // TODO: Additional Token Validation, for ex., searching is active token set return true; } public static bool IsValidToken(byte[] token) => token != null && token.Length >= MinTokenSize && token.Length <= MaxTokenSize && IsValidTokenInternal(token); } }
Класс 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); } }
Перечисление KeySize — перечень допустимых размеров (в битах) ключей шифрования, с определением внутреннего значения в виде длины ключа в байтах.
namespace HelloExtensions.Auth { public enum KeySize : int { KeySize256 = 32, KeySize512 = 64, KeySize1024 = 128 } }
Класс 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() ); } } }
Класс 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); } }
Шаг 2.2
Определим в проекте HelloExtensions.Auth класс CredentialsExtensions, предоставляющий методы расширения для определенных выше интерфейсов, декларирующих различные структуры AuthCredentials, в зависимости от способа авторизации в системе.
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); } }
Как видим, в зависимости от того, какой интерфейс реализует переменная, будет выбран тот или иной метод IsValid для проверки структуры AuthCredentials: на этапе компиляции всегда будет выбираться наиболее «полный» метод — например, для переменной, реализующей интерфейс ICredentialInfo, будет выбираться метод IsValid(this ICredentialInfo info), а не IsValid(this ICredentialUser user) или IsValid(this ICredentialToken token).
Однако, пока не все так хорошо, и есть нюансы:
- Утверждение о выборе при вызове наиболее «полного» метода справедливо, если переменная приведена к своему изначальному типу или наиболее «полному» интерфейсу. А если переменную типа, реализующего интерфейс ICredentialInfo, привести в коде к интерфейсу ICredentialUser, то при вызове IsValid будет вызван метод IsValid(this ICredentialUser user), что приведет к неполной/некорректной проверке структуры AuthCredentials.
- Насколько корректно существование одновременно двух методов IsValid(this ICredentialInfoEx info) и IsValidEx(this ICredentialInfoEx info)? Получается, для интерфейса ICredentialInfoEx возможна неполная/некорректная проверка.
Таким образом, в текущем варианте реализации методов расширений отсутствует «полиморфизм» интерфейсов (условно назовем это так).
Поэтому представляется, что интерфейсы различных вариантов структур AuthCredentials и класс CredentialsExtensions с методами расширениями нужно переписать следующим образом.
Реализуем пустой интерфейс IAuthCredentials, от которого будут наследовать атомарные интерфейсы («корневой» интерфейс для всех вариантов структур AuthCredentials).
(Переопределять интерфейсы-композиции при этом не нужно — они автоматически унаследуют IAuthCredentials, также не требуется переопределять такие атомарные интерфейсы, для которых не предполагается создавать самостоятельные реализации — в нашем случае это IEncryptionKey.)
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; } } }
В классе CredentialsExtensions оставим только один открытый (public) метод расширения, работающий с IAuthCredentials:
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) { { 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(); } return false; } } }
Как видим, при вызове метода IsValid, проверки на то, какой интерфейс реализует переменная, теперь выполняется не на этапе компиляции, а во время выполнения (runtime).
Поэтому при реализации метода IsValid(this IAuthCredentials credentials) важно выполнить проверки на реализацию интерфейсов в правильной последовательности (от наиболее «полного» интерфейса к наименее «полному»), для обеспечения корректности результата проверки структуры AuthCredentials.
Шаг 3
Наполним проекты HelloExtensions.ProjectA.Auth и HelloExtensions.ProjectB.Auth логикой, реализующей интерфейсы AuthCredentials из проекта HelloExtensions.Auth и средства работы с реализациями этих интерфейсов.
Общий принцип наполнения проектов:
- определяем необходимые интерфейсы, наследующие интерфейсы из HelloExtensions.Auth и добавляющие декларации, специфичные для каждого из проектов;
- создаем реализации-заглушки этих интерфейсов;
- создаем вспомогательную инфраструктуру с заглушками, предоставляющую API авторизации в некой системе (инфраструктура создается по принципу — интерфейс, реализация, фабрика).
Project «A»
Интерфейсы:
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 { } }
Реализации интерфейсов:
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(); } } }
Примечание: имена элементов сущности могут отличаться от имен, определенных в интерфейсе; в этом случае необходимо реализовать элементы интерфейса явно (explicitly), обернув внутри обращение к соответствующему элементу сущности.
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:
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»
Интерфейсы:
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 { } }
Реализации интерфейсов:
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 : IUserCredentials { public Guid? UserId { get; set; } 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(); } } }
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:
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; using System.Security.Cryptography; namespace HelloExtensions.ProjectB.Auth { using Entities; using Interfaces; internal sealed class AuthProvider : IAuthProvider { private static readonly Lazy<RandomNumberGenerator> rng; static AuthProvider() { rng = new Lazy<RandomNumberGenerator>(() => RandomNumberGenerator.Create()); } public INonRegistrationSessionCredentials AuthorizeSession() { const int tokenHeaderSize = 8; const int tokenBodySize = 64; const int tokenSize = tokenHeaderSize + tokenBodySize; var credentials = new NonRegistrationSessionCredentials() { Token = new byte[tokenSize] }; Array.Copy(BitConverter.GetBytes(tokenBodySize), credentials.Token, sizeof(int)); rng.Value.GetBytes(credentials.Token, tokenHeaderSize, tokenBodySize); // TODO: Put Addition Information into the Token Header // TODO: Implement Token Storing in a Session Cache Manager 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; } }
Шаг 3.1
Наполним консольное приложение вызовами методов провайдеров авторизации из проектов Project «A» и Project «B». Каждый из методов возвратит переменные некоторого интерфейса, наследующего IAuthCredentials. Для каждой из переменных вызовем метод проверки IsValid. Готово.
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(); } } }
Таким образом, мы достигли цели, когда для различных сущностей, реализующих схожую функциональность (а также имеющих отличную друг от друга функциональность), мы можем реализовать единый набор методов без copy-paste, позволяющий работать с этими сущностями единым образом.
Данный способ использования методов расширений подходит как при проектировании приложения с нуля, так и для рефакторинга существующего кода.
Отдельно стоит отметить, почему задача в данном примере не реализуется через классическое наследование: сущности в проектах «A» и «B» реализуют функциональность специфичную для каждого из проектов — в первом случае сущности могут (де)сериализовываться из/в XML, во втором — из/в JSON.
Это демонстрационное различие, хотя и оно может встречаться в реальных проектах (в которых различий в сущностях может быть еще больше).
Другими словами, если есть некоторый набор сущностей, пересекающихся по функциональности лишь частично, и это само пересечение имеет «нечеткий характер» (где-то используется UserId и SessionToken, а где-то еще и EncryptionKey), то в создании унифицированного API, работающего с этими сущностями в сфере пересечения их функциональности, помогут методы расширения (extensions methods).
Методология работы с методами расширения предложена в данной статье.
Продолжение следует.
ссылка на оригинал статьи https://habrahabr.ru/post/314258/
Добавить комментарий