О плохом и хорошем коде для чайников, часть 4. Level-up: принципы проектирования.

28 0

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

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

Предисловие

- Да ладно, сделаем как получится,
а там сдадим в поддержку, и пусть
разбираются, как хотят!

Типичный внедренец в глазах поддержки

Жизненный цикл любого здорового программного продукта выглядит следующим образом: анализ --> проектирование --> разработка --> тестирование --> использование и поддержка. Первые четыре из них обычно объединяют в один, называемый внедрением. И каждый раз, принимая на поддержку очередной проект, администраторы гадают, с чем же им придётся иметь дело. Хорошо, если на внедрении был толковый инженер: меньше будет проблем с настройкой окружения и системы. Ещё лучше, если к толковому инженеру прилагался такой же разработчик: можно надеяться, что система не сложится словно карточный домик от первого же внесённого изменения. Но ожидания редко сходятся с реальностью на 100%, и хотя идеал недостижим, к нему всё же можно и нужно стремиться. Так какая же она, идеальная система?

Проектирование – наше всё

Чик-чик – и в продакшн!

Первая антизаповедь программиста

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

В объектно-ориентированном программировании есть концепция SOLID, которая устанавливает признаки плохого проектирования и принципы, позволяющие от них избавиться. Давайте рассмотрим эту концепцию более подробно.

Как понять, что сделано плохо

И видел я 4 всадников: на белом коне, рыжем,
чёрном и бледном, и имя этим всадникам
Чума, Война, Голод и Смерть

Что-то библейское

Согласно SOLID, основных признаков плохого проектирования всего 4:

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

  2. Хрупкость (fragility). Очень похожа на жёсткость, но, в отличие от неё, проявляется в возникновении ошибок в компонентах системы, не связанных с изменяемым явно или хотя бы концептуально. Каждое исправление будет больше вносить проблемы, чем решать их.

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

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

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

Как сделать хорошо

И увидел программист новый проект,
и требования к нему, и сказал:
- Хорошо есть, и хорошо весьма!

Оптимистичное представление о программистах

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

  • Принцип единственности ответственности (The Single Responsibility Principle);

  • Принцип открытости/закрытости (The Open Closed Principle);

  • Принцип замещения Барбары Лисков (The Liskov Substitution Principle);

  • Принцип разделения интерфейсов (The Interface Segregation Principle);

  • Принцип инверсии зависимостей (The Dependency Inversion Principle).

Принцип единственности ответственности

То, что ты можешь делать что-то ещё, далеко не
означает, что ты должен этим заниматься.

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

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

  ErrorMessage = SaveContractor(ContractorCard)
  SaveContractor:
  Errors = ‘’
  if ContractorCard.Requisites(‘Name’).IsNull
    Errors = Errors & ‘Наименование контрагента’ & CR
  endif
  …
  if Length(ContractorCard.Requisites(‘INN’).Value) > 12
    Errors = Errors & ‘В поле ИНН должно быть введено не более 12 символов’ & CR
  endif
  …
  ContractorCard.Requisites(‘BankAccountHidden’).Value = ContractorCard.Requisites(‘BankAccount’).Value
  ContractorCard.Requisites(‘BankAccount’).Value = ‘**********’
  …  

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

Принцип открыт-закрыт

Как сделать из чайника кофеварку? Элементарно!
Просто подключите к нему кофемолку и фильтр!

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

  Approvers = GetApproversForReferenceRecord(ReferenceRecord, ApprovalLevel)

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

Принцип замещения (подстановки) Лисков

Выглядит как кофе, пахнет как кофе,
но заваривается молоком, это точно кофе?

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

Предположим, что у нас имеется код, использующий экземпляр класса A, и класс B, унаследованный от A. Экземпляры класса A в нашем коде не создаются, а принимаются в качестве параметров откуда-то снаружи. Это «снаружи» может создать и экземпляр класса B и передать его в наш код под видом A (что выглядит не так криминально, ведь B наследуется от A). Если при этом наш код продолжит работать без ошибок, то класс B соблюдает принцип подстановки Лисков.

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

Принцип разделения интерфейсов

Прежде чем припаивать провода от лампочки
напрямую к розетке, попробуй воспользоваться вилкой

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

Применить этот принцип в прикладной разработке напрямую вряд ли удастся, но можно воспользоваться тем, что он соблюдается в объектной модели IS-Builder. Лучше всего это видно на примере таблицы MBAnalit, хранящей записи всех справочников, в том числе – справочника Пользователи. Для большинства справочников обращение к таблице идёт через интерфейс IReference и его свойство Requisites. Но справочник Пользователи является служебным, и в нём содержится небольшой статичный объём данных, поэтому для обращения к нему выделен интерфейс IUser, у которого нет свойства Requisites, но есть свойства Name, FullName, ID, Code и т.д. Как правило, этих возможностей вполне достаточно для выполнения с пользователями каких-то базовых действий.

Принцип инверсии зависимостей

Хочешь подключить флэшку к ридеру?
Сначала догадайся, в какой разъём!

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

Применительно к ISBL это означает максимальный уровень абстракции, как в примере выше для принципа открыт-закрыт: функция GetApproversForReferenceRecord вернёт объект IUserList независимо от того, какому справочнику принадлежит запись ReferenceRecord. Мы не привязываемся к типу справочника снаружи функции, только внутри. Об инверсии в названии принципа также забывать не стоит: если какая-то функция делает что-то с несколькими справочниками, то чаще всего имеет смысл вызывать не справочники внутри функции, а функцию на событиях справочников.

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

Вместо заключения

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

P.S. А какие принципы проектирования применяются у вас? Делитесь в комментариях 

Пока комментариев нет.

Авторизуйтесь, чтобы написать комментарий