Чистый код: Принцип подстановки Барбары Лисков (LSP)
Принцип подстановки Лисков гласит, что если метод использует базовый класс, то он должен иметь возможность использовать любой из его производных классов без необходимости иметь информацию о производном классе.
Трудно предоставить разумный пример иллюстрирующий этот принцип, так как соблюдение элементарной логики и правил чистого кода по именованию методов и переменных, не позволяет его нарушить. Если в базовом классе есть метод save(), отвечающий за сохранение информации, а вы не пытаетесь его переделать для загрузки данных, у вас все в порядке.
Рассмотрим тонкости соблюдения этого принципа, на довольно сложном примере. Начнем с класса хранения данных.
Особенностью этих данных является наличие классификатора реализованного через перечисление. В самом примитивном случае таблица базы данных выглядела бы следующим образом.
А интерфейс для записи данных в базу и его реализация примерно так.
В данном фрагменте нарушений принципа подстановки Лисков нет. Однако такая структура базы данных и кода не оптимальна. При вхождении товара в несколько групп, придется дублировать записи товара либо создавать новые категории. Например, книга может принадлежать категории печатная продукция и подарки. Дублирование приведет к засорению базы, а новые категории потребуют изменения исходного кода. Приведение таблиц к нормальной форме изменит базу данных вот таким образом.
Теперь отпадает необходимость в дублировании, так как под каждую категорию выделена отдельная таблица, которая хранит ссылки на товары. Маркетологи могут создавать новые категории хоть каждый день, мы просто добавим новую таблицу.
Однако такие изменения заставят нас отказаться от решения, где весь SQL запрос делается в одном методе. Ведь чтобы сделать запись конкретного значения товара, необходимо сделать две последовательные записи. Первая строка делает запись в таблицу товара, а вторая получает значение идентификатора предыдущей записи и в свою очередь делает запись в соответствующую таблицу.
Если мы попытаемся просто добавить еще одну запись в тот же метод, мы не только неоправданно усложним сам метод, но и в прямую нарушим принцип подстановки Лисков. Клиенту использующему этот метод придется «учитывать» такое поведение.
Все это еще не большая проблема, но написание чистого кода подразумевает читаемость и возможность легкого расширения функционала. Наш же код несет как минимум две потенциальные проблемы. Первая возникнет, когда нам понадобится добавить еще один классификатор. SQL команд станет больше. Вторая потенциальная проблема возникнет, если в таблицы классификаторов понадобится записывать дополнительную информацию. Следовательно необходимо не допустить нарушения описываемого принципа, разделив команды между собой.
Цепочка рассуждений достаточно проста, команды записи в разные таблицы должны быть разделены, но на этапе выполнения скомпонованы в одну команду, так как запись товара без классификатора и классификатора без товара не имеют смысла.
Из описания понятно, что для организации работы с командами лучше всего подходит паттерн под названием Компоновщик, который нам позволит из набора небольших однотипных команд компоновать сложные наборы.
Полный код программы приведен в конце статьи однако хотелось бы прокомментировать, несколько использованных приемов.
В первую очереди рассмотрим как с помощью компоновщика создаются команды для записи в базу данных.
Существует три таблицы значений классификатора товара. В каждую из них необходимо поместить ID товара который принадлежит этому значению классификатора. Для реализации функционала создается соответствующее количество команд реализованных в виде классов AddCereals, AddDrinks, AddPacks. Каждая из этих команд может быть выполнена самостоятельно, например при пере классификации товаров.
Перечисленные команды позволяют работать с каждым значением классификатора отдельно. Однако нам необходимо выполнять запись и для всего классификатора одной командой AddClassifier. Фактически эта команда получает информацию о классификации товара и сама выбирает в какую таблицу записывать данные.
Команда AddProduct записывает все данные о товаре единовременно, используя как составные части другие команды.
Теперь нарушение принципа подстановки Лисков устранено. Каждая из этих команд может быть выполнена отдельно и «не знать» о строении других, хотя фактически они работают совместно.
Следующий момент, на котором бы хотелось остановить внимание это функциональный объект.
Он предназначен для группировки всех команд отвечающих за запись значений классификатора. Фактически он предоставляет массив указателей, из которого в последствии, команда AddClassifier, выбирает необходимый. Думаю понятно, почему его необходимо выделить, классификатор может быть со временем расширен.
Однако этот класс не обязан быть реализован в виде полноценного функционального объекта. Это учебный пример и этот класс сделан именно так, чтобы не нарушать принципов объектно-ориентированного программирования. Но язык C++ дает нам возможность упростить этот участок, использовав для этих целей функциональные объекты из стандартной библиотеки, например std::function.
В реальном проекте, лично я бы сделал это именно таким образом, ведь значительно проще добавить новую функцию, чем учитывать возможность изменения классификатора через наследование.
Последним участком кода на который хотелось бы обратить ваше внимание, является класс.
Его задача объединить подключение к базе данных и передаваемые команды.
Во многих проектах это делается линейно, когда мы открываем соединение, отправляем команду, получаем ответ и закрываем соединение. Однако лучше придерживаться такой структуры.
Об этом можно написать отдельную статью, думаю я ее напишу. Но в рамках текущей, хочется пояснить, что такое построение позволяет создать условия для дальнейшей модификации и избежать множества сложностей при реализации взаимодействия с внешними ресурсами.
Отделение соединения от команд позволит:
правильно организовать обработку исключительных ситуаций,
возможность использования много поточного подключения,
при необходимости перейти на другую базу данных без существенных затрат на переработку исходного кода.