Композиція і інтерфейси

У світі об'єктно-орієнтованого програмування вже досить давно піддається критиці концепція успадкування.
Аргументів достатньо багато:

  • дочірній клас наслідує всі дані і поведінку батьківського, що потрібно не завжди (а при доопрацюванні батьківського у дочірній клас взагалі потрапляють дані і поведінка, які не передбачалися на момент розробки дочірнього);
  • віртуальні методи менш продуктивні, а в разі, якщо мова дозволяє оголосити невиртуальный метод, то як бути, якщо у спадкоємця потрібно його перекрити (можна позначити метод словом new, але при цьому не буде працювати поліморфізм, і використання такого об'єкта може привести до очікуваного поведінки, залежно від того, до якого типу наведено об'єкт в момент його використання);
  • якщо виникає необхідність множинного спадкування, то в більшості мов воно відсутнє, а там, де є (C++), вважається трудомістким;
  • є завдання, де спадкування як таке не може допомогти — якщо потрібен контейнер елементів (безліч, масив, список) з єдиним поведінкою для елементів різних типів, і при цьому потрібно забезпечити статичну типізацію, то тут допоможуть узагальнення (узагальнення).
  • і т. д. і т. п.
Альтернативою спадкоємства є використання інтерфейсів і композиція. (Інтерфейси давно використовується в якості альтернативи множинного спадкоємства, навіть якщо в ієрархії класів активно використовується спадкування.)

Декларованим перевагою спадкування є відсутність дублювання коду. У предметної області можуть зустрічатися об'єкти з схожим або однаковим набором властивостей і методів, але мають частково або повністю різні поведінку або механізми реалізації цієї поведінки. І в цьому випадку для усунення дублювання коду доведеться використовувати інші механізми.

А як можна вирішити цю задачу (відсутність дублювання коду) у разі композиції і інтерфейсів?
Цій темі і присвячена ця стаття.

Нехай оголошено деякий інтерфейс, і два або більше класів, що реалізують цей інтерфейс. Частину коду, що реалізує інтерфейс, у кожного класу різна, а частина — однакова.

Для спрощення розглянемо приватний варіант, коли метод 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.

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 easy 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 і засоби роботи з реалізаціями цих інтерфейсів.

Загальний принцип наповнення проектів:

  1. визначаємо необхідні інтерфейси, що походять інтерфейси з HelloExtensions.Auth і додають декларації, специфічні для кожного з проектів;
  2. створюємо реалізації-заглушки цих інтерфейсів;
  3. створюємо допоміжну інфраструктуру з заглушками, що надає 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: saving to Implement 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: saving to Implement 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: saving to Implement 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).

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

Продовження слід.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.