SOLID. Что такое SOLID и кому это надо. Часть 2
Первые два принципа SOLID были рассмотрены в предыдущей части. SOLID. Часть 1. В этой части продолжим рассматривать эти принципы с примерами. Осталось всего три из них.
Если из названия первого и второго принципа было более-менее понятно что они значат, то из следующего ничего не понятно, если вы читаете об этом в первый раз.
Liskov Substitution. Третий принцип SOLID.
С этим принципом все понятно, пойдем дальше. Именно так в университете о нем упомянули бы. Ну а мне не получится его пропустить, увы.
Что же это такое Liskov и зачем делать этот Substitution?
Давайте добавим немного сухости этой статье и опишем формально что этот принцип значит. А дальше все же попробуем копнуть чуток глубже.
Внимание, впереди сухость!
Liskov – это Барбара Лисков, ученая в области информатики, которая выдвинула данный принцип. Википедия вам в помощь.
Liskov Substitution принцип гласит следующее: Если A подтип B, тогда объект типа B может быть заменен объектом A.
Попробуйте теперь сказать что ничего не понятно.
Другими словами, объекты в программе могут быть заменяемы на их наследников (дочерние классы) без серьезных последствий для программы.
Как следствие, наследуемые классы должны дополнять поведение, а не полностью его замещать.
Как вы поняли, этот принцип касается больше наследования. На самом деле из этого пункта можно вывести много других под-принципов, но для начала давайте рассмотрим пример что бы понять с чем мы имеем дело.
Давайте рассмотрим животных, для этого создадим класс Animal. Конечно животные могут делать очень много: есть, пить, бэээкать-мэээкать, испражняться. Но нас в данном примере будут интересовать только лапы. Если быть точнее – то их количество. Давайте абстрагируемся от понятия Животного в биологическом смысле. В данном примере будем понимать его как любое живое существо: зебра, косуля, рыба, человек, насекомые и т.д.
В итоге имеем такой класс:
abstract Class Animal { public function getLegsNumber(); }
Теперь попробуем воспользоваться этим классом. Для этого создадим пару-тройку новых: кота и сороконожку.
Class Cat extends Animal { public function getLegsNumber() { return 4; } }
Class Сentipede extends Animal { public function getLegsNumber() { return 40; } }
На данный момент все хорошо, но а теперь представим что нам понадобилась змея. Но классу Snake на нужен метод getLegsNumber. получается если мы наследуем класс Animal – нам нужно добавить этот метод, что в сущности, будет лишним. В данном случае лучше не. Использовать. Наследование. Вы скажете: “Да поставь ты ноль и все будет по красоте”, на что я отрицательно качну головой. Возможно, нам надо в программе узнать какое давление оказывает животное на поверхность, для этого надо вес животного разделить на поверхность которой он касается, а это площадь лапы умножить на их количество.
Давление = Вес животного / (Площадь лапы х Количество)
То есть для змеи нам прийдется делить на 0. И это Очень! Плохо! Серьезно! И тут наша программа падает… “Поставь единицу вместо нуля”, – возразите вы. Тогда нам просто понадобится умножить поверхность низа змеи, которой она касается земли, на 1 и все заработает. Вроде бы да, но нет. Метод getLegsNumber не будет отображать действительность, так как одноногих змей никто пока не видел.
Interface Segregation. Принцип разделения интерфейса.
Больше – не всегда лучше. Но это не про этот принцип.
Этот принцип гласит о том, что несколько специфических интерфейсов лучше чем один общий.
Если в интерфейсе кажется слишком много методов и/или они четко могут быть разделены на отдельные сущности – смело создавайте несколько интерфейсов.
interface CreateWorld { createGod(); createPeople(); createLanguages(); makePhysics(); killDinosaurus(); createMusic(); ... apocalypse(); }
Вот такой замечательный интерфейс у нас есть. Проблема в том, что если мы заходим использовать этот интерфейс – мы вынуждены будем реализовать каждый его метод. Но возможно не в любом мире нужны будут все детали мира. Возможно в каком-то мире без динозавров будут жить глухонемые люди, которым не понадобится метод createMusic().
Этот пример немного абстрактный и тут тяжело четко разделить его. на более мелкие интерфейсы, так как не ясно какие цели у этой задачи.
Давайте рассмотрим еще один пример. Создадим интерфейс с названием Payment. Он у нас будет платить, добавлять карту, показывать сумму оплаты и процент, который система будет брать себе за процедуру оплаты. Думаю этого будет достаточно.
interface Payment { pay(); addCard(); getAmount(); getFee(); }
Хороший маленький интерфейсик у нас получился. Но давайте подумаем что может пойти не так.
Данный интерфейс у нас привязан к какой-то карте. Но карта – это не единственный способ оплаты в нашем грешном мире. Можно оплатить чеком, наличкой, натурой, взять в долг в конце концов! И во всех случаях нам прийдется реализовать этот метод. Думаю я вас убедил, что этот метод лучше вынести отдельно, подальше от текущего интерфейса.
Второй метод, на который вы должны были обратить внимание (ведь вы же читаете уже 4й принцип SOLID, потому должны уже имеете достаточно опыта для этого) – так это метод getFee. Опять же, большинство систем будут брать какую-то комиссию за транзакцию, но это не всегда будет происходить. Возможно кто-то захочет не брать, а отдавать. Реализовать так скажем кеш-бек.
Исходя из вышеописанного, хорошим решением будет разделить текущий интерфейс на следующие:
interface Payment { pay(); getAmount(); } interface Card { addCard(); deleteCard(); ... } interface Fee { getFee(); calculateFee(); }
И так как нам вселенная разрешает реализовать сколь-угодное количество интерфейсов – мы можем заимплементить их все. Или не все. Или не имплементить. Дальше все на ваше усмотрение, никто не принуждает реализовывать все интерфейсы если вы сами того не хотите.
В этой статье мы рассмотрели два принципа SOLID. Liskov Substitution и Interface Segregation. В следующей, рассмотрим последний принцип и сделаем выводы. Поймем когда надо использовать эти пять SOLID принципов, а когда можно и воздержаться. Разберемся как они нам упрощают жизнь.