Комментарии 94
Предложенный вариант - сплошная боль для тестирования и отладки.
Вы (или коллега) глобальный С или затрете,или забудете его корректно инициализировать. И рекурсивные вызовы будут увлекательными.
Правильное решения - пересмотреть архитектуру, использовать каррирование использовать dependency injection, а иногда и куча параметров - это примлемое решение
Ну и тут статейка про Scala была недавно, но у вас же не скала.
Обычно 1С-ники сопровождают типовой код и поменять его нет возможности.
То что сама 1С пишет криво свои конфигурации, это факт, но мы вынуждены с этим жить.
Механизм расширений 1С позволяет модифицировать типовой код...
да, но вот я привел пример, что можно модифицировать много промежуточных процедур, а можно сделать заплатку только в одну процедуру. Выигрыш решения в том числе и в прозрачности. Чем больше процедур затрагивает расширение, тем хуже в 1С.
Выигрыш решения в том числе и в прозрачности.
В моем понимании "прозрачность" — это когда легко понятно происходящее.
Когда мы в процедуре выставляем какое-то глобальное значение, мы не знаем, на что это повлияет. Это решение как раз менее прозрачно, чем явная передача параметра в другую процедуру.
не знаете почему? Потому что вам комментарий не написали? так я пишу.
Во-первых, если к коду нужно писать комментарий, это намекает на его непрозрачность.
А во-вторых, проблема комментариев к коду в том, что рано или поздно они с ним расходятся. Вот вы написали комментарий в месте присвоения — а потом в месте чтения поменяли код, и теперь комментарий не соответствует реальности. Надо пойти и поправить все места, где вы делали присвоение. Рано или поздно люди забывают это делать (если вообще делали изначально).
но лучше если комментарий есть, чем если его нет.
Если комментарий устарел и не соответствует действительности — лучше, чтобы его не было.
ну почему же, история тоже интересна. А так то конечно да, но на мой взгляд чаще код страдает от нехватки комментариев а не от их избытка.
Вы спросили как это делать - я ответил.Как это вписать в 1С это уже другой вопрос, я с 1С не работаю.
Приведенный пример решается например замыканием C в Down, после чего вы передаёте/ замыкаете получившийся Down+C в Middle
что такое замыкание? Можно как-то передавать по стеку дополнительные параметры?
не нужно вам это, в 1С даже типов-функций нет. Какие тут замыкания?
Замыкания функций позволяют скрыть внутри себя контекст места создания и использовать его в месте вызова.
неплохо, неплохо. Но попробуйте объяснить это научно-популярно. Считайте что Лисп я знаю.
у нас тут ка-бы не ликбез. Это общепринятая терминология https://ru.wikipedia.org/wiki/Замыкание_(программирование)
Как можно знать Лисп и не знать замыкания?
Лямбда в Лиспе захватывающая переменную из контекста объявления и есть замыкание.
Прочтите что-нибудь об этом, хотя бы википедию.
Вы, вроде бы, в своей статье сам пишете:
Но мне кажется, что затронутый в этой теме вопрос касается всех языков программирования, т.к. затрагивает проблему, с которой рано или поздно сталкивается программист при работе со сложным кодом.
Вы уж определитесь, ваша проблема растет из ограничений 1С, или касается всех языков программирования?
Как это в 1С я знаю, хочется понять как эта проблема решается в других языках программирования - это основной посыл статьи. Я определился еще когда писал.
эта проблема решается в других языках программирования
… а потом ответили "обычно 1С-ники сопровождают типовой код и поменять его нет возможности". Ну вот в других языках программирования такой проблемы обычно нет.
ну раз нет, то я рад за другие языки программирования, но т.к. я их знаю штук 10, сдается мне, что вы лукавите
Зачем вы вообще тогда что-то спрашиваете, если сам знаете штук 10 языков программирования, а сторонние мнения вам кажутся лукавством?
лукавство - это ваша попытка манипуляций. Я спрашиваю конкретно по данной теме, а не по языкам программирования.
Ну вот "конкретно данная тема" обычно решается не так, как вы предложили.
а как?
Передачей параметра. Передачей делегата. Передачей стратегии (что снова делегат, только другими средствами). Выделением переопределяемых методов.
Короче, либо явной модификацией кода, либо таким дизайном кода, чтобы он позволял последющие расширения без модификации.
вот в моем случае легаси написано так, что не подразумевает вмешательство в этом участке кода. Об этом просто не подумали заранее. Приходится "шлифовать напильником".
Если не подразумевает — не вмешивайтесь. Переписывайте полностью.
Потому что иначе вы ставите ПО в ситуацию, на которую оно не рассчитано, и последствия исключительно за собственный счет.
я не могу полностью переписывать код за поставщика продукта, который я всего лишь сопровождаю. Я не против, но у моих клиентов нет столько денег. Так что по средствам. Аккуратно, но эффективно.
Ну то есть вы, предположительно, сели вместе со своим клиентом, и внимательно посчитали, во сколько обойдется поддержка каждого из вариантов в краткосрочной и долгосрочной перспективе, и ваш оказался дешевле. Ок, с этим за отсутствием исходных данных невозжно спорить.
Но это не общий случай, а ваша частная ситуация.
А может быть некоторые языки поддерживают такую передачу контекста вызова "играючи"?
Например, классы и вложенные функции
И использовали, и боролись с последствиями активного использования этого способа. Разные люди выбирают совершенно разные объекты, где они считают допустимым временно похранить какое-то значение, которое они ленятся прокидывать нормально, поэтому при доработке подобного кода приходится анализировать чуть ли не всю кодовую базу.
На самом деле, современные языки стараются по возможности затруднить то, что вы описали с целью повышения понятности [чужого] кода. В частности, возможностью описывания неизменяемых переменных и структур. Вместо этого даются альтернативные области видимости объектов, развитые системы типов. Ну и возможности IDE по рефакторингу сейчас достаточно мощные.
И так или иначе, большинство языковых решений для решения обозначенной проблемы - это вариации на тему структуры-контекста (который вы назвали k), И обычно это используется для внедрения зависимостей (DI). Для передачи входных данных обычно используются аргументы функций/методов.
Вот, например, как страшноватенько подобный подход выглядит в Scala с использованием библиотек cats и tofu. Зато всё, что делает этот код понятно отражено в типах.
import cats.data.ReaderT
import cats.syntax.all._
import cats.{Applicative, Id}
import tofu.WithContext
import tofu.optics.Extract
import tofu.optics.macros.ClassyOptics
object ContextsAbuse extends App {
// Макроаннотация генерит линзы для выдирания членов структурыs
@ClassyOptics
// Собственно, описание структуры высокоуровнего контекста
final case class Ctx(
c: Int,
foo: String,
bar: Byte
)
/* Пояснение, какой тип в итоге имеют F в middle и down
по сути, это тип функции Ctx => A (т.е. f(ctx: Ctx): A)
*/
type RunF[A] = ReaderT[Id, Ctx, A]
def top(c: Int) = {
/* Получаем результат middle с погруженным в него результатом (зависящим от c,
которое внутри middle ещё неизвестно. По сути, здесь получается функция Ctx => Int,
с результатом частичного применения математической операции внутри down.
*/
val middleF = middle[RunF]
// На верхнем уровне формируем глобальный контекст нужного вида
val ctx = Ctx(c, "foo", 42)
// По сути, просто доприменяем частично-применённую middleF
middleF.run(ctx)
}
val get1 = 42
val get2 = 108
/* Здесь нет никакого знания того, что в контексте есть c: Int (но есть знание всей структуры контекста)
при разработке в промежуточном коде, которого может быть много, править ничего не потребуется
*/
def middle[F[_]: Applicative: WithContext[*[_], Ctx]] = {
val y = get1
val z = get2
down(y, z)
}
/* Страшненькая сигнатура говорит о том функция определена для любых типов-"эффектов" F,
инкапсулирующих понятие контекста Ctx, для любых типов A (с "числовыми" операциями) и
возможностью "выдрать" значение типа A из контекста Ctx
*/
def down[F[_]: Applicative: WithContext[*[_], Ctx], A: Numeric: Extract[Ctx, *]](y: A, z: A) = {
val num = Numeric[A]
import num._
/* Отображем выдираемое из контекста Int поле на результат операции, использующей
это значение. Результирующее значение остаётся "обрамлено" в типе эффекта F
Получено оно будет уже в top в .run
*/
WithContext[F, Ctx].extract(implicitly[Ctx Extract A]).context.map { c =>
(y + z) * c
}
}
println(top(19))
}
Извините, не осилил. Вы бы кратенько принцип изложили.
Совсем кратко - это как в вашем примере со структурой k, только лениво - контекст (k) остается аргументом итоговой функции, складываемой в значение middleF в верхнем компоненте вычислений. Чуть более подробно - пояснено комментарием. Для ещё более подробного понимания, увы, придется изучить основы функционального программирования на Scala или Haskell
Выше в комментариях уже упоминали Scala. Но в той статье для передачи структуры контекста используется фича имплицитных параметров. Это совсем не то, что в моем примере.
WithContext[*[_], Ctx]
Я скалу читать почти не умею, поэтому короткий вопрос: я же правильно понимаю, что все функции, вплоть до нижней (в нашем примере — и Middle
, и Down
), явно декларируют что им требуется конкретный контекст Ctx
?
Да. Если бы этой информации не требовалось, то это была бы плохая строгая типизация. :)
Но ссылается только на общий объект контекста, а не на то, что в нём должен быть какой-то конкретный элемент c.
Иногда (в контексте DI) уже есть блоки, которые работают с более узким контекстом. Тогда их можно скомпозировать. К примеру, down можно ещё больше разбить:
// функция, которая умеет работать с общим контекстом
def down[F[_]: Applicative: WithContext[*[_], Ctx], A: Numeric: Extract[Ctx, *]](
y: A, z: A
) = {
// указываем способ работы с контекстом типа A
implicit val wc2: WithContext[F, A] =
WithContext[F, Ctx].extract(implicitly[Ctx Extract A])
bottom(y, z)
}
/* функция, которая умеет работать с более конкретным контектом. плюс используется синтаксис для
более компактного доступа к контексту
*/
def bottom[F[_]: Applicative: WithContext[*[_], A], A: Numeric](
y: A, z: A
) = {
val num = Numeric[A]
import num._
import tofu.syntax.context._
for {
c <- context
} yield (y + z) * c
}
Что скажете насчет способа? Использовали?
Скажем, что использовали, и модифицировать это со временем становится невозможно: одна процедура меняет глобальное значение, потом другая, потом третья...
Или что можно предложить взамен?
Взамен можем предложить декомпозицию задачи по бизнес-сценариям. Если сценарий поменялся — должен поменяться и код.
А у меня ровно наоборот, успешный случай применения, в двух случаях. Как я писал выше, мы не можем поменять типовой легаси-код, поэтому приходится применять удачный заплатки.
А у меня ровно наоборот, успешный случай применения, в двух случаях.
Про свой опыт вы уже написали в статье. Вы спросили про мнение читателей? Вот я вам озвучил свое мнение, основанное на своем опыте.
мы не можем поменять типовой легаси-код
Если вы не можете поменять типовой легаси-код, то и задача не имеет решения.
приходится применять удачный заплатки
Недостатки предложенного вами решения никуда не деваются от того, что в каких-то (весьма редких) случаях иначе сделать нельзя.
ваше мнение я услышал.
ну как же не имеет решения, если я решил?
Да, иногда так лучше и проще. и надежнее
ну как же не имеет решения, если я решил?
Не поменяв ни строчки кода?
Да, иногда так лучше и проще. и надежнее
Лучше, проще и надежнее чем что?
да, расширения в 1С не меняют ни строчки исходного кода.
Они замещают или добавляются в исходный код.
Весь вопрос в том, чтобы затронуть не так много исходного кода, как в первом способе, когда дополнительный параметр протаскивается через все функции.
Ну то есть вы можете поменять типовой легаси-код, просто не хотите?
скажем так - типовой легаси код часто меняется. и если я внесу много правок, их будет сложно поддерживать. Поэтому я могу замещениями менять легаси-код, но нужно стремиться уменьшать количество таких правок.
Проблему можно решить передачей дополнительного параметра c, но если цепочка процедур длинная, это вызовет слишком обширные изменения во многих промежуточных процедурах
Погуглите ReactJS property drilling (здесь props это фактически аргументы функции). Например. Проблема с которой многомиллионная индустрия пытается бороться разными способами уже лет 10. Изговорена масса слов, исписаны тонны бумаги. Пробовали и решения влоб (менять так менять), и глобальные переменные как у вас (state), и ООП (функции превращать в методы объекта, а общую для всех них настройку делать атрибутом этого объекта), и монады из функционального программирования (Reader), и контексты (глобальный обьект, изменения в свойствах которого видны только вглубь по стеку вызовов, но не наружу). На практике используется всё вот это вот в перемешку (ну может быть кроме монад - которые для эстетов). А вообще Scope.
" Вообще-то я программирую в 1С, на языке, похожем на Visual Basic" - более мерзкого языка, чем ВБ я не видел (утрирую). Но потом я увидел 1С. Да, это личное мнение, но хуже "язЫка", чем 1С просто нет. Ибо это го.но не язык
Язык как язык, я имел опыт программирования на Visual Basic. По сути один в один.
Один в один?
Опыт можно иметь разный...
Visual Basic обеспечивает поддержку объектно-ориентированного программирования, включая инкапсуляцию, наследование и полиморфизм.
1С - нет.
...1С - среда быстрой разработки в рамках классов/объектов жестко заданных в конфигураторе с помощью мышки.
ООП - это божок, на который молятся?
На Visual Basic как и на PHP, где имеется ООП, оно не дает особых преимуществ.
А насчет "разработки мышкой" у вас серьезные иллюзии. Там вполне полноценный код а-ля Basic пишется + некий аналог SQL. Но я тут не за холиваром об 1с.
Вот вам несколько вопросов на размышление:
- что произойдет, если
Middle
илиDown
будут запущены самостоятельно, не в рамках вызоваTop
? - что произойдет, если
Top
будет запущена несколько раз параллельно в рамках одного "сеанса"?
что будет если метеорит упадет в Золотой Мост? Рассуждения примерно одного порядка. Эти функции так глубоко зашиты в легаси, что отдельного вызова не предусматривают. Я об этом не упомянул в статье, каюсь. Уточню, что middle, Down и Top пишутся поставщиком кода и они закостенели в своем разгуле и разврате легаси.
что будет если метеорит упадет в Золотой Мост? Рассуждения примерно одного порядка.
Нет, это типичные проблемы, которые возникают при работе с подобным решением.
Уточню, что middle, Down и Top пишутся поставщиком кода
Тогда вы не можете поправить ни одну из них, и ваше решение неприменимо, не правда ли?
нет там таких типичных проблем.
Вмешиваться могу, но в ограниченном объеме. Нельзя добавить параметр в каждую функцию, это будет тяжело сопровождать при очередном релизе легаси-кода.
нет там таких типичных проблем.
В общем случае есть. А то, что их нет у вас, как раз и говорит, что ваша проблема далека от общего случая.
Эти функции так глубоко зашиты в легаси, что отдельного вызова не предусматривают.
Вообще, с каждой подобной вашей оговоркой ("отдельного вызова не предусматривают", "нельзя поменять") ваша проблема все дальше уходит от общего случая (который часто встречается, имеет типовые решения и так далее) к конкретной узкой проблеме, которая интересна только в ограниченном контексте.
Во первых, если уж вы поставили тег 1С, то и пишите код на 1С, а не «вот это вот всё».
Во вторых, в 1С нет описанной вами проблемы, поскольку есть параметры по умолчанию, то есть, вы легко можете добавить ещё один параметр в Down и протащить его в своей ветке, а все остальные «типовые» вызовы, будут работать как и раньше, вызывая с двумя параметрами, а вместо третьего придёт «Неопределено».
В третьих, ваш пример это просто говно: процедура с побочными эффектами, к которой вы лепите сбоку глобальное состояние чего-то там…
P. S. ЗУП писали криворукие идиоты, но надеюсь, вам дадут по рукам за то, что вы там сделали. По поводу печатной формы: полный бред, везде в 1С в процедуры печати передаётся ссылка на документ, а значит и — тип, либо это будет куча данных, а значит и тип туда допихнуть — не проблема.
Или что можно предложить взамен?
Писать код нормально Декомпозиция и разделение ответственности. Вычисление в одной функции, вывод в другой.
public sub Top()
Input c
echo CalcMiddle * c
end sub
public sub Middle()
echo CalcMiddle
end sub
private sub CalcMiddle()
y = get1
z = get2
return CalcDown(y, z)
end sub
private sub CalcDown(y, z)
return y + z
end sub
ну пример был довольно условным. Так что то, что вы как-то перетасовали порядок, не особо имеет смысл - на практике там не перетасуешь, тем более в базовом легаси-коде.
Ага, вы начинаете понимать, почему программистам на других языках не нравится 1С.
думаю, в каждой работе достаточно заморочек. И опять же, мы говорим о типовых конфигурациях 1С или об 1с как платформе? Разница.
Мы говорим про ваш пример кода. Чтобы не надо было пробрасывать коэффициенты через глобальные переменные в процедуру, которая что-то вычисляет и выводит, надо разделять вычисление (бизнес-логику) и вывод, тогда композировать вычисления будет проще.
Поэтому нет, "затронутый в этой теме вопрос" касается не всех языков программирования, а только 1С и аналогично написанного плохого кода. В остальных языках нет необходимости использовать глобальные переменные, и не в последнюю очередь в этом помогает ООП.
я вас умоляю, как будто на условном С# или на чем там пишут бизнес логигу (Кобол?) нельзя написать код так, что в него нельзя будет вмешаться. Не идеализируйте языки программирования. Все в руках архитектора системы
Я вижу единственное преимущество ООП что тут если бы был глобальный класс-контекст вычисления, можно было бы добавить в него переменную. Не в глобальный контекст, а как свойство класса. Это да. Но разница не принципиальна.
Можно. Но на нем можно написать код, в который можно будет вмешаться, и довольно несложно. В отличие от 1С.
Вы почему-то решили подменять понятие возможности написания гарантией ненаписания. Отсутствие гарантий ненаписания глобальных переменных никак не отменяет возможности в других языках легко обойтись без их использования.
Я вижу единственное преимущество ООП что тут если бы был глобальный класс-контекст вычисления
Преимущество ООП тут в пробросе зависимостей в конструктор с возможностью их легкой замены и наследовании. Также с ООП проще разделять код на независимые компоненты.
Я так понимаю, в вашем примере Top() пишете вы, Middle() это код платформы, который иногда обновляется, и он вызывает Down(), который снова пишете вы.
С ООП вообще можно сделать так.
class PlatformLogicService
{
public function middle(): void {
y = this->getY();
z = this->getZ();
echo this->calcDown(y, z);
}
protected function calcDown(int y, int z): int {
return y * z;
}
}
class OurLogicService extends PlatformLogicService
{
public function __construct(
private CoefficientStorage coefficientStorage
) {}
public function calcDown(int y, int z): int {
c = this->coefficientStorage->getCoefficientValue();
return parent::calcDown(y, z) * c;
}
}
В дочернем классе надо переопределить всего одну функцию без всяких глобальных переменных.
Или так
class PlatformLogicService
{
public function calcMiddle(): int {
y = this->getY();
z = this->getZ();
return this->calcDown(y, z);
}
private function calcDown(int y, int z): int {
return y * z;
}
}
class OurLogicService
{
public function __construct(
private CoefficientStorage coefficientStorage,
private PlatformLogicService platformLogicService,
) {}
public function top(): int {
c = this->coefficientStorage->getCoefficientValue();
return this->platformLogicService->calcMiddle() * c;
}
}
Тут мы не копируем реализацию системной логики в свой класс и ничего не переопределяем, меньше риск сломать поведение других системных функций, которые завязаны на calcDown(). Первый подход в умных книжках называется наследование, второй композиция.
Спуск контента вызова внутрь процедуры