принцип барбары лисков php
Принцип подстановки Лисков — PHP: Погружаясь в классы
Переопределение методов на техническом уровне ничем не ограничено. Класс наследник может изменить поведение любого метода настолько, насколько это вообще возможно. С одной стороны, может показаться что это здорово, так как открывается большая свобода действий, но с другой, некоторые изменения могут повлечь за собой серьёзные архитектурные проблемы. Самая главная из них – сломанный полиморфизм.
Рассмотрим пример. Допустим, мы решили написать свой собственный логгер (объект, который записывает в журнал произвольные сообщения), базирующийся на PSR3.
Предположим, что нам это не понравилось, и мы решили изменить сигнатуру так, чтобы уровень передавался вторым параметром. Это позволит задать нам значение по умолчанию для того уровня, который чаще всего встречается в приложении. Для этого создадим подтип MyLoggerInterface с переопределённой сигнатурой метода log, а затем реализуем его в классе MyLogger.
Что не так с этим кодом? Если вы прошли курс по полиморфизму, то ответ должен быть очевиден. Так как наш класс реализует интерфейс MyLoggerInterface, то он реализует и LoggerInterface. Это значит, что в любом месте где требуется последний, мы можем передать объект класса MyLogger:
В 1987 году Барбара Лисков сформулировала принцип подстановки (Liskov Substitution Principle – LSP), следование которому позволяет правильно строить иерархии типов:
Пусть q(x) является свойством, верным относительно объектов x некоторого типа T. Тогда q(y) также должно быть верным для объектов y типа S, где S является подтипом типа T.
Звучит математично. Многие разработчики пытались переформулировать это правило так, чтобы оно было интуитивно понятным. Самая простая формулировка звучит так:
Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.
Для любознательных. Этот принцип любят показывать на иерархиях наследования классов, но как вы видите из текста выше, этот принцип относится к интерфейсам, а не классам. Иерархии классов не обязаны следовать ему, хотя было бы неплохо.
Для ещё более любознательных. Почему вообще понадобился этот принцип? Почему бы не поручить эту работу языку? К сожалению, технически невозможно убедиться в соблюдении принципа Лисков. Поэтому его выполнение ложится на плечи разработчиков.
Правила проектирования иерархий типов
Существует несколько правил, которые надо учитывать при работе с типами:
Предусловия – это ограничения на входные данные, а постусловия – на выходные. Причём в силу ограничений систем типов, многие из таких условий невозможно описать на уровне интерфейсов. Их либо придётся описывать просто текстом, как это сделано в документации PSR, либо добавлять проверки в код (проектирование по контракту).
Например, в нашем логгере (в интерфейсе LoggerInterface) предусловием является то, что метод log первым параметром принимает один из 8 уровней сообщений. Принцип Лисков утверждает, что мы не можем создать класс, реализующий этот интерфейс, который может обрабатывать меньшее число уровней. Это и называется усилением предусловий, то есть требования становятся жёстче. Вместо 8 уровней, например 5. Попытка использовать объект такого класса, закончится ошибкой, когда какая-то из систем попробует передать ему уровень, который не поддерживается. Причём не важно, приведёт это к ошибке (исключению) или логгер молча проглотит это сообщение не записав его в журнал. Главное, что поведение стало отличаться.
Встречаются ситуации, когда разработчики не видя причину такого поведения, начинают лечить следствия. В местах, где используются подобные объекты, добавляются проверки на типы. А это убивает полиморфизм.
Принцип подстановки Барбары Лисков
Это гостевой выпуск Пятиминутки PHP — ведёт Кирилл Сулимовский.
Также порекомендую подписаться на телеграм канал Кирилла: https://t.me/beerphp
«Наследующий класс должен дополнять, а не замещать поведение базового класса».
Звучит логично и понятно, расходимся. но блин, как этого добиться? Почему-то многие просто пропускают мимо ушей следующие строки, которые как раз отлично объясняют что нужно делать.
Рассмотрим выражение «Предусловия не могут быть усилены в подклассе»
❗️Другими словами дочерние классы не должны создавать больше предусловий, чем это определено в базовом классе, для выполнения некоторого бизнесового поведения. Вот несложный пример.
❌ Добавление второго условия как раз является усилением. Так делать не надо!
Контравариантность также можно отнести к данному приниципу. Она касается параметров функции, которые может ожидать подкласс. Подкласс может увеличить свой диапазон параметров, но он должен принять все параметры, которые принимает родительский.
• Этот пример показывает, как расширение допускается, потому что метод Bar->process() принимает все типы параметров, которые принимает родительский метод.
Таким образом, мы не добавляем дополнительных проверок, не делаем условия жестче и наш дочерний класс уже ведёт себя более предсказуемо.
В следующих постах мы также рассмотрим постуловия и ковариантность 😉
«Постусловия не могут быть ослаблены в подклассе».
То есть подклассы должны выполнять все постусловия, которые определены в базовом классе. Постусловия проверяют состояние возвращаемого объекта на выходе из функции. Вот такой пример.
❌Условное выражение проверяющее результат является постусловием в базовом классе, а в наследнике его уже нет. Не делай так!
Сюда-же можно отнести и ковариантность, которая позволяет объявлять в методе дочернего класса типом возвращаемого значения подтип того типа (ШО?!), который возвращает родительский метод.
❗️Таким образом в дочернем классе мы сузили возвращаемое значение. Не ослабили. А усилили 🙂
❗️Все условия базового класса — также должны быть сохранены и в подклассе.
Инварианты — это некоторые условия, которые остаются истинными на протяжении всей жизни объекта. Как правило, инварианты передают внутреннее состояние объекта. Например типы свойств базового класса не должны изменяться в дочернем.
Здесь также стоит упомянуть исторические ограничения («правило истории»):
❗️Подкласс не должен создавать новых мутаторов свойств базового класса.
Если базовый класс не предусматривал методов для изменения определенных в нем свойств, подтип этого класса так же не должен создавать таких методов. Иными словами, неизменяемые данные базового класса не должны быть изменяемыми в подклассе.
Единственность ответственности, Открытость/закрытость, Подстановка Лисков, Разделение интерфейсов и Инверсия зависимостей. Пять принципов, которыми необходимо руководствоваться при написании кода.
Так как оба принципа Подстановки Лисков и Разделения интерфейсов довольно легко определить и проиллюстрировать, в этом уроке мы поговорим о них вместе.
Принцип Подстановки Лисков
Производный класс никогда не должен ломать определение родительского класса.
Концепция этого принципа впервые была представлена Барборой Лисков на конференции в 1987 году и позже опубликована в печати вместе с Джанет Винг в 1994 году. Оригинальное определение звучит следующим образом:
Пусть q(x) применимо к объектам x типа T. Тогда q(y) должно быть применимо для объектов y типа S где S является подтипом T.
Позже в публикации о SOLID принципах Роберта С. Мартина в его книге Гибкая Разработка, Принципы, Шаблоны, Практики и затем переизданная для языка C# версия Гибкие Принципы, Шаблоны и практики в C#, определение стало известно как Принцип Подстановки Лисков.
Это приводит нас к определению Роберта С. Мартина:
Подтипы должны быть заменяемы для базового типа.
Проще говоря, подкласс может менять методы родительского класса только так, чтобы не ломать существующий функционал с точки зрения клиента. Вот простой пример, демонстрирующий эту концепцию.
Что приводит нас к простой реализации паттерна проектирования Шаблонного Метода, который мы использовали в уроке про Принцип Открытости/закрытости.
Классический пример нарушения принципа
Чтобы окончательно все проиллюстрировать, мы обратимся к классическому примеру, так как он довольно выразительный и легко понимаемый.
Однако действительно ли Square это Rectangle в программировании?
Вполне возможно, что у клиента есть класс, который проверяет площадь и выбрасывает исключение в случае ошибки.
И мы создали простой тест где мы отправляем пустой объект прямоугольника в метод проверки площади, чтобы тест прошел. Если наш класс Square определен верно, его отправка в клиентский areaVerifier() не должен сломать функционал. В конце концов Square это Rectangle с математической точки зрения. Однако как на самом деле?
Простой тест все сломал. Когда мы запустим этот тест, будет выброшено исключение.
Я особенно люблю этот пример, так как он не только нарушает Лисков но и демонстрирует, что объектно-ориентированное программирование не про сопоставление реальных объектов виртуальным. Каждый объект в нашей программе должен быть абстракцией над концепцией. Если мы попытаемся сопоставить один к одному реальные объекты с объектами в программе, то мы практически всегда потерпим неудачу.
Принцип разделения интерфейса
Во всех модульных приложениях должен быть какой-то интерфейс, через который клиент может им управлять. Это могут быть как просто объявленные интерфейсы или другие классические объекты реализуемые паттернами, как например Фасад. Не важно какое решение используется. У них всегда одно и то же назначение: коммуникация с клиентским кодом о том как работать с модулем. Эти интерфейсы могут находиться между модулями в одном приложении или проекте или между проектом и сторонней библиотекой, обслуживающей другой проект. Опять же, не имеет никакого значения. Общение есть общение и клиент есть клиент, никак не касаются индивида, пишущего код.
Итак, как мы должны определить эти интерфейсы? Мы можем подумать о нашем модуле и обнажить весь функционал, который хотим предложить.
Выглядит как не плохое начало. Отличный способ определить что мы хотим реализовать в нашем модуле. Или нет? Такой старт может привести нас к одному из двух возможных реализаций:
Очевидно, что ни одно и решений не приемлемо для реализации нашей бизнес-логики.
Можно пойти другим путем. Разбить интерфейс на части, ограниченными конкретной реализацией. Это поможет использовать не большие классы, которые заботятся о собственном интерфейсе. Объекты, реализующие эти интересы будут использоваться разными типами транспортных средств, например автомобиль, как в примере выше. Автомобиль будет использовать реализации но зависеть от интерфейсов. Поэтому схема ниже может быть более выразительна.
Но это фундаментально меняет наше восприятие архитектуры. Car становится клиентом а не реализацией. Мы все еще хотим предоставить нашим клиентам пути использовать весь модуль, будучи определенным типом транспортного средства.
Принцип разделения интерфейса утверждает, что клиент не должен зависеть от методов, которые он не использует.
Интерфейсы принадлежат их клиентам, а не реализациям. Таким образом, мы всегда должны проектировать их так, чтобы они отвечали запросам клиентов. Иногда мы точно знаем наших клиентов, иногда нет. Но когда это возможно, мы должны разбить наши интерфейсы на много маленьких, чтобы они лучше удовлетворяли конкретным нуждам клиентов.
Конечно, это приведет нас к некоторой проблеме повтора кода. Но запомните! Интерфейсы это лишь план имен функций. Здесь нет никаких реализаций или какой-либо логики. Поэтому повторы кода минимальны и управляемы.
Тогда мы имеем отличное преимущество для наших клиентов использовать только то, что им действительно нужно. В некоторых случаях клиентам необходимо несколько интерфейсов, что является нормой, до тех пор пока они используют все методы интерфейсов, от которых зависят.
Другой хороший трюк заключается в том, что в нашей бизнес-логике единичный класс может реализовать несколько интерфейсов, если надо. Поэтому мы можем предоставить единую имплементацию для всех общих методов между интерфейсами. Разделенные интерфейсы также вынудят нас думать о коде больше с точки зрения клиента, что в свою очередь приведет к слабой связанности и простому тестированию. Мы сделаем наш код не только лучше для клиента но и проще для самих себя, для понимания, тестирования и реализации.
Заключение
Подстановка Лисков учит нас почему реальность не может быть сопоставлена с программными объектами и как субтипы должны уважать родителей. Мы так же осветили другие принципы, которые уже знаем.
Разделение интерфейсов учит нас уважать клиентов больше чем нам казалось необходимо. Уважение их нужд делает наш код луше а нашу жизнь как программистов проще.
SOLID на практике — Принцип подстановки Барбары Лисков
Принцип подстановки [Барбары] Лисков (Liskov Substitution Principle — LSP, буква L в аббревиатуре SOLID), сформулирован Барбарой Лисков в 1987 году и звучит следующим образом:
Упрощенное описание этого принципа предложил Роберт Мартин:
Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.
Иными словами, поведение реализующих и наследующих классов не должно противоречить поведению базовых типов.
Практическое применение принципа
В посте «SOLID на практике — принцип инверсии зависимостей» фигурировал интерфейс TopicRepository и реализующий его класс JpaEntityManagerTopicRepository:
Следуя принципу подстановки Барбары Лисков, мы можем разработать реализацию интерфейса TopicRepository с применением, например, репозиториев Spring Data, которая будет иметь аналогичное поведение.
Определим интерфейс Spring Data:
и напишем реализацию:
Объект типа SpringDataTopicRepository можно использовать в качестве реализации TopicRepository вместо JpaEntityManagerTopicRepository, поведение приложения от этого не изменится. Если изменится источник данных, и вместо PostgreSQL, например, будет использоваться MongoDB, то достаточно будет написать реализацию TopicRepository, использующую MongoDB. Впрочем, если в качестве базового типа репозиториев Spring Data используется CrudRepository, то и этого делать не придётся — Spring Data сделает всё за разработчика.
Более того, пока функциональность TopicRepository может быть реализована средствами Spring Data, мы можем не писать реализацию этого интерфейса, а наследовать интерфейс SpringDataTopicRepository от TopicRepository и CrudRepository, что не противоречит принципам SOLID, но позволяет уменьшить количество кода:
Принципы SOLID с примерами на php
Шпаргалка
Тема SOLID-принципов и в целом чистоты кода не раз поднималась на Хабре и, возможно, уже порядком изъезженная. Но тем не менее, не так давно мне приходилось проходить собеседования в одну интересную IT-компанию, где меня попросили рассказать о принципах SOLID с примерами и ситуациями, когда я не соблюл эти принципы и к чему это привело. И в тот момент я понял, что на каком-то подсознательном уровне я понимаю эти принципы и даже могут назвать их все, но привести лаконичные и понятные примеры для меня стало проблемой. Поэтому я и решил для себя самого и для сообщества обобщить информацию по SOLID-принципам для ещё лучшего её понимания. Статья должна быть полезной, для людей только знакомящихся с SOLID-принципами, также, как и для людей «съевших собаку» на SOLID-принципах.
Для тех, кто знаком с принципами и хочет только освежить память о них и их использовании, можно обратиться сразу к шпаргалке в конце статьи.
Что же такое SOLID-принципы? Если верить определению Wikipedia, это:
аббревиатура пяти основных принципов дизайна классов в объектно-ориентированном проектировании — Single responsibility,Open-closed, Liskov substitution, Interface segregation и Dependency inversion.
Таким образом, мы имеем 5 принципов, которые и рассмотрим ниже:
Принцип единственной ответственности (Single responsibility)
Итак, в качества примера возьмём довольно популярный и широкоиспользуемый пример — интернет-магазин с заказами, товарами и покупателями.
Принцип единственной ответственности гласит — «На каждый объект должна быть возложена одна единственная обязанность». Т.е. другими словами — конкретный класс должен решать конкретную задачу — ни больше, ни меньше.
Рассмотрим следующее описание класса для представления заказа в интернет-магазине:
Как можно увидеть, данный класс выполняет операций для 3 различный типов задач: работа с самим заказом( calculateTotalSum, getItems, getItemsCount, addItem, deleteItem ), отображение заказа( printOrder, showOrder ) и работа с хранилищем данных( load, save, update, delete ).
К чему это может привести?
Приводит это к тому, что в случае, если мы хотим внести изменения в методы печати или работы хранилища, мы изменяем сам класс заказа, что может привести к его неработоспособности.
Решить эту проблему стоит разделением данного класса на 3 отдельных класса, каждый из которых будет заниматься своей задачей
Теперь каждый класс занимается своей конкретной задачей и для каждого класса есть только 1 причина для его изменения.
Принцип открытости/закрытости (Open-closed)
Принцип подстановки Барбары Лисков (Liskov substitution)
Пожалуй, принцип, который вызывает самые большие затруднения в понимании.
Принцип гласит — «Объекты в программе могут быть заменены их наследниками без изменения свойств программы». Своими словами я бы это сказал так — при использовании наследника класса результат выполнения кода должен быть предсказуем и не изменять свойств метод.
К сожалению, придумать доступного примера для это принципа в рамках задачи интернет-магазина я не смог, но есть классический пример с иерархией геометрических фигур и вычисления площади. Код примера ниже.
Пример иерархии прямоугольника и квадрата и вычислении их площади.
Очевидно, что такой код явно выполняется не так, как от него этого ждут.
Но в чём проблема? Разве «квадрат» не является «прямоугольником»? Является, но в геометрических понятиях. В понятиях же объектов, квадрат не есть прямоугольник, поскольку поведение объекта «квадрат» не согласуется с поведением объекта «прямоугольник».
Тогда же как решить проблему?
Решение тесно связано с таким понятием как проектирование по контракту. Описание проектирования по контракту может занять не одну статью, поэтому ограничимся особенностями, которые касаются принципа Лисков.
Проектирование по контракту ведет к некоторым ограничениям на то, как контракты могут взаимодействовать с наследованием, а именно:
«Что ещё за пред- и постусловия?» — можете спросите Вы.
Ответ: предусловия – это то, что должно быть выполнено вызывающей стороной перед вызовом метода, постусловия – это то, что, гарантируется вызываемым методом.
Поэтому, лучше в рамках ООП и задачи расчёта площади фигуры не делать иерархию «квадрат» наследует «прямоугольник», а сделать их как 2 отдельные сущности:
Хороший реальный пример несоблюдения принципа Лискоу и решения, принятого в связи с этим, рассмотрен в книге Роберта Мартина «Быстрая разработка программ» в разделе «Принцип подстановки Лискоу. Реальный пример».
Принцип разделения интерфейса (Interface segregation)
Данный принцип гласит, что «Много специализированных интерфейсов лучше, чем один универсальный»
Соблюдение этого принципа необходимо для того, чтобы классы-клиенты использующий/реализующий интерфейс знали только о тех методах, которые они используют, что ведёт к уменьшению количества неиспользуемого кода.
Вернёмся примеру с интернет-магазином.
Предположим наши товары могут иметь промокод, скидку, у них есть какая-то цена, состояние и т.д. Если это одежда то для неё устанавливается из какого материала сделана, цвет и размер.
Опишем следующий интерфейс
Данный интефейс плох тем, что он включает слишком много методов. А что, если наш класс товаров не может иметь скидок или промокодов, либо для него нет смысла устанавливать материал из которого сделан (например, для книг). Таким образом, чтобы не реализовывать в каждом классе неиспользуемые в нём методы, лучше разбить интерфейс на несколько мелких и каждым конкретным классом реализовывать нужные интерфейсы.
Принцип инверсии зависимостей (Dependency Invertion)
Принцип гласит — «Зависимости внутри системы строятся на основе абстракций. Модули верхнего уровня не зависят от модулей нижнего уровня. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций». Данное определение можно сократить — «зависимости должны строится относительно абстракций, а не деталей».
Для примера рассмотрим оплату заказа покупателем.
Таким образом, класс Customer теперь зависит только от абстракции, а конкретную реализацию, т.е. детали, ему не так важны.
Шпаргалка
Резюмируя всё выше изложенное, хотелось бы сделать следующую шпаргалку
Надеюсь, моя «шпаргалка» поможет кому-нибудь в понимании принципов SOLID и даст толчок к их использованию в своих проектах.
Спасибо за внимание.