Страницы

Поиск по вопросам

понедельник, 1 октября 2018 г.

Нужно ли виртуальные методы объявлять как protected?

Коллеги, я не вполне понимаю одну из рекомендаций в .NET design guidelines
В ней говорится:
DO prefer protected accessibility over public accessibility for virtual members. Public members should provide extensibility (if required) by calling into a protected virtual member. The public members of a class should provide the right set of functionality for direct consumers of that class. Virtual members are designed to be overridden in subclasses, and protected accessibility is a great way to scope all virtual extensibility points to where they can be used.
то есть
Предпочитайте делать виртуальные члены (например, методы) защищёнными, а не открытыми. Открытые члены класса должны обеспечивать расширяемость (если нужно) путём вызова защищённого виртуального члена. Открытые члены класса должны давать нужную, правильную функциональность для клиентского кода. Виртуальные члены разрабатываются чтобы быть переопределёными в классах-потомках, и защищённый доступ — хороший метод для того, чтобы ограничить видимость мест для расширения только теми, кто будет её использовать.
Прочитав этот текст, я всё же не понимаю, какая проблема может быть в том, если семейство виртуальных функций будет объявлено открытым. Например, вот в таком коде:
class Human : IDisposable { IDisposable property = new Property(); public virtual Dispose() { property.Dispose(); } }
class Spy : Human { IDisposable spyGadgets = new SpyGadgets(); public virtual Dispose() { base.Dispose(); spyGadgets.Dispose(); } }
Какие могут быть проблемы с таким кодом? О чём меня пытается предупредить документация? Если с этим случаем всё хорошо, то в каком случае возможны проблемы?
Приведите, если можно, осмысленный пример с кодом (не с классами Foo и Bar).

Огромное спасибо за ответы! Мне было нелегко выбрать, какой из них отметить галочкой, потому что все ответы очень хороши, и проливают свет на проблему с разных сторон.


Ответ

Это же рекомендации для разработчиков фреймворков. Очевидно, что разработчики фреймворков будут выпускать новые версии своих фреймворков. Также понятно, что одной из важнейших задач для них - сохранение обратной совместимости, по мере возможностей. А значит, что клиентов, что используют их код, следует максимально ограничить. То есть, это мы (ну или только я такой рукожоп) привыкли писать классы наследуемыми - но если мы пишем фреймворк, то нужна достаточно веская причина сделать класс наследуемым. Также нужна веская причина сделать метод виртуальным. Но вот ты сделал публичный метод виртуальным, и теперь клиенты могут наследоваться, перегрузить метод и запускать сами написанный ими же код, используя наш фреймвок - и мы уже не можем это контролировать. Я к тому, что делая публичный метод виртуальным, мы даем право клиенту решать, что будет делать наш АПИ и мы уже никак не можем ничего изменить, не сломав обратную совместимость. Однако, сделав защищенный метод виртуальным, мы не гарантируем клиенту, что этот метод не перестанет использоваться в будущем, если наш публичный АПИ будет изменен. Таким образом клиенты, что перегрузили защищенный метод, сохраняют обратную совместимость, даже если логика публичных методов была изменена.
Походу надо добавить пример, хотя я и не мастер примеров :) Допустим есть следующие классы:
public class CsvWriter1 { protected void WriteHeader(TextWriter stream); protected void WriteBody(IEnumerable obj, TextWriter stream);
protected virtual void WriteInternal(IEnumerable obj, TextWriter stream) { WriteHeader(stream); WriteBody(obj, stream); }
public void Write(IEnumerable obj, TextWriter stream) { WriteInternal(obj, stream); } }
public class CsvWriter2 { protected void WriteHeader(TextWriter stream); protected void WriteBody(IEnumerable obj, TextWriter stream);
public virtual void Write(IEnumerable obj, TextWriter stream) { WriteHeader(stream); WriteBody(obj, stream); } }
Теперь представим, что в следующей версии нашего чудесного фреймворка нам надо писать объекты в CSV без заголовка. То есть, заголовок больше не нужен. И переопределять это больше нельзя. Что делать? Для класса CsvWriter1 все просто
public class CsvWriter1 { protected void WriteHeader(TextWriter stream); protected void WriteBody(IEnumerable obj, TextWriter stream);
protected virtual void WriteInternal(IEnumerable obj, TextWriter stream) { WriteHeader(stream); WriteBody(obj, stream); }
protected void WriteInternalNew(IEnumerable obj, TextWriter stream) { WriteBody(obj, stream); }
public void Write(IEnumerable obj, TextWriter stream) { WriteInternalNew(obj, stream); } }
В случае с классом CsvWriter2 мы в луже. Так как указанное изменение для него будет обрано несовместимым, и конечно сломает логику клиентских классов.

Комментариев нет:

Отправить комментарий