Як правило, ви надсилаєте об'єкти або їх змінні в функції?


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
Перший підхід був би більш трирівневим ОО. Але це має бути тим більше, виключивши базу даних слів із методу. Це має бути "Магазин" або "Персист" і робити або рахунок, або річ (не обидва). Як клієнт цього шару, ви не повинні знати про носій інформації. Під час отримання облікового запису вам потрібно буде ввести id, хоча або комбінацію значень властивостей (а не значень поля) для ідентифікації потрібного об’єкта. Або / та впровадити метод перерахування, який передає всі рахунки.
Мартін Мейт

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об’єкта якось змінилася, перша версія вимагала б змінити лише код бази даних, а друга версія вимагатиме змінити лише код UI . Яке з цих двох є правильним, багато в чому залежить від того, який тип даних dataмістить об'єкт, але це зазвичай дуже очевидно. Наприклад, якщоdataце рядок, що надається користувачем, як "20/05/1999", для перетворення цього файлу у потрібний Dateтип, перш ніж передавати його , повинно бути до UI-коду .


8

Це не вичерпний перелік, але врахуйте деякі наступні фактори, вирішуючи, чи слід об’єкт передавати методу як аргумент:

Чи об'єкт незмінний? Чи функція "чиста"?

Побічні ефекти є важливим фактором для збереження вашого коду. Коли ви бачите код з безліччю змінних стаціонарних об'єктів, що передаються по всьому світу, цей код часто менш інтуїтивний (так само, як глобальні змінні стану часто можуть бути менш інтуїтивними), і налагодження часто стає складнішим і тривалішим, споживаючи.

Як правило, прагніть забезпечити, наскільки це можливо, щоб будь-які об'єкти, які ви передаєте методу, явно незмінні.

Уникайте (знову-таки, наскільки це можливо) будь-якої конструкції, згідно з якою очікується, що стан аргументу буде змінено в результаті виклику функції - одним із найсильніших аргументів такого підходу є Принцип найменшого здивування ; тобто хтось, читаючи ваш код і побачивши аргумент, переданий у функцію, "менше шансів" очікувати, що його стан зміниться після повернення функції.

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

Методи із надмірно довгими списками аргументів (навіть якщо більшість аргументів мають значення "за замовчуванням") починають виглядати як запах коду. Однак іноді такі функції необхідні, і ви можете розглянути можливість створення класу, єдиною метою якого є діяти як об'єкт параметра .

Цей підхід може передбачати невелику кількість додаткового відображення кодового коду від вашого об'єкта 'джерело' до об'єкта параметрів, але це досить низька вартість як з точки зору продуктивності, так і складності, однак є ряд переваг в плані роз'єднання і незмінність об'єкта.

Чи належить переданий об'єкт виключно "шару" у вашій програмі (наприклад, ViewModel або сутність ORM?)

Подумайте про розділення проблем (SoC) . Іноді запитуючи себе, чи об'єкт "належить" тому ж шару або модулю, в якому існує ваш метод (наприклад, ручна прокатка бібліотеки обгортки API або ваш основний бізнес-логічний шар тощо), може повідомити, чи дійсно цей об'єкт повинен бути переданий цьому метод.

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

У випадку передачі даних між "шарами", параметри звичайних даних, що передаються методу, зазвичай кращі, ніж передачі об'єкта з "неправильного" рівня. Хоча, мабуть, гарна ідея мати окремі моделі, які існують на «правильному» шарі, на який можна замість цього зробити карту.

Чи є сама функція занадто великою та / або складною?

Коли функції потрібна велика кількість даних, можливо, варто подумати, чи бере ця функція занадто багато обов'язків; шукати потенційні можливості для рефактора за допомогою менших об'єктів та коротших, простіших функцій.

Чи повинна функція бути об'єктом команди / запиту?

У деяких випадках зв’язок між даними та функцією може бути близьким; у цих випадках враховуйте, чи буде відповідним об'єкт командування чи об’єкт запиту .

Чи додає параметр об'єкта до методу змушує клас, що містить, прийняти нові залежності?

Іноді найсильнішим аргументом для аргументів "Звичайних старих даних" є просто те, що клас прийому вже чітко автономний, і додавання об'єктного параметра до одного з його методів забруднить клас (або якщо клас вже забруднений, то він буде погіршити існуючу ентропію)

Вам дійсно потрібно обійти повний об'єкт або вам потрібна лише невелика частина інтерфейсу цього об’єкта?

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


5

Отже, створюючи функцію, ви неявно декларуєте якийсь контракт з кодом, який його викликає. "Ця функція бере цю інформацію та перетворює її в цю іншу річ (можливо, з побічними ефектами)".

Отже, якщо ваш договір логічно повинен бути з об'єктами (однак вони реалізовані) або з полями, які так трапляються, є частиною цих інших об'єктів. Ви додаєте з'єднання в будь-якому випадку, але як програміст, вирішувати, куди він належить.

Загалом , якщо це незрозуміло, тоді надайте перевагу найменшим даним, необхідним для роботи функції. Це часто означає проходження лише в полях, оскільки функції не потрібні інші речі, знайдені в об'єктах. Але іноді сприйняття цілого об'єкта є більш правильним, оскільки це призводить до меншого впливу, коли речі неминуче змінюються в майбутньому.


Чому ви не зміните ім'я методу на insertAccountIntoDatabase або ви перейдете будь-який інший тип ?. За певної кількості аргументів використовувати obj легко читати код. У вашому випадку я б швидше подумав, якщо в назві методу буде зрозуміло, що я буду вставляти, а не як їм це робити.
Лаїв

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

Чистий код рекомендує мати якомога менше аргументів, а це означає, що «Об’єкт» зазвичай був би кращим підходом, і я думаю, що це має певний сенс. тому що

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()); }

Причина полягає в тому, що зміни, внесені до будь-якого об’єкта вниз по дорозі, до тих пір, поки зміни зберігають ті, хто стає таким, щоб зміни були прозорими поза об'єктом, тоді у вас менше коду для зміни та тестування і менше шансів зірвати додаток.

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

Я не збираюся вступати в конвенції про іменування, але зазначив би, що, хоча цей метод містить у собі слово "база даних", цей механізм зберігання може змінитися вниз по дорозі. З наведеного коду нічого не прив'язує функція до платформи зберігання бази даних, яка використовується - або навіть якщо це база даних. Ми просто припустили, тому що це в назві. Знову ж таки, якщо припустити, що ці геттери завжди зберігаються, змінити, як / де вони зберігаються, буде легко.

Я б переосмислив функцію та два об'єкти, хоча у вас є функція, яка має залежність від двох об'єктних структур, а саме від гетерів, які використовуються. Крім того, схоже, що ця функція пов'язує ці два об'єкти в одну кумулятивну річ, яка зберігається. Моя кишка говорить мені, що третій предмет може мати сенс. Мені потрібно більше дізнатися про ці об’єкти та про те, як вони співвідносяться в дійсності та передбачуваній дорожній карті. Але моя кишка схиляється в тому напрямку.

Оскільки код зараз є, запитання напрошується "Де та чи повинна бути ця функція?" Це частина облікового запису чи OtherThing? Куди воно йде?

Я здогадуюсь, що вже є третя об’єктна "база даних", і я схиляюся до введення цієї функції в цей об'єкт, і тоді це стає завданням об'єктів, щоб мати можливість обробляти обліковий запис та OtherThing, перетворювати, а потім також зберігати результат .

Якщо ви повинні піти так далеко, щоб зробити 3-й об'єкт відповідним об'єктно-реляційному відображенню (ORM), тим краще. Це зробило б дуже очевидним, щоб хтось, хто працює з кодом, зрозумів: «Ага, тут обліковий запис та OtherThing розбиваються та зберігаються».

Але також може бути сенс запровадити четвертий об'єкт, який виконує завдання поєднання та перетворення облікового запису та іншогоThing, але не керує механікою збереження. Я б зробив це, якщо ви передбачите набагато більше взаємодій з або між цими двома об'єктами, тому що при цьому зростаю, я хотів би, щоб шматочки стійкості перетворювались на об'єкт, який управляє лише стійкістю.

Я б намагався зберегти дизайн таким чином, що будь-який обліковий запис, OtherThing або третій ORM-об'єкт можна змінити, не змінюючи інших трьох. Якщо немає вагомих причин цього не робити, я хотів би, щоб Account and OtherThing були незалежними і не знали внутрішньої роботи та структури один одного.

Звичайно, якби я знав повний контекст, що це буде, я міг би повністю змінити свої ідеї. Знову-таки, саме так я думаю, коли бачу такі речі, і як худа.


0

Обидва підходи мають свої плюси і мінуси. Що краще в сценарії, багато залежить від конкретного випадку використання.


Pro Кілька парам, Con Посилання на об'єкт:

  • Caller не прив’язаний до певного класу , він може взагалі передавати значення з різних джерел
  • Стан об'єкта захищений від несподіваного зміни у виконанні методу.

Посилання на об'єкт:

  • Очевидно взаємозв'язок того, що метод пов'язаний з типом посилання на об'єкт, що ускладнює випадкове передавання непов'язаних / недійсних значень
  • Перейменування поля / getter вимагає змін на всіх викликах методу, а не лише в його реалізації
  • Якщо додано нове властивість і його потрібно передати, ніяких змін у підписі методу не потрібно
  • Метод може мутувати стан об'єкта
  • Передача занадто багато змінних подібних примітивних типів робить заплутану для абонента щодо порядку (проблема шаблону Builder)

Отже, що потрібно використовувати і коли багато залежить від випадків використання

  1. Передайте окремі параметри: Загалом, якщо метод не має нічого спільного з типом об'єкта, краще передати окремий список параметрів, щоб він був застосований для широкої аудиторії.
  2. Введіть новий об'єкт моделі: якщо список параметрів зростає великим (більше 3), краще ввести новий модельний об'єкт, що належить API, який називається (перевагу побудової моделі)
  3. Довідкова інформація про об'єкт: Якщо метод пов'язаний з об'єктами домену, то краще з точки зору ремонтопридатності та читабельності передати посилання на об'єкти.

0

З одного боку у вас є обліковий запис та об'єкт "Інше". З іншого боку, у вас є можливість вставити значення в базу даних з урахуванням ідентифікатора облікового запису та ідентифікатора іншого. Це дві задані речі.

Ви можете написати метод, враховуючи Account and Otherthing як аргументи. З іншого боку, абоненту не потрібно знати жодних деталей про обліковий запис та інше. З негативної сторони, користувач повинен знати про методи обліку та іншого. Крім того, немає ніякого способу вставити в базу даних що-небудь інше, ніж значення іншого об'єкта, і немає способу використовувати цей метод, якщо у вас є ідентифікатор об’єкта облікового запису, але не сам об’єкт.

Або ви можете написати метод, який бере два аргументи і значення, як аргументи. З негативної сторони абоненту необхідно знати подробиці облікового запису та іншого. І може виникнути ситуація, коли вам фактично потрібно більше деталей облікового запису чи іншого, ніж просто ідентифікатор, який потрібно вставити в базу даних, і в цьому випадку це рішення є абсолютно марним. З іншого боку, сподіваємось, що потрібні знання облікового запису та іншого не потрібні для виклику, і є більша гнучкість.

Ваш дзвінок про суд: Чи потрібна більша гнучкість? Це часто не є питанням одного дзвінка, але це буде узгоджено через все ваше програмне забезпечення: або ви використовуєте ідентифікатори облікового запису більшу частину часу, або ви використовуєте об'єкти. Змішавши це, ви отримуєте найгірше з обох світів.

У C ++ у вас може бути метод, що приймає два коефіцієнти ідентифікатора плюс значення, а також вбудований метод з урахуванням Account and Otherthing, тож у вас є обидва способи з нульовими накладними витратами.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.