Khi nào một phương pháp riêng nên đi theo con đường công cộng để truy cập dữ liệu riêng tư?


11

Khi nào một phương pháp riêng nên đi theo con đường công cộng để truy cập dữ liệu riêng tư? Ví dụ: nếu tôi có lớp 'số nhân' bất biến này (một chút giả định, tôi biết):

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

Có hai cách tôi có thể thực hiện getProduct:

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

hoặc là

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

Bởi vì ý định ở đây là sử dụng giá trị của a, tức là để có được a , sử dụng getA()để thực hiện getProduct()có vẻ sạch hơn đối với tôi. Tôi muốn tránh sử dụng atrừ khi tôi phải sửa đổi nó. Mối quan tâm của tôi là tôi không thường thấy mã được viết theo cách này, theo kinh nghiệm của tôi a * bsẽ là một triển khai phổ biến hơn getA() * getB().

Các phương thức riêng tư có nên sử dụng cách thức công khai khi chúng có thể truy cập trực tiếp vào một cái gì đó không?

Câu trả lời:


7

Nó phụ thuộc vào ý nghĩa thực tế của a, bgetProduct.

Mục đích của getters là để có thể thay đổi việc thực hiện thực tế trong khi vẫn giữ giao diện của đối tượng như cũ. Ví dụ, nếu một ngày, getAtrở thành return a + 1;, thay đổi được bản địa hóa thành một getter.

Các trường hợp kịch bản thực đôi khi phức tạp hơn trường sao lưu không đổi được chỉ định thông qua một hàm tạo được liên kết với một getter. Ví dụ, giá trị của trường có thể được tính hoặc tải từ cơ sở dữ liệu trong phiên bản gốc của mã. Trong phiên bản tiếp theo, bộ nhớ đệm có thể được thêm vào để tối ưu hóa hiệu suất. Nếu getProducttiếp tục sử dụng phiên bản được tính toán, nó sẽ không được hưởng lợi từ bộ nhớ đệm (hoặc người bảo trì sẽ thực hiện thay đổi tương tự hai lần).

Nếu nó có ý nghĩa hoàn hảo getProductđể sử dụng abtrực tiếp, sử dụng chúng. Nếu không, sử dụng getters để ngăn chặn các vấn đề bảo trì sau này.

Ví dụ trong đó người ta sẽ sử dụng 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;
}

Mặc dù hiện tại, getter không chứa bất kỳ logic nghiệp vụ nào, không loại trừ rằng logic trong hàm tạo sẽ được di chuyển sang getter để tránh thực hiện công việc cơ sở dữ liệu khi khởi tạo đối tượng:

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

Sau đó, bộ nhớ đệm có thể được thêm vào (trong C #, người ta sẽ sử dụng Lazy<T>, làm cho mã ngắn và dễ dàng; Tôi không biết liệu có tương đương trong C ++ không):

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

Cả hai thay đổi đều tập trung vào getter và trường sao lưu, mã còn lại không bị ảnh hưởng. Nếu, thay vào đó, tôi đã sử dụng một lĩnh vực thay vì một getter getPriceWithRebate, tôi cũng sẽ phải phản ánh những thay đổi ở đó.

Ví dụ trong đó người ta có thể sử dụng các trường riêng:

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

Trình getter rất đơn giản: đó là một đại diện trực tiếp của trường hằng số (tương tự như C # readonly) không được dự kiến ​​sẽ thay đổi trong tương lai: rất có thể, ID getter sẽ không bao giờ trở thành giá trị được tính toán. Vì vậy, giữ cho nó đơn giản, và truy cập vào các lĩnh vực trực tiếp.

Một lợi ích khác là getIdcó thể được loại bỏ trong tương lai nếu có vẻ như nó không được sử dụng bên ngoài (như trong đoạn mã trước).


Tôi không thể cung cấp cho bạn +1 vì ví dụ của bạn để sử dụng các trường riêng không phải là một IMHO, chủ yếu là vì bạn đã khai báo const: Tôi cho rằng điều đó có nghĩa là trình biên dịch sẽ thực hiện getIdcuộc gọi bằng mọi cách và nó cho phép bạn thay đổi theo một trong hai hướng. (Nếu không, tôi hoàn toàn đồng ý với lý do của bạn để sử dụng getters.) Và trong các ngôn ngữ cung cấp cú pháp thuộc tính, thậm chí còn có ít lý do hơn để không sử dụng thuộc tính thay vì trực tiếp trường sao lưu.
Đánh dấu

1

Thông thường, bạn sẽ sử dụng các biến trực tiếp. Bạn mong muốn thay đổi tất cả các thành viên khi thay đổi triển khai của một lớp. Không sử dụng các biến trực tiếp chỉ đơn giản làm cho việc cách ly chính xác mã phụ thuộc vào chúng trở nên khó khăn hơn và khiến cho việc đọc thành viên trở nên khó khăn hơn.

Điều này tất nhiên là khác nhau nếu các getters thực hiện logic thực sự, trong trường hợp đó phụ thuộc vào việc bạn có cần sử dụng logic của họ hay không.


1

Tôi sẽ nói rằng sử dụng các phương thức công cộng sẽ tốt hơn, nếu không vì bất kỳ lý do nào khác ngoài việc tuân thủ DRY .

Tôi biết trong trường hợp của bạn, bạn có các trường sao lưu đơn giản cho người truy cập, nhưng bạn có thể có logic nhất định, ví dụ như tải mã lười biếng, mà bạn cần chạy trước lần đầu tiên bạn sử dụng biến đó. Vì vậy, bạn sẽ muốn gọi người truy cập của bạn thay vì trực tiếp tham khảo các lĩnh vực của bạn. Mặc dù bạn không có điều này trong trường hợp này, nhưng vẫn hợp lý khi tuân theo một quy ước duy nhất. Bằng cách đó, nếu bạn từng thay đổi logic của mình, bạn chỉ phải thay đổi nó ở một nơi.


0

Đối với một lớp học nhỏ này, đơn giản chiến thắng. Tôi sẽ chỉ sử dụng một * b.

Đối với một cái gì đó phức tạp hơn nhiều, tôi sẽ cân nhắc mạnh mẽ việc sử dụng getA () * getB () nếu tôi muốn tách biệt rõ ràng giao diện "tối thiểu" khỏi tất cả các chức năng khác trong API công khai đầy đủ. Một ví dụ tuyệt vời sẽ là std :: string trong C ++. Nó có 103 chức năng thành viên, nhưng chỉ có 32 trong số họ thực sự cần truy cập vào các thành viên tư nhân. Nếu bạn có một lớp phức tạp, việc buộc tất cả các hàm "không phải lõi" luôn luôn đi qua "API lõi" có thể giúp việc thực hiện dễ dàng hơn rất nhiều để kiểm tra, gỡ lỗi và tái cấu trúc.


1
Nếu bạn có một lớp phức tạp, bạn nên buộc phải sửa nó, không hỗ trợ nó.
DeadMG

Đã đồng ý. Tôi có lẽ nên chọn một ví dụ chỉ có 20-30 chức năng.
Ixrec

1
"103 chức năng" là một chút cá trích đỏ. Các phương thức quá tải nên được tính một lần, về mặt độ phức tạp của giao diện.
Avner Shahar-Kashtan

Tôi hoàn toàn không đồng ý. Quá tải khác nhau có thể có ngữ nghĩa khác nhau và giao diện khác nhau.
DeadMG

Ngay cả đối với ví dụ "nhỏ" này, getA() * getB()vẫn tốt hơn trong trung và dài hạn.
Đánh dấu
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.