Каков общий способ обработки видимости в библиотеках?

12

Этот вопрос о том, когда использовать приватный, а когда использовать защищенный в классах, заставил меня задуматься. (Я распространю этот вопрос также на окончательные классы и методы, так как он связан. Я программирую на Java, но я думаю, что это актуально для каждого языка ООП)

Принятый ответ говорит:

Хорошее практическое правило: сделайте все как можно более приватным.

И еще один:

  1. Сделайте все классы окончательными, если вам не нужно сразу создать их подкласс.
  2. Сделайте все методы окончательными, если вам не нужно сразу создавать подклассы и переопределять их.
  3. Сделайте все параметры метода окончательными, если вам не нужно изменять их в теле метода, что в любом случае довольно неудобно.

Это довольно просто и понятно, но что если я в основном пишу библиотеки (с открытым исходным кодом на GitHub) вместо приложений?

Я мог бы назвать много библиотек и ситуаций, где

  • Библиотека была расширена таким образом, что разработчики никогда бы не подумали
  • Это должно было быть сделано с помощью "магии загрузчика классов" и других хаков из-за ограничений видимости
  • Библиотеки привыкли к тому, что они не созданы, а необходимый функционал «взломан» в
  • Библиотеки не могут быть использованы из-за небольшой проблемы (ошибка, отсутствие функциональности, «неправильное» поведение), которую нельзя изменить из-за ограниченной видимости
  • Проблема, которая не могла быть устранена, привела к огромным, уродливым и ошибочным обходным путям, где могло бы помочь переопределение простой функции (частной или конечной)

И я фактически начал называть их, пока вопрос не стал слишком длинным, и я решил удалить их.

Мне нравится идея не иметь больше кода, чем нужно, больше видимости, чем нужно, больше абстракции, чем нужно. И это может сработать при написании приложения для конечного пользователя, где код используется только теми, кто его пишет. Но как это действует, если код предназначен для использования другими разработчиками, когда маловероятно, что первоначальный разработчик заранее продумал все возможные варианты использования, а изменения / рефакторы трудно / невозможно сделать?

Поскольку большие библиотеки с открытым исходным кодом не являются чем-то новым, каков наиболее распространенный способ обработки видимости в таких проектах с объектно-ориентированными языками?

piegames
источник
учитывая, что вы спрашиваете об открытом исходном коде, имеет еще меньше смысла изгибать надлежащие принципы кодирования для решения перечисленных вами проблем, чем закрытый исходный код, просто потому, что можно внести необходимые исправления прямо в код библиотеки или разветвить его и сделать свою собственную версию с независимо от поправки они хотят
комар
2
моя точка зрения не об этом, а о вашей ссылке на открытый исходный код, не имеющей смысла в этом контексте. Я могу представить, как прагматические потребности могут оправдать отклонение от строгих принципов в некоторых случаях (также известных как накопление технического долга ), но с этой точки зрения не имеет значения, является ли код закрытым или открытым исходным кодом. Или, точнее, это имеет значение в противоположном направлении, чем вы представляли здесь, потому что код с открытым исходным кодом может сделать эти потребности менее актуальными, чем закрытые, потому что он предлагает дополнительные опции для решения этих проблем
gnat
1
@piegames: я полностью согласен с gnat, проблемы, которые вы описали, гораздо чаще встречаются в библиотеках с закрытым исходным кодом - если это библиотека ОС с разрешающей лицензией, если сопровождающие игнорируют запрос на изменение, можно разветвлять библиотеку и при необходимости измените видимость самостоятельно.
Док Браун
1
@piegames: я не понимаю твой вопрос. «Java» - это язык, а не библиотека. И если «ваша маленькая библиотека с открытым исходным кодом» имеет слишком строгую видимость, то расширение видимости впоследствии не нарушает обратной совместимости. Только наоборот.
Док Браун

Ответы:

15

К сожалению, многие библиотеки пишутся , а не проектируются . Это печально, потому что предварительная мысль может предотвратить множество проблем в будущем.

Если мы планируем проектировать библиотеку, будет некоторый набор ожидаемых вариантов использования. Библиотека может не удовлетворять всем сценариям использования напрямую, но может служить частью решения. Таким образом, библиотека должна быть достаточно гибкой, чтобы адаптироваться.

Ограничение состоит в том, что обычно не стоит брать исходный код библиотеки и модифицировать его для обработки нового варианта использования. Для проприетарных библиотек источник может быть недоступен, а для библиотек с открытым исходным кодом может быть нежелательно поддерживать разветвленную версию. Возможно, нецелесообразно объединить очень специфические адаптации в основной проект.

Вот тут-то и возникает принцип открытого-закрытого: библиотека должна быть открыта для расширения без изменения исходного кода. Это не приходит естественно. Это должно быть намеренной целью дизайна. Здесь есть множество методов, которые могут помочь, классические шаблоны проектирования ООП - некоторые из них. В общем, мы указываем хуки, где пользовательский код может безопасно подключаться к библиотеке и добавлять функциональность.

Просто сделать каждый метод общедоступным или позволить каждому классу быть разделенным на подклассы недостаточно для достижения расширяемости. Прежде всего, действительно трудно расширить библиотеку, если неясно, где пользователь может подключиться к библиотеке. Например, переопределение большинства методов небезопасно, поскольку метод базового класса был написан с неявными допущениями. Вам действительно нужно разработать для расширяемости.

Что еще более важно, если что-то является частью общедоступного API, вы не можете его вернуть. Вы не можете изменить его, не нарушая нижестоящий код. Преждевременная открытость ограничивает библиотеку неоптимальным дизайном. Напротив, сделать внутренний материал частным, но добавить хуки, если позже они понадобятся, - более безопасный подход. Хотя это нормальный способ решения долгосрочной эволюции библиотеки, это неудовлетворительно для пользователей, которым необходимо использовать библиотеку прямо сейчас .

Так что же происходит вместо этого? Если текущее состояние библиотеки вызывает серьезные затруднения, разработчики могут взять все знания о реальных случаях использования, накопленные с течением времени, и написать версию 2 библиотеки. Это будет замечательно! Это исправит все эти ошибки в дизайне! Это также займет больше времени, чем ожидалось, во многих случаях кончится. И если новая версия сильно отличается от старой, может быть трудно поощрить пользователей к миграции. Затем вы оставили две несовместимые версии.

Амон
источник
Поэтому мне нужно добавить хуки для расширения, потому что просто сделать его общедоступным / перезаписываемым недостаточно. И мне также нужно подумать о том, когда выпускать изменения / новый API из-за обратной совместимости. Но как насчет видимости методов в специальных?
piegames
@piegames С видимостью вы решаете, какие части являются общедоступными (часть вашего стабильного API), а какие частными (могут быть изменены). Если кто-то обходит это с помощью рефлексии, это его проблема, когда эта функция сломается в будущем. Кстати, точки расширения часто имеют форму метода, который можно переопределить. Но есть разница между методом, который может быть переопределен, и методом, который должен быть переопределен (см. Также Шаблон метода шаблона).
Амон
8

Каждый открытый и расширяемый класс / метод является частью вашего API, который должен поддерживаться. Ограничение этого набора разумным подмножеством библиотеки обеспечивает максимальную стабильность и ограничивает количество вещей, которые могут пойти не так. Это управленческое решение (и даже проекты OSS управляются в определенной степени), основанное на том, что вы можете разумно поддержать.

Разница между OSS и закрытым исходным кодом заключается в том, что большинство людей пытаются создать и развить сообщество вокруг кода, чтобы библиотеку обслуживало более одного человека. Тем не менее, есть ряд доступных инструментов управления:

  • Списки рассылки обсуждают потребности пользователей и способы их реализации
  • Системы отслеживания ошибок (проблемы JIRA или Git и т. Д.) Отслеживают ошибки и запросы функций
  • Контроль версий управляет исходным кодом.

В зрелых проектах вы увидите что-то вроде этого:

  1. Кто-то хочет сделать что-то с библиотекой, для которой он изначально не предназначен
  2. Они добавляют тикет для отслеживания проблем
  3. Команда может обсудить проблему в списке рассылки или в комментариях, а заявителю всегда предлагается присоединиться к обсуждению.
  4. Изменение API принято и расставлено по приоритетам или отклонено по какой-либо причине

В этот момент, если изменение было принято, но пользователь хочет ускорить его исправление, он может выполнить работу и отправить либо запрос на извлечение, либо исправление (в зависимости от инструмента контроля версий).

Ни один API не является статичным. Однако его рост должен быть определенным образом. Держа все закрытыми до тех пор, пока не возникнет явная необходимость открыть вещи, вы избежите репутации глючной или нестабильной библиотеки.

Берин Лорич
источник
1
Полностью согласен, и я успешно применил процесс запроса на изменение, который вы создали для сторонних библиотек с закрытым исходным кодом, а также для библиотек с открытым исходным кодом.
Док Браун
По моему опыту, даже небольшие изменения в небольших библиотеках - это большая работа (даже если это просто для того, чтобы убедить другие), и они могут занять некоторое время (ждать следующего релиза, если вы не можете позволить себе использовать снимок до этого момента). Так что это явно не вариант для меня. Мне было бы интересно, хотя: есть ли большие библиотеки (в GitHub), которые действительно используют эту концепцию?
piegames
Это всегда много работы. Почти каждый проект, в который я внес вклад, имеет процесс, похожий на этот. В мои дни в Apache мы могли бы обсуждать что-то в течение нескольких дней, потому что мы были увлечены тем, что создали. Мы знали, что многие люди собираются использовать библиотеки, поэтому нам пришлось обсудить такие вещи, как то, что предлагаемое изменение нарушит API, стоит ли предлагаемая функция, когда мы должны это делать? и т. д.
Берин Лорич
0

Я перефразирую свой ответ, так как кажется, что он поразил некоторых людей.

Видимость свойства / метода класса не имеет ничего общего ни с безопасностью, ни с открытостью исходного кода.

Причина, по которой существует видимость, заключается в том, что объекты хрупки по 4 конкретным проблемам:

  1. совпадение

Если вы соберете свой модуль без инкапсуляции, то ваши пользователи привыкнут к непосредственному изменению состояния модуля. Это прекрасно работает в однопоточной среде, но если вы даже подумаете о добавлении потоков; вы будете вынуждены сделать состояние частным и использовать блокировки / мониторы вместе с геттерами и сеттерами, которые заставляют другие потоки ждать ресурсов, а не гоняться за ними. Это означает, что ваши пользовательские программы больше не будут работать, потому что частные переменные не могут быть доступны обычным способом. Это может означать, что вам нужно много переписать.

Правда в том, что кодирование намного проще с учетом однопоточной среды выполнения, а закрытое ключевое слово позволяет просто добавить синхронизированное ключевое слово или несколько блокировок, и код ваших пользователей не сломается, если вы инкапсулировали его с самого начала ,

  1. Помогите пользователям избежать стрельбы в ногу / оптимизировать использование интерфейса. По сути, это помогает вам контролировать инварианты объекта.

У каждого объекта есть куча вещей, которые должны быть истинными, чтобы быть в согласованном состоянии. К сожалению, эти вещи живут в видимом для клиента пространстве, потому что дорого перемещать каждый объект в отдельный процесс и общаться с ним посредством сообщений. Это означает, что объекту очень легко разрушить всю программу, если пользователь имеет полную видимость.

Это неизбежно, но вы можете предотвратить случайный перевод объекта в несогласованное состояние, закрыв интерфейс своих служб, предотвращая случайные сбои, позволяя пользователю взаимодействовать с состоянием объекта только через тщательно разработанный интерфейс, который делает программу намного более надежной. , Это не означает, что пользователь не может преднамеренно повредить инварианты, но если они это сделают, то это их клиент, который падает, все, что им нужно сделать, это перезапустить программу (данные, которые вы хотите защитить, не должны храниться на стороне клиента ).

Еще один хороший пример, где вы можете улучшить удобство использования ваших модулей - сделать конструктор приватным; потому что если конструктор выдает исключение, он убьет программу. Один из ленивых подходов к решению этой проблемы - заставить конструктор выдать ошибку времени компиляции, которую вы не можете создать, если она не находится в блоке try / catch. Сделав конструктор закрытым и добавив открытый статический метод create, вы можете получить метод create, возвращающий значение null, если он не сможет его создать, или воспользоваться функцией обратного вызова для обработки ошибки, что сделает программу более удобной для пользователя.

  1. Сфера загрязнения

Многие классы имеют много состояний и методов, и их легко перепутать, пытаясь прокрутить их; Многие из этих методов - просто визуальный шум, такой как вспомогательные функции, состояние. конфиденциальность переменных и методов помогает уменьшить загрязнение области и облегчить пользователям поиск услуг, которые они ищут.

По сути, это позволяет вам избежать использования вспомогательных функций внутри класса, а не за его пределами; без контроля видимости, не отвлекая пользователя кучей сервисов, которые пользователь никогда не должен использовать, так что вы можете сойти с рук, разбив методы на кучу вспомогательных методов (хотя это все равно будет загрязнять вашу область, но не пользователя).

  1. быть привязанным к зависимостям

Хорошо разработанный интерфейс может скрывать свои внутренние базы данных / окна / образы, от которых он зависит, чтобы выполнять свою работу, и если вы хотите перейти на другую базу данных / другую систему управления окнами / другую библиотеку изображений, вы можете оставить интерфейс таким же, как и у пользователей. не заметит

С другой стороны, если вы этого не сделаете, вы легко можете сделать невозможным изменение ваших зависимостей, потому что они выставлены, и код полагается на это. При достаточно большой системе стоимость миграции может стать недоступной, в то время как ее инкапсуляция может защитить хорошо работающих пользователей клиентов от будущих решений об обмене зависимостями.

Дмитрий
источник
1
«Нет смысла что-либо скрывать» - тогда зачем вообще думать об инкапсуляции? Во многих случаях рефлексия требует особых привилегий.
Фрэнк Хайлеман
Вы думаете об инкапсуляции, потому что она дает вам передышку при разработке модуля и снижает вероятность неправильного использования. Например, если у вас есть 4 потока, которые напрямую изменяют внутреннее состояние класса, это легко вызовет проблемы, в то время как частная переменная побуждает пользователя использовать открытые методы для манипулирования состоянием мира, которые могут использовать мониторы / блокировки для предотвращения проблем. , Это единственное реальное преимущество инкапсуляции.
Дмитрий
Сокрытие вещей ради безопасности - это простой способ создать дизайн, в котором вам нужно будет иметь дырки в вашем API. Хорошим примером этого являются мультидокументные приложения, где у вас много наборов инструментов и много окон с подоконами. Если вы сходите с ума по инкапсуляции, у вас будет ситуация, когда вы будете рисовать что-то на одном документе, вы должны попросить окно попросить внутренний документ, чтобы попросить внутренний документ попросить внутренний документ поставить запрос на что-то нарисовать и лишить законной силы его контекст. Если клиентская сторона хочет играть с клиентской стороной, вы не можете помешать им.
Дмитрий
Хорошо, это имеет больше смысла, хотя безопасность может быть достигнута с помощью контроля доступа, если среда поддерживает это, и это было одной из первоначальных целей при разработке языка ОО. Также вы продвигаете инкапсуляцию и говорите, что не используйте ее одновременно; немного сбивает с толку.
Фрэнк Хайлеман
Я никогда не хотел не использовать это; Я имел в виду не использовать его ради безопасности; Используйте его стратегически, чтобы улучшить работу ваших пользователей и создать более плавную среду разработки. я хочу сказать, что это не имеет ничего общего ни с безопасностью, ни с открытостью источника. Объекты на стороне клиента по определению уязвимы для самоанализа, и перемещение их из пространства пользовательских процессов делает неинкапсулированные вещи такими же недоступными, как и инкапсулированные.
Дмитрий