Является ли это анти-паттерном, если свойство класса создает и возвращает новый экземпляр класса?

24

У меня есть класс с именем, Headingкоторый делает несколько вещей, но он также должен иметь возможность возвращать значение, противоположное текущему значению заголовка, которое, в конечном итоге, должно использоваться посредством создания нового экземпляра самого Headingкласса.

У меня может быть простое свойство, вызываемое reciprocalдля возврата противоположного заголовка текущего значения, а затем вручную создать новый экземпляр класса Heading, или я могу создать метод, подобный createReciprocalHeading()автоматическому созданию нового экземпляра класса Heading и возвращению его в пользователь.

Однако один из моих коллег порекомендовал мне просто создать свойство класса с именем, reciprocalкоторое возвращает новый экземпляр самого класса через его метод getter.

Мой вопрос: не является ли это анти-паттерном для свойства класса, которое ведет себя так?

Я нахожу это менее интуитивным, потому что:

  1. На мой взгляд, свойство класса не должно возвращать новый экземпляр класса, и
  2. Имя свойства, которое reciprocalне помогает разработчику полностью понять его поведение, без получения помощи от IDE или проверки подписи получателя.

Я слишком строг в отношении того, что должно делать свойство класса, или это действительно проблема? Я всегда пытался управлять состоянием класса с помощью его полей и свойств, а его поведения - с помощью его методов, и я не вижу, как это соответствует определению свойства класса.

53777A
источник
6
Как говорит @Ewan в последнем абзаце своего ответа, отнюдь не анти-паттерн, имеющий Headingнеизменный тип и reciprocalвозвращающий новый, Headingявляется хорошей практикой "ямы успеха". (с оговоркой при двух вызовах reciprocalдолжен возвращать «одно и то же», то есть они должны пройти тест на равенство.)
Дэвид Арно
20
«Мой класс не является неизменным». Вот ваш настоящий анти-паттерн, прямо там. ;)
Дэвид Арно
3
@DavidArno Ну, иногда есть огромные потери производительности для того, чтобы сделать определенные вещи неизменяемыми, особенно если язык не поддерживает их изначально, но в остальном я согласен, что во многих случаях неизменность - это хорошая практика.
53777A
2
@dcorking класс реализован как в C #, так и в TypeScript, но я предпочитаю не зависеть от языка.
53777A
2
@Kaiserludi звучит как ответ (отличный ответ) - комментарии для запроса ясности
dcorking

Ответы:

22

Не секрет, что есть такие вещи, как Copy () или Clone (), но да, я думаю, вы правы, что беспокоитесь об этом.

возьмем для примера:

h2 = h1.reciprocal
h2.Name = "hello world"
h1.reciprocal.Name = ?

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

Вы также можете предположить, что:

h2.reciprocal == h1

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

Ewan
источник
1
Просто обратите внимание, что Copy()и Clone()есть методы, а не свойства. Я полагаю, что OP ссылается на получателей свойств, возвращающих новые экземпляры.
Линн
6
Я знаю. но свойства (вроде) также методы в c #
Ewan
4
В этом случае, C # имеет гораздо больше предложения: String.Substring(), Array.Reverse(), Int32.GetHashCode()и т.д.
Линн
If your heading class was an immutable value type- Я думаю, вы должны добавить, что установщики свойств в неизменяемых объектах должны всегда возвращать новые экземпляры (или, по крайней мере, если новое значение не совпадает со старым значением), потому что это определение неизменяемых объектов. :)
Иван Колмычек
17

Интерфейс класса заставляет пользователя класса делать предположения о том, как он работает.

Если многие из этих предположений верны, а немногие ошибочны, интерфейс - это хорошо.

Если многие из этих допущений ошибочны, а немногие верны, интерфейс является мусором.

Распространенное предположение о свойствах заключается в том, что вызов функции get является дешевым. Другое распространенное предположение о свойствах состоит в том, что вызов функции get дважды подряд вернет одно и то же.


Вы можете обойти это, используя последовательность, чтобы изменить ожидания. Например, для небольшой трехмерной библиотеки, где вам нужно Vector, Ray Matrixи т. Д. Вы можете сделать ее такой, чтобы получатели, такие как Vector.normalи Matrix.inverse, почти всегда были дорогими. К сожалению, даже если ваши интерфейсы постоянно используют дорогие свойства, интерфейсы в лучшем случае будут такими же интуитивно понятными, как и тот, который использует, Vector.create_normal()и Matrix.create_inverse()- тем не менее, я не знаю убедительных аргументов в пользу того, что использование свойств создает более интуитивный интерфейс даже после изменения ожиданий.

Питер - Унбан Роберт Харви
источник
10

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

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

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

Брэд Томас
источник
1
DateTime.NowСвойство обычно используется и относится к новому объекту. Я думаю, что имя свойства может помочь устранить неоднозначность того, возвращается ли один и тот же объект каждый раз. Это все во имя и как это используется.
Грег Бургхардт
3
DateTime.Now считается ошибкой. См. Stackoverflow.com/questions/5437972/…
Брэд Томас
@GregBurghardt Побочный эффект нового объекта не может быть проблемой сам по себе, потому что DateTimeэто тип значения и не имеет понятия идентичности объекта. Если я правильно понимаю, проблема в том, что он недетерминирован и каждый раз возвращает новое значение.
Зев Шпиц
1
@GregBurghardt DateTime.Newявляется статическим, и поэтому вы не можете ожидать, что он будет представлять состояние объекта.
Цахи Ашер
5

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

В C # свойства отличаются тем, что они (условно) пишутся с большой буквы (например, методы), но не содержат скобок (например, переменные общедоступного экземпляра). Если вы видите следующий код, отсутствие документации, что вы ожидаете?

var reciprocalHeading = myHeading.Reciprocal;

Как относительный новичок в C #, но тот, кто читал Руководство по использованию собственности Microsoft , я ожидаю Reciprocal, среди прочего:

  1. быть логическим членом данных Headingкласса
  2. быть недорогим, чтобы звонить, чтобы мне не нужно было кэшировать значение
  3. отсутствие видимых побочных эффектов
  4. дать тот же результат, если вызывается дважды подряд
  5. (возможно) предложить ReciprocalChangedмероприятие

Из этих предположений (3) и (4), вероятно, являются правильными (предполагая, Headingчто это тип неизменяемого значения, как в ответе Юана ), (1) является дискуссионным, (2) неизвестен, но также дискуссионен, и (5) вряд ли производить семантический смысл (хотя то , что имеет заголовок , возможно , следует иметь HeadingChangedсобытие). Это наводит меня на мысль, что в C # API «получить или вычислить обратное значение» не следует реализовывать как свойство, но особенно если вычисление дешевое и Headingнеизменное, это пограничный случай.

(Тем не менее, обратите внимание, что ни одна из этих проблем не имеет никакого отношения к тому, создает ли вызов свойства новый экземпляр , даже (2). Создание объектов в CLR само по себе довольно дешево.)

В Java свойства являются соглашением именования методов. Если я увижу

Heading reciprocalHeading = myHeading.getReciprocal();

мои ожидания аналогичны приведенным выше (если они изложены менее четко): я ожидаю, что звонок будет дешевым, идемпотентным и не будет иметь побочных эффектов. Однако вне платформы JavaBeans концепция «свойства» не так уж и значима в Java, и, в частности, при рассмотрении неизменяемого свойства, не имеющего соответствующего setReciprocal(), getXXX()соглашение теперь несколько старомодно. Из Effective Java , второе издание (уже более восьми лет):

Методы, которые возвращают booleanнефункцию или атрибут объекта, для которого они вызваны, обычно именуются существительным, именной или глагольной фразой, начинающейся с глагола get…. Существует вокальный контингент, который утверждает, что только третья форма (начиная с get) является приемлемой, но для этого утверждения мало оснований. Первые две формы обычно приводят к более читаемому коду… (с. 239)

В современном, более свободном API я бы ожидал увидеть

Heading reciprocalHeading = myHeading.reciprocal();

- что еще раз говорит о том, что вызов дешев, идемпотентен и не имеет побочных эффектов, но ничего не говорит о том, выполняется ли новый расчет или создается новый объект. Это хорошо; в хорошем API, мне все равно.

В Ruby нет такой вещи как собственность. Есть «атрибуты», но если я увижу

reciprocalHeading = my_heading.reciprocal

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

В Scala соглашение об именах заключается в том, что методы с побочными эффектами принимают скобки, а методы без них - нет, но

val reciprocal = heading.reciprocal

может быть любым из:

// immutable public value initialized at creation time
val reciprocal: Heading = … 

// immutable public value initialized on first use
lazy val reciprocal: Heading = … 

// public method, probably recalculating on each invocation
def reciprocal: Heading = …

// as above, with parentheses that, by convention, the caller
// should only omit if they know the method has no side effects
def reciprocal(): Heading = …

(Обратите внимание, что Scala допускает различные вещи, которые, тем не менее, не поощряются руководством по стилю . Это одно из моих главных раздражений в Scala.)

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

Короче говоря: знайте язык, который вы используете, и знайте, чего ожидают другие программисты от вашего API. Все остальное - деталь реализации.

Дэвид Моулз
источник
1
+1 один из моих любимых ответов, спасибо, что нашли время, чтобы написать это.
53777A
2

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

Наименование должно идти рука об руку с соглашениями об именовании языка.

Например, в Java я бы, вероятно, ожидал, что он будет вызван getReciprocal();

При этом я бы рассмотрел изменчивые против неизменных объектов.

С неизменными все довольно просто, и не повредит, если вы вернете тот же объект или нет.

С изменчивыми это может стать очень страшным.

b = a.reciprocal
a += 1
b = what?

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

В этих случаях ищите лучшее наименование, которое сообщает, что происходит, например, createReciprocal()может быть лучшим выбором.

Но это действительно зависит от контекста.

Эйко
источник
Вы имели в виду изменчивость ?
Карстен С
@CarstenS Да, конечно, спасибо. Понятия не имею, где моя голова. :)
Eiko
В чем-то не согласен getReciprocal(), поскольку это «звучит» как «нормальный» метод получения стиля JavaBeans. Предпочитают «createXXX ()» или, возможно, «CalcuXXXX ()», которые указывают или
подсказывают
@ user949300 Я немного согласен с твоими проблемами - и упомянул имя создания ... дальше. Хотя есть и минусы. create ... подразумевает новые экземпляры, которые не всегда могут иметь место (подумайте об отдельных выделенных объектах для некоторых особых случаев), вычислите ... утечки деталей реализации - что может привести к неверным допущениям в ценнике.
Эйко
1

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

Мэтт Фрейк
источник
1
+1 для ясности имени, но что не так с SRP в других решениях?
53777A
3
Почему вы рекомендуете фабричный класс по фабричному методу? (На мой взгляд, полезной обязанностью объекта часто является испускание объектов, похожих на него, таких как iterator.next.
Вызывает
2
@dcorking Фабричный класс в сравнении с методом (на мой взгляд) обычно является вопросом того же типа, что и "Является ли объект сопоставимым или я должен сделать компаратор?" Сделать компаратор всегда нормально , но можно сравнивать их, только если это довольно универсальный способ их сравнения. (Например, большие числа больше, чем меньшие, это хорошо для сопоставимых, но вы могли бы сказать, что все четности больше всех шансов, я бы сделал это в компараторе) - Так что для этого, я думаю, есть только один способ перевернуть заголовок, поэтому я склоняюсь к созданию метода, чтобы избежать лишнего класса.
Капитан Мэн
0

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

Но, например, строковый класс может иметь свойства "firstWord", "lastWord" или, если он обрабатывает Unicode, "firstLetter", "lastLetter", которые будут объектами с полными строками - просто обычно меньшими.

gnasher729
источник
0

Поскольку другие ответы касаются части недвижимости, я просто расскажу о вашем # 2 о reciprocal:

Не используйте ничего, кроме reciprocal. Это единственный правильный термин для того, что вы описываете (и это формальный термин). Не пишите некорректное программное обеспечение , чтобы сохранить разработчик от обучения домена они работают. В навигации, термины имеют весьма специфические значения и использовать что - то , как , казалось бы , безобидные , как reverseи oppositeмогут привести к путанице в определенных контекстах.

Shaz
источник
0

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

Взаимное является чистой функцией заголовка, так же, как отрицательное число является чистой функцией этого числа.

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

Теперь, в некоторых языках, может иметь смысл иметь его как свойство, как указано в контексте этого языка, в любом случае, если это дает некоторые преимущества из-за механики этого языка. Например, если вы хотите сохранить его в базе данных, возможно, целесообразно сделать его как свойство, если это позволяет автоматизировать обработку в рамках ORM. Или, может быть, это язык, в котором нет различия между свойством и функцией-параметром без параметров (я не знаю такого языка, но я уверен, что они есть). Затем разработчик и документатор API должен различать функции и свойства.


Изменить: В частности, для C #, я бы посмотрел на существующие классы, особенно из стандартной библиотеки.

  • Есть BigIntegerи Complexструктуры. Но они являются структурами и имеют только статические функции, а все свойства имеют различный тип. Так что не так много дизайна поможет вам Headingкласс.
  • У Math.NET Numerics есть Vectorкласс , который имеет очень мало свойств и, кажется, довольно близок к вашему Heading. Если вы пойдете этим путем, то у вас будет функция для взаимности, а не свойство.

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

Хайд
источник
-1

Действительно очень интересный вопрос. Я не вижу нарушения SOLID + DRY + KISS в том, что вы предлагаете, но, тем не менее, оно плохо пахнет.

Метод, который возвращает экземпляр класса, называется конструктором , верно? поэтому вы создаете новый экземпляр из неконструктивного метода. Это не конец света, но как клиент я бы этого не ожидал . Обычно это делается при определенных обстоятельствах: завод или, иногда, статический метод одного и того же класса (полезно для одиночных игр и для ... не очень объектно-ориентированных программистов?)

Плюс: что произойдет, если вы вызовете getReciprocal()возвращенный объект? Вы получаете другой экземпляр класса, который может быть точной копией первого! И если вы вызываетеgetReciprocal() на ЭТОГО? Кто-нибудь растянулся?

Опять же: что, если invoker требуется обратная величина (я имею в виду скаляр)? getReciprocal()->getValue()? Мы нарушаем Закон Деметры ради небольших выгод.

Доктор Джанлуиджи Зане Занеттини
источник
Я бы с радостью принял контраргументы от downvoter, спасибо!
Доктор Джанлуиджи Зане Занеттини
1
Я не понизил голос, но подозреваю, что причина может заключаться в том, что это на самом деле мало что добавляет к остальным ответам, но я могу ошибаться.
53777A
2
SOLID и KISS часто противоположны друг другу.
whatsisname