Полная неизменность и объектно-ориентированное программирование

43

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

Как изменяемые, так и неизменяемые объекты имеют целый ряд собственных преимуществ и недостатков.

Существуют языки, которые пытаются объединить обе концепции, например, scala, где у вас есть (явно объявленные) изменяемые и неизменные данные (пожалуйста, исправьте меня, если я ошибаюсь, мои знания по scala более чем ограничены).

Мой вопрос: Есть ли в комплекте может мутировать (так!) Неизменность -ie не объекта , как только это было created- никакого смысла в контексте ООП?

Есть проекты или реализации такой модели?

По сути, являются ли (полная) неизменность и ООП противоположными или ортогональными?

Мотивация: В ООП обычно работает на данных, изменение (мутация) основополагающую информации, сохраняя ссылки между этими объектами. Например, объект класса Personс членом, fatherссылающимся на другой Personобъект. Если вы изменяете имя отца, это сразу видно дочернему объекту без необходимости обновления. Будучи неизменным, вам нужно будет создавать новые объекты для отца и ребенка. Но у вас было бы намного меньше путаницы с общими объектами, многопоточностью, GIL и т. Д.

Hyperboreus
источник
2
Неизменность можно смоделировать на языке ООП, выставляя только точки доступа к объектам как методы или свойства только для чтения, которые не изменяют данные. В языках ООП неизменность работает так же, как и в любом функциональном языке, за исключением того, что вам могут не хватать некоторых функциональных языковых функций.
Роберт Харви
5
Изменчивость не является свойством языков ООП, таких как C # и Java, и не является неизменяемостью. Вы указываете изменчивость или неизменность в процессе написания класса.
Роберт Харви
9
Похоже, вы предполагаете, что изменчивость является основной особенностью объектной ориентации. Это не так. Изменчивость - это просто свойство объектов или значений. Объектная ориентация включает в себя ряд внутренних понятий (инкапсуляция, полиморфизм, наследование и т. Д.), Которые практически не имеют ничего общего с мутацией, и вы все равно получите преимущества этих функций. даже если вы сделали все неизменным.
Роберт Харви
2
@MichaelT Вопрос не в том, чтобы сделать определенные вещи изменчивыми, а в том, чтобы сделать все неизменными.

Ответы:

43

ООП и неизменность почти полностью ортогональны друг другу. Однако императивное программирование и неизменность не являются.

ООП можно обобщить двумя основными функциями:

  • Инкапсуляция : я не буду напрямую обращаться к содержимому объектов, а буду общаться через этот объект через определенный интерфейс («методы»). Этот интерфейс может скрыть от меня внутренние данные. Технически это специфично для модульного программирования, а не ООП. Доступ к данным через определенный интерфейс примерно эквивалентен абстрактному типу данных.

  • Динамическая отправка : когда я вызываю метод для объекта, исполняемый метод будет разрешен во время выполнения. (Например, в ООП на основе классов я мог бы вызвать sizeметод для IListэкземпляра, но вызов мог бы быть разрешен для реализации в LinkedListклассе). Динамическая диспетчеризация - это один из способов разрешить полиморфное поведение.

Инкапсуляция не имеет смысла без изменчивости (нет никакого внутреннего состояния, которое могло бы быть повреждено внешним вмешательством), но оно все же имеет тенденцию облегчать абстракции, даже когда все является неизменным.

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

Теперь случается, что ООП исторически всегда было связано с императивным программированием (Simula основана на Algol), и все основные языки ООП имеют императивные корни (C ++, Java, C #,… все корни в C). Это не означает, что сам ООП будет обязательным или изменчивым, это просто означает, что реализация ООП на этих языках допускает изменчивость.

Амон
источник
2
Большое спасибо, особенно за определение двух основных функций.
Гиперборей
Динамическая диспетчеризация не является основной функцией ООП. В действительности, инкапсуляция также не является (как вы уже признали).
Стоп Harm Моника
5
@OrangeDog Да, нет общепринятого определения ООП, но мне нужно было определение для работы. Поэтому я выбрал что-то настолько близкое к истине, насколько мог, не написав целую диссертацию об этом. Тем не менее, я расцениваю динамическую диспетчеризацию как единственное главное отличие ООП от других парадигм. То, что выглядит как ООП, но на самом деле все вызовы разрешены статически, на самом деле является просто модульным программированием с произвольным полиморфизмом. Объекты представляют собой пару методов и данных и, как таковые, эквивалентны замыканиям.
Амон
2
«Объекты - это пара методов и данных». Все, что вам нужно, это то, что не имеет ничего общего с неизменностью.
Стоп Harm Monica
3
@CodeYogi Сокрытие данных является наиболее распространенным видом инкапсуляции. Однако способ, которым данные хранятся внутри объекта, не единственная деталь реализации, которая должна быть скрыта. Не менее важно скрыть, как реализован открытый интерфейс, например, использую ли я какие-либо вспомогательные методы. Такие вспомогательные методы также должны быть частными, вообще говоря. Итак, подведем итог: инкапсуляция - это принцип, тогда как скрытие данных - это метод инкапсуляции.
Амон
25

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

Scala - действительно хорошая иллюстрация того, что изменчивость не требуется для объектной ориентации. Хотя Scala поддерживает изменчивость, его использование не рекомендуется. Идиоматическая Scala очень ориентирована на объект и почти полностью неизменна. В основном это допускает изменчивость для совместимости с Java, а также потому, что в определенных обстоятельствах неизменяемые объекты неэффективны или сложны для работы.

Сравнить в список Scala и список Java , например. Неизменяемый список Scala содержит все те же методы объекта, что и изменяемый список Java. Более того, потому что Java использует статические функции для таких операций, как сортировка , а Scala добавляет методы функционального стиля, такие как map. Все отличительные признаки ООП - инкапсуляция, наследование и полиморфизм - доступны в форме, знакомой объектно-ориентированным программистам, и используются соответствующим образом.

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

Карл Билефельдт
источник
17

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

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

Не все функциональные языки также требуют неизменности. Clojure имеет специальную аннотацию, позволяющую изменять типы, и большинство «практических» функциональных языков имеют возможность указывать изменяемые типы.

Лучше задать вопрос: «Имеет ли смысл полная неизменность в императивном программировании?» Я бы сказал, что очевидный ответ на этот вопрос - нет. Чтобы достичь полной неизменности в императивном программировании, вам придется отказаться от таких вещей, как forциклы (поскольку вам придется мутировать переменную цикла) в пользу рекурсии, и теперь вы, по сути, программируете в любом случае функционально.

Роберт Харви
источник
Спасибо. Не могли бы вы уточнить немного ваш последний абзац («очевидный» может быть немного субъективным).
Гиперборей
Уже сделал ....
Роберт Харви
1
@ Hyperboreus Есть много способов достижения полиморфизма. Подтипы с динамической диспетчеризацией, статический специальный полиморфизм (он же перегрузка функций) и параметрический полиморфизм (он же дженерики) являются наиболее распространенными способами сделать это, и все способы имеют свои сильные и слабые стороны. Современные ООП-языки сочетают в себе все эти три способа, тогда как Хаскелл в основном полагается на параметрический полиморфизм и специальный полиморфизм.
am
3
@RobertHarvey Вы говорите, что вам нужна изменчивость, потому что вам нужен цикл (иначе вы должны использовать рекурсию). Прежде чем я начал использовать Haskell два года назад, я думал, что мне также нужны переменные. Я просто говорю, что есть другие способы «зацикливания» (карта, сгиб, фильтр и т. Д.). После того, как вы удалите циклы из таблицы, зачем вам нужны переменные?
cimmanon
1
@RobertHarvey Но в этом и заключается смысл языков программирования: то, что вам доступно, а не то, что происходит под капотом. Последнее является ответственностью компилятора или интерпретатора, а не разработчика приложения. В противном случае вернемся к ассемблеру.
Гиперборей
5

Часто полезно классифицировать объекты как инкапсулирующие значения или сущности, с отличием в том, что если что-то является значением, код, содержащий ссылку на него, никогда не должен видеть изменения своего состояния каким-либо образом, который сам код не инициировал. Напротив, код, который содержит ссылку на объект, может ожидать, что он изменится способами, не зависящими от держателя ссылки.

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

  1. Никакая ссылка на объект никогда не будет подвергаться воздействию чего-либо, что может изменить состояние, заключенное в нем.

  2. Держатель хотя бы одной из ссылок на объект знает все виды использования, к которым может быть применена любая существующая ссылка.

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

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

Например, если кто-то получает данные по TCP-соединению, он может создать новый объект «состояние мира», который включает эти данные в свой буфер, не затрагивая ссылки на старое «состояние мира», но старые копии состояние мира, которое не включает последнюю порцию данных, будет дефектным и не должно использоваться, так как они больше не будут соответствовать состоянию реального сокета TCP.

Supercat
источник
4

В c # некоторые типы неизменны как строка.

Это, кроме того, предполагает, что выбор был решительно рассмотрен.

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

Я провел эксперимент с профилировщиком, и использование неизменяемого типа действительно требует больше ресурсов процессора и оперативной памяти.

Это также интуитивно понятно, если учесть, что для изменения одной буквы в строке из 4000 символов необходимо скопировать каждый символ в другой области ОЗУ.

Revious
источник
6
Частое изменение неизменяемых данных не должно быть катастрофически медленным, как при многократной stringконкатенации. Для практически всех видов данных / вариантов использования может быть изобретена (часто уже была) эффективная постоянная структура. Большинство из них имеют примерно одинаковую производительность, даже если постоянные факторы иногда хуже.
@delnan Я также думаю, что последний абзац ответа больше о деталях реализации, чем о (im) изменчивости.
Гиперборей
@ Hyperboreus: вы думаете, я должен удалить эту часть? Но как изменить строку, если она неизменна? Я имею в виду ... по моему мнению, но я могу ошибаться, это может быть основной причиной, по которой объект не является неизменным.
Revious
1
@ Не очевидно. Оставьте это, чтобы оно вызвало обсуждение и более интересные мнения и точки зрения.
Гиперборей
1
@ Revious Да, чтение будет медленнее, но не так медленно, как изменение string(традиционное представление). «Строка» (в представлении, о котором я говорю) после 1000 модификаций будет похожа на вновь созданную строку (содержимое по модулю); никакая полезная или широко используемая постоянная структура данных не ухудшает качество после операций X. Фрагментация памяти не является серьезной проблемой (да, у вас было бы много выделений, да, но у современных сборщиков мусора фрагментация вполне не проблема)
0

Полная неизменность всего не имеет большого смысла в ООП или большинстве других парадигм в этом отношении по одной очень важной причине:

У каждой полезной программы есть побочные эффекты.

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

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

Может иметь смысл ограничивать изменчивость частями, которые необходимо изменить. Но если абсолютно ничего не нужно менять, значит, вы ничего не делаете.

Chao
источник
4
Я не понимаю, как ваш ответ связан с вопросом, поскольку я не обращался к чисто функциональным языкам. Возьмем, к примеру, эрланг: неизменные данные, никаких разрушительных заданий, никаких побочных эффектов. Также у вас есть состояние на функциональном языке, только то, что состояние «протекает» через функции, в отличие от функций, работающих над состоянием. Состояние изменяется, но оно не изменяется на месте, но будущее состояние заменяет текущее состояние. Неизменность заключается не в том, изменен ли буфер памяти, а в том, видны ли эти мутации снаружи.
Гиперборей
А будущее государство заменяет нынешнее, как именно? В ОО-программе это состояние где-то является свойством объекта. Замена состояния требует изменения объекта (или замены его другим, который требует изменения объектов, которые к нему относятся (или замены его другим, что ... эх. Вы понимаете, в чем дело)). Вы можете придумать какой-нибудь монадический хак, когда каждое действие заканчивается созданием целого нового приложения ... но даже тогда текущее состояние программы должно быть где-то записано.
cHao
7
-1. Это неверно Вы путаете побочные эффекты с мутацией, и хотя они часто рассматриваются одинаковыми в функциональных языках, они различны. У каждой полезной программы есть побочные эффекты; не у каждой полезной программы есть мутация.
Майкл Шоу,
@Michael: Когда дело доходит до ООП, мутации и побочные эффекты настолько взаимосвязаны, что их невозможно отделить друг от друга. Если у вас нет мутации, то вы не можете иметь побочные эффекты без огромного количества хакерских атак.
Чао
-2

Я думаю, это зависит от того, использует ли ваше определение ООП стиль передачи сообщений.

Чистые функции не должны ничего мутировать, потому что они возвращают значения, которые вы можете сохранить в новых переменных.

var brandNewVariable = pureFunction(foo);

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

sameOldObject.changeMe(foo);

Можно иметь объекты и не изменять их, делая их методы чистыми функциями, которые живут внутри объекта, а не снаружи.

var brandNewVariable = nonMutatingObject.askMe(foo);

Но невозможно смешать стиль передачи сообщений и неизменные объекты.

Presley
источник