Поэтому накидаю свои мысли на этот счет постом, чтобы на него потом ссылаться.
Какие у нас вводные?
1.1) Есть два разных контекста кода, которые схожи друг с другом, но не должны пересекаться по реализации из сообрежений SRP.
ИЛИ
1.2) Есть внешний код, который закрывает нашу потребность (пока что), но в перспективе наши требования могут заставить нас отказаться от этого объекта и спрятать его за свой адаптер.
2) Мы не хотим писать много кода уже сейчас. Поэтому мы не против чуть-чуть накинуть грязи с понятной стратегией ее уборки.
Т.е. у нас есть кусок кода и нам нужно изобрести свой второй пока что такой же (ключевое слово пока что, т.к. таким же он быть не может из-за SRP), но дублировать его мы не хотим (DRY же, да?)
В общем - идея моего "реекспорта" очень простая. Мы делаем все подготовительные действия так, если бы мы писали "правильно" - т.е. создает отдельный модуль под нашу реализация, везде используем импорты на нее, закрываем ее протоколом при необходимости и т.д. Но потом не делаем самое последнее действие - *не пишем свою реализацию*. Или пишем, но частично...
Приведу пример из исходинов FastStream 0.6.0: у нас есть 2 модуля генерации документации
*
faststream.specification.asyncapi.v2_6_0*
faststream.specification.asyncapi.v3_0_0Соответственно, это разные мажорные версии спецификации и даже общие куски между ними представляют собой мнимое дублирование. Т.е. мы не можем создать какой-нибудь
shared модуль, куда вынесем общие классы для этих спецификаций. А что делать? Ну, конечно же, заводить полное дерево объектов для каждого из подмодулей. Т.е. v3_0_0 и v2_6_0 модули имеют внутри себя набор всех необходимых для их работы объектов и не могут импортировать какие-то объекты друг из друга.Но ведь код полностью дублировать не хочется?😉
Ну, поэтому мы просто делаем одинаковые деревья файлов в этих модулях, пишем полную
v2_6_0 реализацию, а в v3_0_0 начинаем "лайфхачить"Например, какой-нибудь
v3_0_0.servers.ServerVariable может на уровне реализации выглядеть вот так
from faststream.specification.asyncapi.v2_6_0.schema import ServerVariable
__all__ = (
"ServerVariable",
)
Или какой-нибудь
ChannelBinding может наследоваться от своей же версии из v2_6_0
from faststream.specification.asyncapi.v2_6_0.schema.bindings.amqp import (
ChannelBinding as V2Binding,
)
class ChannelBinding(V2Binding): ...
Сами по себе такие приемы нарушают SRP и вообще ай-ай-ай. Т.е. у нас контекст знает об объектах из совершенно другого контекста. И даже выставляет их за свою реализацию! Или наследует! Ужас, в общем.
Но суть в том, что об этом за пределами файла с реализацией никто не знает. Весь остальной код притворяется, что использует полноправную самостоятельную реализацию
v3_0_0.bindings.ChannelBinding класса. А это открывает нам возможность маневра по удалению этого нарушения SRP очень простыми способами в дальнейшем (когда оно начнет стрелять нам по ногам)1) Мы можем патчить и дорабатывать реализацию другого модуля через наследование, о котором никто не узнает
2) Мы можем спрятать реализацию другого модуля за фасад
3) Мы можем удалить "реекспорт"/наследование и написать в файле полностью самостоятельную реализиацию
В общем и целом, никто не знает, что там происходит внутри файла, поэтому никто не узнает, что SRP нарушен, а это значит, что ему это не навредит🌚
Однако, тут есть один подводный камень - если мы "реекспортируем" / наследуемся от реализации из другого контекста, то пользователь нашей реализации может завязаться на те методы, которые пришли из исходной реализации и не предпологаются в API текущей. Тогда, при устранении нарушения SRP мы столкнемся с тем, что часть кода пользователя отваливается из-за несовместимости новой реализации API старой. Поэтому в *идеальном мире* мы должны прятать такие "реекспорты" хотя бы за адаптер. Но на практике это излишне.
#программирование