Пара вопросов по ОО и дизайну библиотеки
Хорошо. У меня есть несколько вопросов, касающихся некоторых аспектов ОО и дизайна библиотеки.
Должна ли библиотека быть самодостаточной? Например, может ли он использовать внешнюю среду внедрения зависимостей или должен реализовать ее более легким способом?
Как принцип замещения Лискова подходит для полиморфизма, когда вы не знаете поведение метода или класса? Вы просто ожидаете, что он будет работать так, как должен?
В части исходного кода плохая привычка хранить интерфейсы в отдельной папке (например,
/interfaces
) от их реализации?Это также плохая привычка разграничивать родовые типы (
where T : type
) на интерфейсах, а не только в их реализациях? (я так не думаю, но это только для подтверждения).Является ли интерфейс предпочтительным по сравнению с абстрактным классом, когда объектная взаимосвязь является "может делать" и "является", и нет необходимости в реализации по умолчанию для методов и т. Д.?
Вот и все. Спасибо за ваше время =)
5 ответов
1. Должна ли библиотека быть самодостаточной?
Библиотеки, безусловно, могут использовать, например, структуры внедрения зависимостей. Тем не менее, это спорно, хотите ли, чтобы заставить рамки DI на пользователя библиотеки.
2. Как принцип замещения Лискова соотносится с полиморфизмом?
Принцип подстановки Лискова заключается в возможности взаимозаменять одну реализацию с другой (подстановка), и, независимо от поведения, не должно быть ошибок, когда пользователь придерживается договора. Например, обмен CanonPrinter
для EpsonPrinter
должен по-прежнему позволять вам печатать, если вы используете только методы из Printer
учебный класс. Звонить что-то Printer
но наличие конкретной реализации (Canon, Epson), лежащей в основе этого, является полиморфизмом.
3. Является ли плохой привычкой хранить интерфейсы в папке отдельно от их реализации?
Это личное предпочтение, хотите ли вы держать интерфейсы отдельно от их реализации. Я этого не делаю; У меня даже нет интерфейса для каждой реализации. Я думаю, что имеет смысл иметь интерфейс для реализации, только если он добавляет ценность вашему проекту.
4. Является ли также вредной привычкой ограничивать универсальные типы на интерфейсах, а не только в их реализациях?
Если вы думаете, что универсальный тип должен быть ограничен определенным типом, то вы должны делать это там, где это имеет смысл (поэтому это может быть в интерфейсе). Например, имея IPrintableDocument<TPrinter>
и не ограничивающий TPrinter
в Printer
объекты не имеют смысла, поэтому я бы сделал это.
5. Является ли интерфейс предпочтительным по сравнению с абстрактным классом, когда объектные отношения "могут делать" и "являются"?
В самом деле, большинство людей используют абстрактные классы для отношений и интерфейсов. Причина: класс может наследовать только от одного базового класса, но от нескольких интерфейсов. По сути, это означает, что класс может быть только одним Employee
это Person
) но можно сделать несколько (это может ICopyDocuments
, IWalkAbout
, IMakeCoffee
). Что бы вы ни делали, когда интерфейс оба, зависит от ваших предпочтений.
Большинство ваших вопросов связано с контрактом класса (или интерфейса): контракт определяет, что пользователь (другой класс, чей-либо код) может делать и не может делать с классом и его членами, и определяет, что пользователь может ожидать, что класс будет делать и не будет делать.
Например: пользователь может передавать объекты в качестве аргументов методу, только если они соответствуют типу параметра. Класс будет возвращать только объекты в качестве возвращаемых значений, соответствующих типу возвращаемого значения. Пользователь может вызывать только члены, определенные в классе, и класс будет следить за тем, чтобы эти методы вели себя в соответствии с контрактом (то есть не вызывали ошибок, никаких побочных эффектов, никогда не возвращали ноль).
Общие ограничения типа также являются частью контракта. Я даже рассматриваю документацию как часть контракта: когда говорится, что метод никогда не вызовет исключения, тогда ни одна реализация не должна вызывать исключения. Нет никакого синтаксиса или правила компилятора, обеспечивающего его соблюдение, но это является частью контракта. Некоторые части контракта выполняются компилятором или средой исполнения, другие - нет.
Когда у класса или интерфейса есть конкретный контракт, его можно заменить любым подклассом или реализацией, и он все равно будет работать, когда этот подкласс или реализация будет придерживаться контракта. Если он придерживается контракта, то это принцип замещения Лискова (LSP). И это легко отклониться от этого, и многие программисты делают. Иногда у тебя нет выбора.
Ярким примером нарушения LSP в.NET Framework является ReadOnlyCollection<T>
класс Он реализует IList<T>
интерфейс, но не имеет фактической реализации для многих своих методов. Так что, если вы передаете пользователя, ожидающего IList<T>
ReadOnlyCollection<T>
и пользователь пытается позвонить list.Add
, тогда будет выдано исключение. Таким образом, вы не можете всегда заменить IList<T>
за ReadOnlyCollection<T>
так что это нарушает ЛСП.
Я постараюсь ответить на них по порядку:
Это зависит. Принятие внешней зависимости потенциально ограничивает полезность и область вашей библиотеки, так как она будет использоваться только в ситуациях, когда эта зависимость доступна. Это может быть хорошо, однако, и предпочтительно, чтобы попытаться обойти зависимость. Если вы знаете, что пользователи вашей библиотеки всегда будут использовать конкретную инфраструктуру DI, я бы построил библиотеку вокруг нее, вместо того, чтобы реализовывать что-то, что потребует "адаптации" для работы.
"где вы не знаете поведение метода или класса?" Вы должны знать контракт любых типов, которые вы используете. LSP в основном говорит, что независимо от того, какой конкретный тип вы получите, контракт базового класса / интерфейса всегда будет действителен. Это означает, что вы можете просто "использовать тип" и ожидать, что он будет работать.
Нет, но это частная реализация. Однако, как правило, плохая идея отделять пространства имен, и часто приятно, чтобы дерево исходного кода соответствовало пространствам имен.
Нет. Если интерфейс действительно предназначен для использования только с определенным типом, он должен иметь ограничения на интерфейсе.
В целом, один контракт должен предусматривать только один аспект отношений. Если в контракте предлагаются варианты "есть" и "можно", его, вероятно, следует разделить на два контракта, возможно, абстрактный класс, реализующий интерфейс "можно".
На № 1 - http://jeviathon.com/2012/03/05/roll-your-own-syndrome/
В сущности, смысл в том, зачем "катить свой собственный" фреймворк xyz, когда кто-то, вероятно, уже создал надежное, протестированное и поддерживаемое программное обеспечение, которое делает то же самое.
Потратьте свое время на изучение выбора "правильных" внешних библиотек и фреймворков, а не изобретайте велосипед.
Я сделаю удар в 5.
Единственная разница между абстрактным классом и интерфейсом состоит в том, что абстрактный класс может иметь переменные класса. Я бы выбрал интерфейс, если абстрактный класс состоит только из виратуальных методов.
"Это зависит". Но нет смысла изобретать свое собственное колесо, особенно в таких густонаселенных (и трудно реализуемых) областях, как DI и ORM. И особенно, когда вы можете (а) bin-развернуть ваши зависимости или (б) указать ваши зависимости как пакеты nuget, если ваша библиотека является пакетом nuget (как, конечно, должно быть)
LSP имеет мало отношения к полиморфизму. В нем говорится, что класс (или метод) должен вести себя в соответствии с контрактом, и это, по сути, единственное, что вы знаете о поведении разработчика.
Папки должны соответствовать пространствам имен. Вы размещаете интерфейсы в отдельном пространстве имен? Я нет, и я не думаю, что кто-то должен. И когда у меня есть интерфейс только с одной реализацией (например, с модульным тестированием), я предпочитаю хранить в одном файле с его реализацией.
Нет, это не так. Почему это должно быть?
Если нет необходимости в реализации по умолчанию, есть интерфейс. Интерфейс проще моделировать, а также проще в реализации, потому что он не мешает наследованию вашей реализации. Фактически, вы можете даже реализовать несколько интерфейсов в одном классе, если это как-то подчеркивает ваши намерения (например, если у вас есть конкретный сервис, занимающийся безопасностью, то нередко он реализует оба
IAuthentication
а такжеIAuthorization
). В последнее время я выбираю абстрактные классы только тогда, когда в них есть явная необходимость.