Помощь - Поиск - Пользователи - Календарь
Полная версия этой страницы: Размышления о правильном программировании
Форум на CrossPlatform.RU > Курилка > Трёп
Litkevich Yuriy
Тут наткнулся на статью 10 уродских приёмов программирования — Алик Кириллович

Читаю, вроде пока с автором согласен. :)
Kagami
Я помню эту статью еще с хабра. С частью тезисов не согласен :) В основном претензии к призыву отказаться от сеттеров/геттеров. Плюс отказ от рекурсии. Ну и по мелочи кое-где. Больше всего мне не понравилась категоричность автора. Бывают моменты, когда красоту кода придется приносить в жертву производительности.
Litkevich Yuriy
Цитата(Kagami @ 28.3.2010, 19:34) *
Плюс отказ от рекурсии.
это я тоже не понял, но пока до объяснений не дочитал.
А насчёт параллельных масивов с ним согласен. И насчёт переменных объявляемых в паскалевском стиле полностью согласен (хотя сам до не давнего времени так и делал)
kwisp
Цитата(Kagami @ 28.3.2010, 16:34) *
В основном претензии к призыву отказаться от сеттеров/геттеров.

я вот сейчас прочёл тоже согласен с тобой.
а как автор советует поступать если при изменении свойства нужно выполнить какие нибудь действия, это получается и поведение и изменение свойства? получается никак. либо его притензии к сеттерам и геттерам проявляются только в чистом виде, когда ничего делать по изменению свойства не нужно.
я поддерживаю концепцию того чтобы в открытой секции класса хранились только константные данные. Изменяемые данне должны быть надёжно защищены.
+ пропадает возможность сделать сеттеры и геттеры виртуальными при наследовании.
Litkevich Yuriy
Цитата(kwisp @ 28.3.2010, 20:15) *
я вот сейчас прочёл тоже согласен с тобой.
тут есть нюанс. В целом я согласен с автором, те кому приходилось писать на Делфи, перейдя на любой другой инструмент (Билдер не всчёт так как пытается пародировать Делфи) могли испытать чувство не Объектно-Ориентированности.
Дело в том, что в Делфи, есть понятие свойств, поддерживаемых компилятором. В Qt, например, тоже есть.
И когда в Делфи вы пишите:
value = object.sameProperty;
то вы получаете значение свойства объекта, но не напрямую, а через "геттер", а когда вы пишете
object.sameProperty = value;
то вы устанавливаете значение свойства объекта, но не напрямую, а через "сеттер". Т.е. у вас есть возможность определить и "геттер" и "сеттер".

Но форма записи в коде очень интуитивна, и на мой взгляд, по настоящему Объектно-Ориентирована.

Нюанс в том, что автор абстрагируется в размышлении от конкретного языка программирования. Но редко указывает на каком языке дан пример.

Цитата
Свойство — это интерфейс для красивого и безопасного доступа к данным объекта
Iron Bug
автор статьи работает в веб и сильно зациклен на жабаскрипте. отсюда и его странные представляния о коде :) в жабаскрипте нет ни типов данных, ни функций, по большому счёту. отсюда и весь бардак.
но в большинстве языков того, что автор описывает, просто не существует. так что советы неактуальны.

что касается "общих" вопросов, применимых ко всем языкам, то моё мнение такое:

про возвращаемое значение:
имхо - функция ВСЕГДА должна возвращать int с кодом ошибки и точка. результаты - в параметрах.

про геттеры и сеттеры - для крупных проектов со сложными объектами это единственный нормальный и безопасный способ обращения к данным. при этом обеспечивается полная инкапсуляция и наследование.
Litkevich Yuriy
Цитата(Iron Bug @ 29.3.2010, 0:08) *
про возвращаемое значение:
имхо - функция ВСЕГДА должна возвращать int с кодом ошибки и точка. результаты - в параметрах.
а я вот категорически с этим не согласен.
Сам термин "функция" обязывает к математическому поведению. Как и приведено у автора статьи

Цитата(Iron Bug @ 29.3.2010, 0:08) *
про геттеры и сеттеры - для крупных проектов со сложными объектами это единственный нормальный и безопасный способ обращения к данным.
ну это опять же жёстко завязано на Си++. А в языках где есть свойства, в них нет нужды
AD
Я не согласен с функциями внутри функций. Если нужен какой-то логический блок, то лучше пользоваться пространствами имен.
Litkevich Yuriy
Цитата(AD @ 29.3.2010, 2:52) *
то лучше пользоваться пространствами имен
а пространства имён разве можно определить внутри функции?

Прочитал про рекурсию, ну положим итерационным методом лучше, но кода для сравнения обоих методов я не увидел - это минус товарищу.
AD
Цитата(Litkevich Yuriy @ 29.3.2010, 0:38) *
а пространства имён разве можно определить внутри функции?

Читай внимательно, я сказал следующее, что вместо функций в функциях лучше использовать пространства имен, внутри которых и определять нужные функции.
Iron Bug
Цитата(Litkevich Yuriy @ 28.3.2010, 23:59) *
Цитата(Iron Bug @ 29.3.2010, 0:08) *
про возвращаемое значение:
имхо - функция ВСЕГДА должна возвращать int с кодом ошибки и точка. результаты - в параметрах.
а я вот категорически с этим не согласен.
Сам термин "функция" обязывает к математическому поведению. Как и приведено у автора статьи


все проекты, которые я видела, имели именно такие функции, возвращающие int. и я думаю, что это весьма оправданно. просто когда переходишь к реальному программированию, выясняется, что иногда вообще нет смысла возвращать значение, потому что его нет как такового, а вот зато код результата выполнения нужен абсолютно во всех случаях для проверки. я не разделяю вызовы на "функции" и "процедуры" - это устаревшее условное разделение, ничем не обоснованное на практике.
ufna
и положительные эмоции, и не очень :)

как уже было сказано - цели и практика конкретного приложения могут быть разными. Иногда "красиво" идет в ущерб быстродействию, как минимум.
Tonal
Цитата(Iron Bug @ 29.3.2010, 0:08) *
про возвращаемое значение:
имхо - функция ВСЕГДА должна возвращать int с кодом ошибки и точка. результаты - в параметрах.

Если постоянно работаешь с чистым С, и основные апи на нём или на "С с классами" то это вполне оправданно - для единообразия. :)
В других случаях вполне разумно использовать и остальные возможности С++, например такие как возвращение экземпляра структуры/класса из функции и исключения. :)
А вот смесь этих подходов очень чревата ошибками.
Я как-то видел большой проект, где функции и возвращали код ошибки, и бросали исключения - очень неприятное спагетти получалось...

Кстати, возвращать таки лучше не int, а перечисление. :)

Цитата(Iron Bug @ 29.3.2010, 0:08) *
про геттеры и сеттеры - для крупных проектов со сложными объектами это единственный нормальный и безопасный способ обращения к данным. при этом обеспечивается полная инкапсуляция и наследование.

Зависит от назначения класса/структуры. В том же std::pair геттеры и сеттеры, как и наследование бессмысленны. :)
А наследование, в С++, обеспечивается наличием средствами языка, а не геттерами/сеттерами. :)

Ну и про рекурсию - это маразм. :)
Итеративные алгоритмы - это просто подмножество рекурсивных.
Другое дело, что встроенную в императивный язык рекурсию можно вручную нарисовать с помощью явных стеков на массиве и циклов.
Но такая эмуляция будет гораздо менее понятной и более подверженной ошибкам чем явная. :)
С другой стороны рекурсивные алгоритмы всё-таки пока требуются не так часто.
Рекурсией конечно не следует злоупотреблять, как и любой возможностью. И нужно чётко представлять во что выльется тот или иной код для выбранного языка и компилятора.

П. С. Функциональные языки, типа Haskell и LISP полностью строятся на рекурсии и циклы в них - это довольно простые функции выраженные в терминах рекурсии. :)

Ага, таки решил прочитать опус. :)

Про рекурсию у него примерно то и написано что у меня. :)

Про вложенные функции с ним не согласен.
Просто лексическая вложенность функций ничем не лучше чем объединение их в классе или пространстве имён или модулей и пакетов.
А вот для организации частичных применений/замыканий это очень удобно.
Замечательно что в новом стандарте для этого есть механизм лямбд! :)

С длинной массива тоже не всегда так однозначно. Например алгоритму бинарного поиска нужно именно отдельное количество элементов. :)

Да, свойства я тоже не люблю - из кода не видно что происходит - тривиальное действие присвоения или что-то "великое и ужасное". :)

Ну и вообще, красота кода сильно зависит от языка и парадигмы, в которой код написан - красивый код на Pascal будет уродским на Python и оба они будут корявыми для С++ и наоборот.
Код в стиле MFC|xWidget покоробит в проекте использующем в основном на Boost/Loki. :)
Iron Bug
Цитата(Tonal @ 30.3.2010, 11:14) *
Да, свойства я тоже не люблю - из кода не видно что происходит - тривиальное действие присвоения или что-то "великое и ужасное". :)


вот ради этого "великого и ужасного" и нужны сеттеры и геттеры. потому что когда у тебя закрытая библиотека и незнакомый с тонкостями твоего кода юзер хочет, скажем, написать свой класс на основе твоего базового - то вот тогда без них он пожнёт все возможные грабли. надо всегда делать библиотеки такими, чтобы вызовы были безопасны для структуры внутренних данных объекта. а прямые обращения часто этому противоречат и могут быть небезопасны при многопоточности, к примеру.

а про возвращение результата функции, это элементарно: что должна вернуть функция, если ей задали параметры вне её области определения? ноль? минус один? а где гарантия, что это значение не совпадёт с нормальным возможным значением? а если кидать исключения, то, к примеру, есть у пользователя десять библиотек. в каждой - свои классы исключений, которые каждая функция кидает и надо отлавливать и разгребать это дерьмище. это просто капец, надо заметить! в сложных обработчиках исключения полезны для раскрутки стека. а вот в примитивных функциях - проще возвращать код возврата и значение в параметре. думаете, мелкософт свои стандартные библиотеки зря переписали с "безопасными" вариантами вызовов, где возвращается именно результат выполенения, а указатели или ссылки для параметров передаются в параметрах? хотя они и не пример правильного программирования, но просто так, без причины, они бы этого делать не стали. когда программируешь много и имеешь дело с разношёрстными библиотеками, нет ничего хуже, чем дикое многообразие результатов и необходимость вникать в особенности реализации каждой конкретной мелкой функции.
Tonal
Цитата(Iron Bug @ 30.3.2010, 23:56) *
Цитата(Tonal @ 30.3.2010, 11:14) *
Да, свойства я тоже не люблю - из кода не видно что происходит - тривиальное действие присвоения или что-то "великое и ужасное". :)

вот ради этого "великого и ужасного" и нужны сеттеры и геттеры. потому что когда у тебя закрытая библиотека и незнакомый с тонкостями твоего кода юзер хочет, скажем, написать свой класс на основе твоего базового - то вот тогда без них он пожнёт все возможные грабли. надо всегда делать библиотеки такими, чтобы вызовы были безопасны для структуры внутренних данных объекта. а прямые обращения часто этому противоречат и могут быть небезопасны при многопоточности, к примеру.

При наследовании интерфейса ты с такими граблями не столкнёшься - нет состояния у родителя.
А при наследовании реализации всё сильно зависит от назначения и дизайна.
Довольно часто полное закрытие внутреннего состояния от потомка приводит к необходимости его дублирования и дополнительной синхронизации.
Ну и быстродействие может сильно пострадать, особенно при многопоточке. :)

Цитата(Iron Bug @ 30.3.2010, 23:56) *
а про возвращение результата функции, это элементарно: что должна вернуть функция, если ей задали параметры вне её области определения? ноль? минус один? а где гарантия, что это значение не совпадёт с нормальным возможным значением?

Ну дак давай пойдём до конца: какую сигнатуру должен иметь оператор + или new?
И что они должны делать при возникновении ошибок?
Что и как должен возвращать конструктор? :)

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

Ещё раз, то что хорошо для чистого С или в стиле "С с классами" как в MFC, не обязательно хорошо/удобно/наглядно/быстро/компактно в С++. :)

Цитата(Iron Bug @ 30.3.2010, 23:56) *
а если кидать исключения, то, к примеру, есть у пользователя десять библиотек. в каждой - свои классы исключений, которые каждая функция кидает и надо отлавливать и разгребать это дерьмище.

Ежели ты работаешь с зоопарком библиотек, то кто тебе сможет гарантировать одинаковые стратегии обработки ошибок в них?
Даже если у некоторых стратегия схожа, то нюансы могут отличатся очень сильно.
Например значение кодов возврата и их структура.
Стандарты на сигнатуру - кто-то возвращает код ошибки как результат, а кто-то в параметре.
У кого-то код ошибки получается вовсе вызовом отдельной функции (привет Win32 & Posix)
А кто-то кидает исключение в случае ошибки а в код возврата отдаёт признак наличия предупреждений...

Так что тут всяко придётся каждую отдельным местом оборачивать и обрабатывать.

Цитата(Iron Bug @ 30.3.2010, 23:56) *
Думаете, мелкософт свои стандартные библиотеки зря переписали с "безопасными" вариантами вызовов, где возвращается именно результат выполенения, а указатели или ссылки для параметров передаются в параметрах?

Это про С или про С++?
Ну и думаю, таки зря. :)
BRE
Цитата(Iron Bug @ 30.3.2010, 20:56) *
есть у пользователя десять библиотек. в каждой - свои классы исключений, которые каждая функция кидает и надо отлавливать и разгребать это дерьмище. это просто капец, надо заметить!

К сказанному Tonal хочу добавить свои мысли по поводу зоопарка библиотек.
Для интеграции каждой библиотеки наверное лучше написать свои врапперы, которые будут иметь однотипный интерфейс, как по вызову их функционала, так и по сообщению результата своей работы.
Тогда возможно смена низкоуровневых библиотек без изменения кода основной системы. И даже не обязательно прятать весь функционал библиотеки, достаточно описать и предоставить только используемый.
Iron Bug
Цитата(Tonal @ 31.3.2010, 9:44) *
Ну и быстродействие может сильно пострадать, особенно при многопоточке. :)

практика показывает, что не страдает. там, где идёт прямое обращение, компилятор сам оптимизирует вызовы в inline . так что по скорости это никак не отличается.

Цитата(Tonal @ 31.3.2010, 9:44) *
Ну дак давай пойдём до конца: какую сигнатуру должен иметь оператор + или new?
И что они должны делать при возникновении ошибок?
Что и как должен возвращать конструктор? :)

это уже демагогия. речь шла о функциях. как известно, операторы - особые функции. впрочем, за последние N лет мне ни разу не потребовалось переписывать оператор +, к примеру. просто перегрузка оператора - это довольно экзотическое требование, которое больше относится к удобству программирования, чем к эффективности.
а оператор new имеет строгое формальное описание. мы тут не спорим о синтаксисе С++.

Цитата(Tonal @ 31.3.2010, 9:44) *
Ну и с областью определений - есть функции внешнего интерфейса системы - там функция обязательно должна проверить корректность параметров и сообщить пользователю если что не так.
А есть внутренние - передача в них неверного значения это ошибка кодирования, которую должен отловить компилятор и/или система тестирования.
Для сигнализации о них вполне уместны разного рода ассерты.
Т. е. такие ошибки лучше закрывать более тщательным типизированием.

что значит "сообщить пользователю" когда, к примеру, ты управляешь системой реального времени? ругнуться в свой лог? пользы от этого - ноль. там уже накроется всё оборудование, железяки покорёжит, пока ты пишешь своё "сообщение пользователю". в реальном времени нет возможности пространного ввода-вывода и ассертов. всё решается проще - возврат 0 или 1. через регистры. очень быстро и просто. а типизирование при рантайме не помогает. пришло тебе ошибочное число от аппаратуры и надо его обрабатывать. а как ты заранее предугадаешь, что может прийти?
так что далеко не везде есть возможность развёрнутых сообщений об ошибках и отлова десятков исключений с перебором, чего же там всё-таки попалось. это очень замедляет и усложняет работу софта.

Цитата(Tonal @ 31.3.2010, 9:44) *
Ежели ты работаешь с зоопарком библиотек, то кто тебе сможет гарантировать одинаковые стратегии обработки ошибок в них?
...
Стандарты на сигнатуру - кто-то возвращает код ошибки как результат, а кто-то в параметре.

ну, практически все функции возвращают 0 при удачном завершении. а остальное - в параметрах. по крайней мере, хардварные программеры придерживаются такой идеологии и разнообразия мнений как-то особо не наблюдается.
Tonal
Цитата(Iron Bug @ 31.3.2010, 18:30) *
Цитата(Tonal @ 31.3.2010, 9:44) *
Ну и быстродействие может сильно пострадать, особенно при многопоточке. :)

практика показывает, что не страдает. там, где идёт прямое обращение, компилятор сам оптимизирует вызовы в inline . так что по скорости это никак не отличается.

Практика - показывает что может всяко получится. :)

Например, пусть класс реализует визуальный элемент - эллипс.
При изменении размера по любой из полуосей его нужно перерисовать.
Наследник - круг, при изменении любой полуоси должен изменять синхронно другую.
Если доступ к полуосям эллипса только через геттеры и сеттеры, которые обеспечивают перерисовку элемента при изменении, то круг неизбежно будет дёргаться. :(
Что приведёт к уродскому интерфейсу.

Другой пример - многопоточный логгер. Пишет в файл, который создаётся в конструкторе и закрывается в деструкторе. Все операции защищает мутексом.
Мы хотим сделать наследника, которрый обеспечивает ротирование логов.
Если мы дадим доступ к мутексу клиентам (прямой или через геттер), то и есть прямая возможность нарушить инвариант класса и получить дедлоки...
А если доступ у мутексу получить нельзя, мы не сможем атомарно переоткрыть файл для организации ротирования.

Причём в обоих этих случаях о каких-либо инлайнах можно забыть - всё на уровне логики. :)

Цитата(Iron Bug @ 31.3.2010, 18:30) *
Цитата(Tonal @ 31.3.2010, 9:44) *
Ну дак давай пойдём до конца: какую сигнатуру должен иметь оператор + или new?
И что они должны делать при возникновении ошибок?
Что и как должен возвращать конструктор? :)

это уже демагогия. речь шла о функциях. как известно, операторы - особые функции. впрочем, за последние N лет мне ни разу не потребовалось переписывать оператор +, к примеру. просто перегрузка оператора - это довольно экзотическое требование, которое больше относится к удобству программирования, чем к эффективности.
а оператор new имеет строгое формальное описание. мы тут не спорим о синтаксисе С++.

Демагогия - набор полемических приёмов вводящих заблуждение.
Я никого в заблуждение не вводил, а предложил довести твоё утверждение до логического конца (явного противоречия) для того, чтобы выявить границы его применимости. :)

Сигнатура операторов может изменятся в довольно широких пределах - см. например operator().
А насчёт экзотики - это опять же именно твоё личное мнение, обусловленное тем, что ты видимо мало работала в областях связанных с массовыми расчётами.

У меня есть знакомая библиотека расчётов для векторной графики написанная на С++ в 94-96гг по мотивам реализаций на Fortran и Pascal.
Где-то в 2003 была попытка перевода её на Delphi - провалилась.
Удобство использования совмещения (перегрузки) операторов оказалась критичным.

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

Так какая же сигнатура должна быть у конструкторов, функции std::strlen, std::string::size(), std::find, std::sort, и остальных? :)

Цитата(Iron Bug @ 31.3.2010, 18:30) *
что значит "сообщить пользователю" когда, к примеру, ты управляешь системой реального времени? ругнуться в свой лог? пользы от этого - ноль. там уже накроется всё оборудование, железяки покорёжит, пока ты пишешь своё "сообщение пользователю". в реальном времени нет возможности пространного ввода-вывода и ассертов. всё решается проще - возврат 0 или 1. через регистры. очень быстро и просто. а типизирование при рантайме не помогает. пришло тебе ошибочное число от аппаратуры и надо его обрабатывать. а как ты заранее предугадаешь, что может прийти?
так что далеко не везде есть возможность развёрнутых сообщений об ошибках и отлова десятков исключений с перебором, чего же там всё-таки попалось. это очень замедляет и усложняет работу софта.

Пользователь здесь - это тот код, который вызвал функцию.
Сообщить ему - это значит стандартным для твоей системы образом просигнализировать об этом.
Т. е. возвратить код ошибки, или установить код операции, или вызвать исключение, или показать сообщение. :)
В любом случае, у системы/библиотеки/модуля должен быть стандартный способ такой сигнализации.

0 и 1 через регистры это очень хорошо и быстро, а что делать, если у тебя система работает с серверами баз данных, SOAP и DCOM.
И с одним из них пропала связь или возникла какая другая критическая ошибка?
Критическая здесь - это когда код на данном уровне не может принять решение по её обработке самостоятельно.
Какую 1 или 0 ты будешь через регистры передавать?

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

Ну и "ошибочное число от аппаратуры" придти не может.
Это программа может быть написана так, что она ломается, когда приходят какие-то значения. Например браузер и ОС не должны "упать" если в ответ на запрос www.microsoft.com придёт мусор из свапфайла. :)

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

Цитата(Iron Bug @ 31.3.2010, 18:30) *
Цитата(Tonal @ 31.3.2010, 9:44) *
Ежели ты работаешь с зоопарком библиотек, то кто тебе сможет гарантировать одинаковые стратегии обработки ошибок в них?
...
Стандарты на сигнатуру - кто-то возвращает код ошибки как результат, а кто-то в параметре.

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

Хардварь - не единственная область применения С++, не говоря уже о других языках.
AD
Могу сказать, что крайне интересная тема получилась! Обоим участвующим в дискуссии большое спасибо. Крайне интересно.
Для просмотра полной версии этой страницы, пожалуйста, пройдите по ссылке.
Форум IP.Board © 2001-2024 IPS, Inc.