Ошибки организации проектирования и архитектуры комплекса программ

Архитектура ПО

В этой статье я хотел бы затронуть наиболее часто встречающиеся проблемы в построении архитектуры ПО, — пишет Олег Шаститко в своем посте на DOU.UA. — Я решил написать этот материал, очередной раз столкнувшись с тем, что люди, даже имеющие огромный опыт разработки ПО, продолжают путать модели, относящиеся к разным слоям приложения. Они не придают значения излишней связанности (костности) между частями приложения и прочим аспектам. В итоге эти аспекты оказываются, если не критичными, то существенно влияют на дальнейшую разработку программного продукта.

Почему возникают ошибки

Причина, по которой разработчики, имеющие за плечами довольно большой стаж разработки ПО и построения архитектуры, продолжают реализовывать жёстко связанные решения и решения с нечётко разделёнными моделями — отсутствие негативной «обратной связи» в краткосрочной перспективе. Часто последствия всего этого вылазят через время, после внесения множества других изменений. Те, кто построил эту архитектуру, либо уже не видят негативных результатов своих трудов, либо спихивают их на других людей (чаще всего на тех, кто непосредственно реализовывал).

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

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

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

Что мы рассмотрим

В этой статье я хочу рассмотреть проблемы в архитектуре на основе реального проекта, с которыми мне пришлось столкнуться, когда заказчик решил, что через знакомых ему посоветовали хорошего архитектора с опытом более 40 лет и он занялся «ревизией» этого самого реального проекта.

Проект создан с помощью технологии .NET и языка C#, а также используется база данных MS SQL, но на самом деле это не имеет никакого значения. Точно также это может быть как другая реляционная БД, так и другая ООП технология, например Java.

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

В примерах кода я удалил все неактуальные моменты (например, различные проверки, если пример кода сфокусирован на другом) для повышения читабельности.

В этой статье я хочу рассмотреть следующие вопросы:

1. Использование одной и той же модели для базы данных и бизнес-логики.
2. Выборка записей из БД и их projection на DTO классы.
3. Использование enum или отдельной сущности (отдельной таблицы-списка в БД).
4. Нарушение SRP (Single Responsible Principle).
5. Использование Nullable types.
6. Проверка входных/выходных данных.
7. Использование Exceptions.
8. Вычисляемые поля и их хранение в БД.
9. «Исключительные случаи» и «дублирование» в архитектуре.
10. Интерфейсы и их реализация.
11. «Заглушки» и прочие способы подавления ошибок.
12. Неочевидность использования библиотечного кода.

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

1. Использование одной и той же модели для базы данных и бизнес-логики

Один из самых распространённых подходов, с которым мне приходилось сталкиваться и который в итоге часто приводит к бардаку во всём проекте, — это «перемешивание» слоя базы данных и классов слоя бизнес-логики, которые отвечают за передачу данных (DTO классы). Почему они должны быть разделены? Как минимум потому, что, как мы знаем из азов программирования, классические БД представляют собой реляционную модель, а классы бизнес-логики оперируют объектами! И объектная модель — не тоже самое, что реляционная модель для представления одних и тех же данных. Одного этого аспекта должно быть достаточно, чтобы задуматься о том, что эти модели должны быть разными. И все попытки натянуть одно на другое ведут либо к избыточным и дублирующим полям в БД, либо к запросам, которые вытягивают целые сущности ради одного поля в каждой сущности.

Я могу привести множество аргументов о вреде избыточных и дублирующих полей. Это целая тема для отдельной статьи. Избыточные, а особенно дублирующие поля — это зло, которое создаёт чуть ли не более половины всех проблем в проекте. Но если сказать вкратце, то дублирующие поля создают неопределённость и 2 (или более) «точек» для изменения, вместо одной. Например, если сущность User имеет поля FirstName и LastName, а сущность Driver является User, то если у Driver тоже будут поля FirstName/LastName — это создаст неоднозначность.

То есть если мы обновляем драйверу имя, то должны также обновить его и у пользователя. Если обновляем у пользователя, то должны также обновить и у драйвера. Причём, если мы забудем это сделать для какой-то из сущностей — никакой ошибки мы не получим! Ошибку мы получим тогда, когда одна часть приложения будет возвращать имя, взятое у пользователя, а вторая — взятое у драйвера! Причём ошибку получит клиент. С точки зрения разработчика всё будет компилироваться и вообще будет всё феншуй. А с клиентской точки зрения будет вообще неясно, какие данные валидны и цена этого «незнания» может быть очень высокой. Если развить тему, то пользователь может вообще увидеть данные, которые после момента редактирования не должны быть известны.

Почему я привёл этот пример? Потому что недавно столкнулся с этой ситуацией. Новый архитектор из Сербии, который по возрасту годится мне в отцы и который первые шаги в программировании сделал в начале 70-х, когда меня ещё и в проекте не было, а построением архитектуры ПО занимается с начала 80-х. И он поступил именно так, в рабочий проект добавил эти дублирующие поля для сущности Driver, несмотря на то, что сущности Driver и User находились в зависимости 0..1-1 друг к другу. То есть для каждой сущности Driver обязательно имела место быть сущность User и никакие мои аргументы, почему это делать нельзя и какие последствия это влечёт, не имели успеха. Он просто их не понял и не хотел понимать, потому что костность мышления и аргументы «я создаю архитектуру для приложений с времён, когда ты ещё пешком под стол ходил» ему кажутся железобетонными.

Аналогичным образом влияют избыточные поля, например, вычисляемое поле. Если есть поля A, B и C и по ним можно вычислить поле D, то без особой необходимости поля D не должно быть в БД! Иначе это всё влечёт за собой то же самое — это поле нужно сопровождать, менять при изменении одного из полей и прочие радости жизни.

Что же заставило этого «архитектора со стажем» добавить дублирующие поля? Оказывается, причина этого была банальной — он напрочь забраковал DTO классы! То есть роль DTO классов у него играют те же классы, которые участвуют в построении БД. И он реально не понимает, почему нужны ещё какие-то DTO классы, которые часто похожи на его классы и которые нужно поддерживать. Но вот когда клиентская часть запросила данные с 2-х таблиц, то он не нашёл ничего лучшего, чем добавить эти поля в таблицу Driver, чтобы «был один запрос вместо 2-х».

На самом деле эта проблема элементарно решается с помощью projection (не знаю, как сказать по-русски), когда механизм запросов к БД оптимизирует так, чтобы это был один запрос вместо двух (хоть и с INNER JOIN; в .NET это Linq-to-Entities, например). Результат сохраняется в DTO и далее пробрасывается уже в клиентскую часть. Ситуация становится совсем катастрофической, если клиентская часть хочет видеть список, данные которого формируются из данных из 5 таблиц, причём в каждой из них нас интересует только одно поле. Если у нас в результате будет 100 записей, то вместо одного (пусть и массивного запроса) мы вынуждены будем сделать 5 * 100 + 1 запрос.

Помимо всего прочего, добавление дублирующих полей, как практикует этот мой сербский друг, «решает» только данный конкретный случай. Завтра клиентской части потребуется ещё одно поле и его подход с добавлением дублирующего поля потребует внесения изменений в архитектуру БД (или же плодить дополнительные запросы к БД). Но БД никак не должна зависеть от перипетий на клиентской части. БД — это хранение данных, она вообще ничего не должна знать о клиентах, её использующих. Она только изменяет, хранит и отдаёт данные. Во что их преобразовать и как — задача клиента, её использующего.

Поэтому чётко разделяем классы, работающие с БД, и классы DTO. Классы БД имеют реляционную структуру, классы DTO привязаны к бизнес-модели (и часто очень похожи на конечную клиентскую часть). За это придётся заплатить тем, что нужно будет создать классы DTO, которые, возможно, будут иметь много общих полей с классами, используемыми для построения БД. Но у нас будет гибкая, независимая архитектура, чётко разграниченная по слоям.

Если классы БД должны работать с остальным приложением только через посредника (то есть через классы DTO), то, по идее, их хорошо было бы вообще сделать «невидимыми» для всех остальных частей приложения, кроме как слоя, работающего непосредственно с БД. Я в принципе так и предпочитаю делать. Эти классы изолированы в отдельной сборке вместе со всей инфраструктурой для работы с конкретной БД, и методы этой сборки возвращают уже DTO классы, а вся кухня работы с БД происходит внутри этой сборки.

Однако есть один аспект: часто бывает необходимым просто обновить объект. То есть клиентская часть передаёт новый объект, и нам нужно обновить текущий объект в БД. Мы не знаем, какие поля конкретно были изменены. Или же клиентская часть передаёт только те поля, которые были обновлены. Или же операция представляет собой только добавление объекта, без затрагивания других сущностей в БД. В этих случаях удобно напрямую работать с классами, работающими с БД и не плодить DTO классы, не отличающиеся от БД классов и заниматься их маппингом. То есть мы просто получаем запись из БД (которая отражается на классе, работающим с БД), вносим изменения в этот класс и сохраняем этот объект снова. Либо при добавлении объекта мы сразу заполняем класс БД и сохраняем его, минуя DTO.

В принципе такой подход имеет право на существование и не несёт за собой особых последствий при аккуратном использовании (в таком случае классы БД рассматриваются как часть бизнес-модели). В этом подходе есть плюс — отсутствие дублирующих DTO классов для простых операций. Но есть и жирные минусы — бизнес-модель имеет, по сути, реляционные классы, которые к ней не относятся. А также то, что открытые для бизнес-модели реляционные классы дают предпосылки для их использования «напрямую» даже в случаях, когда этого делать не стоит С изолированными классами в отдельной сборке такой номер вообще не провернёшь, не добавив ссылку на эту сборку. Исходя из сказанного, мы получаем, по сути, нарушение принципа SRP (Single Responsible Principle), о котором поговорим в 4-м разделе.

Стоит ли «открывать» эти классы для использования в простейших CRUD операциях или нет — решать вам. Это зависит от множества других факторов: размера приложения, количества таких операций, частоты изменения и модификации приложения и т.д. Но как только клиентская часть запрашивает данные, которые отличаются от тех, которые хранятся в одной таблице реляционной БД — то не нужно натягивать сову на глобус. Создавайте дополнительный слой с DTO классами и формируйте запрос к БД именно так, чтобы он был оптимизирован и не содержал лишних данных.

В интернете ведётся много дискуссий по поводу того, что классы DTO дублируют многие поля из классов других слоёв. Я не вижу в этом особой проблемы и не считаю, что это создаёт какую-либо избыточность. Это, скорее, наоборот, изолирует каждый слой от других слоёв и убирает ненужные зависимости. А для мэппинга классов между слоями есть различные тулзы, например Automapper (как в примере выше).

Резюме. Не стоит смешивать реляционную модель и доменную модель приложения. По возможности, стоит вообще изолировать каждую из моделей в своём слое.

2. Выборка записей из БД и их projection на DTO классы

Использование projection (не знаю, как правильно перевести на русский) вытекает из моментов, рассмотренных нами ранее. А точнее из того, что реляционная модель данных отличается от объектной бизнес-модели. На практике чаще всего получается так, что в реляционной модели данные, касающиеся одной сущности, разбросаны по нескольким таблицам. Например, у нас есть сущность Employee, и у каждого Employee есть Marital Status. Список статусов часто имеет смысл хранить в отдельной таблице, так как это позволит использовать их повторно, гарантировать их уникальность и избегать дублирования (вторая нормальная форма БД):

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

Теперь для того, чтобы выбрать эти данные, мы можем использовать projection в Linq-to-Entities:

Есть разные инструменты, которые умеют маппить это автоматически, например AutoMapper.EF6. Но суть остаётся той же — нам не нужно выбирать 2 сущности вместо одной, нам не нужно добавлять какие-то дублирующие поля и прочие извращения, о которых мы говорили выше. В нашем случае Linq-to-Entities (в вашем это может быть что угодно, хоть ручное составление SQL-запроса) помогает одним запросом получить необходимые данные, которые соответствуют нашей бизнес-модели.

Резюме. Старайтесь использовать projection при выборе данных из нескольких таблиц в одном запросе. В таком случае ваш запрос к БД будет оптимизирован, что позволит избежать множества запросов к БД и выбора ненужных данных.

3. Использование enum или отдельной сущности (отдельной таблицы-списка в БД)

Довольно часто мы сталкиваемся с ситуацией, когда какая-то сущность представляет собой список. Это может быть список стран/штатов, список допустимых значений, список статусов, которые может иметь другая сущность. Для решения этой задачи можно использовать отдельную таблицу с ключом-значением, где каждая запись имеет уникальное значение, а полный набор записей представляет собой список всех допустимых значений. Либо создать отдельный enum и также задать список всех допустимых значений. Какой из подходов правильный? Правильные на самом деле оба, просто всё зависит от условий!

Давайте рассмотрим на примере. В текущем проекте используется список штатов. Мною он был спроектировал как отдельная сущность-таблица, первичный ключ записи которой используется как внешний ключ в таблицах, где необходима связь с каким-либо штатом. Моему сербскому другу это не понравилось, он всё забраковал, снёс эту таблицу, создал enum и везде, где был внешний ключ на эту таблицу, заменил на обычное значение типа int для хранения значения enum. Правильно ли он сделал? Нет!

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

Представьте, что проект последний раз изменялся год назад, заказчик уже потерял все связи с разработчиками, которые его делали. Как развернуть проект на хостинге скудно описано (если описано) где-то там в документации. И вообще это тоже делал кто-то из разработчиков тоже год назад. Заказчику, по сути, нужно изменить то, что относится к данным, но он вынужден изменять код из-за данных. Код не должен зависеть от данных. Если мы вынуждены изменять код из-за данных, значит что-то не так в нашей архитектуре!

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

Можно сделать вывод, что вообще не стоит использовать enum’ы для отображения списка сущностей. Но это не так. Давайте рассмотрим случай, когда как раз стоит использовать enum.

Допустим, у нас есть маршрут, который должен проехать водитель. У маршрута есть состояние. Это могут быть значения «Waiting», «Started», «In Progress», «Stopped», «Cancelled», «Completed». И у нас в зависимости от состояния выполняется разная логика! Например, если маршрут завершён успешно — то водитель должен загрузить счёт-фактуру. Если остановлен — то мы должны обработать другую логику и посмотреть, что произошло и т. д. То есть у нас логика в коде зависит от состояния маршрута.

Да, я знаю про принцип подстановки Лисков. Но, по-моему, здесь как раз тот случай, когда лучше использовать switch, а не плодить дочерние классы.

Если использовать для этого таблицу, то здесь мы наоборот вынуждены добавлять захардкоденые значения, соответствующие записям в таблице. Теперь значения в таблице нельзя трогать, нельзя удалять, переименовывать, что накладывает дополнительные ограничения и вводит исключительную ситуацию, которая где-то должна быть описана. Этот документ должен быть must have для чтения каждому по 3 раза на неделю, чтобы, не дай бог, он не забыл о том, что эту таблицу трогать нельзя и изменения в ней могут привести к непредсказуемым последствиям, которые могут вылезти только спустя время!

Резюмируя сказанное: если логика приложения не зависит от выбранных значений — то используем таблицу (или другой источник данных). Если зависит и в коде мы должны упоминать какое-то из значений — тогда создаём enum.

Резюме. Что стоит использовать — enum или отдельную сущность — зависит от конкретного случая. Если логика приложения зависит от выбранного значения, то, скорее всего, стоит использовать enum. Если нет, а также этот список может меняться «на ходу» — то всё говорит о том, что нужно посмотреть в сторону использования отдельной сущности.

4. Нарушение SRP (Single Responsible Principle)

Как говорит мой опыт, наиболее часто встречаемое нарушение принципов SOLID в реальных проектах — это нарушение принципа SRP (Single Responsible Principle). И использование реляционной модели как модели для передачи данных между уровнями (вместо специального класса DTO) — одно из проявлений нарушения этого принципа. Даже если бизнес-модель полностью идентична реляционной, всё равно мы имеем 2 причины для изменения этой модели. Когда меняется модель представления, которая тянет за собой бизнес-модель, и когда меняется структура реляционной модели. Например, у нас был следующий класс, используемый для генерации БД:

То есть его семейное положение было записано просто строкой в БД, со всеми вытекающими негативными последствиями. В результате было решено сделать рефакторинг и привести БД ко 2-й нормальной форме:

Но ведь этот же класс использовался для передачи данных в слой представления и теперь всё поломалось! Нужно переписывать выборку (в идеале, добавлять класс DTO).

Аналогичным образом мы будем вынуждены изменить этот класс, если изменится слой представления. Это и есть нарушение принципа Single Responsible Principle, который вытекает из использования одного класса для разных слоёв модели. Старайтесь, без особой на это необходимости, не делать так, даже если модели разных слоёв на данный момент идентичны.

Резюме. Если есть несколько причин для изменения класса — то нарушен принцип SRP. Стоит посмотреть в сторону выделения ещё (как минимум) одного класса, чтобы разделить ответственность на каждого из них.

5. Использование Nullable types

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

Например, в проекте есть сущность Trip, в котором есть поле CalculatedDistance. Это расстояние вычисляется сторонним компонентом, который иногда может быть недоступен или по каким-то внутренним причинам не может посчитать это расстояние, но в целом сущность Trip должна быть создана даже в этом случае (такое бизнес-правило в проекте). Это поле было объявлено как nullable (decimal?).

Мой сербский друг переделал его на not nullable:

И оно стало инициироваться значением по умолчанию, то есть нулём. Я спросил его, почему он так сделал. Он мне ответил, что инициировать нулём — правильно и привёл мне пример из жизни, что когда мы покупаем новую машину — то на одометре мы имеем ноль, и потом, в процессе эксплуатации, это значение увеличивается в зависимости от пройдённых автомобилем километров.

Его аргумент относительно новой машины правильный, но это совсем другая ситуация. В новом автомобиле действительно это значение должно быть ноль, потому что машина проехала 0 километров! А вот если мы покупаем машину на еврономерах где-нибудь в Риге у русскоязычного продавца, который продаёт «Опель» без одометра середины девяностых с почему-то переваренной рамой и кузовом и мамой клянётся, что на машине почти не ездили и точно знает, что машина прошла не более нескольких десятков тысяч км и вообще «мы, гусские, никогда своих не обманываем» ©… Это явно тот случай, когда мы должны были бы инициировать значение одометра как null — неизвестно, отсутствие значения!

Ноль — это тоже число! Например, если мы пришли в магазин купить себе пиво, достали кошелёк и вспомнили, что мы только вчера его купили, и вообще он ещё в упаковочной плёнке, то мы можем определённо сказать, что там нет денег, там ноль! А если мы вчера положили в кошелёк 1000 долларов, сегодня достали его в пивном магазине и вспомнили, что сегодня утром наш кошелёк брала с собой жена на шоппинг, то, не открыв его, мы не знаем, сколько там денег, и не знаем, хватит ли нам на пиво. Там null — неизвестно! 0 — точно означает, что денег на пиво не хватит, и мы не можем сделать транзакцию, с null — это неизвестно. Там может быть как 1 доллар, так и 10, 100, 1000 долларов и даже больше (но это вряд ли). Инициализация нулём неизвестных значений — такое же magic string, как и любое захардкоденное стринговое значение! Мало того, оно может создавать неопределённость, как в примере с кошельком.

Ещё один распространённый пример, когда используется дефолтное значение вместо null. Сущность имеет 2 поля: дату (время) создания и последнюю дату изменения. При этом дата изменения устанавливается вместе с датой создания, например, в конструкторе:

Я также встречал варианты, когда устанавливают для этого поля не текущую дату, а, например, 1 января 1970 года, не в том суть. Суть в том, что изменений ещё не было, а дата последнего изменения уже есть, что противоречит логике! И для того, чтобы понять, было ли реально редактирование, нам нужно сравнить дату изменения с датой создания (или с 01.01.1970, или ещё с какой-то «магической» датой). Но ведь можно просто хранить в этом поле null, что будет правильно с логической и удобно с технической точки зрения!

Если вы всё же ещё не уверены, стоит ли использовать null вместо значений по умолчанию там, где логически должен быть null, и вам до сих пор кажется, что это просто «вопрос удобства» и не может нести каких-либо негативных последствий, то подумайте о том, что логика приложения (даже не сейчас, в будущем) может подразумевать деление на это значение, и если там будет 0 — то мы можем получить ошибку, связанную с делением на 0. Если же там будет какое-то другое «магическое число», то мы просто получим неправильное значение. Это, кстати, ещё хуже, так как ошибку с делением на ноль мы поймаем и оттестируем, а вот арифметическую ошибку — вряд ли.

Резюмируя: nullable поля используем для значений, которые могут быть неизвестны (а могут быть и известны, зависит от бизнес-логики), но сущность может быть инициализирована без этих значений, и в таких случаях используем только nullable, а не 0, −1, −100500, int.MaxValue и прочий изврат. Аналогично и для стринговых значений: если бизнес-логика говорит, что может быть пустая строка, тогда пустая строка означает пустую строку, а null означает отсутствие установленного значения!

Резюме. Если бизнес-логика разрешает нам иметь неинициализированное поле класса после создания объекта этого класса — то нужно использовать nullable-тип, а не придумывать различные невероятные значения, которые в нашем представлении должны выполнять ту же функцию, что и null.

6. Проверка входных/выходных данных

Правило, о котором многие архитекторы забывают или не придают ему значения — валидация данных. Причём в идеале валидация должна быть на входе каждого уровня, потому как компоненты могут использоваться разными приложениями. В БД могут также писать те приложения, которые вы не можете проконтролировать.

Если какой-то уровень (например, слой бизнес-логики) получает данные из внешнего источника в широком понимании (это может быть и слой представления, передающий введённые пользователем данные с формы, и внешний веб-сервис, данные с которого также попадают в слой бизнес-логики через слой представления, и база данных, из которой выбираются данные), то этот слой должен быть уверен, что данные валидны. Если же он не может быть уверен, что они валидны, он должен провести их валидацию «на входе» (которую можно выделить в отдельный «подслой») и только «на входе». Никакой валидации внутри самих методов, которые обрабатывают бизнес-логику, быть не должно! Если метод производит вычисления, то он должен делать только это и априори считать, что входные данные валидны и там не будет, скажем, деления на 0 из-за неправильных входных данных. А если будет брошено такое исключение — то это должно означать, что ошибка в самой логике метода.

Конечно, нет особого смысла добавлять валидацию в каждый уровень, если мы уверены, что наш компонент получает данные только от другого, «доверенного» компонента, где пройдена вся валидация и мы можем проконтролировать эту валидацию. Но как только возникает ситуация, что наш компонент должен получать данные откуда-то извне — мы должны задуматься о его «личной» валидации.

Как только в нашу БД может писать ещё одно приложение, которое мы не контролируем — мы должны задуматься о валидации данных, полученных из этой БД. Хорошо, если мы можем написать правила валидации на уровне БД, но, честно сказать, MS SQL не позволяет легко и гибко писать сложные правила. Если правило чуть сложнее примитивной логики (которая контролируется ключами и constraints) — нужно писать триггер, который необходимо уже писать «в довесок», если мы используем Code-First подход, со всеми вытекающими сложностями.

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

Резюме. Все данные, пришедшие от «ненадёжного» источника (будь то пользовательский интерфейс или БД, доступ к которой имеют другие приложения), всегда должны быть проверены настолько тщательно, насколько это возможно. Это позволит в будущем избежать трудноуловимых и тяжело исправляемых ошибок, связанных с поврежденными данными.

7. Использование Exceptions

Ещё один момент, с которым я часто сталкиваюсь, рассматривая чужие проекты или обсуждая их с архитекторами, — это использование Exceptions. Честно сказать, чаще встречается неправильное его использование, нежели правильное 🙂

Самый «тяжёлый» и один из самых распространённых случаев — это нечто подобное этому:

А. Вариант «заглушки»

Или

Смысл этого действия — подавить исключение. Плюсы: возможно, «пронесёт» и ошибка не будет замечена на клиенте. Минусы: очень тяжело отлавливаемые ошибки и, возможно, порча данных, которые будут либо записаны в хранилище, либо возвращены клиенту.

Б. Вариант бессмысленного исключения

Это то, что сделал мой сербский друг © в проекте — ввёл какое-то своё исключение, причём общее для всех случаев. И он с гордым видом, что он правильно, как ему кажется, использует исключения, везде и всюду начал бросать это исключение с сообщением из исходного исключения. Какой смысл в этом подходе? Смысла нет никакого. Это его общее исключение TmsException абсолютно ничего нам не даёт, и этот префикс Tms для обозначения принадлежности к проекту TMS абсолютно ни о чём. Как всё же правильно использовать исключения?

Прежде всего, нужно понять смысл разных типов исключений: смысл в том, чтобы донести приложению, которое вызвало наш метод и поймало исключение, как правильно обработать это исключение! Если разницы в обработке 2-х исключений нет — тогда нет смысла бросать 2 разных типа исключения! Если же исключение, вызванное невозможностью выполнить какую-то арифметическую операцию по причине того, что результат не попадает в ожидаемый диапазон, должно обрабатываться иначе, нежели исключение, вызванное отсутствием пользователя с таким ID — тогда мы должны бросать 2 разных типа исключения, а не писать умные и лаконичные сообщения об ошибках, обрамлённые в одно общее исключение. Давайте рассмотрим на примере.

У нас есть метод

await geoService.GetPlaceByCoordinatesAsync(lat, lng);

который бросает exception:

Если переданы неправильные значения lat или(и) lng. Это необязательно должны быть значения, не попадающие в диапазон −90..90 и −180..180. Это может быть случай, когда мы должны проверить через какой-то внешний источник, что данная точка не является, например, водной поверхностью.

Бросает exception:

Если внешний сервис для того, чтобы понять, суша там или вода, недоступен.

И бросает exception:

Если такие координаты запрещено использовать нашему пользователю.

Теперь рассмотрим клиентов, которые используют наш метод GetPlaceByCoordinatesAsync. Это может быть Web API. Тогда мы должны вернуть конечному клиенту в первом случае ошибку 400 (Bad Request), во втором случае — 500 (Internal Server error), а в третьем — 403 (Forbidden):

То есть каждый тип исключения мы анализируем в своём блоке catch и результат обрабатывается по-разному. Если у нас это будет MVC приложения, то в первом случае мы можем записать какую-то ошибку в ModelState и вернуть страницу с этой ошибкой. Во втором случае — сделать редирект на какую-то особую страницу, предназначенную именно для этого, а в третьем — ещё как-то обработать. Никаких magic strings, записанных в сообщении общего исключения, которые нужно анализировать, парсить (а если есть локализация — то вообще ужас), никаких общих исключений. Каждому случаю, который может быть обработан по-особенному — своё исключение!

Резюме. Всегда уделяйте внимание правильной обработке ошибок. Почти что никогда (только в редких случаях) не «подавляйте» исключения. Подавление исключений довольно быстро превратит поддержку и дальнейшую разработку приложения в сущий ад. Каждая потенциальная ошибка должна быть перехвачена, обработана соответствующим образом и не должна остаться незаметной для разработчика.

8. Вычисляемые поля и их хранение в БД

Ещё одна любимая фишка горе-архитекторов — хранение вычисляемых полей в БД (или другом хранилище) без острой на то необходимости. Например, есть какое-то вычисляемое поле (по одной записи, по множеству или даже по нескольким таблицам, неважно), и его зачем-то обязательно пытаются записать в БД. Но запись этого поля в БД — дублирование данных и нарушение SRP (Single Responsible Principle).

Если поле вычисляемое, оно и должно, без крайней необходимости, вычисляться. Это может быть специальный класс-сервис с методами, занимающийся этими вычислениями или это может быть сделано средствами БД. Но в случае изменений каких-то данных ни у кого не должна болеть голова о том, что нужно где-то там, какое-то там поле пересчитать и перезаписать! Потому что, во-первых, это обязательно забудется и клиент рано или поздно получит устаревшие данные, которые будут расходиться с текущими данными. Во-вторых, должна быть одна и только одна причина для изменения.

Но что же за острая необходимость иногда хранить эти вычисляемые поля в БД, о которых я обмолвился ранее? Это те случаи, когда вычисления либо слишком сложные и, соответственно, слишком затратные по ресурсам, либо слишком затратные по времени, а результат должен быть возвращён здесь и сейчас. Этакая форма кэша.

Например, у нас есть сущность Trip, общее расстояние которой вычисляется внешним сервисом. Сама сущность меняется довольно редко, но её расстояние запрашивается часто. Было бы нерационально каждый раз делать запрос к внешнему сервису (который, к тому же, может быть ещё и платным), чтобы снова, в сотый раз, получить то же самое значение, потому что сама сущность не менялась и будет ли меняться — неизвестно. Но в таком случае у нас есть момент, который мы не должны упустить. В случае изменения записи, мы обязательно должны пересчитать и переписать её расстояние, иначе у нас просто будут неправильные данные. Это как раз тот случай с острой необходимостью. А хранить в отдельном поле каждой записи общее количество записей, относящихся к данному пользователю, не то, что бессмысленно, но и вредно!

Вот один из примеров, как не нужно делать.

Есть сущность Trip:

Где CalculatedDistance — расстояние всего маршрута, от начальной до конечной точки.

И сущность TripStop:

Где DistanceFromPreviousStop — расстояние от предыдущей точки. Так как весь маршрут составляется из таких вот контрольних точек-стопов и у каждой задано расстояние от предыдущей, то нет никакого смысла иметь общее расстояние в сущности Trip, потому что общее расстояние высчитывается без особых ресурсозатрат. Мало того, это ещё и вредно, так как необходимо следить за тем, чтобы значение всегда было обновлено при каких-то изменениях, касающихся расстояния (по сути, это тоже нарушение SRP (Single Responsible Principle), о котором мы говорили ранее).

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

Резюме. Без необходимости не храните в БД данные, которые можно получить путём вычисления или выборки других данных.

9. «Исключительные случаи» и «дублирование» в архитектуре

Ещё одно зло, постепенно вносящее бардак в проект — это нарушение принятой архитектуры проекта, «исключительные случаи». Это бывает, либо когда проще обойти архитектуру, нежели ей следовать, либо когда новый человек на проекте не хочет вникать в архитектуру или ломать свой привычный стиль разработки и начинает самодельничать — создавать свои сборки в тех случаях, когда стоит расширять существующие, переименовывать классы/интерфейсы/неймспейсы на свой лад и т. д. Большинство тимлидов и архитекторов закрывают глаза на это, не придавая должного значения. Но в первом случае такой подход (когда проще обойти архитектуру, нежели ей следовать) часто говорит о том, что архитектура слишком сложна или неправильна. А во втором случае может привести к тому, что в дальнейшем проект будет состоять из этаких лоскутков разноцветной ткани, сшитой воедино, с кодом, расбросанным по десяткам сборок, который должен быть в одном классе и конфликтами имён.

Поначалу может казаться, будто бы мы разделяем код между 2-мя разработчиками, чтобы их код не пересекался друг с другом, так сказать инкапсулируем код каждого в отдельной сборке, но в результате мы получаем то, что оба класса с обеих сборок будут использоваться в одном месте, часто перекрывая друг друга.

Например, если разработчик «А» создал интерфейс в своей сборке:

А разработчик «Б» создал интерфейс с точно таким же именем в своей сборке:

То рано или поздно оба интерфейса будут использованы в одном и том же классе, удваивая количество зависимостей. Они будут делать похожие действия и конфликтовать друг с другом.

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

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

10. Интерфейсы и их реализация

Как мы знаем из теории, интерфейсы представляют собой этакую абстракцию, описывающую методы и члены, которые должен иметь конкретный класс. Эта абстракция не должна быть привязана ни к какой конкретной реализации. Это как каркас для крыши дома, которую потом можно покрыть шифером, черепицей, металлочерепицей или ещё чем-нибудь. И я никогда не понимал, зачем класть саму реализацию в ту же сборку, где и интерфейс? В чём тогда вообще смысл интерфейса, если его реализация там же? Ведь интерфейс в таком случае косвенно имеет те же зависимости, что и какая-то конкретная реализация, о которой он ничего не должен знать!

Вот что я увидел в реальном проекте:

Обратите внимание, что интерфейсы и их реализации лежат в одной сборке. Это тот случай, как делать не стоит!

При написании юнит-тестов для самого интерфейса придётся тащить все зависимости конкретной реализации. Например, если интерфейс описывает методы для сохранения данных в хранилище, то сборка с юнит-тестами будет зависеть, например, от Entity Framework, если «дефолтная» реализация интерфейса лежит «рядом» с интерфейсом. Интерфейсы — это часть бизнес-логики и они должны быть в слое бизнес-логики. А каждая конкретная реализация интерфейса должна быть в своей «личной» сборке.

11. «Заглушки» и прочие способы подавления ошибок

В этом разделе я хочу поговорить о столь любимых некоторыми архитекторами и разработчиками различных «заглушках», которые позволяют «подавить» ошибки и создать иллюзию, что «всё хорошо». Одной из самых распространённых мы уже касались ранее — подавление каких-либо исключений:

То есть код, обёрнутый в try, выполнился с ошибкой. Ну и ладно, сделаем вид, что всё хорошо. Клиент не увидит на веб-странице противной жёлтой ошибки с куском стека, а в настольном приложении не вылетит раздражающее окошко с последующим закрытием самого приложения. Но, начиная с этого момента, мы не можем гарантировать, что, если мы что-то писали в БД, наши данные не повреждены, или что клиент получил правильные данные, если мы выбирали данные из источника или проводили вычисления. Мы не можем быть уверены, что все дальнейшие вызовы по цепочке имеют правильные значения параметров. Кроме того, при таком подходе мы никогда даже не узнаем, что ошибка была. За такой код (за очень редкими исключениями) стоит разжаловать сеньора или архитектора в джуны или, что лучше всего, уволить сразу без права работать на этом проекте в будущем!

Аналогичный эффект могут иметь «значения по умолчанию» вместо значений null (о чём мы также говорили ранее), если они участвуют в каких-либо математических вычислениях.

Почему лучше получить крах приложения «здесь и сейчас», нежели подавление ошибки каким-либо способом? Потому что отловленная ошибка — это уже больше, нежели половина дела. Это также какая-то гарантия того, что приложение работает или правильно, или никак! Это гарантия того, что приложение не продолжит работать с заведомо неправильными данными! Это гарантия того, что ошибка не прошла незамеченной!

Каждый, кто хоть раз сталкивался с ситуацией, когда подобный код маскировал ошибку и «портил» данные, а всплывало это только спустя полгода. Причём для установления причины приходилось потратить неделю, а большинство данных в итоге были безвозвратно испорчены за эти полгода по причине этой одной замаскированной, а не правильно обработанной ошибки. Тот, кто с этим сталкивался, осознаёт серьёзность подобного подавления потенциальных ошибок в угоду пожеланий заказчика, который хочет, чтобы его приложение никогда не заканчивало работу аварийно 🙂

Резюме. По возможности не смешивайте абстракции (интерфейсы или базовые классы) с их реализацией. Обычно это не приводит к каким-то непоправимым последствиям, но в таком случае часто приходится тянуть ненужные зависимости при использовании только абстракций. Например, при модульном тестировании.

12. Неочевидность использования библиотечного кода

Ещё один довольно распространённый подход, потенциально создающий проблемы в будущем: разработчик добавляет поле в класс, которое может быть заполнено, а может быть и нет. Чаще всего это происходит тогда, когда разработчики или не считают нужным создавать специализированный класс для данного конкретного случая. Или даже наоборот, боятся плодить похожие классы. Но, во-первых, проблема «дублирования» полей в схожих классах вполне решается абстрактным базовым классом на уровне одного слоя. А во-вторых, создание общего класса для нескольких случаев нарушает принцип SRP.

Давайте посмотрим, что может произойти, если мы создаём класс с полями, которые не обязательно заполняются данными (когда эти данные есть).

Например, в Entity Framework (да и наверняка и в других ORM системах) для доступа к данным есть Lazy Loading подход. То есть данные загружаются «по требованию», когда к ним идёт непосредственно обращение. Это опция может быть как включена, так и отключена. Но если вы хотите сделать доступными извне (что уже не очень хорошо) некоторые из классов, имеющие подобные виртуальные свойства с загрузкой «по запросу», то уже делайте так, чтобы эта опция была включена!

Например, у вас есть класс:

Есть интерфейс со следующим методом:

Реализация представляет собой что-то вроде этого (весь дополнительный код и проверки откинуты) и находится в отдельной сборке, как и полагается реализации:

Теперь я хочу использовать этот метод в своём проекте:

Всё компилируется и даже, может быть, всё работает. А может и нет. Точнее работает, но неправильно. Зависит от того, включили ли в «настройках» эту lazy loading. И она (эта lazy loading) может быть даже и была включена, причём случайно. Это, кстати, худший вариант. Потому что точно также ещё «случайно» и выключат, и никто сразу даже и не поймёт, что случилось. Всё будет также компилироваться, никаких эксепшенов бросаться не будет. Только не будут установлены необходимые claims, что, вероятно, будет замечено уже на продакшене (потому что, вроде как, ничего и не сломалось). Причём может быть с непоправимыми последствиями, если из-за этих неустановленных claims кто-то не получил доступ, куда нужно, или наоборот получил, куда не стоило.

Получается, что корректность работы библиотеки зависит от настроек, которые включаются на стороне клиента. Но библиотека не должна зависеть от клиента и его прихотей. Она должна или работать корректно, или валиться с исключением, в котором должно быть чётко указано, что не так (например, включите lazy loading, хотя в таком случае уже лучше включить на уровне библиотеки). Это и есть та «неочевидность использования», когда библиотека корректно работает тогда и только тогда, когда на клиенте выполняются условия A, B и C. В остальных же случаях она просто работает некорректно.

Резюме. Выполнение кода не должно зависеть от каких-либо настроек, либо эта разница в выполнении должна быть очевидна. Код должен либо выполняться правильно, либо не выполняться вообще.

Выводы

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

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

Одной
из основных причин изменений комплексов
программ являются
организационные
дефекты при модификации и расширении
функ
ций
ПС,
которые
отличаются от остальных типов и условно
могут быть выделены
как самостоятельные (см. рис. 10.2). Ошибки
и дефекты данного
типа появляются из-за недостаточного
понимания коллективом специалистов
технологии процесса ЖЦ ПС, а также
вследствие отсутствия четкой
его организации и поэтапного контроля
качества продуктов и изменений.
Это порождается пренебрежением
руководителей к организации всего

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

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

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

  • методология,
    технология и уровень автоматизации
    системного и структурного
    проектирования ПС, а также непосредственного
    программирования
    компонентов;

  • длительность
    с начала процесса тестирования и текущий
    этап разработки
    или сопровождения и модификации
    комплекса программ;

  • класс
    ПС, масштаб (размер) и типы компонентов,
    в которых обнаруживаются
    ошибки;

  • методы,
    виды и уровень автоматизации верификации
    и тестирования, их адекватность
    характеристикам компонентов и
    потенциально возможным
    в программах ошибкам;

  • виды
    и достоверность эталонов-тестов, которые
    используются для обнаружения
    ошибок.

Первичные
ошибки в ПС в порядке уменьшения их
влияния на
сложность
обнаружения и масштабы корректировок
можно разделить на следующие
группы (см. рис. 10.2):

  • ошибки,
    обусловленные сложностью компонентов
    и ПС в целом и наиболее
    сильно влияющие на размеры модификаций;

  • ошибки
    вследствие большого масштаба — размера
    комплекса программ,
    а также высоких требований к его
    качеству;

  • ошибки
    планирования и корректности требований
    модификаций часто
    могут быть наиболее критичным для
    общего успеха ЖЦ ПС и системы;

  • ошибки
    проектирования, разработки структуры
    и функций ПС в более полные и точные
    технические описания сценариев того,
    как комплекс программ и система будут
    функционировать;

  • системные
    ошибки, обусловленные отклонением
    функционирования
    ПС в реальной системе, и характеристик
    внешних объектов от предполагавшихся
    при проектировании;

  • алгоритмические
    ошибки, связанные с неполным формированием
    необходимых условий решения и некорректной
    постановкой целей функциональных
    задач;

  • ошибки
    реализации спецификаций изменений —
    программные дефекты,
    возможно, ошибки нарушения требований
    или структуры компонентов ПС;

  • программные
    ошибки, вследствие неправильной записи
    текстов программ
    на языке программирования и ошибок
    трансляции текстов изменений
    программ в объектный код;

  • ошибки
    в документации, которые наиболее легко
    обнаруживаются и в наименьшей степени
    влияют на функционирование и применение
    версий
    ПС;

  • технологические
    ошибки подготовки физических носителей
    и документации, а также ввода программ
    в память ЭВМ и вывода результатов на
    средства отображения.

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

  • сложность
    ошибок при создании корректировок
    компонентов
    и комплекса
    программ — статическая сложность,
    когда реализуются его требуемые функции,
    вносятся основные дефекты и ошибки;

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

К
группе факторов, влияющих на сложность
ошибок
комплексов
программ, относятся:

  • величина
    — размер модифицируемой программы,
    выраженная числом
    строк текста, функциональных точек или
    количеством программных модулей в
    комплексе;

  • количество
    обрабатываемых переменных или размер
    и структура памяти, используемой для
    размещения базы данных корректировок;

  • трудоемкость
    разработки изменений комплекса программ;

  • длительность
    разработки и реализации корректировок;

  • число
    специалистов, участвующих в ЖЦ комплекса
    программ.

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

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

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

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

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

Масштаб
размер
комплексов программ и их изменяемой
части
наиболее
сильно влияет на количество ошибок, а
также на требования к качеству
ПС
(см. лекцию 5). Качество откорректированного
ПС характеризуется
многими показателями, состав которых
зависит от класса и конкретного
назначения комплекса программ. Ниже
предполагается, что всегда модификации
ПС соответствуют заданному функциональному
назначению и основным требованиям
заказчика к их качеству. По мере увеличения
размера и повышения требований к качеству
ПС и его корректировкам
затраты на обнаружение и устранение
ошибок ПС увеличиваются
все более высокими темпами. Одновременно
расширяется диапазон неопределенности
достигаемого качества. В зоне высокого
качества программ возрастают трудности
измерения этих характеристик, что может
приводить
к необходимости изменения затрат в
несколько раз в зависимости
от применяемых методов и результатов
оценки качества ПС. Вследствие этого в
ЖЦ сложных и сверхсложных ПС всегда
велики проявления неустраненных ошибок
и недостаточна достоверность оценок
достигнутого
качества.

Ошибки
корректности формирования и планирования
выполне
ния
требований к ПС
часто
считаются наиболее критичными для
общего успеха
версий программного продукта и системы.
Ошибки требований являются наиболее
трудными для обнаружения и наиболее
сложными для исправления.
Вот почему исправление ошибок требований
может быть в 15—70
раз дороже, чем ошибок их программирования.
Требование к изме-

нению
может быть пропущено в спецификации к
системе и ПС. Это ведет к
неудовлетворенности пользователя, и
программа считается заказчиком и
пользователем ошибочной. Пропуск
некоторых требований — это наиболее
обычная проблема среди ошибок требований.
Ошибка требований может представлять
собой конфликтующие требования в
спецификации модификаций.
Например, два требования, которым
необходимо следовать, имеют противоположный
смысл. Может проявляться неопределенность
требований
— такой способ формулирования требования,
что даже если и не
конфликтует с другим требованием, оно
выражено недостаточно ясно, чтобы
привести к единственному, конструктивному
решению при разработке изменения.
Конечный пользователь часто называет
это ошибкой, хотя
на самом деле это выбор конструктивного
решения на основе неполного или
неопределенного требования. Многочисленные
исследования показали,
что ошибки требований дороже всего
исправить и труднее всего обнаружить.

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

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

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

Ошибки
перевода — наиболее коварные среди
всех ошибок структурного
уровня. Они проявляются, когда требования
заказчика интерпретируются
неправильно, по крайней мере, с точки
зрения конечного пользователя.
Если разработчик структуры либо неверно
прочитает требования, либо
не увидит содержание требования, так
же как конечный пользователь,
появится ошибка разработки структуры
данного компонента или ПС.

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

Характеристики
внешних объектов, принятые в качестве
исходных данных в процессе разработки
алгоритмов, могут являться результатом
аналитических расчетов, моделирования
или исследования аналогичных систем.
Во всех случаях может отсутствовать
полная адекватность условий получения
предполагаемых и реальных характеристик
внешней среды, что является причиной
сложных и трудно обнаруживаемых ошибок.
Это
усугубляется тем, что очень часто
невозможно заранее предусмотреть все
разнообразие возможных внешних условий
и реальных сценариев функционирования
и применения версий программного
продукта.

При
автономной и в начале комплексной
отладки версий ПС относительная доля
системных ошибок может быть невелика
(около 10%), но она существенно
возрастает (до 35—40%) на завершающих
этапах комплексной отладки новых базовых
версий ПС. В процессе сопровождения
системные ошибки являются преобладающими
(около 60—80% от всех оши-

бок).
Следует также отметить большое количество
команд, корректируемых при исправлении
каждой такой ошибки (около 20—50 команд
на одну ошибку).

Алгоритмические
ошибки
программ
трудно поддаются обнаружению
методами статического автоматического
контроля. Трудность их обнаружения
и локализация определяется, прежде
всего, отсутствием для многих логических
программ строго формализованной
постановки задачи,
полной и точной спецификации, которую
можно использовать в качестве
эталона для сравнения результатов
функционирования программ. К алгоритмическим
ошибкам следует отнести, прежде всего,
ошибки, обусловленные
некорректной постановкой требований
к функциональным задачам,
когда в спецификациях не полностью
оговорены все условия, необходимые
для получения правильного результата.
Эти условия формируются и уточняются
в значительной части в процессе
тестирования и выявления ошибок в
результатах функционирования программ.
Ошибки, обусловленные
неполным учетом всех условий решения
задач, являются наиболее
частыми в этой группе и составляют до
50—70% всех алгоритмических ошибок.

К
алгоритмическим ошибкам следует отнести
также ошибки интерфейса
модулей и функциональных групп программ,
когда информация, необходимая
для функционирования некоторой части
программы, оказывается не полностью
подготовленной программами, предшествующими
по
времени включения, или неправильно
передаются информация и управление
между взаимодействующими модулями.
Этот вид ошибок составляет около 10% от
общего количества, и их можно квалифицировать
как ошибки некорректной постановки
задач. Алгоритмические ошибки проявляются
в неполном учете диапазонов изменения
переменных, в неправильной оценке
точности используемых и получаемых
величин, в неправильном
учете корреляции между различными
переменными, в неадекватном
представлении формализованных условий
решения задачи в виде частных спецификаций
или блок-схем, подлежащих программированию.
Эти
обстоятельства являются причиной того,
что для исправления каждой алгоритмической
ошибки приходится изменять в среднем
около 20 команд (строк
текста), т.е. существенно больше, чем при
программных ошибках.

Особую,
весьма существенную, часть алгоритмических
ошибок в системах
реального времени, при сопровождении
составляют просчеты в

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

Ошибки
реализации спецификаций компонентов

это программные
дефекты, возможно, ошибки требований,
структуры или программные ошибки
компонентов. Ошибки реализации наиболее
обычны и, в общем, наиболее легки для
исправления в системе, что не делает
проблему легче для
программистов (см. таблицу 10.1). В отличие
от ошибок требований и структурных
ошибок, которые обычно специфичны для
приложения, программисты часто совершают
при кодировании одни и те же виды ошибок.

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

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

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

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

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

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

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

Технологические
ошибки
документации
и фиксирования программ в памяти ЭВМ
составляют иногда до 10% от общего числа
ошибок, обнаруживаемых
при тестировании. Большинство
технологических ошибок выявляется
автоматически статическими методами.
При ручной подготовке текстов машинных
носителей при однократном фиксировании
исходные данные
имеют вероятность искажения около 10
«3
10~4
на символ. Дублированной
подготовкой и логическим контролем
вероятность технологической
ошибки может быть снижена до уровня 10
5

10′7
на символ. Непосредственное
участие человека в подготовке данных
для ввода в ЭВМ и
при анализе результатов функционирования
программ по данным на дисплеях определяет
в значительной степени их уровень
достоверности и не позволяет полностью
пренебрегать этим типом ошибок в
программах.

В
примере
анализа ошибок конкретного крупного
проекта
было
принято, что завершилась инспекция
начального запрограммированного кода
крупного ПС на предмет его соответствия
рабочей проектной спецификации,
в ходе которой было обнаружено 3,48 ошибки
на тысячу строк кода. Наибольшее
совпадение аппроксимации рэлеевской
кривой распределения
ошибок с фактическими данными установлено
для момента получения этих данных, ему
соответствует значение, равное также
3,48. Значения
числа ошибок на тысячу строк получены
при пересчетах на более ранние
этапы соответственно эскизного — (3,3)
и рабочего — (7,8) проектирования
программ. При прогнозировании в
соответствии с рэлеевской кривой
распределения вероятности проявления
дефектов программ на следующем
этапе квалификационного тестирования
компонентов следовало ожидать
обнаружения около 2,12 ошибки на тысячу
строк исходного кода.

В
случае сохранения той же закономерности
в момент поставки клиенту на
испытания программный продукт мог
содержать менее 0,07 ошибки на тысячу
строк кода. Отмечается также, что частость
проявления 0,1—0,05 ошибки
на тысячу строк кода можно считать
допустимой для ответственных
систем реального времени.

В
исследованиях 20 крупных поставляемых
программных продуктов, созданных в 13
различных организациях, коллективы
специалистов добились среднего уровня
0,06 дефекта на тысячу строк нового и
измененного программного
кода. При использовании структурного
метода в пяти проектах достигнуто
0,04—0,075 ошибки на тысячу строк. Таким
образом, уровень
ошибок около 0,05 на тысячу строк кода
в
разных публикациях считается близким
к предельному для высококачественных
программных продуктов.

Другим
примером оценок уровня ошибок критического
ПС особенно высокого
качества может служить программный
продукт бортовых систем «Шаттла»,
созданный NASA.
По оценке авторов, в нем содержится
менее
одной ошибки на 10 000 строк кода. Однако
стоимость программного
продукта достигает 1000 $ за строку кода,
что в среднем в сто раз больше,
чем для административных систем, и в
десять раз больше, чем для ряда
ординарных критических управляющих
систем реального времени.

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

мах
приводит к целесообразности
последовательного, методичного
фиксирования и анализа возможности
проявления любого типа дефектов и
необходимости их исключения на наиболее
ранних этапах ЖЦ ПС при минимальных
затратах.

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

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

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

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

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

  • анализ
    рисков следует начинать с подготовки
    детальных исходных требований
    и характеристик проекта ПС, системы и
    внешней среды, для которых должны
    отсутствовать риски функционирования
    и применения;

  • для
    управления рисками и их сокращения в
    рассматриваемых проектах
    сложных комплексов программ рекомендуется
    выделять три класса

276

рисков:
функциональной пригодности ПС,
конструктивных характеристик качества
и нарушения ограничений ресурсов при
реализации процессов ЖЦ ПС;

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]

  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #

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

Ошибка № 1. Слишком узкое имя класса или интерфейса

Не всегда получается предусмотреть направление развития программы и/или библиотеки. Это может привести к тому, что имя класса или интерфейса оказывается слишком узким для его фактической области применения.

Первый вариант подобной проблемы состоит в том, что имя перестает соответствовать иерархии наследования. Например, изначально в корне иерархии находился интерфейс Camera. Затем обнаружилось, что крайне удобно реализовать класс для работы с электронным телескопом Telescope, унаследовав этот интерфейс, чтобы иметь возможность использовать существующий полиморфный код. Но телескоп — это не камера, поэтому не стоит нарушать логику.

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

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

См: Пример полиморфизма в C++ на основе ООП

Ошибка № 2. Неконтролируемое разрастание иерархий наследования

Наследование — это полезный и мощный прием программирования. Но нужно знать меру. Если в приложении есть некая сущность, то это не означает, что для нее должен обязательно существовать класс.

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

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

См: Профессия программиста: Абстрактное мышление

Ошибка № 3. Вызов функций не по смыслу, а из «удобства»

Иногда, когда поджимают сроки сдачи проекта, возникает соблазн что-то где-то срезать и упростить. Почти всегда это приводит к проблемам в будущем.

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

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

Ошибка № 4. Осознанное дублирование фрагментов кода

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

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

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

См: Принцип DRY в действии

Ошибка № 5. Неподходящие имена функций и переменных

Этот тип ошибок во многом пересекается с ошибками № 1 и 3, но все же я решил выделить его отдельно, поскольку между ними имеются концептуальные отличия.

Часто из-за спешки мы даем функциям и переменным не самые подходящие имена. Иногда это связано с неполным пониманием предметной области или с ошибками проектирования. Но это не оправдание.

Конечно, придерживаться 100%-ой чистоты тоже нет необходимости. В простом for-цикле вполне можно использовать переменную i, но чем больше область видимости, тем осмысленнее должно быть имя.

Еще хуже, если имя осмысленное, но вводит в заблуждение. Т.е. из названия следует одно, а на самом деле переменная хранит что-то другое; или функция делает не то, что от нее можно ожидать (например, имеются побочные эффекты).

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

См: Принцип единой ответственности

Похожие публикации

Предложите, как улучшить StudyLib

(Для жалоб на нарушения авторских прав, используйте

другую форму
)

Ваш е-мэйл

Заполните, если хотите получить ответ

Оцените наш проект

1

2

3

4

5

Понравилась статья? Поделить с друзьями:
  • Ошибки ораторов картинки
  • Ошибки ноутбука hp caps lock мигает
  • Ошибки оратора и пути их исправления
  • Ошибки нотариуса при оформлении наследства
  • Ошибки нотариуса при оформлении завещания