Andrey on .NET | C# 8 – Реализация по-умолчанию для метода интерфейса

C# 8 – Реализация по-умолчанию для метода интерфейса

C# logoСреди нововведений C# 8 есть одно, которое можно назвать неоднозначным. Это возможность определить реализацию по-умолчанию для метода интерфейса. Рассмотрим её синтаксис, варианты использования, а также какие потенциальные проблемы могут возникнуть из-за её применения.

Синтаксис и использование

По словам разработчиков C# 8, рассматриваемая возможность упрощает процесс добавление нового метода в уже существующий интерфейс. В предыдущих версиях языка такое изменение требовало сразу реализовать новый метод во всех существующих реализациях интерфейса. В противном случае проект просто не соберется.

C# 8 позволяет определить реализацию по-умолчанию непосредственно в самом интерфейсе. В этом случае код скомпилируется без ошибок, даже если классы реализации еще не были обновлены. При этом, во время выполнения используются следующая схема вызова метода интерфейса:

  • если реализация интерфейса содержит вызываемый метод, то будет выполнен код из данной реализации;
  • в противном случае будет выполнен код реализации по-умолчанию.

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

public interface ILogger
{
    private static string _defaultMessage = "[No message]";

    void Log(LogLevel level, string message);

    // Метод с реализацией по-умолчанию
    void Debug(string message)
    {
        this.Log(LogLevel.Debug, message ?? _defaultMessage);
    }

    // Метод с реализацией по-умолчанию в виде выражения
    void Exception(Exception ex) => this.Log(LogLevel.Error, ex.Message);
}

public class DebugLogger : ILogger
{
    public void Log(LogLevel level, string message) => Debug.Write(message);
}

Обратите внимане, что для реализаций по-умолчанию доступно объявление статических членов.

Будем считать, что в данном примере методы Debug(…) и Exception(…) были добавлены после того, как был создан класс DebugLogger. Однако, данный код будет скомпилирован без ошибок.

Важная деталь – если метод с реализацией по-умолчанию отсутствует в конкретном классе, то он будет доступен только при обращении через интерфейс. Например:

var logger = new DebugLogger();
// error CS1061: 'DebugLogger' does not contain a definition for 'Exception'
logger.Exception(ex); 

if (logger is ILogger iLog)
    iLog.Exception(ex); // OK

Можно сказать, что реализация по-умолчанию – это явная реализация метода интерфейса.

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

Потенциальные проблемы

На первый взгляд может показаться что это удобная новая возможность C# 8. Однако использовать ее необходимо очень осторожно, т.к. это может также привести к следующим проблемам.

Смешение абстракции и реализации

Реализация по-умолчанию является точкой "протечки" реализации в абстракцию. Это видно даже на простом примере выше. В качестве крайнего примера можно привести сервис, у которого нет необходимости в private членах класса для хранения состояния (stateless). Значит вся его реализация может быть определена в интерфейсе как “по-умолчанию”.

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

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

Некорректные и неоптимальные реализации

Даже если оставить в стороне вопрос смешивания абстракции и реализации, то существует другая проблема. Реализация по-умолчанию может быть построена на некорректных предположениях о деталях реализаций в классах. Например, что конфигурация находятся в web.config, или что переданный как параметр DbContext поддерживает работу со всеми операциями LINQ и т.д. Такой код хоть и скомпилируется без ошибки, но вызовет проблемы во время выполнения.

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

Один из способов постараться избежать этого – не делать предположений о реализациях в классах и использовать только методы и свойства самого интерфейса.

Но даже в этом случае можно сделать некорректное предположение и получить неоптимальную реализацию. Например, если метод интерфейса возвращает коллекцию, то невозможно сказать – создается ли она каждый раз с нуля, хранится в памяти или вообще загружается с внешнего сервиса. Упрощенно это можно представить так:

public class Currency { … }

public interface ICurrencyService
{
    IReadOnlyCollection<Currency> GetAll();

    bool IsSupported(Currency currency) => this.GetAll().Any(x => x == currency);
}

Если реализация использует внешний сервис для получения данных о валютах, то каждый вызов IsSupported(…) будет приводить к новому запросу. Можно возразить, что c подобной проблемой может столкнуться любой код, использующий интерфейс. Да, но в отличии от него, IsSupported(…) это часть реализации интерфейса, а значит может знать и использовать детали самой реализации (например, положить в кэш данные запроса).

Тестирование

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

Можно попробовать для тестирования использовать mock-библиотеки. Но здесь есть 2 проблемы:

  1. Сейчас они не поддерживают работу с реализациями по-умолчанию.
  2. Даже если поддержка появится, то все равно придется писать код для методов интерфейса, которые вызываются в реализации по-умолчанию. Т. е. по прежнему будет необходима реализация интерфейса. Только теперь код вместо класса будет размещен в Mock объекте. Таким образом, использование Mock библиотеки не только не упростит, но даже усложнит создание самого теста. А это делает бессмысленным ее применение.

Комментарии (1) -

Спасибо!

Добавить комментарий