Рубрика:
Программирование /
Теория
|
Facebook
Мой мир
Вконтакте
Одноклассники
Google+
|
АНДРЕЙ ЧИНКОВ, разработчик в Kaspersky Lab. Область интересов – OOP/OOD
На языке C++ Некоторые аспекты применения
Программист на С++ – это комбинатор, как Остап Бендер. Читатель вправе поспорить со мной, так как по большей части мои доводы имеют научно-популярный характер
Сегодня С++ идет в ногу со временем. В нем могут найти «отдушину» все (или почти все) типы программистов: одаренные студенты; программные менеджеры, которым С++ нужен лишь для того, чтобы сдать проект и забыть его (есть исключения, и их немало); эксперты и даже убежденные Си-программисты, которые не уважают С++ по идеологическим соображениям. Такую лазейку в этом языке я нашел и для себя, хотя не отношусь ни к одной из перечисленных выше групп, мне нравится писать на С++ программы и обсуждать с коллегами результат.
С++ – мощный язык программирования с широкими возможностями, и для эффективного их использования надо немножко изменить способ мышления. Это еще один плюс, который привлекает меня в С++. Давайте вернемся на студенческую скамью и вспомним, как начиналось знакомство с этим языком – кто-то кусал локти, не зная, как сдать очередную «мучительную» лабораторную работу, а у кого-то горели глаза от осознания того, что тут не просто «Си c классами».
Итак, сегодня С++ – это язык программирования, основанный на нескольких парадигмах. Своего рода конгломерат процедурного, функционального, объектно-ориентированного и обобщенного программирования. Найти смысл в таком языке просто, если понимать как правильные, так и ошибочные аспекты его применения. Всем известно, что cтандартная библиотека шаблонов (STL) – это хорошо, Boost (набор библиотек, невообразимо широко расширяющих возможности стандартного С++) – тоже, а глобальные переменные и аргументы по умолчанию в виртуальной функции – это зло. Что const – это наш помощник, что исключения генерируются по значению, а перехватываются по константной ссылке. И еще десяток таких основных утверждений. Что же еще нужно, чтобы написать хорошую программу?
Цель программирования – не предоставить средства для решения задачи, а обеспечить возможность достижения цели существующими способами, комбинируя их. Программист на С++ – это комбинатор, с позволения сказать, Остап Бендер. В его распоряжении мегабайты денег (читать – памяти) и множество помощников. Хороший комбинатор доберется до солнечной Бразилии, плохой – будет задержан на границе. Итак, давайте прорываться.
Появления TR1 разрешило стандарту С++ использовать некоторые компоненты Boost. Среди них интеллектуальные (умные) указатели и функторы, которые уже пронизывают имплементации многих коммерческих продуктов.
Умный указатель – это не только гарантия не забыть освободить ресурс при выходе из области, в которой данный ресурс необходим. На мой взгляд, это скорее просто бонус. Гораздо более важной возможностью при использовании RAII представляется реализация безопасного кода, в котором можно легко вовремя уступить дорогу какому-то событию (например, возникновению исключения) так, чтобы сохранить объекты и программу в корректном состоянии не только на этапе выполнения, но и при разработке кода.
Что такое корректность? Простые указатели грешны в том плане, что они напрямую работают с памятью. Умные указатели – самостоятельные объекты, которые кроме решения этой проблемы, как часто получается, содействуют написанию безопасного кода путем разделения управления и владения ресурсом. Принцип «разделяй и властвуй» в С++ проявляется очень хорошо. Зачем вообще думать о корректности? Работает – и ладно? Я отвечаю себе так: сделать корректную программу быстрой легко, наоборот – очень трудно.
Под корректностью кода я понимаю способность легко вносить изменения так, чтобы иметь возможность повторно использовать уже существующий код. Я встречал программистов, которые любили читать код, как книгу. При этом функции занимают несколько листингов, с многоуровневыми вложенными конструкциями if-else. Внесение изменений в такую программу заключается в том, чтобы что-то куда-то скопировать.
Как правило, это что-то обязательно потеряется по дороге. Результат – баг, который трудно найти. Поэтому необходим рефакторинг. Если бы я изначально работал с такими понятиями, как самостоятельные объекты, и использовал бы STL, то, кроме уменьшения размера функции и улучшения ее читабельности, получился бы более масштабируемый продукт.
Прежде чем написать что-то, полезно подумать над тем, как это можно назвать, будь то отдельная функция или класс. Написав логику в терминах названий функций и классов, ее гораздо легче имплементировать и меньше вероятность допустить опечатку или ошибку. Обратный вариант – написали килобайты кода, а потом сидеть и думать, что же это мы написали. Так быть не должно. Каждый модуль (функция или класс) должен решать строго определенную для него задачу – не более и не менее.
Функторы позволяют делать объекты-функции буквально из всего. Но в этом и их преимущество – инкапсуляция запроса в объект. Однако давайте покритикуем только что сказанное – злоупотреблять функторами не стоит, так как они обезличивают программу, позволяя работать не с типами, а с чем-то, что выдает себя за функцию, с уже готовыми контекстом и аргументами.
Существует такое понятие, как «утиная типизация», то есть если крякает, значит, утка. Поэтому не будем злоупотреблять трехэтажными std::tr1::bind’ами, а рассмотрим применение паттерна «Команда».
Цель абстрактного интерфейс Command состоит в отделении объекта, посылающего запрос, от объекта, который знает, как ее выполнить. Интерфейс Command может содержать операции Execute(), Prepare() и/или Commit(), в то время как потребовалось бы несколько отдельных функторов для каждой их этих операций. С++ – строго типизированный язык программирования, типы – наши помощники. Потому что лучше заставить компилятор ошибиться на этапе компиляции, чем уронить программу на этапе выполнения.
Интерфейс Command явно говорит нам, что тот, кто за него себя выдает, умеет выполнять команды. Дополнительно, например, интерфейс AsyncCommand конкретизирует, что команда будет выполняться параллельно. Функторы в этом плане более грубы, так как за ними можно спрятать что угодно, даже то, что совсем не предусматривалось быть использованным как команда.
При написании кода это удобно, потому что быстро. Однако уверен, стоит вернуться к написанному месяц назад коду, изобилующему функторами – и ничего не будет понятно. При наследовании абстрактного Command явно видно, что мы реализуем класс, который будет использоваться как команда, что позволит реализовать класс наиболее подходящим способом уже на этапе написания кода. В общем, чем больше компилятор знает, что мы от него хотим – тем лучше и безопаснее.
Объектно-ориентированное проектирование позволяет решать новые задачи старыми способами. Сложность, на мой взгляд, состоит не в написании нового решения, а в отыскании критериев, по которым можно будет применить тот или иной шаблон проектирования. Само программирование на С++ или любом другом похожем языке не ставит своей целью удивить начальство остроумностью решения. Наоборот, можно получить гораздо больше пользы, удачно применив готовый паттерн проектирования.
Первым признаком неудачного проектирования является наличие в программе «слишком умных» классов, которые очень многого хотят для своей работы и очень много обещают. Такие классы трудно повторно использовать и сопровождать в будущем. Также классы, которые отдают пользователю ссылки на свои внутренние данные посредством «геттеров» и «сеттеров», также представляются неудачными, так как нарушают инкапсуляцию.
Давайте покритикуем нарушение инкапсуляции не с точки зрения того, что это просто нарушение чего-то, а с точки зрения архитектуры. Как нарушение инкапсуляции информации может сказаться на проектировании системы в целом?
Интуитивный подход говорит нам, что каждый объект должен выполнять только те операции, для выполнения которых он имеет достаточно информации. Это поведение предполагает наличие так называемого Information Expert’a для выполнения операции. Однако если объект будет распылять содержащуюся внутри него информацию, ничего хорошего не получится. Потому что трудно будет организовать распределение обязанностей между Information Expert’ами.
Приведу пример из реального мира. Когда мы садимся за руль, мы являемся Information Expert"ами в том плане, что знаем, куда ехать. Машина является Information Expert"ом потому что знает, как ехать. Было бы нелогично требовать от объекта «машина» реализации операции «GetПриводКоленвала()», а от водителя машины – операции «КрутитьПриводКоленвала». Машина предоставляет только необходимый интерфейс (руль и рычаги) для того, чтобы ею можно было пользоваться.
Иногда может показаться, что, выставив наружу внутренние данные какого-то объекта, мы оптимизируем и ускоряем программу, приближая данные к месту их непосредственного использования или обработки. Однако эта теория порочна.
Например, показать это можно на классе MyDataBase, работающим с базой данных, хранящейся на диске, посредством SQL-запросов. Написав программу работы с базой данных, использующей этот класс, мы заметили, что она работает слишком медленно, постоянно «хрустит» жестким диском. Покопавшись в коде и немного «подебажив», мы обнаружили, что в каком-то цикле очень часто происходят SQL-запросы к базе данных. Немного поразмыслив, можно неверно решить реализовать в этом классе MyDataBase функцию GetAll(), которая возвращает вызывающей стороне всю таблицу – “SELECT * FROM TABLE”. Вызывающая сторона кэширует эти данные, тем самым минимизировав обращения к жесткому диску и увеличив скорость работы программы. На самом деле такой подход хорош только при близком рассмотрении.
Давайте покритикуем его. Первое, что хочется сказать, это то, что решив одну проблему с быстродействием, мы получили как минимум другую проблему. Клиент класса MyDataBase должен решать две проблемы, вместо старой одной – осуществлять логику работы с данными из базы и хранить эти самые данные в кэше.
Причина в том, что изначально приложение было спроектировано так, что предусматривало слишком частые обращения к БД. Эту проблему не должен решать код, осуществляющий работу с БД.
Тем более – изменять интерфейс MyDataBase ради решения проблемы в бизнес-логике программы категорически неприемлемо. MyDataBase – это Information Expert в том плане, что только он знает, как работать с базой данных. MyDataBase должен предоставлять интерфейс, состоящий из набора элементарных функций, а не GetAll().
Необходимо выяснить общую причину, заставляющую клиента MyDataBase в цикле производить обращения к базе данных. Исправив ситуацию на уровне бизнес-логики, мы как минимум решим поставленную задачу. А как максимум предотвратим появление десятка других потенциальных ошибок и вариантов некорректного поведения программы в будущем.
***
Целью данной статьи было обсудить некоторые наболевшие проблемы программирования. Если вдобавок получилось заставить читателя улыбнуться пару раз, то цель перевыполнена.
Facebook
Мой мир
Вконтакте
Одноклассники
Google+
|