Comments 90
А если бы мы избрали подход с композицией и внедрением зависимостей, то получилось бы так:Обратная сторона копозиции это повторяющийся код. В вашем примере фактически каждый контроллер в системе должен иметь такой конструктор.
class BlogController {
public BlogController (
TemplateRenderer templateRenderer
) {
}
}
Оно является чем-то плохим, если надо синхронизировать изменения во всех местах.
Представьте, что сигнатура рендерера изменилась, например, в качестве данных шаблона стал ожидаться мэп, а не список туплов. В случае абстрактного контроллера достаточно изменить один метод в нём…
class Container {
public List<Component> Components;
public Container(Component[] сomponents);
}
class TemplateRenderer extends Component;
// использование:
var blogController = new Container([templateRenderer]);
В результате для создания любого Controller-а можно обойтись без дополнительного наследования, абстрактных классов и дублирования кода.
blogController обычно имеет специфически только для него методы типа showPosts(page = 1), showPost(id), createPost(postData) и т. п. которые и дергают templateRenderer с теми или иными параметрами. Можно, конечно, вместо наследования от базового класса контроллера и явной их имплементации как-то собирать их через лямбды, переданные в конструктор, но, скорее всего, потеряем, как минbмум, автодополнение, контроль типов, удобную навигацию в IDE и т. п.
Component templateRenderer = new TemplateRenderer();
Component postController = new PostController();
postController.templateRenderer = templateRenderer;
Container blogController = new Container([templateRenderer, postController]);
Еще в класс Component можно добавить ссылку на объект-контейнер
и реализовать в нем метод поиска нужного компонента, например по типу.
Тогда вместо строки postController.templateRenderer = templateRenderer;
можно в самом postController сделать поиск templateRenderer среди компонентов связанного объекта-контейнера.
Откуда я такой подход взял:
docs.unity3d.com/ru/current/Manual/UsingComponents.html
martalex.gitbooks.io/gameprogrammingpatterns/content/chapter-5/5.1-component.html
Так, класс Square никогда не должен наследовать класс Rectangle.
а что мешает? раз математически / логически квадрат — частный случай прямоугольника, программирование продиктовано логикой.
Например, вам нужна функция которая увеличивает площадь прямоугольников в 2 раза и подходящее решеним, например, увеличить их ширину в 2 раза:
public static void doubleSquare(List<Rectangle> rectangles) {
for (Rectangle rectangle : rectangles) {
rectangle.setWidth(rectangle.getWidth() * 2);
}
}
Но если в списке прямоугольников будут и квадраты, то поведение становится неправильным.
Например, так, чтобы каждый изменял сразу обе стороны.
Тогда квадрат останется квадратом.
Сначала нужно зафиксировать контракт. В посте он не зафиксирован.
Понамешают функциональщины с ООП, потом сами разобраться не могут и кричат, что ООП профнепригодно :)
Функция doubleSquare в том виде как представлена, нарушает принцип инкапсуляции ООП (все изменения объекта только средствами самого объекта). Программист — ССЗБ в этом случае, ООП ни при чем.
мы ожидаем, что изменим каждую из сторон независимо
Почему вы этого ожидаете? Контракт к setWidth/setHeight вам этого не обещал.
Мы нарушили контракт ожидаемого поведения Rectangle?
Без понятия, я не знаю что мне обещает документация к Rectangle. Вы на основании чего-то считаете что после вызова setWidth метод getHeight будет возвращать то же, что и до вызова. Но на основании чего вы так решили?
Без понятия, я не знаю что мне обещает документация к Rectangle.
Учебник геометрии за пятый класс достаточно точно дает определение тому, что есть прямоугольник. Обсуждается как раз таки классический случай из геометрии. Контекст обсуждения предельно ясен и прозрачен. Вы можете, конечно, его пытаться искусственно усложнить в угоду своей позиции, но боюсь на этом дальнейшее обсуждение скатится к софистике.
Вы на основании чего-то считаете что после вызова setWidth метод getHeight будет возвращать то же, что и до вызова.
Не знаю даже. Может быть вот en.wikipedia.org/wiki/Rectangle
На основании того, что стороны прямоугольника являются не связанными друг с другом величинами, но в совокупности придающими прямоугольнику дополнительные свойства. Например, периметр, выведение которого производится общеизвестным методом.
А может быть и: https://en.wikipedia.org/wiki/Golden_rectangle
Вы зря в обсуждении программной модели пытаетесь сослаться на математические абстракции, которые даже в самой математике имеют множество разных интерпретаций. Классический пример: https://en.wikipedia.org/wiki/0#Mathematics
Наследники должны расширять поведение родителей, а не замещать.
Если обещал. Например если есть тесты, которые проверяют, что ширина после изменения длины не изменилась. С другой стороны, должны быть тесты, которые проверяют, что длина квадрата изменяется синхронно с шириной, чтобы квадрат не прошёл тесты такого прямоугольника
Если обещал.
Ну собственно само название
setWidth
это подразумеваетЕсли квадрат является потомком прямоугольника то он обязан проходить все тесты, которые проходит прямоугольник. Гуглите LSP
Если наследник проходит абсолютно все тесты предка, то его поведение не меняется.
Неверно. Его поведение может меняться без нарушения прохождения тестов. Например могут появиться новые методы или свойства. Могут появиться опциональные аргументы в старых методах.
Зависит от уровня рефлекси. Какой-то user instanceof User обычно возвращает true как для самого User, так и для его наследников. Главное не лезть в имя класса и не проверять отсутствие членов.
Но думаю тут можно простить его нарушение, все-таки instanceof чаще всего возвращает true для подклассов, а если разработчик городит свои костыли, то он ССЗБ.
Да, с setWidth и setHeight всё просто — объект может обеспечить свои инварианты изменя связанные состояния. А вот с методом прямоугольника setDimensions( width , height )
уже сложнее, так как реализовать этот контракт квадрат в принципе не сможет.
На всякий случай — если он начнёт кидать исключение или тупо игнорировать в случае неравных параметров, то это тоже нарушение контракта.
Можно пойти функциональным путем и при вызове методов меняющих длину стороны квадрата не менять свойства самого объекта, а создавать новый объект (типа прямоугольник) и возвращать его как результат.
Что же касается функции, которая увеличивает площадь в 2 раза, первое, что мне пришло в голову — не ваш пример, а увеличение каждой из сторон в
sqrt(2)
раза, тогда одна и та же функция подойдет как для прямоугольника, так и для квадрата. С одной стороны увеличить одну из сторон проще, но тогда меняются пропорции прямоугольника, что в некоторых случаях неприемлемо. В истории квадрата и прямоугольника функции нахождения периметра и площади одинаковы в общем случае, этот факт и побуждает приводить этот пример, когда речь заходит про ООП.
А в целом вы говорите верно, ООП и ФП — всего лишь инструменты для достижения поставленной задачи. Вот тут хотелось бы увидеть больше конкретики и примеров что в каких случаях больше использовать. Дарю идею для
P.S. Мне в JS поначалу сильно не хватало трейтов из php для организации поведений и «мультинаследования» в ООП. Но со временем я научился думать по-другому.
P.P.S. В своих проектах использую оба подхода, но в крупных чаще ООП.
Что же касается функции, которая увеличивает площадь в 2 раза, первое, что мне пришло в голову — не ваш пример, а увеличение каждой из сторон в sqrt(2) раза
Это будет работать, пока компилятор не решит поменять порядок чтения:
- прочитать длину, увеличить длину, записать длину,
- прочитать ширину, увеличить ширину, записать ширину.
В таком варианте квадрат все равно увеличится в 4 раза.
Этот классический пример рассматривает только один из возможных контекстов использования квадратов и прямоугольников из, наверное, бесконечного множества.
Это же классический пример из литературы: у прямоугольника есть методы setWidth и setHeight, которые поидее работают независимо друг от друга. Но когда вы наследуете от него квардрат, вам нужно сделать так, чтобы при изменении ширины/высоты он оставался квдратом, то есть менялась и вторая величина. Кажется, что в этом нет ничего страшного, но нарушается LSP.
а наследуйте прямоугольник от квадрата да и всё )
Логически квадрат это не частный случай прямоугольника а вычисляемое свойство, то есть сущность прямоугольника которая позволяет менять ширину и длину время от времени может становится квадратом при совпадении длины и ширины
class Square {
isRectangle() {
return this.getWidth() == this.getHeight()
}
}
Только наоборот, Rectangle и isSquare
Нет никакого "логически является" пока нет конкретной задачи или, хотя бы, контекста. Для графического редактора квадрат может вообще с прямоугольником быть не связанным, а для геометрических задач сама операция изменения размеров не применима обычно.
struct Square { struct Rectangele
: Square {
float a; //float a
float b;
float perimeter() { float perimeter() {
return a * 4 return (a + b) * 2
} }
float area() { float area() {
return a * a return a * b;
} }
} }
struct Diamond
: Square {
float angle;
float area() {
return a * a / 2;
}
}
struct Parallelogram
: Diamond , Rectangele {
//...
}
С точки зрения операций чтения квадрат является подтипом прямоугольника. А вот с точки зрения операций записи уже прямоугольник является подтипом квадрата. В системе, где возможны и чтение и запись, очевидно, ни один из них не сможет быть подтипом другого.
Наследование должно удовлетворять требованию X является Y, а квадрат выполняет его не всегда.
Зависит от набора операций, определённых над прямоугольником. Точнее от его инвариантов и прочих *условий.
Если класс прямоугольника гарантирует, что соотношение сторон (угол диагонали) измениться не может после создания, то квадрат без особых проблем наследуется.
Если класс прямоугольника гарантирует, что изменение одной стороны не влияет на другую, что операция изменения угла диагонали определена ожидаемым образом, то очень мешает наследовать квадрат от него.
Мне кажется, что так как этот пример приводят в контексте геометрии, подразумевается, что единственный инвариант прямоугольника — сохранение прямых углов. И тогда квадрат от него наследовать нельзя, т.к. у него инвариантов больше.
Конечно, вариантов интерпретации может быть много, но это же чисто умозрительный пример, а не реальная задача.
Статья на мой взгляд какая-то немного странная.
С одной стороны лично я как-то не вижу чтобы прямо куча людей считали что "парадигма ООП умерла" или что она теперь не особо актуальна или ещё что-то в этом роде.
С другой стороны если такие люди и есть, то этой статьёй и приведёнными в ней примерами таких людей вряд ли убедишь в обратном.
Интересно, но не объяснено почему нельзя наследовать Square от Rectangle ("ведут себя по-разному" так себе аргумент). Один комментатор предложил, что у Rectangle меняются обе стороны, а у Square — одна. Так-то оно так, но я бы сказал, что можно реализовать по-другому. Раз Square — частный случай Rectangle (в математике), то я бы написал так:
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
class Square(Rectangle):
def __init__(self, size):
super().__init__(size, size)
А потом какой-нибудь код, не знающий разницы между квадратом и прямоугольником меняет созданному Square width.
Контракт Rectangle подразумевает изменение только одной стороны одной функцией.
Кто сказал?
У вас контракты описываются исключительно через имя метода?
Не расскажете тогда какой у вас в данном случае Naming convention что он позволяет понять что метод setWidth меняет ещё скажем и площадь фигуры?
Как бы вы отнеслись к коллекции, у которой метод sort вместо сортировки всех данных, удалял бы все содержимое? Не посчитали ли бы вы naming convention такой библиотеки, как минимум, странным? Или если бы коллекция ImmutableItems на самом деле была бы мутабельной? Если вы будете игнорировать naming convention во время составления контракта, то вы потеряете очень важную составляющую.
А контракт класса тоже идёт по naming convention? А что мешает кому-то в контракте класса прописать что высота у данного конкретного класса тоже "вычисляется" и зависит от ширины?
Дело не в этом. Ясное дело что наш пример примитивен и достаточно просто понять как он работает. И как он по идее должен работать мы вроде себе тоже представляем.
Но это не мешает кому-то написать свою собственную реализацию этого класса, в которой этот класс будет для него интуитивен и понятен, а для нас нет. Но при этом всё ещё будет нормально описывать геометрические фигуры.
А если взять не такой банальный пример, а какую-нибудь сложную бизнес-логику из реальных процессов, то там ещё "опаснее" полагаться на интуицию. И только на naming conventions тоже опасно полагаться.
И для того чтобы определить противоречат наименования или нет надо иметь на руках этот самый контракт.
И даже в примере с квадратами/прямоугольниками этот "контракт" похоже немного разный у разных людей.
Контракт конвеншина set об ычно подразумевает исключительно что get вернёт то же самое. Ни разу не видел тестов, проверяющих, что результаты других get не изменились после вызова конкретного set.
Более того, как основным аргументом введения сеттеров вместо публичных свойств является соблюдение инвариантов класса посредством сколь угодно сложной логики
Вы постулируете, что площадь вычисляемое свойство в комментарии Н-го уровня. Не спортивно.
Все три сеттера могут быть непримитивными и выставлять значения и других приватных свойств, все три геттера могут быть вычислимыми. Например, приватные свойства могут отражать размеры прямоугольник в полярной системе координат, типа радиуса описанной окружности и угла диагонали.
Ну мы же говорим о различных вариантах определения классов в ООП, а не о математике как таковой. И теоретически никто не запрещает вам сделать класс у которого площадь это будет просто обычный проперти. И если вы меняете ширину, то вам надо самому вычислять новую площадь и тоже её менять "вручную".
Это в общем случае не особо логично, но возможно.
Да нигде, что вы. Вы на ссылку-то посмотрите — она ведет, для начала, на обсуждение функции left_pad в NPM. С каких-это пор Javascript (node) и его NPM стали хоть сколько-то репрезентативным примером применения ФП?
>Просто берёшь и заворачиваешься в монаду.
Ну да. Правильнее было бы сказать как-то так: инверсия зависимостей в ФП реализуется и действует иногда совсем не так, как это принято в ООП, что может вводить в заблуждение неопытных разработчиков (я бы сюда включил и автора оригинала). Это было бы правильнее.
Почему совсем не так? Вы не путаете инверсию зависимостей и иньекцией зависимостей?
Не, не путаю. В ООП инверсия зависимостей в том, что зависеть вы должны от интерфейсов, а не от реализаций. В ФП это выглядит иначе. В принципе, мое замечание касается обоих: как инверсии, так и иньекции.
Автор оригинала не удосужился пояснить, что именно он имел в виду, а переводчик усугубил:
Если же вы хотите наработать базу кода, удобную в поддержке, то лучше придерживаться принципов чистого кода, в частности, применять инверсию зависимостей, при которой ФП на практике также значительно усложняется.
и почему это оно вдруг усложняется. При этом ссылка в данном абзаце в переводе ведет совсем не туда, как в оригинале:
Оригинал: pasztor.at/blog/clean-code-dependencies
Перевод: www.piter.com/collection/all/product/spring-vse-patterny-proektirovaniya
Ну т.е. автор ссылается на себя, а переводчик — рекламирует свои переводы.
В ФП по большому счёту это выглядит так же, по-моему. Просто вместо классового интерфейса или абстрактного класса, ФП функция будет зависеть от функционального типа, сигнатуры "колбэка"
В целом, хорошо бы вообще обходиться без состояния – хранить в классах изменяемые данные, когда только возможно
вы наверно имели в виду «неизменяемые» данные?
При правильном использовании ООП (да и вообще это одна из главных фишек подхода) объекты в программе всегда находятся в одном из множества корректных состояний. Согласен, энтропию объекта стоит уменьшать. Однако в большинстве случаев иммутабельность для ООП — оверкилл.
- Rectangle { setHeigth, setArea; getHeight, getArea, getWidth }
- Square: Rectangle
- Profit
В природе есть два типа полезных данных — статические и динамические. Пример первых — фотография, пример вторых — видео. Для статических данных (СД) важно их хранение. Например, для фото надо хранить: координаты пикселя, его яркость, его RGB. А для динамических данных (ДД) ничего хранить не надо, единственная задача — это успевать их правильно обрабатывать и выдавать «в никуда», где они могут бесследно и безболезненно исчезать. Пример — видео и смотрящий его человек. Для работы с СД придумали ООП, а для работы с ДД — ФП.
И всё работало более менее нормально, пока не возникла необходимость взаимодействия одновременно с двумя типами данных. И тут пошло поехало…
В чем я вижу проблемы ООП.
а) Идея, что «всё есть объект» (слава Богу, встречается редко). Так как объект — это не просто данные, а нечто, обладающее своим поведением/характером (привет инкапсуляция).
б) Идея, что надо объединять данные и связанные с ним методы в одну сущность. Фактически современный класс — это монолитная программа допроцедурного программирования. А нам достаточно, чтобы объект хранил переменную под присмотром своего «характера». А возможные операции с этими данными надо выносить за пределы класса как такового (привет функциям из ФП).
в) бесконечные попытки выдать наследование и полиморфизм как признаки ООП. Ведь первое — это просто переиспользование кода, а второе — на самом деле ближе к ФП (так как обработка данных).
Проблема ФП.
а) попытка хранить состояние (то есть СД) способами, разработанными для операций с ДД (привет монады).
Мой идеальный мир…
Объекты хранят только состояния, а за пределами объектов — чистое ФП.
Хорошие книги написал Бертран Мейер, но они у него объёмные и сложные, т.ч. могут быть трудности с их продажей.
По поводу жалоб на ООП: почти везде эта технология воплощена урезанно и криво, и жалобы больше относятся к языкам программирования, чем к ООП.
Мочи мочало, начинай сначала. Автор статьи неграмотен: ни что из перечисленного не является неотъемлемой чертой ООП, ни наследование, ни инкапсуляция, ни полиморфизм, ни абстракция. И всё это есть в чисто функциональном Haskell. Так что, объявим Haskell объектно-ориентированным?
А почему никто не сказал, что полиморфизм в статье определен в корне неверно? Автор банально не знает самых основ ООП.
На самом деле очень комично наблюдать со стороны как адепты ООП боба мартина в последние годы открещиваются от наследования, инкапсуляции и полиморфмизма фразами «вы не так понимаете ООП».
Я еще понимаю если бы ООП при этом всеми понималось, трактовалось и училось как завещал Алан Кей - обмен сообщениями между объектами. Но тут именно соль в том, что открыв любую книгу по ООП вы непременно на первой строке прочитаете про вышеперечисленные «устаревшие» принципы.
Сами себе изобрели какие-то неработающие механизмы, сами теперь открещиваются от них. Java Enterprise Hello World какой-то.
Типичные заблуждения об ООП