Инкапсуляция
Моё мнение следующее. Понятно, что инкапсуляция полезна для данных - каждый объект должен сам разбираться со своими данными. Но приватные методы не нужны, ибо их неудобно тестировать, они показывают недостатки дизайна. Лучше передать эти методы в какой-либо агрегированный объект.
Но и с данными должна быть некая условная инкапсуляция. То есть должен быть не полный запрет, а генерирование предупреждений компилятором (интерпретатором) в случае их использования. Такой подход может пригодиться при рефакторингах.
Не знаю, почему создаётся такое впечатление. На самом деле пытаюсь в разговоре осознать некоторые мысли, которые пришли мне в голову, когда я читал вот эту книгу:
http://www.williamspublishing.com/Books/978-5-8459-1530-6.html
В ней не написано прямо так, как я говорю, но её текст навёл меня на такие мысли про инкапсуляцию. При том, что в книге все примеры на языках со статической типизацией.
А далее я вспомнил, что в Smalltalk все данные являются частными, а все методы публичными. Ну и понесло...
Зачем городить класс там, где можно обойтись методом, тем более что у него большой потенциал повторного использования? Велика вероятность, что и другим классам могут потребоваться функции calcArithmeticMean, calcSum. Более того, sum (accumulate) есть в стандартных библиотеках известных мне языков, что подтверждает мысль о повторном использовании.
Нормальный пример, только ты не пытаешься рассмотреть его локально. И не поняв сути примера, говоришь удачный он или нет.
Ты зачем то пытаешься представить, что вокруг этого класса. Зачем такой класс существует - в данном случае не важно. Вопрос был: Класс стал "выполнять не свои обязанности"?
Изменились обязанности этого класса между примерами, если его реализация осталась неизменной?
Ок я изменю пример.
Так было и так надо, не надо пытаться понять место этого класса во Вселенной:
{
some_do;
some_do;
some_do;
some_do;
some_do;
}
а теперь чисто для своих внутренних целей, я сделал так:
{
some_do;
some_do;
private_method();
}
void Class::private_method()
{
some_do;
some_do;
some_do;
}
а теперь вопрос: Класс стал "выполнять не свои обязанности"? Если поведение и реализация не изменились?
А причина в том, что кто-то уделяет этому чересчур много внимания и сил.
Я бы сказал, это какая-то стадия развития программиста, когда он пытается следовать всем заветам TDD буквально. Т.е. буквально писать сначала тест, потом реализацию, всегда так и без исключений.
Будьте гибче и проще (это я ни к кому конкретно не обращаюсь).
Смыслов от тестов на практике два явных (есть и еще).
1) Убеждение, что код - bug-free.
2) Убеждение, что интерфейс кода юзабелен настолько, чтобы его собственный автор мог написать простейшего клиента (а тест есть не что иное, как простейший клиент метода, класса или модуля) быстро и без проблем.
Отсюда что вытекает? Мои мысли применимы к большинству общих случаев при разработке в коммерческих компаниях, и не касаются всякого embedded-a, управляющего критическим оборудованием, и прочей экзотики.
- Время на написание тестов всегда ограничено. Очевидно, если вы тратите два дня на написание кода, и еще два дня на написание юнит-теста, вы пишете свои тесты неправильно. Когда приблизиться дедлайн, вы придете к тому, что на тесты все будут забивать и писать функциональность.
- Покрытие разумно-простыми базовыми тестами всей системы лучше, чем идеальное покрытие одного куска и отсутствия тестов для трех четвертей системы вовсе.
- Тесты должны быть компактными по размеру, быстрыми в написании и модификации. В частности, тесты по принципу белого ящика по определению сложны в поддержке. Если требования меняются часто, вы будете разбираться в падениях тестов половину своего времени, пока либо не начнете писать более простые и общие тесты, либо не начнете комментировать их с пометкой "TO DO".
- Тесты должны выполнять быстро. Если полный набор тестов работает около часа локально (+ 20 минут на clean build кода), никто не будет прогонять их все перед каждым коммитом. Отсюда вывод, что надо писать больше простых тестов, и с настороженностью относиться к идее неумеренно плодить функциональные тесты, которые требует для работы развертывания тестовой схемы базы данных, или чего-то подобного. Имеет смысл использовать моки / DI для упрощения и ускорения тестов.
Или же, функциональные тесты, если их много, должны быть вынесены в какую-то отдельную процедуру функционального тестирования, которая работает скажем каждую ночь на билд-сервере.
- Тесты мало что стоят без работающей и поддерживаемой в исправном состоянии системы непрерывной интеграции.
- Есть места, которые тестировать юнит-тестами сложно. Пример - UI, код связывающий между собой слои бизнес-логики и БД. Там часто нужны или специальные тесты написанные с помощью TestComplete, Selenium или чего-то подобного для UI, или спец фреймворки для моделирования "тестовой сцены" в базе данных.
А насчет динамических языков. Я внимательно слежу за развитием Groovy (и подумываю стать контрибьютором), так вот, в последнее время в комьюнити разработчиков его наметилась тенденция, что пишутся инструменты для эвристического статического (времени компиляции) анализа кода, написанного на динамическом языке :) Примеры -
http://codenarc.sourceforge.net/
http://www.groovylint.com/
Наводит на мысль, что в динамических языках проблема тестов вызывает больше фрустрации :)
Так было и так надо, не надо пытаться понять место этого класса во Вселенной
Если я не буду пытаться понять "место класса во вселенной", то получу обыкновенное процедурное программирование, замаскированное под объектно-ориентированное. Вот такое у меня странное понимание ООП.
http://codenarc.sourceforge.net/
http://www.groovylint.com/
Наводит на мысль, что в динамических языках проблема тестов вызывает больше фрустрации :)
И в Python есть подобные средства. Но есть подозрения, что эти инструменты разрабатывают люди, которые до этого программировали на языках со статической типизацией :)
С другой стороны, лишняя проверка не мешает.
А если говорить на счёт тестов, то в языках с динамической типизацией обычно проще использовать mock-объекты и fake-объекты, то есть меньше препятствий для реализации тестирования.
Не совсем пустышкой. Он будет реализовывать свои открытые методы. Классы внутренних объектов - свои открытые методы, их внутренние объекты - свои и т.д. пока не дойдем до класса, у которого останутся только простые внутренние объекты.
Ого... все ещё хуже... :) Т.е. такая матрешка. А где будут храниться данные? В каком классе?
И ещё не понятно, где заканчиваются открытая реализация и начинается закрытая?
Я уже приводил пример: если я вместо макарон в открытом методе сделаю несколько вызовов приватных методов, уже надо разносить такой код по классам? Зачем?
Не совсем так. Класс-помощник будет независим от главного.
Тогда возвращаемся к уже заданному вопросу, а как этот клас "помощник" будет оперировать с приватными данными главного класса?
Они будет передаваться в конструкторе? Кем будут передаваться? Конструктором главного класса? А где будут храниться в вспомогательном классе в его личных полях? Да хоть пусть и по ссылкам.
Т.е. вспомогательный класс жестко зависит от основного, т.к. оперирует его данными. А основной класс зависит от вспомогательного, т.к. тот оперирует его данными. Думаю, не надо объяснять, к чему ведет такое перекрестное связывание?
А при тестах что будем тестировать? Основной класс или вспомогательный?
И тот и другой? Причем в отрыве?
Тогда получается, что класс помощник - самостоятельный класс, только зависит от внешних ресурсов, содержит методы манипуляции не своими данными. А как же тогда инкапсуляция? Или она действительна только для белых, а не для негров-помощников?
Ок, допустим у нас есть класс который содержит один вид данных. И к нему есть этот злосчастный помощник. Тесты помощника отлично работают.
А теперь мы изменяем реализацию основного класса, меняем публичные методы или типы данных. Тесты неизмененного помощника продолжают работать. Они же не связаны с тестами основного класса. Тесты основного класса так же работают, они же не охватывают функциональность помощника. А вот в реальности программа сломана, т.к. изменения основного класса не учтены в помощнике и соотв-но в его тестах.
Или при изменении основного класса я должен изменить помощьника и его тесты? А как это зафиксировать? Видимо, только документации.
И как же "Класс-помощник будет независим от главного"?
Ок, пусть тогда тесты основного класса охватывают и тестирование вспомогательного, тогда мы обнаружим ошибку рассинхронизации изменений в классах. А нафига тогда вспомогательный класс с его тестами, если нам все равно надо писать тесты для основного класса с учетом покрытия вспомогательного?
Юнит-тесты ради которых все и затевалось потеряли смысл, т.к. они уже тестируют не юниты, а какие-то связки.
У него будет потенциал использования в другом классе.
А нафига? Вот когда понадобиться в другом классе, тогда и вынесем. Только вынесем с учетом инкапсуляуции, т.е. соберем воедино поля и методы, а не разнесем их по системе.
Но при чем тут тестирование?
Не всегда есть время на такое "развёртывание" в маленьких проектах, но в больших проектах оно должно экономить время, за счёт того, что каждый класс имеет одну обязанность, то есть, может использоваться более часто. Это устранит дублирование.
Да такой код будет совершенно невозможно поддерживать.
Допустим возьмем этих 100 классов представим, что они бьются ещё класса на 4, те ещё на парочку... м-да...
А для чего все это? Для тестов? А нет... " Это устранит дублирование"
Да и при чем тут "дублирование"? Какое отношение имеет дублирование к тестированию приватных методов?
Для того, чтобы использовать код повторно. У нас косвенно выйдет так, что каждый класс пройдёт проверку на альтернативное использование путём использования в тесте.
Погоди, погоди... мы начали с юнит-тестирования, а не с декомпозиции для повторного использования. И тут опять встает впечатление, что:
У меня есть предположение, что у тебя была ситуация с некоторой плохой архитектурой класса. Где, действительно, часть реализации было правильным вынести в отдельную сущность. И у тебя сложилось мнение, что это и есть "универсальная таблЭтка", "серебряная пуля" для приватных членов.
Только в этом случае вынесение реализации было скорее всего делом логичной архитектура, а не необходимостью тестирования.
Т.е. твои выводы не имеют никакого отношения к тестированию привата.
Они имеют отношение к повторному использованию кода. И здесь америки нет - одинаковый код лучше вынести в отдельную сущность.
Но и здесь хочу предостеречь: не надо наворачивать систему основываясь на тотальном переиспользовании кода. Когда это потребуется, тогда и зарефакториться.
Реюз - один из видов оптимизации, а преждевременная реализация, как известно...
Но опять же, это не имеет никакого отношения к тестированию, о котором в начале говорилось.
Не получишь. Как раз наоборот ООП предполагает разбиение вселенной на отдельные законченные фрагменты. :)
Да нет тут никакого холивара на тему TDD. :)
Именно. Но ты не поверишь, сколько людей понимает юнит-тестирование как попытки окружить систему какими то тяжелыми, неуклюжими, ломающимися лесами, за которыми самой системы не видно.
И в Python есть подобные средства. Но есть подозрения, что эти инструменты разрабатывают люди, которые до этого программировали на языках со статической типизацией :)
С другой стороны, лишняя проверка не мешает.
Логично, это попытка преодолеть объективный недостаток динамической типизации.
А если говорить на счёт тестов, то в языках с динамической типизацией обычно проще использовать mock-объекты и fake-объекты, то есть меньше препятствий для реализации тестирования.
Не меньше, Хардкейз об этом уже писал и я с ним во многом согласен. Писать тесты проще потому что кодить в принципе быстрее и проще. А простой код (тесты должны быть простыми, ага) писать на динамических языках вообще просто. Не зря же во многих крупных проектах на Java начинают писать теста на Groovy. А вот тестировать код написанный на динамических языках не проще. Потому что надо проверят то, что в статических проверяет компилятор.
Осмелюсь напомнить что програма пишется не для того чтоб под нее тесты сделать. а для того чтоб ее продать.
Чтоб ее ктото захотел купить она должна быть дешевой и бисто разрабатываться.
Тести нужни для того чтоб ее не поломать если придется ее долго сапортить и изменять.
Если ее придется долго сапортить и изменять то она и без того сложная, мелденно реализируемая и дорогая.
Зачем ее тогда еще больше усложнять тисячами служебних класов? Чтоб было красиво? Вам не кажется что ето не красиво, а смешно?
Какая матрешка? Если уж нужна метафора, то тут будет некое дерево: главный объект оперирует объектами поменьше, те - ещё меньшими и т.д. до элементарных объектов. Аналогично ствол дерева разделяется на ветви. Данные главного объекта - это те самые объекты-"помощники" и есть. Зачем же главному классу хранить какие-то другие данные?
Green, если тебе это не очевидно, то может это и к лучшему. Если ты не видишь некорректности своих примеров, то значит мы дошли до некоторых догм и дальше мне нет смысла спорить. Возможно, я поумнею когда-нибудь и до меня дойдёт мудрость твоих доводов, но пока это не так.
Зачем? В языках со строгой динамической типизацией будет достаточно просто запустить код на выполнение. Язык сам проверит.
Смешно, конечно. Если воспринимать классы как модули-мутанты, то все эти разговоры смешны.
Выкладывай кратенький код, в котором будет главный класс с публичными методами и частными полями, и класс-помощник.
Смешно, конечно. Если воспринимать классы как модули-мутанты, то все эти разговоры смешны.
Чтото я не до конца понял что такое модули-мутанты?
Как по мне то основная задача класа - отображение реальных сущостей в логике програмы. Скажем, это некие роботники сотрудники фирмы "програма".
Если я не могу представить себе какую именно целосную задачу должен виполнять клас я не вижу необходимости его выделять или создавать. Если я не могу выделить один клас из общей кучи неструктуризированого функционала - значит я плохо представляю себе какой должна бисть структура програмы или не знаком с предметной областю или (что случается крайне редко) я столкнулся с очень специфической монолитной предметной областью, но примера такой области я вот так сходу и представить не могу.
Теперь представим себе некий класс. Пускай он должен заниматься парсингом некого формата данных. Как по мне - вполне отдельная и логичная задача которую можно и нужно предоставить отдельному роботнику.
Как вы знаете, парсинг можно проводить с помощю некого конечного автомата с множеством состояний и правилами перехода с одного состояния в другое на основании входных сигналов (коими в данном случае будут байты входного потока).
Соответственно в зависимости от состояния парсера и входного байта необходимо выполнять некие действия. Их удобно групировать в методы.
Внимание вопрос. Нужно ли делать эти методы приватными или публичными в другом классе.
Если второе - то какую именно отдельную задачу будет выполнять второй класс? Можно ли представить его как некого работника или как некую сущность предметной области? Как скрыть сей второй класс от пользователя самого парсера?
Отнюдь. Язык не сможет найти такие мелкие ошибки, как плюс вместо минуса в формуле. И еще миллион других.
Смешно, конечно. Если воспринимать классы как модули-мутанты, то все эти разговоры смешны.
Мне смешны они по другой причине. У меня ощущение (субъективное, разумеется), что ты воспринимаешь некоторые вещи чересчур академично, что ли. Т.е. слишком буквально воспринимаешь некоторые положения популярных концепций и не рассматриваешь целесообразность компромиссов тут и там.
Мне случалось по полгода писать проект без нормальных тестов вовсе. По некоторым причинам. А случалось тратить полтора месяца на написание фреймворка для реалистичного тестирование планировщиков (включая моделирование данных в базе). И для всего этого были определенные бизнес- и политические причины, а вовсе не только моя лень и ограниченность:)
Матрешка - частный случай дерева.
Я так и не понимаю ДЛЯ ЧЕГО УСЛОЖНЯТЬ простые вещи?
Green, если тебе это не очевидно, то может это и к лучшему. Если ты не видишь некорректности своих примеров, то значит мы дошли до некоторых догм и дальше мне нет смысла спорить. Возможно, я поумнею когда-нибудь и до меня дойдёт мудрость твоих доводов, но пока это не так.
Что ж, будем ждать... :)
Только ты не ответил на мои вопросы про декомпозицию и её связи с юнит-тестированием.
Возможно, ответив на них (в первую очередь себе, а не мне) момент понимания мудрости приблизится... :)
А я щитаю применение таких вот концепций для улутшения тестируемости кода ни чем инным как стрельбой по воробям из пушки.
Если вам по каким-то существенным причинам нужно подменять реализацию неких интерфейсов в рантайме или в зависимости от запросов разных потребителей одной и той же системы - это одно. Если вы делаете это для того чтоб наполнить свойпроект моками ..... не уверен что это адеквато и целесообразно.
Теперь представим себе что нам кровь с носа нужно тестировать приватные методы классов.
Я уже давно не помню ни С++ ни Паскаля ни многого другого но ..... Разве слово "френдс" не может помочь? Разве нет такого понятия как доступ в пределах одного пакета?
Да на худой конец можно сделать некий препроцессор програмного кода для глобального реплейса слов "прайвет" словами "паблик" перед компиляцией и запуска тестов. Это усложняет процес тестирования, но разве это настолько важно чтоб позволить себе усложнить продакшен код?
Выкладывай кратенький код, в котором будет главный класс с публичными методами и частными полями, и класс-помощник.
+1
а ещё лучше взять уже существующий общеизвестный класс с приватными членами и переделать его в соот-вии с предлагаемой концепцией.
Все пути исполнения программы? ;)
а ещё лучше взять уже существующий общеизвестный класс с приватными членами и переделать его в соот-вии с предлагаемой концепцией.
Есть мысль, что отднократно используемые приватные методы в классах возникают в языках, которые не поддерживают локальных функций. В хорошо написанном коде на Nemerle однократно используемых приватных методов просто нет.
{
class Insider()
public int DoSomethig()
{
//do something
};
}
когда можно написать сразу
{
private int DoSomethig()
{
//do something
};
}
Какое реальное премущество первого кода перед вторым?
Иногда даже в ЯП с поддержкой локальных функций, просто, визуально приятнее вынести часть кода куда-нибудь по-дальше (в приватный метод), чтоб не засорять общую линию кода.
Но чаще, конечно, приватные методы получаются используемые неоднократно, но это отнюдь не делает их самостоятельными, т.к. они используются в контексте своего класса.
Ты лучше меня знаком с концепциями языков программирования и компиляторов :) Скажи - в языках типа Nemerle в простой программе, можно построить такую систему типов, которая делает так, что если код компилируется, то он корректен? Я почему-то думаю что нет.
Компиляция кода в строих статически типизированных языках исключает ошибки связанные с типами. В динамически типизированных языках чтобы исключить эти ошибки нужно покрыть тестами все пути исполнения программы. И, кстати, для статически типизированных программ становятся возможными такие инструменты анализа кода как PEX.
Выкладывай кратенький код, в котором будет главный класс с публичными методами и частными полями, и класс-помощник.
а ещё лучше взять уже существующий общеизвестный класс с приватными членами и переделать его в соот-вии с предлагаемой концепцией.
Это не будет чистый эксперимент - я могу подобрать примеры подтверждающие мою идею. Будет вернее, если я попытаюсь проделать такой фокус с классами, которые вы предложите. Думаю, это вам тоже ничего не докажет, но для меня будет очередным упражнением :)
Ну так разве это не относится и к языкам со статической типизацией?
Вот это уже правильный вопрос. Могу рассмотреть подробнее.
Пример 1.
do_something()
else:
do_armageddon()
Тут нет разницы.
Пример 2.
do_something()
else:
do_something_with_int(my_string)
Пока a != 99 такую ситуацию пропустят все интерпретируемые языки (пусть даже и со статической типизацией), а также компилируемые языки с нестрогой статической типизацией (те пропустят и когда a == 99). Такую ситуацию выловит только язык со строгой статической типизацией. Я знаю hardcase, что ты это знаешь, но разбирал для остальных.
Однако, разве это говорит о том, что в языках со строгой статической типизацией не надо проверять случай, когда a == 99? А вдруг функция do_something_with_int() запустит какой-нибудь нехороший процесс?
Мне тоже непонятно, потому что не знаю язык на котором твои примеры. Ещё хуже того, я не понимаю, почему некоторые путают объекты и классы.
Странно, что ты не можешь ответить на конкретно поставленные вопросы. Создается впечатление, что они идут в разрез с твоей идеей, поэтому ты их и игнорируешь.
Я повторю вопросы:
1) как этот клас "помощник" будет оперировать с приватными данными главного класса? где будут храниться во вспомогательном классе данные основного класса?
2) Они будет передаваться в конструкторе? Кем будут передаваться? Конструктором главного класса?
3) при тестах что будем тестировать? Основной класс или вспомогательный?
4) при изменении основного класса я должен изменить помощника и его тесты? А как это зафиксировать?
5) Какое отношение имеет дублирование к тестированию приватных методов?
Желательно представить любой законченный пример с тестами (пусть примитивный), чтоб понять об одном мы говорим или нет. Пофиг до чистоты эксперимента. Только пример не должен представлять логичную и явную декомпозицию, очевидную ещё на этапе проектирования. Хотелось бы увидеть вполне логичный класс с приватными методами, а потом преобразование этого класса в "матрешку" и появление соотв. тестов.
Пример должен показывать в т.ч. где будут содержаться приватные данные и как обрабатываться.
Думаю, пример лучше привести тебе, т.к. ты же представляешь о чем ведешь речь, а значит сможешь и показать.
Свой вариант классов для эксперимента предложу позднее, если необходимость этого ещё останется.
Во первых, не класс, а объект. Поэтому одним из приватных данных будет этот объект. Получается, что он сам себя хранит. Но это только разновидность хранения.
Кроме того, что я указал в первом пункте, данные могут передаваться в конструкторе или других методах. В этом случае ссылка на них может даже не сохраняться в агрегированном объекте дольше выполнения конкретного метода.
На этот вопрос у меня пока нет чёткого ответа. Потому дам предварительный.
При разработке - оба. Потом можно будет удалить дублирование, если оно появится.
Это зависит от изменений. Вопрос из ряда "если я поменяю код в одном месте программы, то надо ли мне менять в другом". Слишком абстрактно.
Приватные методы являются индикатором того, что класс выполняет более одной обязанности (возможно, не всегда, но об этом ниже). В этом случае желательно разделить класс на меньшие, каждый из которых будет выполнять по одной. Так увеличим вероятность использования этих обязанностей в других классах побольше без написания повторяющегося кода.
Для подбора хорошего примера может потребоваться время - день-два, а может и неделя. Буду думать над этим. Пока же использую твой первый пример. Чтобы он не выглядел идиотским, допустим, что он разрабатывается для языка, в библиотеке которого нет функции типа sum и нет функций вне классов.
Было:
{
private:
vector<double> _items;
float _calcSum()
{
double sum = 0;
for (size_t i=0; i < _items.size(); ++i)
{
sum += _items;
}
return sum;
}
public:
void push(double d)
{
_items.push_back(d);
}
float calcArithmeticMean()
{
return _calcSum() / _items.size();
}
};
Стало:
{
public:
float calcSum(vector<double> &items)
{
double sum = 0;
for (size_t i=0; i < items.size(); ++i)
{
sum += items;
}
return sum;
}
};
class SomeClass
{
private:
vector<double> _items;
public:
void push(double d)
{
_items.push_back(d);
}
float calcArithmeticMean()
{
return Summator().calcSum(_items) / _items.size();
}
};
Казалось бы, я внёс лишнюю сущность. Но теперь я смогу напрямую протестировать функцию calcSum на то, что она корректно считает сумму (а не предполагать, что она работает через тестирование calcArithmeticMean) и могу использовать объект класса Summator в другом классе. Тестов тут не привожу, но подразумевается, что прямо тестируются функции calcSum, calcArithmeticMean, косвенно push.
Будет интересно.
Сегодня я пытался найти примеры, где приватные методы могут пригодиться. Несколько нашёл. В основном, используется скрытие конструкторов (в том числе конструкторов копирования) и деструкторов, а также перекрытие методов класса-предка. Первое используется, например, в синглтонах, вторые используются там, где, возможно, было бы проще использовать композицию.
{
private:
class ChildSomeClass
{
........
};
.............
};
Во первых, не класс, а объект. Поэтому одним из приватных данных будет этот объект. Получается, что он сам себя хранит. Но это только разновидность хранения.
Кроме того, что я указал в первом пункте, данные могут передаваться в конструкторе или других методах. В этом случае ссылка на них может даже не сохраняться в агрегированном объекте дольше выполнения конкретного метода.
Все же в рамках темы - класс, а не объект, т.к. ты в примере создаешь вспомогательный класс и перемещаешь в него метод из основного, а объект он лишь в процессе использования.
Т.е. ты предлагаешь отказаться от инкапсулирования?
Данные будут храниться в одном месте, а обрабатываться в другом.
Т.е. возвращаемся к процедурному программированию?
Это зависит от изменений. Вопрос из ряда "если я поменяю код в одном месте программы, то надо ли мне менять в другом". Слишком абстрактно.
Вовсе не абстрактно.
Модульные тесты фиксируют интерфейс и поведение объекта, давая т.о. возможность изменять реализацию. Введя матрешечность тесты начинают мешать изменению реализации. Как пример, можно создать циклическую очередь на основе связанного списка, а можно на основе пула (вектора или массива). Скорее всего у такого класса будут приватные методы по добавлению/удалению элементов из контейнера. Если эти методы будут вынесены в отдельный класс, то как уже говорил, изменение в одном классе будут тянуть за собой изменения в другом, а сл-но потребуются тесты, которые охватывают оба класса.
Приватные методы являются индикатором того, что класс выполняет более одной обязанности (возможно, не всегда, но об этом ниже).
Ну вот откуда такая аксиома? ИМХО вся загвоздка именно в этом, что ты принял это утверждение за аксиому и доказываешь далее, что сторонние обязанности можно вынести в отдельную сущность.
В этом случае желательно разделить класс на меньшие, каждый из которых будет выполнять по одной. Так увеличим вероятность использования этих обязанностей в других классах побольше без написания повторяющегося кода.
А зачем? Ещё раз: зачем делать то, что скорее всего никогда не понадобиться?
Для подбора хорошего примера может потребоваться время - день-два, а может и неделя. Буду думать над этим. Пока же использую твой первый пример. Чтобы он не выглядел идиотским, допустим, что он разрабатывается для языка, в библиотеке которого нет функции типа sum и нет функций вне классов.
Да не идиотский пример, а примитивный. Идиотским, наверное, выглядит и пример строкового класса или т.п., т.к. такие классы уже существуют "в библиотеке"?
Да и "функций вне классов" нет, т.к. мы разговариваем об инкапсуляции.
Стало:
{
public:
float calcSum(vector<double> &items)
{
double sum = 0;
for (size_t i=0; i < items.size(); ++i)
{
sum += items;
}
return sum;
}
};
Казалось бы, я внёс лишнюю сущность. Но теперь я смогу напрямую протестировать функцию calcSum на то, что она корректно считает сумму (а не предполагать, что она работает через тестирование calcArithmeticMean) и могу использовать объект класса Summator в другом классе. Тестов тут не привожу, но подразумевается, что прямо тестируются функции calcSum, calcArithmeticMean, косвенно push.
Ну, во-первых, ты изменил мой пример, нагло подогнав его под свои цели. :D
В моем примере было иначе:
а ведь могло быть и ещё чуть сложнее
sum += _item.value;
Во-вторых, а если бы ф-ция calcSum использовала не один а с десяток приватных членов класса?
Передавал бы остальные так же? И надеялся бы, что сможешь использовать его в другом месте?
А если _items - это не вектор, а массив или список (заметь, при этом интерфейс основного класса не изменится), сколько придется изменить классов, тестов и т.п.?
Тут есть один вопрос - как проверить скрытые методы? Например, мы предполагаем, что в скрытых методах есть ошибка. Можно их на время открыть, например, и проверить временными тестами (а потом благополучно забыть закрыть). Можно скопировать в отдельный маленький проект и там протестировать (и при каждом сомнительном изменении там проверять). А можно использовать предложенный мною принцип (точнее не мною, а создателями Smalltalk).
Проверять всё не обязательно, да и невозможно. Но желательно иметь возможность протестировать любой конкретный метод, не меняя системы.
Нет. Предлагаю изменить характер инкапсуляции.
Т.е. возвращаемся к процедурному программированию?
Опять нет. Данные будут обрабатываться тем, у кого обязанность обрабатывать, и храниться в том, у кого обязанность хранить. Это может быть и один объект.
Есть такое мнение: "Тестирование черного ящика (когда мы намеренно игнорируем реализацию) обладает рядом преимуществ. Если мы игнорируем код, мы наблюдаем иную систему ценностей: тесты сами по себе предоставляют для нас ценность. В некоторых случаях это вполне оправданный подход, однако он отличается от TDD". Кент Бек (TDD, Питер, 2003, стр 199).
Я пытаюсь понять помыслы создателей языка SmallTalk, которые сделали все методы открытыми, а данные скрытыми. Неужели разгадка в том, что SmallTalk - это процедурный ЯП?
Почему не понадобится? Если структура состоит из отдельных деталей, то увеличивается вероятность, что часть деталей может пригодится где-то ещё. Если у детали одна обязанность, то больше вероятности, что её можно использовать где-то ещё.
Но это бонус, конечно. Основное преимущество в том, что можно протестировать методы всех классов, не нарушая инкапсуляцию крупных объектов, собранных из экземпляров этих классов.
Ок. Не буду трогать святое. Однако не вижу, почему я подогнал "нагло". Сделал код конкретным, вот и всё :)
Передавал бы остальные так же? И надеялся бы, что сможешь использовать его в другом месте?
"Десяток приватных членов класса" обычно показывает, что класс неприлично разросся. Особенно, если все их надо использовать в одном приватном методе. В большинстве случаев их можно сгруппировать.
А в чём тут преимущество скрытого метода? В том, что его можно не проверять? Авось сработает?
Вот поэтому я всю тему и объясняю каждому, что речь не о вложенных классах. Понятия не имею почему некоторые думают, что агрегированный объект - это вложенный класс.
А зачем их проверять? Я одного не могу понять, зачем тестировать "кишки"? Нужно тестировать интерфейс класса - работать-то приходится именно через него.
В общем, я на этот вопрос отвечал в предыдущем сообщении, цитаты даже приводил... Но могу ещё раз.
Протестировать всё невозможно - об этом у Макконнелла доказывается. Поэтому есть вероятность того, что при прохождении всех тестов интерфейса останутся ошибки, которые будут выявлены при использовании программы. Как найти код, который вызывает эти ошибки? Может у тебя на выходе то, что требуется, а внутренний код вызывает какой-нибудь левый разрушительный метод? Думаю, тут поможет возможность потестировать "кишки".
Кроме того, подход "чёрного ящика" противоречит классическому TDD (смотри цитату, смотри книгу). Но можно конечно использовать и другую систему.
А что классический TDD говорит о локальных функциях? Их както тестировать надо?
Проверять всё не обязательно, да и невозможно. Но желательно иметь возможность протестировать любой конкретный метод, не меняя системы.
Ты рассуждаешь, что тестирование проще с паблик методами у агрегированного объекта, но покажи, как в готовом проекте тебе легче будет тестировать кишки классов.
Объект все равно приват и дотянуться до него тоже не так то просто, я бы даже сказал, так же трудно, как и до приватного метода. Будешь создавать отдельную песочницу для имитаций, или сборки, где появятся тесты отдельного агрегированного объекта? Тогда чем этот подход отличается от сборок, которые тестируют приватные методы самого класса? Можно банально для каждого класса ввести паблик функцию "test", а там уже устраивать операции над внутренностями чего и кого угодно. Потом одну функцию убираем/комментируем/делаем приватной и все работает как часы.
Чем отличается такой подход от твоего? Один избыточный (ли?) элемент в классе, против твоих конструкторов/деструкторов, сжирающих 70% процессора (цифра среднепотолочная, сильно не ругаться, используется для устрашения :rolleyes:).
Я пытаюсь понять помыслы создателей языка SmallTalk, которые сделали все методы открытыми, а данные скрытыми. Неужели разгадка в том, что SmallTalk - это процедурный ЯП?
Лучше попытайся понять помыслы создателей BrainFuck'а. Просто люди создают то, что удобно им, и им кажется правильным. Это не истина в последней инстанции. Если у авторов есть свои доводы, то лучше спросить непосредственно авторов, а не пытаться понять их через книжки, там есть другое мнение, писателя книжки.
Страус Труп не хотел того, что получилось сейчас. Он, когда создавал классы и ООП пытался упростить программирование именно мат.моделей, где обычных структур явно не хватало. То, как все его идеи были извращены и доведены до абсурда можно легко посмотреть в C#, когда на все есть своя сущность, даже если логикой это не продиктовано.
Так зачем же говорить о замыслах авторов, если только им они известны и пока они сами не скажут (и то, даже если они скажут, то все остальные будут интерпретировать слова, и часть послания будет потеряна) никто ничего не узнает.
Почему не понадобится? Если структура состоит из отдельных деталей, то увеличивается вероятность, что часть деталей может пригодится где-то ещё. Если у детали одна обязанность, то больше вероятности, что её можно использовать где-то ещё.
Но это бонус, конечно. Основное преимущество в том, что можно протестировать методы всех классов, не нарушая инкапсуляцию крупных объектов, собранных из экземпляров этих классов.
Ты предлагаешь жестко связывать два класса. Вероятность будет минимальной. Гораздо больше вероятность использования с помощью наследований и прочих приблуд.
Пример:
Есть агрегированный объект копалка для класса лопата. Ты задал ее работу, показал в какое место надо давить и так далее, все работает.
Назови вероятность повторного использования этого объекта? Для экскаватора подойдет? Или для бура? Или для еще чего? Так что это не бонус, а туфта.Если не согласен, приведи пример, когда есть вероятность повторного использования.
А в чём тут преимущество скрытого метода? В том, что его можно не проверять? Авось сработает?
Я сторонник того, что приватные методы уже давно проверены и ошибки в них исправлены. Трижды. И только потом они попадают в класс.
"Кишки" по-хорошему не должны получать информации больше, чем они получат из публичного интерфейса, посему тестируя публичный интерфейс мы косвенно тестируем детали реализации. Естественно "детали реализации" не должны зависеть от какого-то глобального состояния.
Взять тесткейз из багрепорта и прогнать софту под отладчиком.
Ну мало ли глупостей сделано в разных языках? Лопухнулись создатели SmallTalk'а, сделали все методы открытыми. Увы, до сих пор нет идеального языка.
А всё-таки - условная компиляция не решит эту вопрос? Вставляешь в любой приватный метод любые ассерты, можно сделать публичную оболочку этого метода для тестирования извне, - всё это в конфигурации дебаг. А в релизе ничего этого не будет.
На этот случай у меня нет цитаты :)
Но, думаю, тут зависит от размера её и размера охватывающей функции. При небольшом размере их можно считать одним целым (тут тестируем вместе), при большом - лучше их разделить на несколько методов (тут тестируем раздельно). Такое моё мнение.