В первую голову проблема касается продуктовых софтостроителей, хотя и в проектном тоже не все гладко.
Начнем с недавней истории. Технология COM (и другие, но суть та же) дала возможность разработчикам компонентов простым образом отделить интерфейсы от реализации. Для прикладных разработчиков это означало, например, что при обновлении компонентов старые интерфейсы продолжали бы работать. В теории, конечно. Но и на практике это выглядело гораздо лучше, чем «ад динамических библиотек», имеющих всегда версию «текущая».
Каков механизм? Он очень прост.
Объявляет программист компонента интерфейс
Потом программист этот интерфейс реализует. Скомпилированная программа устанавливается на устройстве пользователя, где в некотором реестре означенному UUID ставится в соответствие исполняемый модуль. Например, в Program Files\CoolPrinting1_0\cool.exe.
В сторонней прикладной программе, использующей этот интерфейс, у того же «реестра» по UUID запрашивается интерфейс и вызывается нужная функция.
Что происходит, если разработчик компонента меняет функционал?
В простом варианте расширяется интерфейс. Это означает что разработчик добровольно обязан (ощутите это прекрасное сочетание слов):
Теперь даже установленная вместо старой, новая версия продолжает работать по-прежнему. Прикладному программисту, использующему компонент, ничего переделывать не надо. То есть, совсем ничего. Круто, да?
В более сложном варианте разработчик выпускает новую версию, не поддерживающую старый интерфейс. Но при этом он опять же добровольно обязан предусмотреть возможность одновременной установки новой версии рядом со старой, куда-нибудь в Program Files\CoolPrinting2_0.
И в этом случае прикладному программисту тоже ничего переделывать не надо.
Однако вышеописанное происходит только в мире, где программисты хотя бы примерно представляют, что они производят. А их приказчики знают дело, которым руководят. Такое тоже бывает.
Чаще происходит наоборот.
В наиболее гуманном варианте программист не создает новый интерфейс, а просто правит старый, меняя, заодно, его UUID. Менять UUID надо обязательно, иначе невозможно будет отличить новый интерфейс от старого. Новая версия устанавливается вместо старой.
Программа, использующая компонент, сразу перестает работать. Потому что ссылка по прежнему UUID ведет в пустоту. Но обойти эту проблему достаточно просто, поскольку интерфейс можно:
Такая «хитрая» техника будет работать до тех пор, пока программист компонента не удалит старые методы и интерфейсы. Тогда при попытке использовать функционал в вашей прикладной программе произойдет ошибка.
Положение усугубляется тем, что установить две версии компонента на одном и том же устройстве чаще всего оказывается невозможно. Потому что если разработчик не сумел обеспечить совместимость на уровне интерфейсов, то чтобы обеспечить бесконфликтную работу разных версий компонента, мозгов у него тем более не хватит.
Шайба переходит к прикладному программисту. Доброжелатели с форума шепчут: «Паттерн адаптер». А то и «стратегия». А чему удивляться? Основное назначение шаблонов так называемого проектирования — создание костылей и подпорок.
И вот простая и ясная прикладная программа из двух строчек — получение ссылки на компонент и вызов метода, превращается в кашу из сотен строк «шаблонного» кода и нескольких классов, сообразно числу версий компонента. Разумеется, я не буду их тут приводить.
Примеры? Пожалуйста. Компонент PdfCreator, развиваясь с версии 0.х до нынешней 2.х, разрабатывался именно так. Вначале правкой старых интерфейсов с заменой UUID, а потом созданием новых, несовместимых со старыми, но уже удалёнными. Две разные версии на одном компьютере устанавливать нельзя.
Можно возразить, что PdfCreator — бесплатная утилита с открытым кодом. Но что это меняет? Разве нерадивость разработчиков можно компенсировать пословицей «Дареному коню в зубы не смотрят»? Есть и другие примеры вполне коммерческих компонентов, не обеспечивающих совместимость между версиями.
Мораль. Правило «Сделано не здесь» (not invented here) имеет под собой веские основания. Как минимум, иметь под рукой исходники будет нелишним.
Начнем с недавней истории. Технология COM (и другие, но суть та же) дала возможность разработчикам компонентов простым образом отделить интерфейсы от реализации. Для прикладных разработчиков это означало, например, что при обновлении компонентов старые интерфейсы продолжали бы работать. В теории, конечно. Но и на практике это выглядело гораздо лучше, чем «ад динамических библиотек», имеющих всегда версию «текущая».
Каков механизм? Он очень прост.
Объявляет программист компонента интерфейс
[uuid(a03d1421-b1ec-11d0-8c3a-00c04fc31d2f)]
interface ICoolPrinting
{
void Print(const string fileName);
}
Потом программист этот интерфейс реализует. Скомпилированная программа устанавливается на устройстве пользователя, где в некотором реестре означенному UUID ставится в соответствие исполняемый модуль. Например, в Program Files\CoolPrinting1_0\cool.exe.
В сторонней прикладной программе, использующей этот интерфейс, у того же «реестра» по UUID запрашивается интерфейс и вызывается нужная функция.
ICoolPrinting printing = GetInterface(...);
printing.Print("c:\myfile.pdf");
Что происходит, если разработчик компонента меняет функционал?
В простом варианте расширяется интерфейс. Это означает что разработчик добровольно обязан (ощутите это прекрасное сочетание слов):
- создать новый интерфейс ICoolPrinting2 с новым UUID, возможно, унаследовав его от прежнего;
- реализовать его, не ломая старую реализацию.
[uuid(d26d1421-g1ec-12d0-8c3a-12c84ef31d2f)]
interface ICoolPrinting2 : ICoolPrinting
{
bool SetupPrinter();
}
Теперь даже установленная вместо старой, новая версия продолжает работать по-прежнему. Прикладному программисту, использующему компонент, ничего переделывать не надо. То есть, совсем ничего. Круто, да?
В более сложном варианте разработчик выпускает новую версию, не поддерживающую старый интерфейс. Но при этом он опять же добровольно обязан предусмотреть возможность одновременной установки новой версии рядом со старой, куда-нибудь в Program Files\CoolPrinting2_0.
И в этом случае прикладному программисту тоже ничего переделывать не надо.
Однако вышеописанное происходит только в мире, где программисты хотя бы примерно представляют, что они производят. А их приказчики знают дело, которым руководят. Такое тоже бывает.
Чаще происходит наоборот.
В наиболее гуманном варианте программист не создает новый интерфейс, а просто правит старый, меняя, заодно, его UUID. Менять UUID надо обязательно, иначе невозможно будет отличить новый интерфейс от старого. Новая версия устанавливается вместо старой.
Программа, использующая компонент, сразу перестает работать. Потому что ссылка по прежнему UUID ведет в пустоту. Но обойти эту проблему достаточно просто, поскольку интерфейс можно:
- запросить по имени, а не по UUID;
- использовать динамические вызовы функций (на стадии исполнения).
OleVariant printing = GetInterface("ICoolPrinting");
printing.Print("c:\myfile.pdf");
Такая «хитрая» техника будет работать до тех пор, пока программист компонента не удалит старые методы и интерфейсы. Тогда при попытке использовать функционал в вашей прикладной программе произойдет ошибка.
Положение усугубляется тем, что установить две версии компонента на одном и том же устройстве чаще всего оказывается невозможно. Потому что если разработчик не сумел обеспечить совместимость на уровне интерфейсов, то чтобы обеспечить бесконфликтную работу разных версий компонента, мозгов у него тем более не хватит.
Шайба переходит к прикладному программисту. Доброжелатели с форума шепчут: «Паттерн адаптер». А то и «стратегия». А чему удивляться? Основное назначение шаблонов так называемого проектирования — создание костылей и подпорок.
И вот простая и ясная прикладная программа из двух строчек — получение ссылки на компонент и вызов метода, превращается в кашу из сотен строк «шаблонного» кода и нескольких классов, сообразно числу версий компонента. Разумеется, я не буду их тут приводить.
Примеры? Пожалуйста. Компонент PdfCreator, развиваясь с версии 0.х до нынешней 2.х, разрабатывался именно так. Вначале правкой старых интерфейсов с заменой UUID, а потом созданием новых, несовместимых со старыми, но уже удалёнными. Две разные версии на одном компьютере устанавливать нельзя.
Можно возразить, что PdfCreator — бесплатная утилита с открытым кодом. Но что это меняет? Разве нерадивость разработчиков можно компенсировать пословицей «Дареному коню в зубы не смотрят»? Есть и другие примеры вполне коммерческих компонентов, не обеспечивающих совместимость между версиями.
Мораль. Правило «Сделано не здесь» (not invented here) имеет под собой веские основания. Как минимум, иметь под рукой исходники будет нелишним.
lair
Скажите, пожалуйста, честно, что вы предлагаете? Отказаться от использования внешних зависимостей? Или вот скажите, вы про nuget не слышали?
PS «Основное назначение шаблонов так называемого проектирования — создание костылей и подпорок.»
Ну да, конечно, человек, за плечами которого две сотни проектов по всему миру, докторская по архитектуре и еще некоторое количество бумажек, строил здания на костылях и подпорках.
ololoepepe
cross_join Автор
В тексте упомянуты, как минимум, 2 тезиса по частным решения на относительно малом масштабе:
— ответственно относиться к разработке сторонних компонентов
— снизить риски наличием исходного кода
В общем случае проблема не решается. Подтверждается великим множеством эксплуатируемого ПО, которого поставщики уже не поддерживают. Далеко не всегда у такого ПО есть исходники, что вынуждает клиентов иметь в резерве уже давно не производящееся «железо». Далеко не всегда даже имеющиеся исходники могут быть перенесены в новую архитектуру.
lair
Что такое «разработка сторонних компонентов»? Как наличие исходных кодов избавляет от dependency hell?
Чем вас в общем случае не устраивают решения с локально распространяемыми зависимостями?
cross_join Автор
Компонент программной системы — это минимальная единица развертывания. В данном случае речь идет еще и компоненте в понятии COM.
В продуктовом софтостроении каждый сторонний компонент, то есть поставляемый третьей стороной, увеличивает риски несовместимости и отказа отдельных функций основной системы.
Жестко зафиксировать конфигурацию установленного ПО у сотен клиентов не представляется возможным. Поэтому для относительно небольших компонентов выбирается вариант его внедрения в общую систему. В этом случае наличие исходников значительно облегчает задачу внедрения и последующего сопровождения.
Простой пример был приведен — экспорт в PDF. Поскольку разработчики PDF Creator безответственно относятся к разработке, то эта функция должна быть внедрена в основную систему, возможно уже на базе другой реализации.
Другие часто встречающиеся примеры — архивация и распаковка, сканирование (без драйверов), доступ к БД, генерация отчетности, реализации сетевых протоколов разных уровней и т.д.
Чем больше основная система, тем больше в ней внедрено сторонних решений, зависимость с которыми разорвана на уровне поглощения исходников.
lair
Нет, должен быть выбран другой компонент.
То есть кроме поглощения исходников вы других вариантов разрыва dependency hell не знаете?
cross_join Автор
Давайте вернемся к примеру. У вас есть система, развернутая на сотнях рабочих мест. Предположим, она использует упомянутый PDFCreator для печати. Разработчики PDFCreator выпустили новую версию. Несколько клиентов обновили PDFCreator и обнаружили, что печать больше не работает (технические причины описаны в тексте). Они начинают звонить в поддержку вендора.
Вы предлагаете:
— выбрать другой компонент (надо полагать, его разработчики более ответственны, такое тоже бывает). Извините, этот вариант нам не подходит.
— какой-то другой вариант кроме (1) поглощения или (2) поддержки совместимости со всеми версиями (механизм также описан в тексте)
Готов внимательно выслушать про другой вариант, только не теоретический, а подкрепленный реальной практикой в похожих условиях.
lair
Я предлагаю вам выбрать компонент, используемую версию которого вы сможете гарантированно зафиксировать (например, через bin-deploy).
cross_join Автор
То есть поглотить, но без исходников, взяв на себя ответственность по развертыванию зафиксированной версии со всеми вытекающими.
Спасибо, как вариант иногда проходит. Например, в случае embedded СУБД, они развертываются примерно так.
lair
Нет никакого поглощения — вы не несете ответственность за этот код, это просто используемый вами компонент. Когда он обновляется у производителя, и если вам нужна функциональность этого обновления — вы прогоняете тесты и включаете в свой следующий релиз.
Win-win.
cross_join Автор
Совершенно верно, это называется поглощение бинарников. Развертывая их в составе своей системы, вы несете ответственность за них перед клиентами, не имея исходного кода, полагаясь исключительно на тесты типа «черный ящик».
В некоторых случаях это возможно, когда компонент достиг определенного уровня зрелости и риск обнаружения в нем критичных ошибок невелик. Иначе это не работает., к сожалению.
lair
Вы не несете ответственность за них, вы несете ответственность за вашу систему. И да, предполагается, что вы критичные ошибки найдете до выпуска вашего продукта.
cross_join Автор
Если вы не несете ответственность перед клиентом, скажем, за ошибку в SQL Server Compact (который поставляется вместе с вашей системой простым copy deployment), это означает, что вы можете клиенту предложить обратиться в Микрософт, а не к вам.
Поскольку это не так практически всегда, предлагаю не заниматься терминологическими изысками.
С сутью же ваших предложений понятно, спасибо.
lair
Вы не понимаете, о чем я. Когда вы несете ответственность за компонент — пользователь предполагает, что он может использовать компонент независимо от вашей программы.
cross_join Автор
В случае развертывания SQL Server Express — может использовать. В случае PDFCreator — тоже. Зафиксировать их версии возможным не представляется. Поэтому предлагаемый подход в таких случаях не работает.
lair
Значит, не используйте эти компоненты (собственно, SQL Server — это не компонент, это сервис). Это не означает, что сам по себе компонентный подход мертв или плох.
cross_join Автор
Конечно не означает. Компонентный подход был серьезным шагом в индустриализации интеграции.
lair
Что возвращает нас к вопросу «о чем ваш пост, чем вас не устраивают существующие решения, и что вы предлагаете».
Gorthauer87
Для таких случаев придумали opensource.
Deranged
Нет, .NET.
Gorthauer87
Проблема тогда просто в другой фантик заворачивается.
Deranged
Почему это? Там все сборки версионируются, угробить другое приложение, просто разместив сборку в GAC уже сложнее.
cross_join Автор
К сожалению, это лишь аналог пожеланий того, что поставщик компонента должен обеспечить функционирование нескольких версий одновременно на одном устройстве.
В случае COM-DLL сделать это даже проще, чем для сборки в GAC: всего лишь установить DLL новой версии в другой директорий, и не надо никаких strong name и прочего.
Однако… см. по тексту.
mayorovp
Для таких случаев придумали таскать зависимости вместе с приложением.
Gorthauer87
А если лицензия говорит нельзя? В случае общих библиотек опенсорс дохрена головной боли снимает.
mayorovp
Для таких случаев придумали redistributable packages… Разумеется, о портабельности библиотеки должен думать ее создатель.
PS как там по-русски будет «портабельность»? «Таскаемость»? :)
lair
«Переносимость».
mayorovp
Да, точно. Спасибо.
qw1
Навскидку не могу вспомнить коммерческой библиотеки, которую по лицензии пользователь должен покупать самостоятельно.
Gorthauer87
Почти все драйвера на видео в линуксе так распространяются, такая же хрень творится с oracle jvm.
cross_join Автор
Коммерческие CORBA-реализации, например.
Deranged
Не туда.