Проблема стилю кодування: Чи повинні ми мати функції, які беруть параметр, змінюють його, а потім ВЗАЄМО цей параметр?


19

У мене з товаришем трохи дискутують про те, чи є ці дві практики лише двома сторонами однієї монети, чи чи справді краща.

У нас є функція, яка приймає параметр, заповнює його член, а потім повертає його:

Item predictPrice(Item item)

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

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

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

  • У мовах, що не управляються пам’яттю, функція, що виділяє новий примірник, вимагає володіння пам’яттю, і тому нам доведеться реалізувати метод очищення, який викликається в якийсь момент.

  • Розподіл на купі потенційно дорогий, і тому важливо, чи робить це викликана функція.

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

void predictPrice(Item item)

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

Отже, якісь думки?


strcat змінює параметр на місці і все одно повертає його. Чи повинен повернути strcat недійсним?
Джеррі Єремія

Java та C ++ мають дуже різну поведінку стосовно параметрів проходження. Якщо Itemє, class Item ...а ні typedef ...& Item, місцева itemвже копія
Caleth

google.github.io/styleguide/cppguide.html#Output_Parameters Google вважає за краще використовувати значення RETURN для виведення
Firegun

Відповіді:


26

Це насправді є питанням думки, але для того, що варто, я вважаю оманливим повернення модифікованого елемента, якщо предмет змінено на місці. Окремо, якщо ви predictPriceзбираєтеся змінити елемент, він повинен мати ім’я, яке вказує, що він буде робити це (як- setPredictPriceнебудь або щось подібне).

Я вважаю за краще (по порядку)

  1. Це predictPriceбув методItem (по модулю це вагома причина не може бути, що може бути у вашому загальному дизайні), який або

    • Повернув прогнозовану ціну, або

    • Мав setPredictedPriceзамість цього ім’я

  2. Це predictPrice не змінило Item, але повернуло прогнозовану ціну

  3. Це predictPriceбув voidметод , званийsetPredictedPrice

  4. Це predictPriceповернулося this(для прив'язування методів до інших методів у будь-якому екземплярі, до якого воно входить) (і називалося setPredictedPrice)


1
Дякую за відповідь. Щодо 1, ми не можемо передбачити ціну в пункті. 2 - гарна пропозиція - я думаю, що це, мабуть, правильне рішення тут.
Squimmy

2
@Squimmy: І досить впевнено, я щойно провів 15 хвилин на помилку, яку викликав хтось, використовуючи саме ту схему, про яку ви сперечалися: a = doSomethingWith(b) модифікував b і повертав її з модифікаціями, які згодом підірвали мій код, подумавши, що він знав, що bмає в цьому. :-)
TJ Crowder

11

У межах об'єктно-орієнтованої парадигми дизайну речі не повинні змінювати об'єкти поза самим об'єктом. Будь-які зміни стану об’єкта повинні здійснюватися за допомогою методів на об’єкті.

Таким чином, void predictPrice(Item item)як член-член якогось іншого класу є неправильним . Можливо, це було прийнятним за часів C, але для Java та C ++ модифікація об'єкта передбачає більш глибоке з’єднання з об’єктом, що, ймовірно, призведе до інших проблем дизайну вниз по дорозі (коли ви переробляєте клас та змінюєте його поля, тепер, коли 'predictPrice' потрібно змінити в інший файл.

Повертаючи новий об'єкт назад, не має побічних ефектів, переданий параметр не змінюється. Ви (метод predictPrice) не знаєте, де ще використовується цей параметр. Є чи Itemключ до хеш де - небудь? ви змінили його хеш-код, роблячи це? Чи хтось ще тримається за це, сподіваючись, що він не зміниться?

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


Давайте подивимось, що станеться, якщо хтось поспішає з полями чогось у хеші. Давайте візьмемо якийсь код:

import java.util.*;

public class Main {
    public static void main (String[] args) {
        Set set = new HashSet();
        Data d = new Data(1,"foo");
        set.add(d);
        set.add(new Data(2,"bar"));
        System.out.println(set.contains(d));
        d.field1 = 2;
        System.out.println(set.contains(d));
    }

    public static class Data {
        int field1;
        String field2;

        Data(int f1, String f2) {
            field1 = f1;
            field2 = f2;
        }

        public int hashCode() {
            return field2.hashCode() + field1;
        }

        public boolean equals(Object o) {
            if(!(o instanceof Data)) return false;
            Data od = (Data)o;
            return od.field1 == this.field1 && od.field2.equals(this.field2);

        }
    }
}

ідеоне

І я визнаю, що це не найбільший код (безпосередньо доступ до полів), але він служить своєю метою продемонструвати проблему, коли мутабельні дані використовуються як ключ до HashMap, або в цьому випадку, просто вводяться в HashSet

Вихід цього коду:

true
false

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

Таким чином, повертаючись до початкового аспекту питання, названий метод не "знає", як використовується об'єкт, який він отримує як параметр. Надання "дозволяє мутувати об'єкт" як єдиний спосіб зробити це означає, що існує ряд найтонших помилок, які можуть повзати. Як і втрата значень у хеші ... якщо ви не додасте більше, і хеш переосмислиться - повірте мені , ось жахлива помилка на пошук (я втратив значення, поки не додаю ще 20 елементів до hashMap, а потім раптом він з’являється знову).

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

Пов'язане: Переписування та повернення значення аргументу, що використовується як умовне твердження if, всередині того ж, якщо оператор


3
Це не звучить правильно. Швидше за все, predictPriceвін не повинен прямо поспілкуватися з Itemчленами 's, але що не так з методами виклику Itemцього зробити? Якщо це єдиний модифікований об'єкт, можливо, ви можете зробити аргумент, що це має бути метод модифікованого об'єкта, але в інших випадках кілька об'єктів (які точно не повинні бути одного класу) змінюються, особливо в досить високого рівня.

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

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

2

Я завжди дотримуюсь практики не мутувати чужого предмета. Тобто, лише мутуючи об’єкти, що належать класу / struct / whathaveyou, що ви всередині.

З огляду на такий клас даних:

class Item {
    public double price = 0;
}

Це нормально:

class Foo {
    private Item bar = new Item();

    public void predictPrice() {
        bar.price = bar.price + 5; // or something
    }
}

// Elsewhere...

Foo foo = Foo();
foo.predictPrice();
System.out.println(foo.bar.price);
// Since I called predictPrice on Foo, which takes and returns nothing, it's
// apparent something might've changed

І це дуже ясно:

class Foo {
    private Item bar = new Item();
}
class Baz {
    public double predictPrice(Item item) {
        return item.price + 5; // or something
    }
}

// Elsewhere...

Foo foo = Foo();
Baz baz = Baz();
foo.bar.price = baz.predictPrice(foo.bar);
System.out.println(foo.bar.price);
// Since I explicitly set foo.bar.price to the return value of predictPrice, it's
// obvious foo.bar.price might've changed

Але це занадто каламутно і загадково на мій смак:

class Foo {
    private Item bar = new Item();
}
class Baz {
    public void predictPrice(Item item) {
        item.price = item.price + 5; // or something
    }
}

// Elsewhere...

Foo foo = Foo();
Baz baz = Baz();
baz.predictPrice(foo.bar);
System.out.println(foo.bar.price);
// In my head, it doesn't appear that to foo, bar, or price changed. It seems to
// me that, if anything, I've placed a predicted price into baz that's based on
// the value of foo.bar.price

Будь ласка, додайте до коментарів будь-які голоси, щоб я міг написати кращі відповіді в майбутньому :)
Ben Leggiero

1

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

У Java Itemє еталонним типом - це тип покажчиків на об'єкти, на які є екземпляри Item. У C ++ цей тип записується як Item *(синтаксис для однієї і тієї ж речі відрізняється між мовами). Тож Item predictPrice(Item item)у Java еквівалентно Item *predictPrice(Item *item)C ++. В обох випадках це функція (або метод), яка бере вказівник на об’єкт і повертає вказівник на об’єкт.

У C ++ Item(без нічого іншого) - це тип об'єкта (припускаючи, що Itemце ім'я класу). Значення цього типу "є" об'єктом і Item predictPrice(Item item)оголошує функцію, яка приймає об'єкт і повертає об'єкт. Оскільки &він не використовується, він передається і повертається за значенням. Це означає, що об’єкт копіюється при передачі та копіюється при поверненні. У Java немає еквівалента, оскільки у Java немає типів об'єктів.

Так що це? Багато речей, про які ви ставите запитання (наприклад, "Я вважаю, що це працює на той самий об'єкт, який передається в ...") залежить від розуміння того, що саме передається та повертається сюди.


1

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

void predictPrice(Item * const item);

Цей стиль призводить до:

  • Викликаючи це, ви бачите:

    predictPrice (& my_item);

і ви знаєте, що це буде змінено вашим дотриманням стилю.

  • В іншому випадку віддайте перевагу передачі через const ref або value, коли ви не хочете, щоб функція модифікувала примірник.

Слід зазначити, що, хоча це синтаксис набагато чіткіше, він недоступний на Java.
Ben Leggiero

0

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

Зауважте, що це має сенс лише тоді, коли метод має свободу повертати інший об'єкт. Якщо ваш javadoc каже, @returns the same value of parameter aто це марно. Але якщо ваш javadoc каже @return an instance that holds the same data than parameter a, то повернення того ж чи іншого екземпляра - це деталь реалізації.

Наприклад, у моєму поточному проекті (Java EE) у мене є бізнес-рівень; щоб зберігати в БД (і призначати автоматичний ідентифікатор) працівника, скажімо, працівника. У мене щось подібне

 public Employee createEmployee(Employee employeeData) {
   this.entityManager.persist(employeeData);
   return this.employeeData;
 }

Звичайно, немає різниці з цією реалізацією, оскільки JPA повертає той самий об'єкт після присвоєння ідентифікатора. Тепер, якби я перейшов на JDBC

 public Employee createEmployee(Employee employeeData) {
   PreparedStatement ps = this.connection.prepareStatement("INSERT INTO EMPLOYEE(name, surname, data) VALUES(?, ?, ?)");
   ... // set parameters
   ps.execute();
   int id = getIdFromGeneratedKeys(ps);
   ??????
 }

Тепер на ????? У мене є три варіанти.

  1. Визначте сетер для Id, і я поверну той самий об’єкт. Некрасивий, бо setIdбуде доступний скрізь.

  2. Виконайте важкі роздуми і встановіть ідентифікатор в Employeeоб’єкті. Брудне, але принаймні обмежене createEmployeeзаписом.

  3. Надайте конструктор, який копіює оригінал Employeeта приймає також idнабір.

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

Тепер для 1. і 2. ви можете повертати той самий екземпляр, але для 3. і 4. ви повернете нові екземпляри. Якщо з принципу, який ви зафіксували у своєму API, що "реальне" значення буде тим, яке повертається методом, у вас є більше свободи.

Єдиний реальний аргумент, проти якого я можу подумати, - це те, що, використовуючи implementatios 1. або 2., люди, які використовують ваш API, можуть не помітити призначення результату, і їх код порушиться, якщо ви зміните 3. або 4. реалізацію).

У будь-якому випадку, як бачите, мої уподобання базуються на інших особистих уподобаннях (наприклад, забороняти сетер для ІД), тому, можливо, це не стосується всіх.


0

При кодуванні в Java слід спробувати змусити використання кожного об'єкта змінного типу відповідати одному з двох шаблонів:

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

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

Мені не подобається, щоб методи мутували базовий об'єкт і повертали посилання, оскільки вони надають вигляд підгонки другого шаблону вище. Є кілька випадків, коли підхід може бути корисним, але код, який буде мутувати об'єкти, повинен виглядати так, як це буде робити; Код як myThing = myThing.xyz(q);виглядає набагато менше, як він мутує об'єкт, визначений, myThingніж просто myThing.xyz(q);.

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