Достаточно ли определить IMyInterface<T>? Нужен ли мне IMyInterface?

Достаточно ли определить IMyInterface<T>? Нужен ли мне IMyInterface?

4 января 2023 г.

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

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

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


Photo by Susan Q Yin on Unsplash

Достаточно ли определить IMyInterface? мне также нужен IMyInterface?

Чтобы ответить на этот вопрос, давайте шаг за шагом рассмотрим приведенный ниже пример, так что потерпите меня и не торопите события.

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

:::

Основные классы сущностей

Это основные классы сущностей, которые мы собираемся использовать.

// This class is representing the simple entity we are going to use
// in our example. 
public class Data
{
    public int DataId { get; set; }
    public string DataDescription { get; set; }
}

// This class is the first kind of the parent Data class.
public class EmployeeData : Data
{
    public string EmployeeName { get; set; }
}

// This class is the second kind of the parent Data class.
public class AssetData : Data
{
    public int AssetId { get; set; }
    public string AssetName { get; set; }
}

Общий интерфейс

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

// This is the interface we are going to focus on.
// It is an interface for something that would be able
// to read and write data to something that is ready
// to be used this way.
public interface IReaderWriter<TData> where TData : Data
{
    void Initialize();
    TData Read(int dataId);
    void Write(TData data);
}

В этом интерфейсе мы заметили следующее:

  1. Это универсальный интерфейс, который принимает общий параметр типа Data.
  2. У него есть метод Initialize.
  3. У него есть метод Read(int dataId), который принимает целочисленный параметр и возвращает TData.
  4. У него есть метод Write(TData data), который ожидает параметр TData.

Универсальный интерфейс

Это класс, реализующий наш универсальный интерфейс. Сам класс является общим.

// This is the class implementing our IReaderWriter<TData>
// but now we know that it is going to save and retrieve
// data to and from a file. We would not care about the
// implementation so don't give it too much thought.
public class FileReaderWriter<TData> : IReaderWriter<TData> where TData : Data
{
    public void Initialize() { throw new NotImplementedException(); }

    public TData Read(int dataId) { throw new NotImplementedException(); }

    public void Write(TData data) { throw new NotImplementedException(); }
}

В этом классе мы заметили следующее:

  1. Это общий класс.
  2. Методы генерируют исключения, и это сделано намеренно, чтобы сосредоточить основное внимание на том, чему он принадлежит.

Внутри модуля «Сотрудник»

Теперь в вашей системе есть модуль, предназначенный для управления данными о сотрудниках, назовем этот модуль; Модуль сотрудников.

Внутри Employee Module вы уверены в типе данных, с которыми имеете дело, очевидно, что это тип EmployeeData.

Вот почему вы можете написать код, подобный приведенному ниже, без каких-либо проблем.

var employeeDataFileReaderWriter = new FileReaderWriter<EmployeeData>();
employeeDataFileReaderWriter.Initialize();

employeeDataFileReaderWriter.Write(new EmployeeData
    { DataId = 1, DataDescription = "Some description.", EmployeeName = "Ahmed" });

var ahmed = employeeDataFileReaderWriter.Read(1);

Внутри модуля объектов

Теперь в вашей системе есть модуль, предназначенный для управления данными актива, назовем этот модуль; Модуль ресурсов.

Внутри Модуля активов вы уверены в типе данных, с которыми имеете дело, очевидно, что это тип AssetData.

Вот почему вы можете написать код, подобный приведенному ниже, без каких-либо проблем.

var assetDataFileReaderWriter = new FileReaderWriter<AssetData>();
assetDataFileReaderWriter.Initialize();

assetDataFileReaderWriter.Write(new AssetData
{ DataId = 2, DataDescription = "Some description.", AssetId = 5, AssetName = "Asset 5."});

var asset5 = assetDataFileReaderWriter.Read(2);

Как насчет общего модуля

Теперь предположим, что у вас есть общий модуль с методом Run(IReaderWriter readerWriter). Внутри этого метода вы хотите вызвать метод Initialize переданного параметра readerWriter.

Вы бы попробовали написать что-то вроде этого:

Image by Ahmed Tarek

Теперь понятно, что вы не можете этого сделать, так как у вас нет неуниверсального определения интерфейса IReaderWriter. Другими словами, у нас есть только IReaderWriter<TData>, а не IReaderWriter.


Photo by Brett Jordan on Unsplash

Неправильные ожидания

Теперь я слышу, как кто-то издалека кричит:

<цитата>

Да, это проще простого. Воспользуемся IReaderWriter<object>. Каждый класс является дочерним по отношению к Object, верно?….. гениально.

Мой ответ ему, что он должен делать свою домашнюю работу, так как это не сработает. Если вы не доверяете мне в этом, просто попробуйте, и вы увидите следующее:

Image by Ahmed Tarek

Вам нужно больше объяснений, краткий ответ заключается в том, что ваш интерфейс является инвариантным; вы не можете вызвать метод, ожидающий IReaderWriter<SomeClass>, и передать экземпляр IReaderWriter<AnyOtherClass>. Единственным приемлемым вызовом будет передача IReaderWriter<SomeClass> и ничего больше.


Photo by Brett Jordan on Unsplash

Это путь

Теперь вы понимаете, почему нам нужно определить неуниверсальный интерфейс IReaderWriter.

Поэтому, переходя к реализации, мы можем получить вот такой код:

public interface IReaderWriter
{
    void Initialize();
}

public interface IReaderWriter<TData> : IReaderWriter where TData : Data
{
    TData Read(int dataId);
    void Write(TData data);
}

Теперь мы можем заметить следующее:

  1. Мы определили новый интерфейс, но на этот раз это не универсальный интерфейс.
  2. В этом интерфейсе будет определен только метод Initialize(), так как это то, что нам действительно нужно в общем модуле или даже в новых модулях, если они появятся в будущем.
  3. Теперь другой общий интерфейс может безопасно расширять обычный интерфейс с помощью методов Read и Write, ничего не меняя в подписи.

Давайте попробуем

Итак, теперь вернемся к нашему общему модулю и посмотрим, заработает он или нет.

Image by Ahmed Tarek

Наконец-то он работает. Давайте отпразднуем и поедим и выпьем, какая поездка :)

Я не хочу сообщать здесь плохие новости, но у меня есть плохие новости…


Photo by Nik Shuliahin on Unsplash

Почему так грустно

У вас есть новые требования, и общий модуль нуждается в некоторых изменениях. Общий модуль теперь должен иметь возможность сохранять и извлекать данные в хранилище BLOB-объектов и из него. Хранилище BLOB-объектов может хранить любые данные.

Итак, на основе этого ввода вы попытаетесь сделать что-то вроде этого:

Image by Ahmed Tarek

Теперь понятно, что работать он не будет, так как в интерфейсе IReaderWriter не определены методы Read и Write. Они определены в IReaderWriter<TData>. Однако на общем модуле и в момент вызова методов StoreInBlob и RetrieveFromBlob мы не знаем тип данных. Так что же делать!!!


Photo by NeONBRAND on Unsplash

Неправильный путь

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

public interface IReaderWriter
{
    void Initialize();
    Data Read(int dataId);
    void Write(Data data);
}

public class FileReaderWriter : IReaderWriter
{
    public void Initialize() { throw new NotImplementedException(); }

    public Data Read(int dataId) { throw new NotImplementedException(); }

    public void Write(Data data) { throw new NotImplementedException(); }
}

Итак, теперь общий модуль подойдет следующим образом:

Image by Ahmed Tarek

Однако вы утратили преимущество при работе с объектами со строгой типизацией следующим образом:

Image by Ahmed Tarek

Теперь вам нужно преобразовать объект Employee, чтобы вы могли получить доступ к его уникальным членам, таким как EmployeeName, как показано на изображении.

Image by Ahmed Tarek

Точно так же вы должны преобразовать свой объект Asset, чтобы иметь доступ к его уникальным элементам, таким как AssetName, как показано на изображении.

Ну и что теперь????


Photo by Michael Carruth on Unsplash

Момент истины

Ключевым словом для лучшего решения здесь является слово new. Позвольте мне объяснить это для вас.

public interface IReaderWriter
{
    void Initialize();
    Data Read(int dataId);
    void Write(Data data);
}

public interface IReaderWriter<TData> : IReaderWriter where TData : Data
{
    new TData Read(int dataId);
}

Мы можем заметить следующее:

  1. Интерфейс IReaderWriter теперь определяет необходимые методы.
  2. Однако методы Read и Write теперь используют родительский тип сущности Data.
  3. Интерфейс IReaderWriter<TData> теперь расширяет интерфейс IReaderWriter.
  4. Это означает, что он также косвенно определяет три известных нам метода.
  5. Однако внутри интерфейса IReaderWriter<TData> нам нужно использовать универсальный тип TData, а не родительский Data. ли>
  6. Для этого нам нужно добавить методы TData Read(int dataId); и void Write(TData data); в класс IReaderWriter<TData> интерфейс.
  7. Для метода чтения вы не можете сделать это, потому что родительский интерфейс, не универсальный, уже определяет точно такой же метод с точки зрения имени и входных параметров, но только с другим типом возвращаемого значения.
  8. Это могло бы запутать компилятор во время выполнения, поскольку он не знал бы, какой метод вызывать, возвращающий Data или возвращающий TData.
  9. Вот почему компилятор не позволит вам сделать это, если вы не добавите ключевое слово new в начале определения метода, как в приведенном выше коде.
  10. Это указывает компилятору скрыть метод Read, унаследованный от родителя, и заменить его методом, определенным после ключевого слова new.
  11. Теперь вы можете спросить, почему мы не сделали то же самое с методом Write?
  12. Ответ прост: нам это не нужно. В родительском интерфейсе у нас уже есть метод с именем Write, который ожидает параметр типа Data, который является родительским для всех типов, которые могут быть переданы в Напишите метод.
  13. Это означает, что этот метод может быть вызван с передачей любого TData, который может прийти.
  14. Еще один момент: если вы попытаетесь использовать ключевое слово new с методом Write, вы получите предупреждение о том, что на самом деле вы ничего не скрываете от родительского интерфейса. Это логично, так как два метода Write имеют разные типы входных параметров, поэтому компилятору понятно, что это два разных метода.

public class FileReaderWriter<TData> : IReaderWriter<TData> where TData : Data
{
    public void Initialize() { throw new NotImplementedException(); }

    public TData Read(int dataId) { throw new NotImplementedException(); }

    public void Write(TData data) { throw new NotImplementedException(); }

    Data IReaderWriter.Read(int dataId) { return Read(dataId); }

    void IReaderWriter.Write(Data data) { Write((TData)data); }
}

Мы можем заметить следующее:

  1. Три старых метода остались прежними.
  2. Теперь у нас есть еще два реализованных метода.
  3. Первый метод: Data IReaderWriter.Read(int dataId) { return Read(dataId); .
  4. Этот метод является явной реализацией метода Data Read(int dataId);, определенного в родительском интерфейсе IReaderWriter.
  5. Это означает, что всякий раз, когда объект класса FileReaderWriter<TData> неявно или явно приводится к неуниверсальному интерфейсу IReaderWriter, этот Read будет использоваться реализация метода.
  6. Второй метод: void IReaderWriter.Write(Data data) { Write((TData)data); .
  7. Этот метод является явной реализацией метода void Write(Data data);, определенного в родительском интерфейсе IReaderWriter.
  8. Это означает, что всякий раз, когда объект класса FileReaderWriter<TData> приводится, явно или неявно, к неуниверсальному интерфейсу IReaderWriter, этот Write будет использоваться реализация метода.

Теперь это приводит к следующему:

Image by Ahmed Tarek

И

Image by Ahmed Tarek

Наконец-то все работает как надо :)


Photo by Nick Fewings on Unsplash

Это то, что есть

Эта методика проектирования (боюсь даже назвать ее шаблоном) уже используется в классах .NET, которые вы используете ежедневно. Вы заметили, что в .NET у нас есть IEnumerable и IEnumerable<T>?

Могли бы вы представить, какой была бы жизнь, если бы у нас не было IEnumerable :) ?

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

Вы можете утверждать, что вы все еще можете написать метод, который принимает <T>, а затем передает его в IEnumerable<T>, но, мой друг, это сохранит пузырится до тех пор, пока вам в конечном итоге не придется выбирать тип сущности. Этот тип сущности не всегда определяется на всех уровнях или уровнях кода, как мы доказали выше.

Поэтому мой последний совет вам: не пытайтесь ходить вокруг да около, это то, что есть…

Вот и все, надеюсь, вам было так же интересно читать эту статью, как мне было ее писать.


Также опубликовано здесь


Оригинал