Можем ли мы действительно использовать неизменяемость в ООП, не теряя все ключевые функции ООП?

11

Я вижу преимущества создания неизменных объектов в моей программе. Когда я действительно глубоко задумываюсь над хорошим дизайном для своего приложения, я часто, естественно, считаю, что многие из моих объектов неизменяемы. Часто приходит момент, когда я хочу, чтобы все мои объекты были неизменными.

Этот вопрос касается той же идеи, но ни один ответ не говорит о том, что является хорошим подходом к неизменности и когда на самом деле его использовать. Есть ли хорошие шаблоны дизайна? Общая идея, по-видимому, заключается в том, чтобы «сделать объекты неизменяемыми, если вам абсолютно не нужно их менять», что на практике бесполезно.

Мой опыт показывает, что неизменность все больше и больше приводит мой код к функциональной парадигме, и такое развитие всегда происходит:

  1. Я начинаю нуждаться в постоянных (в функциональном смысле) структурах данных, таких как списки, карты и т. Д.
  2. Работать с перекрестными ссылками крайне неудобно (например, узел дерева ссылается на своих потомков, а дети ссылаются на своих родителей), что вообще не позволяет использовать перекрестные ссылки, что снова делает мои структуры данных и код более функциональными.
  3. Наследование перестает иметь смысл, и вместо этого я начинаю использовать композицию.
  4. Все основные идеи ООП, такие как инкапсуляция, начинают разрушаться, и мои объекты начинают выглядеть как функции.

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

Просто для удобства приведу пример. Давайте иметь ChessBoardкак неизменную коллекцию неизменных шахматных фигур (расширение абстрактного классаPiece). С точки зрения ООП фигура отвечает за генерацию действительных ходов из своей позиции на доске. Но чтобы генерировать ходы, фигура должна иметь ссылку на свою доску, а доска должна иметь ссылку на свои фигуры. Ну, есть несколько хитростей для создания этих неизменяемых перекрестных ссылок в зависимости от вашего языка ООП, но ими сложно управлять, лучше не иметь части, чтобы ссылаться на свою доску. Но тогда фигура не может генерировать свои ходы, поскольку она не знает состояния доски. Тогда кусок становится просто структурой данных, содержащей тип куска и его положение. Затем вы можете использовать полиморфную функцию для генерации ходов для всех видов фигур. Это вполне достижимо в функциональном программировании, но почти невозможно в ООП без проверок типов во время выполнения и других плохих практик ООП ... Тогда

lishaak
источник
3
Мне нравится базовый вопрос, но мне трудно с деталями. Например, почему наследование перестает иметь смысл, когда вы максимизируете неизменность?
Мартин Ба,
1
У ваших объектов нет методов?
Прекратить причинять вред Монике
4
«С точки зрения ООП фигура отвечает за генерацию действительных ходов из своей позиции на доске». - определенно нет, разработка такой вещи, скорее всего, повредит SRP.
Док Браун
1
@lishaak Вы «изо всех сил пытаетесь объяснить проблему с наследованием простыми словами», потому что это не проблема; плохой дизайн является проблемой. Наследование является самой сущностью ООП, непременным условием, если хотите. Многие из так называемых «проблем с наследованием» на самом деле являются проблемами с языками, не имеющими явного синтаксиса переопределения, и они намного менее проблематичны в лучше разработанных языках. Без виртуальных методов и полиморфизма у вас нет ООП; у вас есть процедурное программирование с забавным синтаксисом объекта. Поэтому неудивительно, что вы не видите преимуществ ОО, если избегаете этого!
Мейсон Уилер,

Ответы:

24

Можем ли мы действительно использовать неизменяемость в ООП, не теряя все ключевые функции ООП?

Не понимаю, почему нет. Я делал это в течение многих лет, прежде чем Java 8 все равно заработала. Вы когда-нибудь слышали о струнах? Хороший и неизменный с самого начала.

  1. Я начинаю нуждаться в постоянных (в функциональном смысле) структурах данных, таких как списки, карты и т. Д.

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

  1. Работать с перекрестными ссылками крайне неудобно (например, узел дерева ссылается на своих потомков, а дети ссылаются на своих родителей), что вообще не позволяет использовать перекрестные ссылки, что снова делает мои структуры данных и код более функциональными.

Циркулярные ссылки - это особый вид ада. Неизменность не спасет вас от этого.

  1. Наследование перестает иметь смысл, и вместо этого я начинаю использовать композицию.

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

  1. Все основные идеи ООП, такие как инкапсуляция, начинают разрушаться, и мои объекты начинают выглядеть как функции.

Страшно подумать, какова ваша идея «ООП, как инкапсуляция». Если в нем участвуют геттеры и сеттеры, просто прекратите вызывать эту инкапсуляцию, потому что это не так. Это никогда не было. Это ручное Аспектно-ориентированное Программирование. Шанс проверить и место для установки точки останова - это хорошо, но это не инкапсуляция. Инкапсуляция сохраняет мое право не знать и не заботиться о том, что происходит внутри.

Ваши объекты должны выглядеть как функции. Они мешки функций. Это набор функций, которые движутся вместе и переопределяют себя вместе.

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

  • Функциональное программирование формально о назначениях.

  • ООП формально относится к указателям на функции.

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

Позволь мне показать тебе что-то:

f n (x)

Это функция. На самом деле это континуум функций:

f 1 (x)
f 2 (x)
...
f n (x)

Угадайте, как мы выражаем это на языках ООП?

n.f(x)

Этот маленький nвыбор выбирает, какая реализация fиспользуется, И он решает, какими являются некоторые из констант, используемых в этой функции (что откровенно означает то же самое). Например:

f 1 (x) = x + 1
f 2 (x) = x + 2

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

g 1 (x) = x 2 + 1
g 2 (x) = x 2 + 2

Да, вы уже догадались:

n.g(x)

f и g - функции, которые изменяются вместе и перемещаются вместе. Поэтому мы сунем их в одну сумку. Это то, что на самом деле является объектом. Сохранение nпостоянной (неизменной) просто означает, что легче предсказать, что они будут делать, когда вы их называете.

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

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

Давайте создадим шахматную доску как неизменную коллекцию неизменных шахматных фигур (расширяя класс абстрактных фигур). С точки зрения ООП фигура отвечает за генерацию действительных ходов из своей позиции на доске. Но чтобы генерировать ходы, фигура должна иметь ссылку на свою доску, а доска должна иметь ссылку на свои фигуры.

Arg! Опять с ненужными круговыми ссылками.

Как насчет: A ChessBoardDataStructureпревращает XY шнуры в части ссылки. У этих частей есть метод, который берет x, y и конкретный элемент ChessBoardDataStructureи превращает его в коллекцию новых марок ChessBoardDataStructure. Затем сует это в то, что может выбрать лучший ход. Теперь ChessBoardDataStructureмогут быть неизменными, как и куски. Таким образом, у вас в памяти только одна белая пешка. Есть только несколько ссылок на него в правильных xy местах. Объектно-ориентированный, функциональный и неизменный.

Подожди, мы уже не говорили о шахматах?

candied_orange
источник
6
Каждый должен прочитать это. А потом прочитал на понимание данных абстракции, Revisited по Уильяму Р. Куку . А потом прочитайте это снова. А затем прочитайте предложение Кука по упрощенным, современным определениям «объект» и «объектно-ориентированный» . Бросьте слова Алана Кея « Большая идея -« обмен сообщениями » », его определение OO
Йорг Миттаг
1
И последнее, но не менее важное, Орландский договор .
Йорг Миттаг
1
@Euphoric: Я думаю, что это довольно стандартная формулировка, которая соответствует стандартным определениям для «функционального программирования», «императивного программирования», «логического программирования» и т. Д. В противном случае C - это функциональный язык, потому что вы можете кодировать FP в нем, логический язык, потому что вы можете кодировать логическое программирование в нем, динамический язык, потому что вы можете кодировать динамическую типизацию в нем, актерский язык, потому что вы можете кодировать актерскую систему в нем и так далее. А Haskell - это лучший императивный язык в мире, поскольку вы можете назвать побочные эффекты, хранить их в переменных, передавать их как…
Йорг Миттаг
1
Я думаю, что мы можем использовать идеи, схожие с идеями гениальной статьи Матиаса Феллайзена о языковой выразительности. Перемещение OO-программы, скажем, с Java на C♯ может быть выполнено только с помощью локальных преобразований, но перемещение ее на C требует глобальной реструктуризации (в основном, вам необходимо ввести механизм отправки сообщений и перенаправить все вызовы функций через него), поэтому Java и C♯ могут выражать OO, а C может «только» кодировать его.
Йорг Миттаг
1
@lishaak Потому что вы указываете это на разные вещи. Это удерживает его на одном уровне абстракции и предотвращает дублирование хранения позиционной информации, которое в противном случае могло бы стать противоречивым. Если вы просто возмущаетесь лишней типизацией, вставьте ее в метод, чтобы набирать ее только один раз ... Кусок не должен помнить, где он находится, если вы скажете ему, где он находится. Теперь части хранят только информацию, такую ​​как цвет, движения и изображения / аббревиатуры, которые всегда верны и неизменны.
candied_orange
2

На мой взгляд, наиболее полезные концепции, введенные ООП в основное русло:

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

Все эти преимущества также могут быть реализованы без традиционных деталей реализации, таких как наследование или даже классы. Первоначальная идея Алана Кея о «объектно-ориентированной системе» использовала «сообщения» вместо «методов» и была ближе к Эрлангу, чем, например, C ++. Посмотрите на Go, который покончил со многими традиционными деталями реализации ООП, но все еще чувствует себя достаточно объектно-ориентированным.

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

Посмотрите, как Scala пытается объединить неизменяемость и подходы FP с ООП. По общему признанию это не самый простой и изящный язык. Это практически практически успешно, хотя. Кроме того, взгляните на Kotlin, который предоставляет множество инструментов и подходов для подобного микса.

Обычная проблема, связанная с попыткой использовать другой подход, нежели тот, о котором думали создатели языка N лет назад, - это «несоответствие импеданса» стандартной библиотеке. OTOH и Java, и .NET экосистемы в настоящее время имеют разумную поддержку стандартных библиотек для неизменных структур данных; Конечно, сторонние библиотеки также существуют.

9000
источник