Здравствуйте, _NN_, Вы писали:
_NN>Как именно нарушают ? _NN>Старый код не станет работать по другому ведь.
Только благодаря костылю — возможности добавлять в функцию, объявленную в интерфейсе, какую-то реализацию. Эта возможность противоречит самому духу понятия "интерфейс". И зачем это понадобилось, когда класс реализации всегда мог объявить, что он собирается реализовать и интерфейс Map, и интерфейс Map1 и, возможно, сто других. Вот так языки и движутся в направлении ада: добавляя и добавляя новые фичи, которые просто позволяют сделать что-то, что уже можно было (в данном случае даже изящнее) сделать раньше.
Здравствуйте, Kswapd, Вы писали:
K>Здравствуйте, _NN_, Вы писали:
_NN>>Как именно нарушают ? _NN>>Старый код не станет работать по другому ведь.
K>Только благодаря костылю — возможности добавлять в функцию, объявленную в интерфейсе, какую-то реализацию. Эта возможность противоречит самому духу понятия "интерфейс". И зачем это понадобилось, когда класс реализации всегда мог объявить, что он собирается реализовать и интерфейс Map, и интерфейс Map1 и, возможно, сто других. Вот так языки и движутся в направлении ада: добавляя и добавляя новые фичи, которые просто позволяют сделать что-то, что уже можно было (в данном случае даже изящнее) сделать раньше.
А как надо делать чтобы всем угодить да и совместимость не сломать ?
В C# решили не менять интерфейсы, а применить методы расширения.
В языках, которые этого не поддерживают, невозможно писать такой же простой код как в C#.
Можно конечно заставить всех перекомпилировать и пользоваться новыми интерфейсами, но, видимо, посчитали, что никто не будет заморачиваться и более практично иметь такое решение.
Кстати, часть методов расширений классов таки перешла в реализацию классов, хотя казалось бы правильней по канонам оставить как методы расширения.
Здравствуйте, Kswapd, Вы писали:
K>Попробуем посмотреть на пример с расширением интерфейса с точки зрения одного из принципов SOLID — OCP, Open/Closed Principle. Одна из его формулировок звучит так: "компонент должен быть открыт для расширения и закрыт для модификации". Добавление дефолтного метода, очевидно — модификация. Композиция старого интерфейса целиком (без изменения) вместе с другим интерфейсом в новый общий интерфейс — явно расширение. Следовательно, дефолтные методы интерфейсов (и вообще добавление новых функций в старый интерфейс на позднем этапе разработки) нарушают OCP. А без нарушения можно было бы добавить интерфейс и заставить новый класс реализовать оба, ведь это нормальный подход в C#/Java.
Вы чего-то увлеклись каким-то заоблачным теоретизированием.
Для чего вообще нужны все эти дефолтные реализации и екстеншн методы?
Для того, чтобы покрыть частый сценарий.
Классика жанра — интерфейс потока байт IWriter.
Понятно, что для его реализации необходимо и достаточно реализовать единственный метод bool WriteByte(byte b).
Клиентам, однако, неудобно пихать в него байты по одному. Хочется иметь метод типа int WriteBytes(byte[] bytes), а также метод int WriteBytes(byte[] bytes, int offset, int count).
Теперь вопрос — куда мы включим эти методы?
В декларацию интерфейса?
Но тогда каждый реализатор будет вынужден писать по три метода. А ведь ещё же есть искушение работать со Span<byte> и ReadOnlySpan<byte>...
Все эти методы тривиально строятся на методе WriteByte. Функционально.
То есть, мы могли бы написать простенький класс WriteWrapper, который наружу выставляет их все, а сам использует только IWriter.WriteByte.
Но на практике мы скорее всего бы потеряли в производительности — в разы. Надо как-то магически давать возможность пользоваться тривиальными реализациями, но в случае необходимости подменять их более эффективными перегрузками.
Хорошо, если у нас есть уверенность в одиночном наследовании. Тогда мы отказываемся от интерфейса, и пилим простейший класс Writer, в котором WriteByte — абстрактный, а остальные методы — виртуальные.
А если у нас такой уверенности нет? т.е. мы не можем потребовать от всех реализаций наследоваться от Writer?
Тогда мы имеем классическое инженерное противоречие. Мы хотим одновременно иметь в интерфейсе IWriter один метод и много методов.
Концепции дефолтных методов и методов-расширений позволяют нам это противоречие решить.
То есть мы пишем w.WriteBytes(...), и это биндится либо к специфичному методу w, либо к дефолтной медленной реализации — в зависимости от того, что имел в виду автор класса, стоящего за w.
С методами-расширениями всё, к сожалению, не так хорошо, т.к. биндинг выполняется в call site на основе статической информации о типе переменной, а не о фактическом типе объекта. Но идея — примерно та же.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Sinclair, Вы писали:
S>Вы чего-то увлеклись каким-то заоблачным теоретизированием.
Почему бы и нет, в философском подфоруме .
S>Классика жанра — интерфейс потока байт IWriter. S>Понятно, что для его реализации необходимо и достаточно реализовать единственный метод bool WriteByte(byte b). S>Клиентам, однако, неудобно пихать в него байты по одному. Хочется иметь метод типа int WriteBytes(byte[] bytes), а также метод int WriteBytes(byte[] bytes, int offset, int count).
А если объявить три интерфейса: ByteWriter, BytesWriter, ByteCountWriter? Каждый содержит ровно один соответствующий метод. И реализаторы имплементируют тот набор, который им нужен (можно же имплементировать множество интерфейсов). Так не принято делать в C#? В Go, например, это рекомендованный подход; принцип ISP (Interface Segregation Principle), доведённый до логического предела.
Здравствуйте, Sinclair, Вы писали:
S>Для чего вообще нужны все эти дефолтные реализации и екстеншн методы?
Полагаю, в качестве довольно кривого костыля, призванного через пень-колоду закрыть дыру под названием "прибитость гвоздями функций к данным". Что, впрочем, является одним из столпов так называемого ООП.
Здравствуйте, Kswapd, Вы писали:
K> А если объявить три интерфейса: ByteWriter, BytesWriter, ByteCountWriter? Каждый содержит ровно один соответствующий метод. И реализаторы имплементируют тот набор, который им нужен (можно же имплементировать множество интерфейсов). Так не принято делать в C#? В Go, например, это рекомендованный подход; принцип ISP (Interface Segregation Principle), доведённый до логического предела.
Здравствуйте, AlexRK, Вы писали:
ARK> ·>Дефолтные методы интерфейса — круто. Методы-расширения — не нужны. ARK> Дефолтные методы не заменяют методов расширения.
Ну да. Разные вещи, понятное дело. Я имею в виду, что дефолтные методы — ничем не заменимы.
Методы расширения — обычные статические методы, только синтаксис немного отличается.
Здравствуйте, Kswapd, Вы писали:
K>А если объявить три интерфейса: ByteWriter, BytesWriter, ByteCountWriter? Каждый содержит ровно один соответствующий метод. И реализаторы имплементируют тот набор, который им нужен (можно же имплементировать множество интерфейсов). Так не принято делать в C#? В Go, например, это рекомендованный подход; принцип ISP (Interface Segregation Principle), доведённый до логического предела.
Ок, допустим, я ленивый и реализовал в своём потоке только ByteWriter.
Как будет выглядеть клиентский код, который записывает в мой поток байтовый массив?
Если завтра я пойму, что меня не устраивает производительность, и реализую дополнительно в своём классe BytesWriter — как поменяется клиентский код?
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Sinclair, Вы писали:
K>>А если объявить три интерфейса: ByteWriter, BytesWriter, ByteCountWriter? Каждый содержит ровно один соответствующий метод. И реализаторы имплементируют тот набор, который им нужен (можно же имплементировать множество интерфейсов). Так не принято делать в C#? В Go, например, это рекомендованный подход; принцип ISP (Interface Segregation Principle), доведённый до логического предела.
S>Ок, допустим, я ленивый и реализовал в своём потоке только ByteWriter. S>Как будет выглядеть клиентский код, который записывает в мой поток байтовый массив? S>Если завтра я пойму, что меня не устраивает производительность, и реализую дополнительно в своём классe BytesWriter — как поменяется клиентский код?
А что принимает клиент? По идее, должен принимать интерфейс ByteWriter. В вашем юзкейсе клиент ничего не заметит. Но он ничего не заметит и если будет принимать расширенный интерфейс с пустым методом. Всё равно для повышения производительности придётся вникать в код клиента и заменять цикл на вызов. То есть сверхзадача увеличения производительности без изменения клиента не выполнена.
Здравствуйте, Kswapd, Вы писали:
K>А что принимает клиент? По идее, должен принимать интерфейс ByteWriter. В вашем юзкейсе клиент ничего не заметит. Но он ничего не заметит и если будет принимать расширенный интерфейс с пустым методом. Всё равно для повышения производительности придётся вникать в код клиента и заменять цикл на вызов. То есть сверхзадача увеличения производительности без изменения клиента не выполнена.
Ну, вот видите, как плохо, когда один из собеседников отказывается писать код.
Вот вам код с выполненной сверхзадачей:
public void Client(IWriter w)
{
var b = new byte[100];
w.WriteBytes(b);
}
Вот две реализации интерфейса IWriter:
public class LazyCountingWriter: IWriter
{
IWriter Inner {protected get; private set};
int Count {public get; private set;} = 0;
public LazyCountingWriter(IWriter inner) => Inner = inner ?? throw new ArgumentNullException(nameof(inner));
public bool WriteByte(byte b) => Count += Inner.WriteByte(b) ? 1 : 0;
}
public class LessLazyCountingWriter: LazyCountingWriter
{
public LazyCountingWriter(IWriter inner): base(inner) {};
public int WriteBytes(byte[] bytes) => Count += Inner.WriteBytes(bytes);
}
Понятно, как это работает?
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Sinclair, Вы писали:
S>Ну, вот видите, как плохо, когда один из собеседников отказывается писать код.
Разве было обязательство? У нас ведь не спор, а просто обмен мнениями. Я не собираюсь никого переубеждать, просто высказываю некоторые, возможно, сомнительные идеи и интересуюсь, как другие их понимают и как принято строить в разных языках. Ну, а писать на C#, C++ или Java тем более не хочется :D.
S>Понятно, как это работает?
Тогда я неправильно понял условие. Сначала было вроде про интерфейс, реализующий только WriteByte. Очевидно, клиент такого интерфейса должен был бы записывать массив в цикле.
Здравствуйте, Kswapd, Вы писали:
S>>Понятно, как это работает?
K>Тогда я неправильно понял условие. Сначала было вроде про интерфейс, реализующий только WriteByte. Очевидно, клиент такого интерфейса должен был бы записывать массив в цикле.
Нет, это как раз вы предложили разделить интерфейсы. Поэтому именно в вашем решении клиент был бы вынужден писать массив в цикле.
Это и есть недостаток вашей идеи по сравнению с default implementation.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.