Является ли ссылка на базовый тип реализации в интерфейсе запахом кода?
Я столкнулся с дизайнерским решением, которое не пахнет для меня, но заставляет меня задуматься. Взгляните на следующий пример кода:
public interface IGenerator
{
///<summary>
/// Combines two generators; performs magic as well
/// </summary>
BaseGenerator Combine(BaseGenerator next);
///<summary>
/// Does what a generator does.
/// </summary>
object GenerateLolKThx();
}
public abstract class BaseGenerator : IGenerator
{
///<summary>
/// Combines two generators; performs magic as well
/// </summary>
public BaseGenerator Combine(BaseGenerator next)
{
// do stuff that I really don't want implementors to try and do
// because its complex and can result in bad juju if done wrong
return SuperSecretCombine(this, next);
}
///<summary>
/// Does what a generator does.
/// </summary>
public abstract object GenerateLolKThx();
/* other base class methods */
}
Я не хочу вдаваться в подробности о ПОЧЕМУ, я не хочу доверять разработчикам с помощью метода Combine; достаточно сказать его сложный. Однако я хочу сделать все возможное, чтобы заставить любого, кто хочет внедрить IGenerator, расширить BaseGenerator, поскольку это единственный способ правильно объединить два генератора. Это обеспечивается самим интерфейсом.
Я обеспокоен тем, что есть непредвиденные проблемы (обозначенные "запахом"), вызванные моей ссылкой на реализацию интерфейса в этом интерфейсе. Но я также знаю, что такого рода вещи не являются неслыханными в CS, и при этом они сами по себе не плохие (то есть схема XML, которая описывает XSD и языковые компиляторы, которые написаны на языке, который они компилируют).
Меня интересуют причины, по которым это может быть запах кода, возможные подводные камни, вызванные этим типом дизайна, и альтернативные проекты, которые выполняют то, что я желаю (убедитесь, что моя реализация Combine используется для объединения любых типов генераторов). ТИА.
6 ответов
Я заинтересован в причинах, почему это может быть запах кода
Вот мой ответ: одна из целей (возможно, даже основной) отделения интерфейса от реализации состоит в том, чтобы сделать возможным создание множества различных реализаций для интерфейса. Привязка интерфейса к конкретному классу реализации нарушает это.
Вы фактически говорите - "этот интерфейс должен быть реализован BaseGenerator или его подклассом", но тогда зачем IGenerator
от BaseGenerator
?
Основываясь на показанном вами ограниченном коде, мне кажется, что вы используете интерфейс только для того, чтобы более подходящий абстрактный базовый класс. Если вы не доверяете кому-либо еще в объединении BaseGenerators, тогда это должно стать основой вашей иерархии, и Combine не должен быть виртуальным, чтобы никто не мог его переопределить.
Может ли это быть сделано с помощью метода расширения? Тогда вы можете просто использовать интерфейс. Методы расширения являются хорошим способом добавления общей логики реализации в интерфейс.
Это работает лучше всего из вас в первую очередь говорить о IGenerator
(не конкретные типы) - поскольку разрешение расширения предпочитает типы - т.е. FredsGenerator
может объявить на заказ Combine
метод, который будет (если вы печатаете переменные как FredsGenerator
) возьмите предпочтение. Но это ничем не отличается от вашего примера, так как FredsGenerator
может повторно реализовать интерфейс, украдя реализацию:
using System;
interface IFoo {
void Bar();
}
abstract class FooBase {
public void Bar() { Console.WriteLine("Non-virtual; you can't override me!!!"); }
}
class FooImpl : FooBase, IFoo {
new public void Bar() { Console.WriteLine("mwahahahahah"); }
}
static class Program {
static void Main() {
IFoo foo = new FooImpl();
foo.Bar();
}
}
По крайней мере, с помощью метода расширений ваш код не может быть обманутым до запуска повторно реализованной версии; ваш код знает только о IGenerator
так что только Generator.Combine
версия будет использоваться. Когда-либо.
Пример (отредактируйте переработанный из вашего примера):
using System;
public interface IGenerator
{
// note: no Combine
object GenerateLolKThx();
}
public static class Generator
{
///<summary>
/// Combines two generators; performs magic as well
/// </summary>
public static IGenerator Combine(this IGenerator current, IGenerator next)
{
// do stuff that I really don't want implementors to try and do
// because its complex and can result in bad juju if done wrong
Console.WriteLine("Super secret logic here...");
// and prove we have access the the objects...
Console.WriteLine(current.GenerateLolKThx());
Console.WriteLine(next.GenerateLolKThx());
return next; // just for illustration
}
}
class MyGenerator : IGenerator
{
private readonly object state;
public MyGenerator(object state) {this.state = state;}
public object GenerateLolKThx() { return state; }
}
public static class Program
{
static void Main()
{
IGenerator foo = new MyGenerator("Hello"),
bar = new MyGenerator("world");
IGenerator combined = foo.Combine(bar);
}
}
Является ли BaseGenerator (и его подклассы) единственным классом, реализующим интерфейс IGenerator?
- Если так, то зачем вообще нужен IGenerator (а не просто BaseGenerator)?
- Если нет, ожидаете ли вы (или нет), что другие не-BaseGenerator классы, которые реализуют IGenerator, смогут определить разумную реализацию метода Combine?
[Это похоже на вопрос, а не на ответ, но он должен быть сократовским.]
Почему бы не ссылаться на интерфейс?
public interface IGenerator
{
///<summary>
/// Combines two generators; performs magic as well
/// </summary>
IGenerator Combine(IGenerator next);
///<summary>
/// Does what a generator does.
/// </summary>
object GenerateLolKThx();
}
Похоже, что все это могло бы быть реализовано лучше, если бы было доступно множественное наследование. Если я прав, здесь нет никакого запаха кода. Если я ошибаюсь, вполне может быть странный запах кода, поскольку этот интерфейс тесно связан с одной из его реализаций.