Вы обычно отправляете объекты или их переменные-члены в функции?


31

Что является общепринятой практикой между этими двумя случаями:

function insertIntoDatabase(Account account, Otherthing thing) {
    database.insertMethod(account.getId(), thing.getId(), thing.getSomeValue());
}

или

function insertIntoDatabase(long accountId, long thingId, double someValue) {
    database.insertMethod(accountId, thingId, someValue);
}

Другими словами, лучше ли передавать объекты целиком или только те поля, которые вам нужны?


5
Это будет полностью зависеть от того, для чего предназначена функция и как она связана (или не связана) с рассматриваемым объектом.
MetaFight

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

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

1
Первый подход был бы более 3-х уровневым ОО. Но это должно быть еще больше, исключив базу данных слов из метода. Это должно быть "Store" или "Persist" и делать либо Account, либо Thing (но не оба). Как клиент этого уровня вы не должны знать о носителе данных. При извлечении учетной записи вам потребуется ввести идентификатор или комбинацию значений свойств (не значений полей), чтобы идентифицировать нужный объект. Или / и неуместен метод перечисления, который передает все учетные записи.
Мартин Маат

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

Ответы:


24

Ни один из них, как правило, лучше, чем другой. Это вызов, который вы должны сделать в каждом конкретном случае.

Но на практике, когда вы в состоянии принять это решение, это потому, что вы сами решаете, какой слой в общей архитектуре программы должен разбивать объект на примитивы, поэтому вам следует подумать обо всем вызове. стек , а не только этот метод, в котором вы сейчас находитесь. Предположительно, разделение должно быть где-то сделано, и было бы бессмысленно (или это было бы излишне подвержено ошибкам) ​​делать это более одного раза. Вопрос в том, где это место должно быть.

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

function addWidgetButtonClicked(clickEvent) {
    // get form data
    // get user's account
    insertIntoDatabase(account, data);
}
function insertIntoDatabase(Account account, Otherthing data) {
    // open database connection
    // check data doesn't already exist
    database.insertMethod(account.getId(), data.getId(), data.getSomeValue());
}

против

function addWidgetButtonClicked(clickEvent) {
    // get form data
    // get user's account
    insertIntoDatabase(account.getId(), data.getId(), data.getSomeValue());
}
function insertIntoDatabase(long accountId, long dataId, double someValue) {
    // open database connection
    // check data doesn't already exist
    database.insertMethod(accountId, dataId, someValue);
}

В первой версии код пользовательского интерфейса пропускает dataобъект вслепую, и код базы данных должен извлечь из него полезные поля. Во второй версии код пользовательского интерфейса разбивает dataобъект на его полезные поля, и код базы данных получает их напрямую, не зная, откуда они взялись. Ключевым выводом является то, что если структура dataобъекта каким-либо образом изменится, первая версия потребует изменения только кода базы данных, а вторая версия потребует изменения только кода пользовательского интерфейса . Какой из этих двух правил верен, во многом зависит от того, какие данные dataсодержит объект, но обычно это очень очевидно. Например, еслиdataявляется предоставленной пользователем строкой, такой как «20/05/1999», она должна соответствовать коду пользовательского интерфейса, чтобы преобразовать его в соответствующий Dateтип перед передачей.


8

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

Является ли объект неизменным? Является ли функция «чистой»?

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

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

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

Сколько аргументов у метода уже есть?

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

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

Передается ли объект исключительно к «слою» в вашем приложении (например, ViewModel или объекту ORM?)

Подумайте о разделении интересов (SoC) . Иногда спрашивая себя, «принадлежит» ли объект к тому же слою или модулю, в котором существует ваш метод (например, отобранная вручную библиотека-обертка API или ваш базовый уровень бизнес-логики и т. Д.), Можно сообщить, действительно ли этот объект следует передать этому метод.

SoC является хорошей основой для написания чистого, слабосвязанного, модульного кода. например, объект сущности ORM (отображение между вашим кодом и схемой базы данных) в идеале не должен передаваться на вашем бизнес-уровне или, что еще хуже, на уровне презентации / пользовательского интерфейса.

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

Является ли сама функция слишком большой и / или сложной?

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

Должна ли функция быть объектом команды / запроса?

В некоторых случаях связь между данными и функцией может быть тесной; в этих случаях подумайте, подходит ли вам объект Command или объект Query .

Вызывает ли добавление параметра объекта к методу содержащий класс принять новые зависимости?

Иногда самым сильным аргументом для аргументов «Обычные старые данные» является просто то, что принимающий класс уже аккуратно самодостаточен, и добавление параметра объекта к одному из его методов приведет к загрязнению класса (или, если класс уже загрязнен, тогда он будет усугубить существующую энтропию)

Вам действительно нужно обойти весь объект или вам нужна только небольшая часть интерфейса этого объекта?

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


5

Поэтому, когда вы создаете функцию, вы неявно объявляете некоторый контракт с кодом, который ее вызывает. Msgstr "Эта функция берет эту информацию и превращает её в другую вещь (возможно, с побочными эффектами)".

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

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


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

3

Это зависит.

Чтобы уточнить, параметры, которые принимает ваш метод, должны семантически соответствовать тому, что вы пытаетесь сделать. Рассмотрим EmailInviterтри возможных реализации inviteметода:

void invite(String emailAddressString) {
  invite(EmailAddress.parse(emailAddressString));
}
void invite(EmailAddress emailAddress) {
  ...
}
void invite(User user) {
  invite(user.getEmailAddress());
}

Передача туда, Stringгде вы должны пройти EmailAddress, ошибочна, потому что не все строки являются адресами электронной почты. EmailAddressКласс лучше семантически соответствует поведению метода. Однако прохождение в a Userтакже является недостатком, потому что зачем EmailInviterограничиваться приглашением пользователей? А как насчет бизнеса? Что делать, если вы читаете адреса электронной почты из файла или командной строки, и они не связаны с пользователями? Списки рассылки? Список можно продолжить.

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


0

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

insertIntoDatabase(new Account(id) , new Otherthing(id, "Value"));

это более читаемый звонок, чем

insertIntoDatabase(myAccount.getId(), myOtherthing.getId(), myOtherthing.getValue() );

не могу согласиться там. Два не являются синонимами. Создание 2 новых экземпляров объекта просто для передачи их методу не очень хорошо. Я бы использовал insertIntoDatabase (myAccount, myOtherthing) вместо любого из ваших вариантов.
jwenting

0

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

Что произойдет, если вы измените поля в Otherthing? Возможно, вы измените тип, добавите поле или удалите поле. Теперь все методы, подобные тому, который вы указали в своем вопросе, должны быть обновлены. Если вы передадите объект, никаких изменений интерфейса не будет.

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

public User getUser(String primaryKey) {
  return ...;
}

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


1
"Что произойдет, если вы измените поля Otherthing?" (1) Это было бы нарушением принципа открытого / закрытого. (2) даже если вы передадите весь объект целиком, код внутри него затем получит доступ к членам этого объекта (а если нет, зачем передавать объект?) Все равно сломается ...
Дэвид Арно

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

0

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

// this has exactly one way to call it
insertIntoDatabase(Account ..., Otherthing ...)

// the parameter order can be confused in practice
insertIntoDatabase(long ..., long ...)

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


0

Из двух я предпочитаю первый метод:

function insertIntoDatabase(Account account, Otherthing thing) { database.insertMethod(account.getId(), thing.getId(), thing.getSomeValue()); }

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

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

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

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

Теперь, когда код стоит, возникает вопрос: «Где будет или должна быть эта функция?» Это часть Аккаунта или Другое? Куда это идет?

Я предполагаю, что уже существует третий объект «база данных», и я склоняюсь к тому, чтобы поместить эту функцию в этот объект, и тогда это становится заданием объектов, чтобы иметь возможность обрабатывать Account и OtherThing, преобразовывать, а затем также сохранять результат ,

Если вам нужно сделать так, чтобы этот 3-й объект соответствовал шаблону объектно-реляционного отображения (ORM), тем лучше. Для любого, кто работает с кодом, было бы совершенно очевидно, что «А вот где Account и OtherThing разбиты вместе и сохраняются».

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

Я бы стрелял за сохранение дизайна таким образом, чтобы любой из объекта Account, OtherThing или третьего объекта ORM мог быть изменен без необходимости изменения других трех. Если нет веской причины не делать этого, я бы хотел, чтобы Account и OtherThing были независимыми и не должны были знать внутреннюю работу и структуру друг друга.

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


0

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


Pro Несколько параметров, Con Ссылка на объект:

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

Pro объект ссылки:

  • Понятно, что метод связан с ссылочным типом Object, что затрудняет случайную передачу несвязанных / недопустимых значений
  • Переименование поля / получателя требует изменений при всех вызовах метода, а не только в его реализации
  • Если новое свойство добавлено и должно быть передано, никаких изменений в подписи метода не требуется
  • Метод может изменять состояние объекта
  • Передача слишком большого количества переменных с одинаковыми примитивными типами приводит к тому, что вызывающая сторона не понимает порядок (проблема с шаблоном Builder)

Итак, что нужно использовать и когда многое зависит от вариантов использования

  1. Передача отдельных параметров. Как правило, если метод не имеет ничего общего с типом объекта, лучше передать отдельный список параметров, чтобы он был применим для более широкой аудитории.
  2. Представьте новый объект модели: если список параметров становится большим (более 3), лучше ввести новый объект модели, принадлежащий к вызываемому API (предпочтительный шаблон построителя)
  3. Передать ссылку на объект : если метод связан с объектами домена, то лучше с точки зрения удобства обслуживания и читаемости передавать ссылки на объекты.

0

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

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

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

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

В C ++ у вас может быть метод, принимающий два идентификатора плюс значение, и встроенный метод, принимающий Account и прочее, так что вы получаете оба способа с нулевыми накладными расходами.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.