В мире объектно-ориентированного программирования уже достаточно давно подвергается критике концепция наследования.


Аргументов достаточно много:


  • дочерний класс наследует все данные и поведение родительского, что нужно не всегда (а при доработке родительского в дочерний класс вообще попадают данные и поведение, которые не предполагались на момент разработки дочернего);
  • виртуальные методы менее производительные, а в случае, если язык позволяет объявить невиртуальный метод, то как быть, если в наследнике нужно его перекрыть (можно пометить метод словом 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, состоящее из четырех проектов:

  1. HelloExtensions — непосредственно консольное приложение, в котором будет вызываться основной код примера, вынесенный в библиотеки (Class Library);
  2. HelloExtensions.Auth — основная библиотека, содержащая интерфейсы, позволяющие продемонстрировать решение задачи, рассматриваемой в данной статье;
  3. HelloExtensions.ProjectA.Auth — библиотека с реализацией интерфейсов, определенных в HelloExtensions.Auth;
  4. HelloExtensions.ProjectB.Auth — библиотека с альтернативной реализаций интерфейсов, определенных в HelloExtensions.Auth.

Шаг 2


Определим в проекте HelloExtensions.Auth следующие интерфейсы. (Здесь и далее — предлагаемые интерфейсы имеют демонстрационный характер, в реальных проектах содержимое интерфейсов определяется бизнес-логикой.)

Интерфейс ICredentialUser — для случая, когда пользователь может авторизоваться в системе по своему логину или иному идентификатору (без возможности анонимной авторизации) и без создания сессии пользователя; в случае успешной авторизации возвращается идентификатор пользователя в базе данных (UserId), в противном случае возвращается null.

interface ICredentialUser
using System;

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

Интерфейс ICredentialToken — для случая, когда пользователь может авторизоваться в системе анонимно; в случае успешной авторизации возвращается идентификатор (токен) сессии, в противном случае возвращается null.

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

Интерфейс ICredentialInfo — для случая традиционной авторизации пользователя в системе по логину (или иному идентификатору), с созданием сессии пользователя; интерфейс является композицией интерфейсов ICredentialUser и ICredentialToken.

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

Интерфейс IEncryptionKey — для случая, когда при успешной авторизации в системе возвращается ключ шифрования, с помощью которого пользователь может зашифровать данные перед отправкой их в систему.

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

Интерфейс ICredentialInfoEx — композиция из интерфейсов ICredentialInfo и IEncryptionKey.

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

Шаг 2.1


Определим в проекте HelloExtensions.Auth вспомогательные классы и другие типы данных. (Здесь и далее — декларации и логика вспомогательных классов имеют демонстрационных характер, логика выполнена в виде заглушек (stubs). В реальных проектах вспомогательные классы определяются бизнес-логикой.)

Класс TokenValidator — предоставляет логику валидации идентификатора токена (например, проверки на допустимость значения, внутреннюю консистентность и существование в множестве зарегистрированных в системе активных токенов).

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

Класс IdentifierValidator — предоставляет логику валидации идентификатора (например, проверки на допустимость значения и на существование идентификатора в базе данных системы).

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

Перечисление KeySize — перечень допустимых размеров (в битах) ключей шифрования, с определением внутреннего значения в виде длины ключа в байтах.

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

Класс KeySizes — перечень допустимых размеров ключей шифрования в виде списка.

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

Класс KeyValidator — предоставляет логику проверки корректности ключа шифрования.

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

Шаг 2.2


Определим в проекте HelloExtensions.Auth класс CredentialsExtensions, предоставляющий методы расширения для определенных выше интерфейсов, декларирующих различные структуры AuthCredentials, в зависимости от способа авторизации в системе.

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

Как видим, в зависимости от того, какой интерфейс реализует переменная, будет выбран тот или иной метод 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.)

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

В классе CredentialsExtensions оставим только один открытый (public) метод расширения, работающий с 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;
        }
    }
}

Как видим, при вызове метода IsValid, проверки на то, какой интерфейс реализует переменная, теперь выполняются не на этапе компиляции, а во время выполнения (runtime).

Поэтому при реализации метода IsValid(this IAuthCredentials credentials) важно выполнить проверки на реализацию интерфейсов в правильной последовательности (от наиболее «полного» интерфейса к наименее «полному»), для обеспечения корректности результата проверки структуры AuthCredentials.

Шаг 3


Наполним проекты HelloExtensions.ProjectA.Auth и HelloExtensions.ProjectB.Auth логикой, реализующей интерфейсы AuthCredentials из проекта HelloExtensions.Auth и средства работы с реализациями этих интерфейсов.

Общий принцип наполнения проектов:

  1. определяем необходимые интерфейсы, наследующие интерфейсы из HelloExtensions.Auth и добавляющие декларации, специфичные для каждого из проектов;
  2. создаем реализации-заглушки этих интерфейсов;
  3. создаем вспомогательную инфраструктуру с заглушками, предоставляющую API авторизации в некой системе (инфраструктура создается по принципу — интерфейс, реализация, фабрика).

Project «A»

Интерфейсы:

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
    {
    }
}

Реализации интерфейсов:

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

Примечание: имена элементов сущности могут отличаться от имен, определенных в интерфейсе; в этом случае необходимо реализовать элементы интерфейса явно (explicitly), обернув внутри обращение к соответствующему элементу сущности.

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:

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»

Интерфейсы:

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
    {
    }
}

Реализации интерфейсов:

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:

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

Шаг 3.1


Наполним консольное приложение вызовами методов провайдеров авторизации из проектов Project «A» и Project «B». Каждый из методов возвратит переменные некоторого интерфейса, наследующего IAuthCredentials. Для каждой из переменных вызовем метод проверки IsValid. Готово.

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

Таким образом, мы достигли цели, когда для различных сущностей, реализующих схожую функциональность (а также имеющих отличную друг от друга функциональность), мы можем реализовать единый набор методов без copy-paste, позволяющий работать с этими сущностями единым образом.

Данный способ использования методов расширений подходит как при проектировании приложения с нуля, так и для рефакторинга существующего кода.

Отдельно стоит отметить, почему задача в данном примере не реализуется через классическое наследование: сущности в проектах «A» и «B» реализуют функциональность специфичную для каждого из проектов — в первом случае сущности могут (де)сериализовываться из/в XML, во втором — из/в JSON.

Это демонстрационное различие, хотя и оно может встречаться в реальных проектах (в которых различий в сущностях может быть еще больше).

Другими словами, если есть некоторый набор сущностей, пересекающихся по функциональности лишь частично, и это само пересечение имеет «нечеткий характер» (где-то используется UserId и SessionToken, а где-то еще и EncryptionKey), то в создании унифицированного API, работающего с этими сущностями в сфере пересечения их функциональности, помогут методы расширения (extensions methods).

Методология работы с методами расширения предложена в данной статье.

Продолжение следует.

Update:


Изначально в статье не была упомянута одна вещь — предлагаемый подход в большей степени применим для случаев, когда проекте в разных сборках уже есть похожие по своей функциональности сущности (классы), а в сборках верхнего уровня (в клиентском коде) осуществляется работа с этими сущностями схожим образом, путем обращения к их свойствам и проведению некоторых проверок (copy-paste со всеми последствиями).

Каким образом лучше реализовать единообразие работы с этими сущностями, не производя их переработку и рефакторинг?

В данном случае представляется целесообразным объявить набор интерфейсов (свойства и методы интерфейсов будут повторять уже имеющиеся свойства и методы у классов), и объявить, что классы реализуют эти интерфейсы.
Возможно, некоторые элементы реализации интерфейсов у классов потребуется объявить явно (explicitly) путем обращения к соответствующим элементам классов, если элементы классов названы «вразнобой», а рефакторинг классов нецелесообразен.

Затем реализуем класс с методами расширения для новых интерфейсов, и во всех местах обращения к классам заменяем copy-paste некоторой работы с этими классами на вызов одного метода расширения.

Таким образом, предлагаемый подход применим для работы с legacy-кодом, когда необходимо быстро «починить» и реализовать единообразие работы с некоторым набором классов со схожими (в определенном разрезе) декларациями и функциональностью.

(Вопрос, каким образом в проекте может оказаться такой набор классов, вынесем за скобки.)

При разработке API проекта и иерархии классов с нуля следует применять другие подходы.
Каким образом при этом можно реализовать код без copy-paste, если два или более классов имеют один и тот же по смыслу метод, но с немного разной логикой — тема отдельного разговора.
Возможно, это тема новой статьи.
Поделиться с друзьями
-->

Комментарии (38)


  1. gkislin
    02.11.2016 14:05

    Картинок маловато… Вот например на тему композиция+делегирование: коротко, жизненно и с картинками:
    http://citforum.ru/programming/oop_rsis/glava2_1_10.shtml


  1. lair
    02.11.2016 14:17
    +4

    (Эмм, полиморфизм через as? Больно же будет, особенно при модификациях.)


    На мой взгляд, методы-расширения не стоит считать полноправной ОО-функциональностью в C#, это скорее та его часть, которая идет в функциональную часть. Если у вас есть некий интерфейс, экспонирующий некий (простите) метод, то я хочу иметь возможность этот метод определить в своей реализации этого интерфейса. Решение, когда этот метод внезапно уезжает в расширение, приводит к тому, что я не могу этого сделать.


    Просто представьте себе, что я добавил новый наследник от IAuthCredentials. Что нужно сделать, чтобы для него продолжало работать IsValid?


  1. Razaz
    02.11.2016 14:37
    +2

    Как только вы начали добавлять Ex и Info и плодить тонны интерфейсов — все поехало по швам. Экстеншены тут вообще не к месту.

    В процессе аутентификации взаимодействуют аккаунты и креденшалы: IAccount и ICredentials.

    IAccount — Сущности, которым разрешено проходить процесс аутентификации.
    ICredentials — это набор параметров, секретов и тд., которые предъявляет вызывающая сторона.
    ICredentialsValidator — валидирует конкретный тип креденшалов.

    Так же некорректно использовать понятие провайдер аутентификации — вы в своей задаче решаете проблему валидации креденшалов, что является частью процесса, а не им самим.

    Весь ваш код сводится к набору:
    User: IAccount

    UsernamePasswordCredentials: ICredentials
    TokenCredentials: ICredentials

    UsernamePasswordCredentialsValidator: ICredentialsValidator
    TokenCredentialsValidator: ICredentialsValidator

    Ну и простенький ресолв валидаторов. Заодно валидаторы сразу могут получать ссылки на нужный сторадж и выставлять провалидированный аккаунт в ICredentials.
    Расширять можно как угодно.


  1. yar3333
    02.11.2016 15:22

    Что и требовалось доказать: совсем отказываться от наследования пока рано! :)


  1. Ares_ekb
    02.11.2016 15:33

    Интересная статья, но очень сложно воспринимать без иллюстраций. На шаге 2 вы определили 5 интерфейсов, 3 хелпера и 1 класс расширений. Мне пришлось их нарисовать, чтобы понять о чём идёт речь, потому что для воображения обычного человека это слишком круто.

    Затем, на шаге 2.2 добавили ещё 1 интерфейс и переписали класс расширений с использованием as. Я согласен с lair, что это немного странное решение. А что если появятся ещё интерфейсы? Придётся допиливать этот класс расширений?

    Затем на шаге 3 в проекте А добавили ещё 3 интерфейса, 2 реализации, 1 «инфраструктурный» интерфейс, 1 его реализацию и 1 фабрику.

    Потом идет проект B, в котором добавляются уже 4 интерфейса, а не 3 как в проекте А. Соответственно 3 реализации, а не 2 как в проекте А. Я не понял чем вызвана эта асимметрия. Ну, и ещё «инфраструктура»: 1 интерфейс, 1 реализация, 1 фабрика.

    Итого: 15 интерфейсов, 3 хелпера, 1 класс расширений, 7 классов реализаций, 2 фабрики. Даже нарисовав схему, я не очень понимаю что именно там происходит.

    Может быть это решается более простыми способами? Внедрением зависимостей, чтобы не хардкодить варианты через as? Или, например, что вы думаете о примесях (mixin)? Есть интересная статья, правда на примере JavaScript, где примеси рассматриваются как более общий случай наследования, при котором не известен заранее родительский класс. Мне это кажется безумно интересным и по-моему это решает проблемы, описанные в вашей статье. Я писал очень немного про такой подход в этой статье, там есть один пример использования: категория множеств наследуется от абстрактной категории и к ней примешивается поведение полной и колполной категорий.


    1. iCpu
      02.11.2016 20:23

      По большому счёту, примеси — синтаксический сахар для шаблонов. Пока ни в одном языке программирования шаблоны не решили поставленных задач, не породив при этом тонны новых проблем.


      1. Ares_ekb
        03.11.2016 05:49

        В той статье говорится, что есть разные способы реализовать примеси, а они предлагают следующий:

        class BaseClass { }
        class Mixin1<T> : T where T : new() { }
        class Mixin2<T> : T where T : new() { }
        class DerivedClass : Mixin2<Mixin1<BaseClass>> { }
        

        Если какие-то примеси часто используются вместе, то их можно объединять в смеси:
        class CompoundMixin<T> : Mixin2<Mixin1<T>> where T : new() { }
        

        Т.е. примесь — это более общий случай наследования, при котором заранее не известен суперкласс.


        1. iCpu
          03.11.2016 06:40

          Это уже метапрограммирование. И оно сохраняет все проблемы обычного наследования, только вдобавок ещё и отнимает у программиста любые гарантии по поведению кода.


          1. Ares_ekb
            03.11.2016 07:31

            Как минимум две проблемы это точно решает:
            1) Это элегантный способ встроить в класс методы из нескольких классов. Проще чем шаблон делегирования.
            2) Примеси не зависят от конкретного родительского класса. Не нужно пытаться заранее выстроить какую-то идеальную иерархию классов на все случаи в жизни. Эту иерархию можно подстраивать под каждый случай отдельно.

            Лично мне в JavaScript такой подход помог решить некоторые проблемы, тут пример.


            1. iCpu
              03.11.2016 08:21

              Для вас сверху — возможно. Тем не менее, это метапрограммирование, на выходе которого получаются обычные классы-наследники. Со всеми вытекающими проблемами. Например, traits не позволяют вам во множественное наследование. И не смогут.
              Например, traits не позволят вам единообразно обращаться ко всем базовым классам, пережившим встраивание. Возможно, js гораздо толерантнее к подобным запросам, но C++ без плясок с шаблонами не позволит единообразно обращаться к элементам, а любые попытки хранить их единообразно закончатся как и должны: болью.
              https://ideone.com/MZOUOk


              1. Ares_ekb
                03.11.2016 09:06

                Я уже очень много лет не писал на C# и C++. И оказывается мой пример на C# не компилируется, нельзя наследоваться от типа, указанного через параметр. Видимо, поэтому в C# есть расширения, которые вероятно позволяют немного иначе решать те же задачи.

                А в C++ как-раз можно. Переписал ваш пример. Всё работает. Единственное, на мой взгляд, неправильно, что примесь обращается к каким-то свойствам расширяемой структуры. По идее, в примеси должна быть реализована какая-то самодостаточная функциональность, независящая от других классов. Но если это нужно, то можно сделать как в моём примере.


                1. Sirikid
                  03.11.2016 09:39

                  Из-за устройства массивов в C/C++ (массив это n-кусков по m-байтов) мы не можем хранить в них какой-то наиболее общий для всех элементов тип, придется хранить указатели на базовый тип или писать свой собственный массив.


                  1. Ares_ekb
                    03.11.2016 09:54

                    Да, но тут вообще непонятно зачем по сути разные объекты (со структурами bar и baz из примера) хранить в одном массиве. К обоим структурам примешивается traitFoo, но всё-равно структуры разные.


                    1. Sirikid
                      03.11.2016 10:03

                      Если это не ваш вопрос то извините, не понял.


                1. iCpu
                  03.11.2016 11:07

                  Если добавляется независимая функциональность, зачем её добавлять в класс? Для реализации независимой функциональности есть, простите за бранное слово, функция.
                  Смысл наследования именно в доступе к защищённым полям и перекрытии базовой функциональности. На плюсах это требует специальной подготовки самих базовых классов, что вы сделали. При этом разрушив базовую бизнес-логику, чего делать не должны были.
                  https://ideone.com/xmDK4b

                  То есть, опять же, это просто шаблоны, собирающие классы вместо вас. Кто-то говорит, что они безопаснее, чем простое наследование. Лично я смогу прострелить себе ногу даже из своего пальца, хватит желания и достаточного объёма обезболивающих.


    1. sand14
      03.11.2016 23:21

      Потом идет проект B, в котором добавляются уже 4 интерфейса, а не 3 как в проекте А. Соответственно 3 реализации, а не 2 как в проекте А. Я не понял чем вызвана эта асимметрия.

      В проекте «A» две credential-сущности, а в проекте «B» — три.
      Одно не является подмножеством другого.
      Эти множества сущностей имеют только «пересечение» (в части условно стандартных Credential с id пользователя и токеном сессии).
      Асимметрия проектов «A» и «B» как раз призвана продемонстрировать тезис статьи, когда в решении по разным сборкам разбросан набор похожих сущностей, решающих один класс задач.


  1. AlexLeonov
    02.11.2016 16:05
    +1

    trait THasEmail 
      implements IHasEmail 
    {
      protected $email;
      public function getEmail()
      {
        return $this->email;
      }
    }
    
    class User {
      use THasEmail;
    }
    
    assert(User instanceof IHasEmail);
    


    https://wiki.php.net/rfc/traits-with-interfaces

    P.S. Писал комментарий с калькулятора, возможны неточности.


  1. Bonart
    02.11.2016 18:29
    +3

    Зачем вообще так сложно?
    Для задачи, сформулированной во вступлении, достаточно определить один интерфейс с единственным методом IsValid.
    Это расходится с предыдущим текстом, где речь шла о двух методах.
    С двумя методами все опять-таки проще — общая функциональность двух классов выносится в третий путем композиции (объекты двух классов содержат в себе ссылки на объекты третьего вместо наследования).
    Изначальная аргументация столь же хаотична и противоречива — с точки зрения уменьшения дублирования кода композиция лучше наследования реализаций. Накладные расходы на вызов виртуальных методов связаны не с наследованием, а с полиморфизмом.
    Предлагаемая реализация страдает перепроектированием — для иллюстрации достаточно трех классов и одного интерфейса.


  1. AlexSys
    02.11.2016 19:51
    +1

    OverEngeneering) Много кода для простой задачи


  1. iCpu
    02.11.2016 20:42

    Так, подождите, я не понимаю, как можно одновременно разрешить и задачу динамической типизации в рантайме, и статической типизации во время компиляции, не реализовав два разных синтаксиса? И вопрос не в наследовании, композиция точно так же будет реализована через вызовы методов по указателю, если будет производиться в динамике.


  1. sand14
    02.11.2016 22:02

    Изначально в статье не была упомянута одна вещь — предлагаемый подход в большей степени применим для случаев, когда проекте в разных сборках уже есть похожие по своей функциональности классы, а в сборках верхнего уровня (в клиентском коде) осуществляется работа с этим классами схожим образом путем обращения к их свойствам и проведению некоторых проверок (copy-paste со всеми последствиями).

    Каким образом лучше реализовать единообразие работы с этими классами, не производя переработку и рефакторинг самих классов?

    В данном случае представляется целесообразным объявить набор интерфейсов (свойства и методы интерфейсов будут повторять уже имеющиеся свойства и методы у классов), и объявить, что сущности реализуют эти интерфейсы.
    Возможно, некоторые элементы реализации интерфейсов у классов потребуется объявить явно (explicit) путем обращения к соответствующим элементам классов, если элементы классов названы «вразнобой», а рефакторинг классов нецелесообразен.

    Затем реализуем класс с методами расширения для новых интерфейсов, и во всех местах обращения к классам заменяем copy-paste некоторой работы с этими классами на вызов одного метода расширения.

    Таким образом, предлагаемый подход применим для работы с legacy-кодом, когда необходимо быстро «починить» и реализовать единообразие работы с некоторым набором классов со схожими (в определенном разрезе) декларациями и функциональностью.

    (Вопрос, каким образом в проекте может оказаться такой набор классов, вынесем за скобки.)

    При разработке API проекта и иерархии классов с нуля следует применять другие подходы.
    Каким образом при этом можно реализовать код без copy-paste, если два или более классов имеют один и тот же по смыслу метод, но немного с разной логикой — тема отдельного разговора.
    Возможно, это тема новой статьи.


    1. iCpu
      02.11.2016 22:22

      Я правильно понимаю, вы только что напыщенно описали паттерн «стратегия»?


    1. lair
      02.11.2016 22:50

      Каким образом лучше реализовать единообразие работы с этими классами, не производя переработку и рефакторинг самих классов?
      В данном случае представляется целесообразным объявить набор интерфейсов (свойства и методы интерфейсов будут повторять уже имеющиеся свойства и методы у классов), и объявить, что сущности реализуют эти интерфейсы.

      Эти два утверждения друг другу противоречат. Вы либо можете заставить классы реализовывать эти интерфейсы — и тогда это рефакторинг, — либо не можете.


      Если не можете, то шаблон "адаптер" ваш друг.


  1. atoshin
    03.11.2016 12:20
    +1

    Госсподи боже! Такое чувство, что люди вообще не понимают, о чём сами пишут.


  1. Sirikid
    03.11.2016 18:45

    Честно говоря я не смог прочитать статью. Я считаю нужны более конкретные условия, потому что в Java 8+ можно в интерфесах описывать default-методы, проблемы разделения реализации по умолчанию уже нету.


  1. dim2r
    05.11.2016 12:08

    Изначально ООП было задумано для моделирования биологических систем и игр. Сейчас его применяют для автоматизации бизнеса. По опыту скажу, что бизнес-процессы более простые, они не требуют многого того, что заложено в ООП.


    1. lair
      05.11.2016 17:28

      Изначально ООП было задумано для моделирования биологических систем и игр

      Это вы почему так считаете?


      По опыту скажу, что бизнес-процессы более простые, они не требуют многого того, что заложено в ООП.

      Например, чего?


      (вообще, утверждение, что "бизнес-процессы более простые" — оно, конечно, громкое, да)


      1. dim2r
        05.11.2016 18:33

        Это вы почему так считаете?

        Когда изобрели первые языки ООП, типа small talk, я уже зарабатывал программированием. Одни из первых лекций по Оберону нам читал сам Вирт в НГУ

        выдержка из https://habrahabr.ru/company/hexlet/blog/303754/
        Я считал объекты чем-то вроде биологических клеток, и/или отдельных компьютеров в сети, которые могут общаться только через сообщения.


        1. lair
          05.11.2016 19:18

          Одни из первых лекций по Оберону нам читал сам Вирт в НГУ

          Кул, точную аттрибутированную цитату привести можете?


          Я считал объекты чем-то вроде биологических клеток

          Когда что-то построено "по подобию" чего-то (хотя, на самом деле, даже это не так), еще не факт, что первое построено для моделирования второго. Тем более, что в процитированной вами фразе на равным правах указаны "компьютеры в сети", так что можно было бы сказать, что "изначально ООП было задумано для моделирования компьютеров в сети", но согласитесь, что это странно?


  1. dim2r
    05.11.2016 20:26
    -1

    точную аттрибутированную цитату привести можете?


    Внезапная просьба. Вам зачем?


    1. Sirikid
      05.11.2016 20:32

      «Пруф или не было»


      1. dim2r
        05.11.2016 21:02

        Что такое «пруф»? Если нужно непосредственое доказательство, то оплачивайте приезд Вирта, Страуструпа и других основателей в ваше заведение и с них трясите…


    1. lair
      05.11.2016 20:39
      +1

      Чтобы понять, на основании чего — кроме своего личного мнения — вы утверждаете, что "изначально ООП было задумано для моделирования биологических систем и игр".


      А то тут в соседнем посте рассказывают, что Симула была разработана для дискретно-событийной симуляции (вики раскрывает: "для физического моделирования, такого как изучение и улучшение движения судов и их содержимого через грузовые порты". Не сходится.


      1. dim2r
        05.11.2016 21:51
        -1

        Тут особо моего мнения нет, но и ссылку привести не могу. Степень отражения модели и реальности строгому счету пока не поддается. Но посудите сами ООП — объектно-ориентировано, а бизнес просессы процессно-ориентированы. Какая-то разница уже намечается.


        1. lair
          05.11.2016 22:33
          +1

          Но посудите сами ООП — объектно-ориентировано, а бизнес просессы процессно-ориентированы. Какая-то разница уже намечается.

          Тот факт, что есть разница, не означает, что "что бизнес-процессы более простые, они не требуют многого того, что заложено в ООП".


          Более того, хотя процессы и процессо-ориентированы, участвующие в них сущности (и акторы, кстати) часто прекрасно поддаются объектно-ориентированному подходу (собственно, ничего не мешает представить бизнес-процесс в виде обмена сообщениями).


          Наконец, есть и третий взгляд на все это: ООП может быть слоем разработки ниже модельного уровня.


          1. dim2r
            05.11.2016 22:45
            -1

            Давайте на конкретику перейдем, а то слишком много общих слов. У вас есть примеры, когда ну никак нельзя без ООП?


            1. lair
              05.11.2016 22:53
              +1

              Подождите-подождите, а я разве где-то говорил, что есть случаи, когда никак нельзя без ООП? Нет таких случаев, умный программист может решить любую задачу в той парадигме, которая сейчас доступна.


              1. dim2r
                05.11.2016 23:01
                -1

                предлагаю закончит беседу