Не совершает ошибок только тот, кто ничего не делает. Программисты — тоже люди и постоянно совершают ошибки. Рассмотрим пять типичных ошибок проектирования и программирования, встречающихся в исходном коде программ, но не влияющих на их работоспособность. Чаще всего они появляются из-за невнимательности, лени и непредусмотрительности разработчиков.
Ошибка № 1. Слишком узкое имя класса или интерфейса
Не всегда получается предусмотреть направление развития программы и/или библиотеки. Это может привести к тому, что имя класса или интерфейса оказывается слишком узким для его фактической области применения.
Первый вариант подобной проблемы состоит в том, что имя перестает соответствовать иерархии наследования. Например, изначально в корне иерархии находился интерфейс Camera. Затем обнаружилось, что крайне удобно реализовать класс для работы с электронным телескопом Telescope, унаследовав этот интерфейс, чтобы иметь возможность использовать существующий полиморфный код. Но телескоп — это не камера, поэтому не стоит нарушать логику.
Другая форма этой ошибки связана с расширением функциональных возможностей класса. На ум сразу приходит класс XMLHttpRequest, включенный в стандартную библиотеки языка Javascript. На самом деле он может работать не только с данными в формате XML, но это не очевидно из его названия, что может вводить в заблуждение.
Справиться с такой ошибкой можно средствами рефакторинга или операции поиска/замены по исходному коду. Но есть одно но. Если вы разрабатываете не самодостаточный проект, а прикладную библиотеку, то сделать это может быть очень сложно, ведь от вашего кода зависят другие проекты. Поэтому у вас не получится просто так изменить имя (как в ситуации с XMLHttpRequest). В этом случае класс можно переименовать, но для совместимости создать псевдоним, соответствующий его старому названию. При этом старое название нужно пометить как нерекомендуемое к использованию ( deprecated).
См: Пример полиморфизма в C++ на основе ООП
Ошибка № 2. Неконтролируемое разрастание иерархий наследования
Наследование — это полезный и мощный прием программирования. Но нужно знать меру. Если в приложении есть некая сущность, то это не означает, что для нее должен обязательно существовать класс.
Например, если вы разрабатываете более или менее сложную игру, то в ней будет множество объектов: игровой персонаж, враги, препятствия, оружие и т.д. Если для каждой отдельной сущности (например, меч, пистолет, винтовка и т.д.) создать класс, то программа просто утонет в сотнях, а то и тысячах похожих классов.
Решение заключается в том, чтобы выделить несколько базовых классов, характеризующих понятия: враг, препятствие, оружие и т.д. Далее самих врагов можно разделить на наземных, летающих и т.д., задать им некие типичные характеристики (скорость движения, размер, сила атаки и т.д.) в виде полей класса. То же самое проделать по необходимости для других сущностей. Для динамических аспектов поведения (траектория перемещения и элементы искусственного интеллекта) используйте делегирование, которое также полезно применять для параметризации поведения классов.
См: Профессия программиста: Абстрактное мышление
Ошибка № 3. Вызов функций не по смыслу, а из «удобства»
Иногда, когда поджимают сроки сдачи проекта, возникает соблазн что-то где-то срезать и упростить. Почти всегда это приводит к проблемам в будущем.
Например, имеется функция-обработчик нажатия кнопки. Она выполняет некую последовательность действий. Вдруг мы замечаем, что эту же последовательность действий нужно воспроизвести при возникновении того или иного условия в работе программы. Сложно устоять, и не сделать вызов обработчика напрямую. Однако это приводит к появлению неочевидных зависимостей, которые в будущем превратятся в ошибки, если нам понадобится изменить код обработчика.
Правильное решение в данной ситуации — создание еще одной функции (а может и нескольких), которая имеет осмысленное имя и делает то, что изначально происходило в обработчике. Тогда эта новая функция будет вызываться и в обработчике, и во всех местах, где это необходимо. Когда придет время что-то менять, то если изменения имеют общий характер, их можно внести в созданную функцию, иначе — индивидуально по месту, не затрагивая логику работы остального кода.
Ошибка № 4. Осознанное дублирование фрагментов кода
Дублирование в программировании обычно приносит только проблемы. Но одно дело, когда оно происходит случайно, а другое, когда код дублируется намерено. С этим сталкивается большинство разработчиков. Иногда кажется, что проще скопировать кусок кода из одного места и вставить его в другое, а не выделять отдельную функцию (или класс), для которой еще нужно найти подходящее место, придумать хорошее имя и удачную сигнатуру. Но это ощущение обманчиво.
Очень часто оказывается, что если некий фрагмент пришлось копировать один раз, то его придется копировать и еще. Хуже всего, если разработчик уже сам понимает всю глубину проблемы, но не может отступиться от начатого, и в очередной раз пользуется «копи-пастом». Таким образом он топит себя. Поскольку, если в будущем выяснится, что скопированный код содержал ошибку и ее нужно исправить в каждом продублированном фрагменте, то ему предстоят «веселые» часы рутинной работы с массой потенциальных ошибок.
Единственное правильное решение в этой ситуации — не лениться и создавать вспомогательные функции и классы вовремя. В будущем потраченное время окупится многократно и вы сможете избежать множества сложностей и проблем. Если вовремя это сделать не удалось, то могут помочь инструменты рефакторинга, которые во многих IDE стали настолько интеллектуальными, что сами умеют находить дублирование при создании функций по фрагментам кода.
См: Принцип DRY в действии
Ошибка № 5. Неподходящие имена функций и переменных
Этот тип ошибок во многом пересекается с ошибками № 1 и 3, но все же я решил выделить его отдельно, поскольку между ними имеются концептуальные отличия.
Часто из-за спешки мы даем функциям и переменным не самые подходящие имена. Иногда это связано с неполным пониманием предметной области или с ошибками проектирования. Но это не оправдание.
Конечно, придерживаться 100%-ой чистоты тоже нет необходимости. В простом for-цикле вполне можно использовать переменную i, но чем больше область видимости, тем осмысленнее должно быть имя.
Еще хуже, если имя осмысленное, но вводит в заблуждение. Т.е. из названия следует одно, а на самом деле переменная хранит что-то другое; или функция делает не то, что от нее можно ожидать (например, имеются побочные эффекты).
Решение заключается в критическом анализе кода, ведь чаще всего простым переименованием не обойтись. Плохое имя переменной или функции не редко связано с ошибками проектирования — что-то находится не на своем месте или делает больше, чем должно. Поэтому устранение проблемы требует аккуратности и временных затрат. Но оно того стоит, ведь чем яснее код, тем меньше шансов, что в нем появятся ошибки.
См: Принцип единой ответственности