Так много интерфейсов, часть 2

В статье за апрель 2011 года о реализации интерфейсов я упомянул о том, что язык C# поддерживает редко используемую возможность, под названием «повторная реализация интерфейсов» (interface re-implementation). Когда она вам понадобится, эта возможность будет полезной, но вы создадите себе неприятности, если будете использовать ее неправильно или необдуманно.

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

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

 interface I
{
  void M();
}
class Echo : I
{
  void I.M() { ... }
}
class Foxtrot : Echo, I
{
void I.M() { ... }
}

Класс Echo не содержит открытого, защищенного или внутреннего (internal) виртуального метода M, который может быть переопределен в классе Foxtrot; если классу Foxtrot понадобится заменить поведение класса Echo, то сделать это он сможет только путем повторной реализации интерфейса.

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

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

 internal interface IInternal
{
void M();
void N();
}
public interface IPublic
{
void M();
}

public abstract class Bravo : IInternal (*)
{
 // Это внутренние механизмы, которые мы используем для взаимодействия
 // с нашими закрытыми элементами реализации;
 // эти классы реализуют интерфейс явно,
 // так что эти методы нельзя использовать извне.
void IInternal.M() { ... }
void IInternal.N() { ... }
}

public abstract class Charlie: Bravo, IPublic
{
 // С другой стороны, класс Charlie, хочет предоставлять оба метода M
 // в качестве метода класса Charlie, так и в виде явной реализации метода 
// IPublic.M:
public void M() { ... }
}

public sealed class Delta : Charlie, IInternal
{
 // Delta наследует Bravo через класс Charlie; и ему нужно
 // изменить поведение IInternal.N, поэтому он реализует интерфейс 
// повторно и обеспечивает новую, переопределенную реализацию:
void IInternal.N() { ... }
}

Но это неправильно. Класс Delta должен предоставить три метода: IInternal.M, IInternal.N и IPublic.M. Метод IPublic.M класса Delta реализован классом Charlie. Но мы повторно реализуем интерфейс IInternal, так что мы начинаем с чистого листа. Где находится реализация метода IInternal.N? Очевидно в виде явной реализации в классе Delta. А где находится реализация метода IInternal.M? Это открытый метод в классе Charlie, а не невидимый для всех явно реализованный метод класса Bravo. Теперь, при передаче экземпляра класса Delta во внутреннюю подсистему, вместо корректного варианта, явно реализованного метода в классе Bravo, будет вызван открытый метод Charlie.M.

Класс Delta никак не может добраться до реализации метода IInternal.M класса Bravo, поскольку последний является закрытым элементом реализации в классе Bravo. В этом случае, повторная реализация интерфейса просто не подходит; данный сценарий слишком сложный, чтобы вы случайно не поранили себя. Более разумным вариантом является только одна реализация внутреннего интерфейса, которая вызывает еще один внутренний метод:

 public abstract class Bravo : IInternal
{
  void IInternal.M() { this.InternalM(); }
  internal virtual void InternalM() { ... }
  void IInternal.N() { this.InternalN(); }
  internal virtual void InternalN() { ... }
}

public abstract class Charlie: Bravo, IPublic
{
  public void M() { ... }
}

public sealed class Delta: Charlie
{
  internal override void InternalN() { ... } 
}

В данном случае если классу Delta понадобится вызвать Bravo.InternalN, то он вызовет его с помощью base.InternalN.

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


(*) Некоторые разработчики удивляются, что этот код компилируется. Область доступа класса наследника должна быть подмножеством области доступа базового класса; т.е. открытый класс не может наследовать внутренний. Но это условие не выполняется для интерфейсов; интерфейсы могут быть насколько угодно «закрытыми»; открытый класс может реализовывать закрытый интерфейс, объявленный внутри этого же класса!

Оригинал статьи