Корректная реализация для свойства всех объектов, которые равны - PullRequest
3 голосов
/ 09 мая 2019

проблема

Рассмотрим реализацию графа, SampleGraph<N>. Рассмотрим реализацию узлов графа Node extends N, корректно переопределяющую hashCode и equals для отражения логического равенства между двумя узлами.

Теперь, допустим, мы хотим добавить какое-либо свойство p к узлу. Такое свойство привязано к логическим экземплярам узла, то есть для Node n1, n2, n1.equals(n2) подразумевает p (n1) = p (n2)

Если я просто добавлю свойство как поле класса Node, это произойдет со мной:

  • Я определяю Node n1, n2 таким, что n1.equals(n2), но n1 != n2
  • Я добавляю n1 и n2 к графику: n1 при вставке логического узла и n2 при обращении к узлу при вставке ребер. На графике хранятся оба экземпляра.
  • Позже я извлекаю узел из графика (возвращается n1) и устанавливаю для него свойство p в какое-то значение. Позже я пересекаю все ребра графа и извлекаю узел у одного из них (возвращается n2). Свойство p не установлено, что вызывает логическую ошибку в моей модели.

Подводя итог, текущее поведение :

graph.addNode(n1) // n1 is added
graph.addEdge(n2,nOther) // graph stores n2
graph.queryForNode({query}) // n1 is returned
graph.queryForEdge({query}).sourceNode() // n2 is returned

Вопрос

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

S1 - Реализация графика плохая. После добавления узла граф всегда должен внутренне проверять, запомнен ли экземпляр того же узла (equals оценивается как true). Если это так, такой экземпляр всегда должен быть единственной ссылкой, используемой графом.

graph.addNode(n1) // n1 is added
graph.addEdge(n2,nOther) // graph internally checks that n2.equals(n1), doesn't store n2
graph.queryForNode({query}) // n1 is returned
graph.queryForEdge({query}).sourceNode() // n1 is returned

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

graph.addNode(n1) // n1 is added
graph.addEdge(n1,nOther) // the programmer uses n1 every time he refers to the node
graph.queryForNode({query}) // n1 is returned
graph.queryForEdge({query}).sourceNode() // n1 is returned

S3 - Свойство реализовано неправильно. Это должна быть информация, которая является внешней по отношению к классу Node. Коллекция, такая как HashMap<N, Property>, будет работать нормально, обрабатывая разные экземпляры как один и тот же объект на основе hashCode.

HashMap<N, Property> properties;

graph.addNode(n1) // n1 is added
graph.addEdge(n2,nOther) // graph stores n2
graph.queryForNode({query}) // n1 is returned
graph.queryForEdge({query}).sourceNode() // n2 is returned

// get the property. Difference in instances does not matter
properties.get(n1)
properties.get(n2) //same property is returned

S4 - То же, что S3, но мы могли бы скрыть реализацию внутри Node, таким образом:

class Node {
  private static HashMap<N, Property> properties;

  public Property getProperty() {
    return properties.get(this);
  }
}

Редактировать : добавлены фрагменты кода для текущего поведения и предварительные решения после Стивен C answer . Для пояснения, весь пример взят из использования реальной структуры данных графа из проекта Java с открытым исходным кодом.

Ответы [ 2 ]

2 голосов
/ 09 мая 2019

Кажется, что S1 имеет больше смысла. Некоторые реализации Graph внутренне используют Set<Node> (или некоторый эквивалент) для хранения узлов. Конечно, использование такой структуры, как Set, обеспечит отсутствие дубликатов Node, где Node n1 и Node n2 считаются дубликатами, если и только если n1.equals(n2). Конечно, реализация Node должна гарантировать, что все соответствующие свойства учитываются при сравнении двух экземпляров (т. Е. При реализации equals() и hashCode()).

Некоторые проблемы с другими утверждениями:

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

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

1 голос
/ 09 мая 2019

На мой взгляд, все сводится к выбору между API с сильной или слабой абстракцией.

  • Если вы выберете сильную абстракцию, API будет скрывать тот факт, что Node объекты имеют идентичностьи канонизирует их при добавлении в SimpleGraph.

  • Если вы выберете слабую абстракцию, API будет предполагать, что Node объекты имеют идентичность, и это будетвызывающей стороне канонизировать их перед добавлением в SimpleGraph.

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

Тогда есть более тонкие детали дизайна API, которые могут соответствовать или не соответствовать вашему конкретному сценарию использования для графиков.

Дело в том, что вам нужно сделать выбор.

(Это немного похоже на решение использовать интерфейс коллекций List и его чистую модель по сравнению с реализацией собственной структуры данных связанного списка, так что вы можете эффективно "склеить" 2 списка вместе. Любой подход может быть правильным, в зависимости от требований вашего приложения.)

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

  • Вы можете использовать его как есть.(Смиритесь!)
  • Вы можете попытаться повлиять на дизайн.(Удачи!)
  • Вы можете переключиться на другой API;т. е. другой поставщик.
  • Вы можете выбрать раскладку API и настроить его в соответствии с вашими собственными требованиями (или предпочтениями, если речь идет об этом)
  • Вы можете разработать и внедрить свойсобственный API с нуля.

И если у вас действительно нет выбора, то этот вопрос спорный.Просто используйте API.


Если это API с открытым исходным кодом, то у вас, вероятно, нет выбора, чтобы заставить дизайнеров изменить его.Значительные изменения API имеют тенденцию создавать много работы для других людей;то есть множество других проектов, которые зависят от API.Ответственный дизайнер API / команда разработчиков принимает это во внимание.Или же они обнаруживают, что теряют актуальность, потому что их API получают репутацию нестабильных.

Итак ... если вы хотите повлиять на существующий дизайн API с открытым исходным кодом ... потому что вы думаете, что они делают это неправильно (для некоторого определения неверно) ... вам, вероятно, лучше«разветвление» API и устранение последствий.


И, наконец, если вы ищете совет «передового опыта», помните, что нет передового опыта .И это не просто философский вопрос.Это то, из-за чего вас обескуражат, если вы будете искать / искать советы «передового опыта», а затем будете следовать им.


В качестве сноски: вы когда-нибудь задумывались, почему стандарт Java и Androidбиблиотеки классов не предлагают никаких API-интерфейсов или реализаций графов общего назначения?И почему они так долго появлялись в сторонних библиотеках (версия Guava 20.0)?

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...