Я хочу задать следующий вопрос:
Является ли отказ от дерева наследования (например, к более специализированному классу) изнутри абстрактного класса простительно или даже хорошо, или это всегда плохой выбор, поскольку доступны лучшие варианты?
Теперь пример того, почему я думаю, что это можно использовать во благо.
Недавно я реализовал Бенкодирование по протоколу BitTorrent на C#. Достаточно простая проблема, как представить данные. Я выбрал так,
У нас есть класс abstract BItem, который обеспечивает некоторые базовые функции, включая static BItem Decode(string), который используется для декодирования Bencoded строки в необходимую структуру.
Также существует четыре производных класса: BString, BInteger, BList и BDictionary, представляющих четыре различных типа данных, которые должны быть закодированы. А теперь самое сложное. BList и BDictionary имеют средства доступа this[int] и this[string] соответственно, чтобы разрешить доступ к свойствам, подобным массивам, этих типов данных.
Теперь наступает потенциально ужасная часть:
BDictionary torrent = (BDictionary) BItem.DecodeFile("my.torrent");
int filelength = (BInteger)((BDictionary)((BList)((BDictionary)
torrent["info"])["files"])[0])["length"];
Ну, вы поняли ... Ой, это тяжело для глаз, не говоря уже о мозге. Итак, я добавил кое-что в абстрактный класс:
public BItem this[int index]
{
get { return ((BList)this)[index]; }
}
public BItem this[string index]
{
get { return ((BDictionary)this)[index]; }
}
Теперь мы можем переписать этот старый код как:
BDictionary torrent = (BDictionary)BItem.DecodeFile("my.torrent");
int filelength = (BInteger)torrent["info"]["files"][0]["length"];
Вау, привет, НАМНОГО более читаемый код. Но разве я просто продал часть своей души за то, что подразумевал знание подклассов абстрактному классу?
Обновлено: В ответ на некоторые из поступающих ответов вы полностью сбились с пути по этому конкретному вопросу, поскольку структура является переменной, например, мой пример torrent["info"]["files"][0]["length"] действителен, но также и torrent["announce-list"][0][0], и оба будут в 90% торрент-файлы. Дженерики - это не выход, по крайней мере, с этой проблемой :(. Щелкните ссылку на спецификацию, которую я связал, это всего лишь 4 маленькие точки большого размера.





Вам действительно не следует обращаться к каким-либо производным классам из базового класса, поскольку это в значительной степени нарушает идею ООП. Читаемость, безусловно, имеет большое значение, но я бы не променял ее на возможность повторного использования. Рассмотрим случай, когда вам нужно добавить еще один подкласс - вам также потребуется соответствующим образом обновить базовый класс.
Если длина файла - это то, что вы часто извлекаете, почему бы не реализовать свойство в классе BDictionary (?) ... чтобы ваш код стал:
BDictionary torrent = BItem.DecodeFile("my.torrent");
int filelength = torrent.FileLength;
Таким образом, детали реализации скрыты от пользователя.
Думаю, я бы сделал виртуальные аксессоры this [int] и this [string] и переопределил их в BList / BDictionary. Классы, в которых методы доступа не имеют смысла, должны приводить NotSupportedException () (возможно, имея реализацию по умолчанию в BItem).
Это заставляет ваш код работать таким же образом и дает вам более читаемую ошибку в случае, если вы должны написать
(BInteger)torrent["info"][0]["files"]["length"];
по ошибке.
Кстати, это то, что я фактически делал, за исключением того, что я использовал new для переопределения вместо virtual / override. Глупо, глупо. Гораздо чище и приятнее, спасибо!
«новый» на самом деле не отменяет. Вы просто получаете 2 метода с одинаковым именем - если кто-то применяет к базовому классу, а затем вызывает метод, они получат исходный метод базового класса, а НЕ переопределенный
Вы не задумывались о том, чтобы разобрать простой «путь», чтобы можно было написать его так:
BDictionary torrent = BItem.DecodeFile("my.torrent");
int filelength = (int)torrent.Fetch("info.files.0.length");
Возможно не лучший способ, но читаемость увеличивается (немного)
Если вам нужно это сделать, по крайней мере, оберните его (управляйте доступом к списку) в класс, который имеет строго типизированные сигнатуры методов.
BString GetString(BInteger);
SetString(BInteger, BString);
Принимайте и возвращайте BStrings, даже если вы храните их внутри BList BItems. (позвольте мне разделиться, прежде чем я сделаю свои 2 B или нет 2 B)
Хм. На самом деле я бы сказал, что первая строка кода более читабельна, чем вторая - требуется немного больше времени, чтобы понять, что в ней происходит, но более очевидно, что вы обрабатываете объекты как BList или BDictionary. Применение методов к абстрактному классу скрывает эти детали, что может затруднить понимание того, что на самом деле делает ваш метод.
На мой взгляд, не все BItems являются коллекциями, поэтому не все BItems имеют индексаторы, поэтому индексатор не должен быть в BItem. Я бы унаследовал другой абстрактный класс от BItem, назовем его BCollection и поместим туда индексаторы, примерно так:
abstract class BCollection : BItem {
public BItem this[int index] {get;}
public BItem this[string index] {get;}
}
и сделайте BList и BDictionary наследованными от BCollection. Или вы можете приложить дополнительные усилия и сделать BCollection универсальным классом.
Тем не менее, средства доступа возвращают BItem, так что это все равно никому не поможет в долгосрочной перспективе для данного приложения.
Если вы введете дженерики, вы сможете избежать приведения типов.
class DecodedTorrent : BDictionary<BDictionary<BList<BDictionary<BInteger>>>>
{
}
DecodedTorrent torrent = BItem.DecodeFile("mytorrent");
int x = torrent["info"]["files"][0]["length"];
Хм, но это, вероятно, не сработает, поскольку типы могут зависеть от пути, который вы пройдете через структуру.
ЕЕК! Хорошо, это работает для этого (очень) конкретного примера ... но на самом деле это древовидная структура переменных ... torrent["info"]["files"] действителен. Но torrent["announce-list"][0][0] тоже. Ваши дженерики просто сломались :(
Это только я
BDictionary torrent = BItem.DecodeFile("my.torrent");int filelength = (BInteger)((BDictionary)((BList)((BDictionary) torrent["info"])["files"])[0])["length"];
Приведение BDictionary не требуется, torrent объявлен как BDictionary.
public BItem this[int index]{ get { return ((BList)this)[index]; }}public BItem this[string index]{ get { return ((BDictionary)this)[index]; }}
Они не дают желаемого результата, так как возвращаемый тип по-прежнему является версией abstrat, поэтому вам все равно нужно выполнить приведение.
Переписанный код должен быть
BDictionary torrent = BItem.DecodeFile("my.torrent");int filelength = (BInteger)((BList)((BDictionary)torrent["info"]["files"])[0])["length"];
Что так же плохо, как и первая партия
Средняя часть добавляется к абстрактному классу (BItem), поэтому у них есть аксессоры, и их больше не нужно приводить для их использования.
Я бы порекомендовал ввести больше абстракций. Меня сбивает с толку то, что у BItem есть DecodeFile (), который возвращает BDictionary. Я не знаю, это может быть разумным поступком в торрент-домене.
Однако я бы нашел более разумным api, подобный следующему:
BFile torrent = BFile.DecodeFile("my.torrent");
int filelength = torrent.Length;
Decode и DecodeFile возвращают BItems, это просто случай в (допустимом) торрент-файле, что корневой элемент на самом деле является словарем и, следовательно, может быть безопасно приведен сразу (если это недопустимый файл, исключение приведения перехватывается общим Обработка исключений)
Хорошо, меня это устраивает. Если бы я кодировал это, я бы все равно добавил свои собственные абстракции / классы, чтобы инкапсулировать детали. Таким образом, я могу ввести классы, которые устранят необходимость в отрицательных значениях.
Для протокола, я. Но мне все еще нужен способ синтаксического анализа и представления данных, поскольку Bencoding используется во всем протоколе, а не просто для кодирования торрент-файлов.
Если вы читаете спецификацию (которая была связана с целых 6 строк), словари могут быть где угодно в структуре данных, поэтому этот ответ действительно не имеет никакого смысла, извините.