Коли приватний метод повинен приймати загальнодоступний шлях для доступу до приватних даних?


11

Коли приватний метод повинен приймати загальнодоступний шлях для доступу до приватних даних? Наприклад, якби у мене був цей незмінний клас мультиплікатора (трохи надуманий, я знаю):

class Multiplier {
public:
    Multiplier(int a, int b) : a(a), b(b) { }
    int getA() const { return a; }
    int getB() const { return b; }
    int getProduct() const { /* ??? */ }
private:
    int a, b;
};

Я можу реалізувати два способи getProduct:

    int getProduct() const { return a * b; }

або

    int getProduct() const { return getA() * getB(); }

Тому що намір тут - використовувати значення a, тобто отримати a , використовуючи getA()для реалізації, getProduct()здається мені більш чистим. Я вважаю за краще уникати використання, aякщо мені не довелося його змінювати. Мене хвилює те, що я часто не бачу коду, написаного таким чином, на мій досвід, це a * bбуло б більш поширеною реалізацією, ніж getA() * getB().

Чи повинні приватні методи коли-небудь використовувати публічний спосіб, коли вони можуть отримати доступ до чогось безпосередньо?

Відповіді:


7

Це залежить від фактичного значення a, bі getProduct.

Метою геттерів є можливість змінити фактичну реалізацію, зберігаючи інтерфейс об'єкта тим самим. Наприклад, якщо один день getAстає return a + 1;, зміна локалізується на геттера.

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

Якщо це має ідеальний сенс для getProductвикористання aта bбезпосередньо, використовуйте їх. В іншому випадку використовуйте геттери, щоб запобігти проблемам з технічним обслуговуванням пізніше.

Приклад, де можна було б отримати getters:

class Product {
public:
    Product(ProductId id) : {
        price = Money.fromCents(
            data.findProductById(id).price,
            environment.currentCurrency
        )
    }

    Money getPrice() {
        return price;
    }

    Money getPriceWithRebate() {
        return getPrice().applyRebate(rebate); // ← Using a getter instead of a field.
    }
private:
    Money price;
}

Хоча на даний момент, getter не містить жодної бізнес-логіки, не виключено, що логіка в конструкторі буде перенесена на getter, щоб уникнути виконання роботи з базою даних при ініціалізації об'єкта:

class Product {
public:
    Product(ProductId id) : id(id) { }

    Money getPrice() {
        return Money.fromCents(
            data.findProductById(id).price,
            environment.currentCurrency
        )
    }

    Money getPriceWithRebate() {
        return getPrice().applyRebate(rebate);
    }
private:
    const ProductId id;
}

Пізніше може бути додано кешування (у C #, можна було б скористатися Lazy<T>, зробивши код коротким та легким; я не знаю, чи є еквівалент у C ++):

class Product {
public:
    Product(ProductId id) : id(id) { }

    Money getPrice() {
        if (priceCache == NULL) {
            priceCache = Money.fromCents(
                data.findProductById(id).price,
                environment.currentCurrency
            )

        return priceCache;
    }

    Money getPriceWithRebate() {
        return getPrice().applyRebate(rebate);
    }
private:
    const ProductId id;
    Money priceCache;
}

Обидві зміни були зосереджені на геттері та резервному полі, а решта коду не впливали. Якби я замість цього не використовував поле getPriceWithRebate, я мав би відобразити і зміни.

Приклад, коли можна було б використовувати приватні поля:

class Product {
public:
    Product(ProductId id) : id(id) { }
    ProductId getId() const { return id; }
    Money getPrice() {
        return Money.fromCents(
            data.findProductById(id).price, // ← Accessing `id` directly.
            environment.currentCurrency
        )
    }
private:
    const ProductId id;
}

Геттер простий: це пряме подання постійного (подібного до C # 's readonly) поля, яке не очікується змінити в майбутньому: швидше за все, отримання ID ніколи не стане обчисленим значенням. Тому нехай це буде просто та безпосередньо отримуйте доступ до поля.

Ще одна перевага полягає в тому, що він getIdможе бути видалений у майбутньому, якщо виявиться, що він не використовується зовні (як у попередньому фрагменті коду).


Я не можу дати вам +1, оскільки ваш приклад для використання приватних полів - це не один IMHO, головним чином тому, що ви оголосили const: я припускаю, що це означає, що компілятор в getIdбудь-якому випадку вбуде вкладений дзвінок, і це дозволяє вам вносити зміни в будь-якому напрямку. ( В іншому випадку я повністю згоден з вашими причинами для використання видобувачів.) І в мовах , які надають синтаксис властивостей, є навіть менше причини не використовувати властивість , а не поле підкладки безпосередньо.
Марк Херд

1

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

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


1

Я б сказав, що використання загальнодоступних методів було б кращим, якщо не з якоїсь іншої причини, але відповідати DRY .

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


0

Для класу ця маленька простота виграє. Я б просто використав * b.

Щось набагато складніше, я б настійно розглядав можливість використання getA () * getB (), якби я хотів чітко відокремити "мінімальний" інтерфейс від усіх інших функцій у повному загальнодоступному API. Прекрасним прикладом може бути std :: string в C ++. Він має 103 членські функції, але лише 32 з них дійсно потребують доступу до приватних членів. Якщо у вас був такий складний клас, змушення всіх «непрофільних» функцій послідовно проходити через «основний API» може полегшити реалізацію, перевірку, налагодження та рефактор.


1
Якщо у вас був клас такого складного, ви повинні змусити його виправити, а не допомагати йому.
DeadMG

Домовились. Я, мабуть, мав обрати приклад із лише 20-30 функціями.
Іксрек

1
"103 функції" - це трохи червона оселедець. Перевантажені методи слід зараховувати один раз у термінах складності інтерфейсу.
Авнер Шахар-Каштан

Я повністю не згоден. Різні перевантаження можуть мати різну семантику та різні інтерфейси.
DeadMG

Навіть для цього «маленького» прикладу getA() * getB()краще в середньо- та довгостроковій перспективі.
Марк Херд
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.