Когда приватный метод должен использовать публичный маршрут для доступа к приватным данным?

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().

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

0x5f3759df
источник

Ответы:

7

Это зависит от фактического значения a, bи getProduct.

Цель геттеров - иметь возможность изменять фактическую реализацию, сохраняя интерфейс объекта неизменным. Например, если один день getAстановится return a + 1;, изменение локализуется для получателя.

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

Если это имеет смысл для getProductиспользования aи bнепосредственно, используйте их. В противном случае используйте геттеры для предотвращения проблем с обслуживанием позже.

Пример, где можно использовать геттеры:

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

Хотя на данный момент получатель не содержит никакой бизнес-логики, не исключено, что логика в конструкторе будет перенесена на получатель, чтобы избежать работы с базой данных при инициализации объекта:

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 # readonly), которое не ожидается в будущем: скорее всего, идентификатор геттера никогда не станет вычисленным значением. Так что будьте проще и получите доступ к полю напрямую.

Другое преимущество заключается в том, что getIdв будущем он может быть удален, если окажется, что он не используется снаружи (как в предыдущем фрагменте кода).

Арсений Мурзенко
источник
Я не могу дать вам +1, потому что ваш пример использования закрытых полей не является одним из IMHO, главным образом потому, что вы объявили const: я предполагаю, что это означает, что компилятор в getIdлюбом случае встроит вызов и позволит вам вносить изменения в любом направлении. ( В противном случае я полностью согласен с вашими причинами для использования добытчиков.) И в языках , которые предоставляют синтаксис свойств, есть даже меньше причины не использовать свойство , а не поле подложки непосредственно.
Марк Херд
1

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

Это, конечно, отличается, если получатели реализуют реальную логику, в этом случае это зависит от того, нужно ли вам использовать их логику или нет.

DeadMG
источник
1

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

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

rory.ap
источник
0

Для класса эта маленькая простота побеждает. Я бы просто использовал * б.

Для чего-то гораздо более сложного я настоятельно рекомендовал бы использовать getA () * getB (), если бы хотел четко отделить «минимальный» интерфейс от всех других функций в полном публичном API. Отличным примером будет std :: string в C ++. Он имеет 103 функции-члена, но только 32 из них действительно нуждаются в доступе к частным членам. Если бы у вас был такой сложный класс, то принудительное выполнение всех «неосновных» функций через «базовый API» могло бы значительно упростить реализацию, тестирование, отладку и рефакторинг.

Ixrec
источник
1
Если у вас был такой сложный класс, вы должны его исправить, а не бинт.
DeadMG
Согласовано. Я, наверное, должен был выбрать пример только с 20-30 функциями.
Ixrec
1
«103 функции» - это немного красной селедки. Перегруженные методы должны учитываться один раз с точки зрения сложности интерфейса.
Авнер Шахар-Каштан
Я полностью не согласен. Разные перегрузки могут иметь разную семантику и разные интерфейсы.
DeadMG
Даже для этого «маленького» примера getA() * getB()лучше в среднесрочной и долгосрочной перспективе.
Марк Херд