Kiedy metoda prywatna powinna skorzystać z publicznej drogi dostępu do prywatnych danych?


11

Kiedy metoda prywatna powinna skorzystać z publicznej drogi dostępu do prywatnych danych? Na przykład, gdybym miał tę niezmienną klasę „mnożnika” (nieco wymyśloną, wiem):

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

Są dwa sposoby na wdrożenie getProduct:

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

lub

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

Ponieważ intencją tutaj jest wykorzystanie wartości a, tj. Uzyskanie a , użycie getA()do implementacji getProduct()wydaje mi się czystsze. Wolałbym unikać używania, achyba że musiałbym go zmodyfikować. Obawiam się, że często nie widzę kodu napisanego w ten sposób, z mojego doświadczenia wynika, że ​​jest a * bto bardziej powszechna implementacja niż getA() * getB().

Czy prywatne metody powinny kiedykolwiek korzystać z publicznego sposobu, gdy mają bezpośredni dostęp do czegoś?

Odpowiedzi:


7

To zależy od rzeczywistego znaczenia a, bi getProduct.

Celem modułów pobierających jest możliwość zmiany rzeczywistej implementacji przy jednoczesnym utrzymaniu interfejsu obiektu bez zmian. Na przykład, jeśli któregoś dnia stanie getAsię return a + 1;, zmiana jest zlokalizowana na getter.

Rzeczywiste przypadki scenariuszy są czasem bardziej skomplikowane niż stałe pole zaplecza przypisywane przez konstruktor powiązany z modułem pobierającym. Na przykład wartość pola może być obliczona lub załadowana z bazy danych w oryginalnej wersji kodu. W następnej wersji można dodać buforowanie w celu zoptymalizowania wydajności. Jeśli getProductnadal będzie używać wersji obliczonej, nie skorzysta z buforowania (lub opiekun dokona tej samej zmiany dwa razy).

Jeśli jest to sensowne w getProductużyciu ai bbezpośrednio, użyj ich. W przeciwnym razie użyj programów pobierających, aby zapobiec problemom z obsługą później.

Przykład użycia gettersa:

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

Chociaż w tej chwili moduł pobierający nie zawiera żadnej logiki biznesowej, nie jest wykluczone, że logika w konstruktorze zostanie poddana migracji do modułu pobierającego, aby uniknąć wykonywania pracy bazy danych podczas inicjowania obiektu:

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

Później można dodać buforowanie (w języku C # można użyć Lazy<T>, dzięki czemu kod jest krótki i łatwy; nie wiem, czy istnieje odpowiednik w 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;
}

Obie zmiany koncentrowały się na module pobierającym i polu zaplecza, pozostały kod pozostał nienaruszony. Gdybym zamiast tego użył pola zamiast gettera getPriceWithRebate, musiałbym również uwzględnić tam zmiany.

Przykład, w którym prawdopodobnie użyłbyś prywatnych pól:

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

Moduł pobierający jest prosty: jest to bezpośrednia reprezentacja stałego (podobnego do C # readonly) pola, które nie powinno się zmieniać w przyszłości: istnieje prawdopodobieństwo, że moduł pobierający ID nigdy nie stanie się wartością obliczoną. Uprość to i uzyskaj bezpośredni dostęp do pola.

Kolejną korzyścią jest to, że getIdmoże zostać usunięty w przyszłości, jeśli okaże się, że nie jest używany na zewnątrz (jak w poprzednim fragmencie kodu).


Nie mogę dać ci +1, ponieważ twój przykład użycia prywatnych pól nie jest jednym IMHO, głównie dlatego, że zadeklarowałeś const: Zakładam, że oznacza to, że kompilator i getIdtak wywoła połączenie i pozwoli ci dokonać zmian w obu kierunkach. (W przeciwnym razie w pełni zgadzam się z powodami, dla których warto korzystać z funkcji pobierających.) A w językach, które zapewniają składnię właściwości, istnieje jeszcze mniej powodów, aby nie korzystać z właściwości zamiast bezpośrednio pola kopii zapasowej.
Mark Hurd

1

Zazwyczaj zmienne byłyby używane bezpośrednio. Oczekujesz zmiany wszystkich członków podczas zmiany implementacji klasy. Nieużywanie zmiennych bezpośrednio po prostu utrudnia prawidłowe wyodrębnienie zależnego od nich kodu i utrudnia odczyt elementu.

Jest to oczywiście inne, jeśli moduły pobierające implementują prawdziwą logikę, w takim przypadku zależy to od tego, czy należy użyć ich logiki, czy nie.


1

Powiedziałbym, że stosowanie metod publicznych byłoby lepsze, gdyby nie z jakiegokolwiek innego powodu, ale aby zachować zgodność z DRY .

Wiem, że w twoim przypadku masz proste pola zapasowe dla swoich akcesorów, ale możesz mieć pewną logikę, np. Leniwy kod ładujący, który musisz uruchomić przed pierwszym użyciem tej zmiennej. Tak więc, chciałbyś zadzwonić do swoich akcesorów zamiast bezpośrednio odwoływać się do swoich pól. Nawet jeśli nie masz tego w tym przypadku, warto trzymać się jednej konwencji. W ten sposób, jeśli kiedykolwiek zmienisz logikę, musisz ją zmienić tylko w jednym miejscu.


0

Dla klasy ta niewielka prostota wygrywa. Po prostu użyłbym * b.

W przypadku czegoś o wiele bardziej skomplikowanego, zdecydowanie rozważyłbym użycie getA () * getB (), gdybym chciał wyraźnie oddzielić „minimalny” interfejs od wszystkich innych funkcji w pełnym publicznym API. Doskonałym przykładem może być std :: string w C ++. Ma 103 funkcje członków, ale tylko 32 z nich naprawdę potrzebuje dostępu do członków prywatnych. Jeśli posiadasz tak złożoną klasę, wymuszenie konsekwentnego przechodzenia przez wszystkie funkcje inne niż „core” przez „core API” może znacznie ułatwić testowanie, debugowanie i refaktoryzację implementacji.


1
Jeśli miałeś tak złożoną klasę, powinieneś być zmuszony to naprawić, a nie wspomagać go.
DeadMG,

Zgoda. Prawdopodobnie powinienem był wybrać przykład z jedynie 20-30 funkcjami.
Ixrec,

1
„103 funkcje” to trochę czerwony śledź. Przeciążone metody należy policzyć raz, pod względem złożoności interfejsu.
Avner Shahar-Kashtan

Całkowicie się nie zgadzam. Różne przeciążenia mogą mieć różną semantykę i różne interfejsy.
DeadMG,

Nawet ten „mały” przykład getA() * getB()jest lepszy w średnim i długim okresie.
Mark Hurd
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.