Обновить
0
@MacInread⁠-⁠only

Пользователь

5
Подписчики
Отправить сообщение
Набережные Челны: дома обозначаются как по улицам, так и по комплексам, Например «Набережные Челны, 25/12, кв. 1». Или «25 комплекс дом 12»
Это (условно) независимый бинарный файл.

Но вы же говорите, что composite root в тестовой среде определен статически. Пара «модуль+composite root(тест)» и «модуль+composite root(релиз)» бинарно различны, нет?

и при этом заменять реализацию этого компонента при сборке, сохраняя компонент-пользователь бинарно неизменным, то ничем.

Так и получается — в процедурной парадигме реализация — это отдельный объектный файл, а «интерфейс» — заголовок, прототип процедуры.

у вас хоть вся система может быть в одном модуле, на тестирование это не повлияет

В приципе, если добавить один кросс-модуль с глобальными переменными процедурного типа, которые будут инициализироваться по-разному в разных сборках — это будет то же самое, что у вас composite root.
Не работаю ни с Net, ни с Java платформами, поэтому наберитесь терпения.

Отнюдь. В моем случае модуль в тесте и в продуктиве бинарно идентичен, это уменьшает степень неуверенности.

Вы говорите про тестируемый модуль, или про всю программу? Тестируемый модуль и в процедурной парадигме бинарно идентичен.

Нет такого кода. Просто нет.

Тем не менее — через один и тот же интерфейс мы контактируем либо с логгером, либо с заглушкой. Чем это определяется, если, как вы утверждаете, не линкером и не динамически(рантайм)?

Composition root, вызываемый в продуктиве, всегда подставляет продуктивную реализацию логгера… В тестировании composition root находится прямо внутри теста

Т.е. все-таки есть внешний модуль/сущность, которая определяет связь между тестируемым модулем и логгером либо заглушкой. Причем эта связь различна в тестовой версии и в релизе?
У вас есть модуль А, который вызывает Б или В:
А-(1)>Б
А-(2)>В
если вы говорите, что связи (1) и (2) не устанавливаются во время рантайма, значит они установлены статически при сборке, соответственно, комплексы различны — либо А+Б в релизе, либо А+В в тестовой среде.
В чем тогда разница по сравнению с ситуацией, когда связь А-> прописана в виде прототипа, объектный файл модуля А бинарно идентичен в тесте и в релизе, но при сборке мы используем объектный модуль либо Б, либо В, причем это задано make файлом?
В таком случае имеет смысл говорить об идентичном поведении, а не бинарно идентичных модулях. Потому что все равно есть код, который решает в зависимости от каких-то условий подставить заглушку или логгер. Здесь мы должны быть уверены не в бинарной идентичности, а в верности подстановки. Ведь суть принципа «идентичности» в том, чтобы получить некоторый блок, который будет при одинаковых воздействиях выдавать одинаковый результат, а у вас есть некоторая внутренняя закрытая логика, которая меняет поведение блока.

И даже это реализуемо в рамках процедурной парадигмы, например при помощи процедурного типа. Или просто прямого перенаправления в другую функцию в случае тестирования — ведь все равно у нас есть код, определяющий, что подставить в ILogger.
Да это ясно, вы не поняли вопроса: в чем разница-то? Тестируемый блок зависит от интерфейса ILogger, но что подставляется — логгер или заглушка — определяется на этапе линковки, верно? Это просто точка стыковки двух блоков, но сама комбинация определяется при сборке, верно? В релиз версии через ModuleUnderTest будет задан реальный логгер, в тестовой сборке — тестовая заглушка. Бинарно идентичными сборки «модуль+логгер» и «модуль+заглушка» не могут быть. А то, что сам «модуль» бинарно идентичен — так он и в процедурной реализации бинарно идентичен. Различается второй блок (логгер vs заглушка), а он и у вас отличается.
Мы, наверно, о разном говорим.
где модель-под-тестированием бинарно идентичен тому

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

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

Более того, вашим продуктом может быть не программа, а программный комплекс. Который полагается на другие части — например, базу данных.
Вот вы протестировали у себя свою программу с СУБД, например, MySQL 5.1.44, в требованиях у вас указано, что ПО работает с версией не ниже 5.1.44.
А у клиента — 5.1.45 и в ней, например, исправлен какой-то баг, который не вылазил у вас в предыдущей версии. И так можно продолжать долго — вплоть до ОС и самого железа. Вы не можете быть уверены, что ваш продукт будет работать у клиента так же, если только вы не отдадите ему компьютер тестировщика со всеми настройками. Это все — допущения — о том, что сторонние части будут работать так же. В ситуации с блоками программы та же логика, можно рассматривать те два блока из примера как два компонента программного комплекса.

Откуда я знаю, что еще подменила условная компиляция?

Это только вариант. Система тестирования может подменять весь модуль, например, просто редирект на другую директорию, где хранится модуль с тестовыми заглушками.

Ведь так или иначе — будь то ООП или голый машинный код без всякой парадигмы вообще — для перехвата вызовов (в логгер) вам придется подменять его заглушками (неважно как технически — будь то танцы с условной компиляцией или подстановка другого класса через интерфейс/наследование) для компиляции тестовой сборки.
P.S. Было бы интересно узнать, для чего по Вашему мнению нужны классы/объекты (или ООП).

Говорить об объектах можно тогда, когда у нас есть некоторое внутреннее состояние, которое изменяется в течение времени жизни объекта.

в том числе неявным контрактом того куска кода, где они используются. А если у меня нет доступа к исходникам?

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

Вам придется, если надо работать с другим типом данных.

Я хочу прямо в конструкторе видеть все зависимости класса-потребителя

В примере выше потребитель у функции только один — func1. Входные и выходные параметры известны, вот все ваши зависимости. В рамках этого примера покажите, пожалуйста, почему функционал positive надо реализовывать при помощи класса.

я хочу нормальное переиспользование кода за счет SRP

Так не используется positive нигде более. SRP соблюден.

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

Не в интерфейсах дело. Вы доказываете примерно такую точку зрения: «Существуют ситуации, когда нам требуется сделать X, Y и Z, наиболее просто и полно это реализуется в рамках ООП парадигмы».
Тогда как я говорил «Существуют ситуации, когда нам не треубется делать X, Y и Z, наиболее просто это реализуется в рамках процедурной парадигмы, ООП же излишне». Вы воююте с ветряной мельницей — так, будто я утверждал никчемность ООП, тогда как моя позиция была «создавать класс каждый раз не всегда нужно и полезно».
P.S. Чистая функция, пишущая в файл (в вашем исходном примере, с которого все началось)? Вы издеваетесь? :)

Э, нет, началось не с записи в файл. Там было 3 разных случая, только в одном шла работа с файлом. И пример, приведенный выше — просто ящик: data in — data out.

Прямая зависомость кода от юзер-функции в другом модуле — это тоже неявный контракт.

Так разница-то — вы зависите от интерфейса в другом модуле, или от процедуры-обертки?
Естественно, и это — нормально, обычная практика.
Если вы хотите сказать, мол, мы тестируем не то — так я не понимаю — какая разница — вы через ООП подставляете заглушку-отладочный логер, который фиксирует вызов метода, или через условнуб компиляцию — другу процедуру-заглушку-отладочный логгер?
Говорить про различные бинарные сборки ни к чему — вы тестируете «черными ящиками». Вот тестируете процедуру, как описали, которая вызывает логер. Значит, считаем логгер рабочим черным ящиком и сейчас конкреткно «зажимаем в тиски» юнит-теста то, что логгер вызывает, а не его самого. Так что без разницы, та же самая сборка, или нет. Все равно сделать т.н. полный тест невозможно из-за обилия сочетаний.
Кому что удобнее. Все равно вам придется создавать класс-заглушку и подставлять либо его, либо рабочий класс через интерфейс учитывая вариант сборки.

Да и это не тот случай, как я уже написал. Я же не говорю «нет таких ситуаций, когда использование ООП оправдано», я говорю, что есть случаи, когда не оправдано.
Нет, на шарпе не пишу, к сожалению. Ваша задача сводится к тому, чтобы зафиксировать факт вызова этих двух процедур, выполняюших логирование. Например, вы можете использовать conditionals для перехвата. Или компиляции другого, отладочного модуля-заглушки с теми де процедурами. Или библиотеку, перехватывающую вызов любой процедуры, втч встроенной в язык (у нас в проекте используется такая).
Но это все не важно, потому что я говорил о другом, тривиальном использовании (см. пример кода, пусть утрированный, но все же, ниже).
Без конкретных случаев говорить не о чем, слишком общая фраза. Да и как писал выше: если процедура — обертка, то это тот же интерфейс по сути.
Даже и в этом случае, с другим модулем: в чем разница между зависимостью от интерфейса класса, прописанном в модуле X и зависимостью от реализации в виде процедуры в классе Y? Учитывая, что процедура перегружаема в зависимости от типа данных, с которыми она работает и то, что она может быть просто однострочной оберткой (тот же интерфейс по сути)?
Класс — чтобы объявить его реализующим интерфейс, и в потребитель передать именно этот интерфейс.

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

Простой пример: у вас функция, реализующая некоторый алгоритм. Совершенно неважно, это функция-член класса, или отдельно стоящая функция. Часть ее, являющуюся логически законченным блоком, выделяется в отдельную функцию для повышения читаемости. Данная функция работает с одним и тем же типом данных, ее функционал никогда не будет использован повторно, более того она не вызывается не только из других модулей, но даже из других процедур. Она решает одну конкретную задачу. Попробуйте, покажите, зачем здесь нужен абстрактный интерфейс.
Упрощенно: у вас есть кусок кода (простой тип данных использую условно, just to make a point):
bool func1(){

if(a > 0) {
действие1
}

}

он преобразован в функции:

bool positive(int b){
return (b > 0);
}

bool func1(){

if(positive(a)){
действие1
}

}

Покажите, почему функцию positive следует реализовать в парадигме ООП «чтобы передать потребителю — функции func1 — интерфейс». Без додумывания вида «а что если потом нам потребуется работать не только с int, мы тогда сможем перегрузить». Постановка задачи именно такова — тип данных — int — зависит от внутренней реализации функции func1 и меняться не будет. Даже если и будет — positive тоже перегружаема, без ООП.
Рефакторинг позволяет же привлекать объективные критерии, вроде соответствия SOLID.

Ни на секунду не отвергая и не критикуя SOLID, хочу лишь заметить, что соответствие той или иной концепции может быть объективным критерием только если верность концепции объективна (доказуема).
Тем более, что выше автор говорит "… когда пришлось менять поведение".

Это о другом случае, там ряд примеров.

ибо в каждом отдельно взятом модуле делается что-то одно и спорить там не о чем.

Так то модуль, а то — класс, я о другом говорил.

Как раз специализированная задача и должна быть вынесена в отдельный класс. С единственной целью избавить класс-потребитель от знания деталей этой специализации.


Зачем? «чтобы был класс»? Это ООП ради ООП.
Такую задачу можно вынести в отдельную процедуру в отдельном модуле. Которая будет тестироваться отдельно и далее по тексту.

Просто создавать класс-обертку с 1 статическим методом или пустыми конструктором-деструктором + необходимость объект конструировать там, где нам просто надо 1 раз вызвать процедуру — бессмыслица. Это бездумное следование мантрам.

Уже много лет дубликация не является единственным, и даже основным, поводом выделения юнита.

Я разве спорю? Вот есть отдельная процедура (пусть даже разбитая еще на части) в отдельном модуле.
Класс зачем?

Если хотите, могу подробно расписать, почему это так, без упоминания абрревиатур и ссылок на Гугл.

Вы не знаете исходных данных задачи, поэтому не можете полностью судить о правоте. Я лишь привел пример, на самом деле, там была работа с файлом, но это был не лог. Я просто упростил для изложения здесь.
Может, но там не тот случай, это был именно «лог в файл», который не перенаправлялся бы ни в какой другой носитель, да и сама реализация класса не позволяла переопределеить носитель, это была по сути обертка, в которой процедуру log разодрали на части, неудачно смешав с инициализацией самого класса. Просто класс ради класса. Если у нас есть процедурка, которая работает с типом данных А, и выдает типы Б, В, вовсе необязательно создавать специальный класс-обертку из одного метода просто «чтобы было ООП».

Информация

В рейтинге
Не участвует
Зарегистрирован
Активность