Обертка классов как отличный помощник программиста на C#
Класс-обертка, что это такое и с чем он может нам помочь? Был ли у вас какой-то код, основанный на какой-то библиотеке фреймворка, который вы хотели бы протестировать в юнит тестом? Возможно добавить новые функции к объекту, на изменение которого у вас нет разрешения? Или у вас есть существующий объект, который вы хотели бы использовать как другой объект? Что ж, обертка над классом здесь для того, чтобы помочь!
Обертка может использоваться для предоставления доступа к определенным методам в рамках существующего класса или функциональности. Это позволяет легко упрощать или расширять существующие классы/функции, а также изменять доступные для них свойства/методы с помощью безопасности/аутентификации.
Итак, почему вы хотите выполнить что-либо из вышеперечисленного? Ну, если вы помните первый абзац, иногда мы хотим провести юнит тестирование кода, который использует фреймворковый код / сторонние библиотеки, расширить существующую функциональность или сделать существующий тип совместимым с другим типом. В любом случае, давайте рассмотрим несколько примеров.
Первый пример — у нас есть класс FileStorage, который использует статический класс System.IO.File. Итак, вот наш исходный код.
public class FileStorage
{
public void DeleteFile(string fileName)
=> File.Delete(fileName);
public string LoadFile(string fileName)
=> File.ReadAllText(fileName);
public void SaveFile(string fileName, string fileContents)
=> File.WriteAllText(fileName, fileContents);
}
У нас есть несколько базовых функций: удаление, сохранение и загрузка. Теперь, что, если бы мы захотели провести юнит тестирование этого? Что, если бы мы захотели обновить эти методы, чтобы использовать облачное хранилище? Или даже опубликовать содержимое в WebAPI? Хотим ли мы создать совершенно новый класс со своими собственными методами или создать новые методы для нашего существующего FileStorage? Если бы мы создали совершенно новый класс, нам пришлось бы при необходимости создавать правильный класс, если бы мы создавали новые методы, нам пришлось бы обновлять код для вызова этих новых методов, а как насчет юнит тестирования? Это совсем не подходит для обслуживания.
Почему бы нам не извлечь интерфейс из нашего класса FileStorage? Это даст нам определение интерфейса, позволяющее создавать и использовать файловое хранилище любого типа. Итак, давайте сначала создадим наш интерфейс.
public interface IFileStorage
{
void SaveFile(string fileName, string fileContents);
string LoadFile(string fileName);
void DeleteFile(string fileName);
}
Теперь все, что нам нужно сделать, это применить этот интерфейс к нашему существующему FileStorage следующим образом
public class FileStorage : IFileStorage
{
public void DeleteFile(string fileName)
=> File.Delete(fileName);
public string LoadFile(string fileName)
=> File.ReadAllText(fileName);
public void SaveFile(string fileName, string fileContents)
=> File.WriteAllText(fileName, fileContents);
}
Итак, на данный момент все, что мы сделали это создали интерфейс, ну да, это правда, но что мы на самом деле получили, обернув наше FileStorage интерфейсом? Ну, во-первых, теперь у нас есть интерфейс, который можно использовать при необходимости, это означает, что нам больше не нужно полагаться на FileStorage, который основан на реализации System.IO.File, например, теперь мы можем имитировать наш IFileStorage, чтобы его можно было протестировать в модульном режиме. Мы могли бы даже использовать совершенно новую реализацию нашего хранилища файлов, подобную приведенной ниже.
public class InMemoryFileStorage : IFileStorage
{
private IDictionary<string, string> Files { get; set; }
public InMemoryFileStorage()
{
Files = new Dictionary<string, string>();
}
public void DeleteFile(string fileName)
{
if(Files.ContainsKey(fileName))
Files.Remove(fileName);
}
public string LoadFile(string fileName)
{
if (Files.ContainsKey(fileName))
return Files[fileName];
return string.Empty;
}
public void SaveFile(string fileName, string fileContents)
=> Files[fileName] = fileContents;
}
Вместо использования нашего хранилища файлов мы могли бы использовать наше хранилище InMemoryFIleStorage, таким образом, мы фактически не записываем файлы на диск, а записываем их в память. Также помните, что поскольку у нас есть интерфейс для наших требований к IFileStorage, мы можем создать любую реализацию и использовать ее, когда захотим.
Естественно, что это не лучший пример для класса обертки, это всего лишь интерфейс, но он позволяет нам обернуть фреймворковый код с помощью интерфейса. Таким образом, нам не нужно полагаться на фреймворковый код в соответствии с конкретными требованиями, такими как System.IO.File. Если это поможет, мысленно замените System.IO.File на System.Net.Http.HttpClient, HttpClient используется для создания HTTP-запросов, мы бы не хотели на самом деле вызывать веб-запрос в наших действующих юнит тестах, а также что, если мы отойдем от HTTP и начнем использовать служебную шину? Лучше всего обернуть это в интерфейс, чтобы вы могли провести юнит тестирование и при необходимости изменить реализацию.
Второй пример - аудит. У нас есть класс BankAccount, который занимается добавлением средств, снятием средств и обновлением имени учетной записи. Вот пример кода
public interface IBankAccount
{
string AccountId { get; }
string AccountName { get; }
decimal CurrentFunds { get; }
void AddFunds(decimal funds);
void RemoveFunds(decimal funds);
void UpdateDetails(string name);
}
public class BankAccount : IBankAccount
{
public string AccountId { get; private set; }
public string AccountName { get; private set; }
public decimal CurrentFunds { get; private set; }
public BankAccount(string accountId, string accountName, decimal currentFunds)
{
AccountId = accountId;
AccountName = accountName;
CurrentFunds = currentFunds;
}
public void AddFunds(decimal funds)
=> CurrentFunds += funds < 0 ? 0 : funds;
public void RemoveFunds(decimal funds)
=> CurrentFunds -= funds < 0 ? 0 : funds;
public void UpdateDetails(string name)
=> AccountName = string.IsNullOrWhiteSpace(name) ? AccountName : name;
}
Нас попросили разрешить проведение аудита всеми тремя из этих методов. Аудит будет настраиваться владельцем учетной записи, это означает, что не все банковские счета нужно будет проверять, поэтому нам нужно оставить существующую функциональность в покое. Итак, во-первых, мы не хотим дублировать исходный класс, потому что, если бы нам пришлось добавить новый метод или модифицировать метод, нам пришлось бы не забыть обновить наш BankAccount и наш новый AuditingBankAccount, а также обновить любые другие классы IBankAccount.
Мы могли бы добавить логический флаг для аудита в наш BankAccount, но действительно ли мы хотим добавлять флаг? Что, если бы мы захотели добавить больше флагов? Наш класс BankAccount мог бы раздуться, и код мог бы начать выглядеть очень запутанным из-за того, что повсюду были инструкции if. Лучше всего реализовать тот же интерфейс, что и BankAccount, и вызвать исходный объект.
Таким образом, мы можем обойти эту проблему, создав программу-оболочку аудита, которая будет реализовывать наш интерфейс IBankAccount, таким образом, он будет соответствовать правилам нашего BankAccount, но мы также расширим существующие методы нашего BankAccount. Давайте посмотрим, как мы можем добиться нашего нового AuditingBankAccount
<pre><code lang="csharp">public class AuditingBankAccount : IBankAccount { public string AccountId => _bankAccount.AccountId; public string AccountName => _bankAccount.AccountName; public decimal CurrentFunds => _bankAccount.CurrentFunds; public ICollection<string> Audit { get; private set; } private readonly IBankAccount _bankAccount; public AuditingBankAccount(BankAccount bankAccount) { _bankAccount = bankAccount; Audit = new List<string>(); } public void AddFunds(decimal funds) { var audit = new StringBuilder(); audit.AppendLine($"Current funds [{CurrentFunds}]"); audit.AppendLine($"Funds to add [{funds}]"); _bankAccount.AddFunds(funds); audit.Append($"New current funds [{CurrentFunds}]"); Audit.Add(audit.ToString()); } public void RemoveFunds(decimal funds) { var audit = new StringBuilder(); audit.AppendLine($"Current funds [{CurrentFunds}]"); audit.AppendLine($"Funds to remove [{funds}] "); _bankAccount.RemoveFunds(funds); audit.Append($"New current funds [{CurrentFunds}]"); Audit.Add(audit.ToString()); } public void UpdateDetails(string name) { var audit = new StringBuilder(); audit.AppendLine($"Current account name [{AccountName}]"); _bankAccount.UpdateDetails(name); audit.AppendLine($"New account name [{AccountName}]"); Audit.Add(audit.ToString()); } }С нашим описанным выше AuditingBankAccount все, что мы сделали, это обернули наш оригинальный класс BankAccount, но мы добавили дополнительную функцию, которая просто проверяет каждый метод. Это было достигнуто двумя способами: во-первых, мы реализовали тот же интерфейс, что и у нашего BankAccount, и, во-вторых, в нашем конструкторе мы использовали существующий BankAccount. Интерфейс позволил нам расширить методы, поместив наш код аудита в каждый метод, и мы сохранили исходный функционал, вызвав наш банковский счет, который был предоставлен при создании.
Этот пример показывает, что мы можем функционально расширить существующий объект, не изменяя сам исходный объект. Все, что нам нужно сделать, это обернуть исходный объект и предоставить те же методы (интерфейс или абстрактный класс) и вызвать тот же метод для исходного объекта. Это очень полезно, когда у вас может не быть доступа к изменению исходного объекта, например, объекта из библиотеки фреймворка или кода стороннего производителя.
Третий пример - у нас есть класс Printer, которому можно присвоить Job, добавляемое в коллекцию, как только мы будем готовы, мы можем вызвать метод Print для нашего Printer, который вызовет метод Print для Job, присутствующих в коллекции. Вот пример кода.
public class Printer
{
private ICollection<Job> Jobs;
public Printer() => ResetJobs();
public void Add(Job job)
=> Jobs.Add(job);
public void Print()
{
var output = new StringBuilder();
foreach (var job in Jobs)
output.AppendLine(job.Print());
ResetJobs();
Console.Write(output.ToString());
}
private void ResetJobs()
=> Jobs = new List<Job>();
}
public class Job
{
public string Content { get; set; }
public string Print() => Content;
}
У нас есть несколько существующих объектов, которые мы хотели бы предоставить нашему принтеру, чтобы их можно было вывести. Итак, прежде всего, вот наши существующие объекты.
public class Employee
{
public string Name { get; set; }
public string EmployeeReference { get; set; }
public decimal YearlySalary { get; set; }
}
public class Sqaure
{
public int Size { get; set; }
}
public class Weapon
{
public string Title { get; set; }
public WeaponType Type { get; set; }
public Rarity Rarity { get; set; }
public int BaseDamage { get; set; }
}
public enum WeaponType
{
Sword,
Axe,
Bow,
Mace,
Staff
}
public enum Rarity
{
Uncommon,
Common,
Rare,
Epic,
Legendary
}
Для каждого из этих объектов мы хотели бы, чтобы они были доступны для вывода, и каждый из них выводился по-разному. Итак, как мы можем сделать так, чтобы наши классы Employee, Square и Weapon использовались нашим классом Printer? У нас есть класс Job. Мы могли бы создать новый метод для каждого из вышеперечисленных объектов, который возвращал бы новый Job, например, так
public class Employee
{
public string Name { get; set; }
public string EmployeeReference { get; set; }
public decimal YearlySalary { get; set; }
public Job Print()
{
return new Job()
{
Content = $"Имя: {Name} ({EmployeeReference}) - Годовая зарплата: {YearlySalary}"
};
}
}
Но тогда нам пришлось бы поддерживать этот метод три раза, и это могло бы привести к путанице, если бы мы позволили большему количеству объектов возвращать новый объект Job.
Ну, мы могли бы создать интерфейс и реализовать это, но почему бы нам вместо этого не пометить наш класс Job как абстрактный класс, чтобы любой класс, который может быть доступен для вывода, мог наследовать Job, и тогда они будут вынуждены реализовать метод Print. Давайте обновим наш класс Job следующим образом
public abstract class Job
{
public abstract string Print();
}
Это было большое изменение! Итак, давайте обновим наши три класса, которые мы хотели бы сделать доступными для печати в нашем классе Printer.
public class Employee : Job
{
public string Name { get; set; }
public string EmployeeReference { get; set; }
public decimal YearlySalary { get; set; }
public override string Print()
=> $"Имя: {Name} ({EmployeeReference}) - Годовая зарплата: {YearlySalary}";
}
public class Square : Job
{
public int Size { get; set; }
public override string Print()
{
var sb = new StringBuilder();
for (var y = 0; y < Size; y++)
{
for (var x = 0; x < Size; x++)
{
sb.Append("#");
}
if(y != (Size -1))
sb.AppendLine();
}
return sb.ToString();
}
}
public class Weapon : Job
{
public string Title { get; set; }
public WeaponType Type { get; set; }
public Rarity Rarity { get; set; }
public int BaseDamage { get; set; }
public override string Print()
{
var sb = new StringBuilder();
sb.AppendLine(Title);
sb.AppendLine($"Тип: {Type}");
sb.AppendLine($"Редкость: {Rarity}");
sb.AppendLine($"Сила: {BaseDamage}");
return sb.ToString();
}
}
Наш класс Printer не нужно будет менять, потому что в нашем классе Job по-прежнему используется метод Print, и в целом все, что мы сделали, это пометили его как абстрактный.
Отличная работа, так что же мы, собственно, сделали? Итак, мы сделали доступными для вывода три существующих объекта, которые по какой-либо причине могут использоваться нашей системой, просто унаследовав их от абстрактного класса Job. Мы просто объединили класс Job с нашими тремя исходными классами. Наши три класса могут быть изменены в любом виде, если они реализуют метод вывода, унаследованный от класса Job, и могут использоваться классом Printer, благодаря полиморфизму.
Комментарии
Для того чтобы оставить свое мнение, необходимо зарегистрироваться на сайте