Có ổn không khi một lớp sử dụng phương thức công khai của riêng mình?


23

Lý lịch

Tôi hiện đang có một tình huống trong đó tôi có một đối tượng được truyền và nhận bởi một thiết bị. Thông báo này có một số cấu trúc, như sau:

public void ReverseData()
public void ScheduleTransmission()

Các ScheduleTransmissionphương pháp cần để gọi ReverseDataphương pháp bất cứ khi nào nó được gọi. Tuy nhiên, có những lúc tôi sẽ cần gọi ReverseDatabên ngoài (và tôi nên thêm hoàn toàn bên ngoài không gian tên ) từ nơi đối tượng được khởi tạo trong ứng dụng.

Đối với "nhận", tôi có nghĩa là ReverseDatasẽ được gọi bên ngoài trong một object_receivedtrình xử lý sự kiện để đảo ngược dữ liệu.

Câu hỏi

Có phải thường được chấp nhận cho một đối tượng để gọi các phương thức công khai của chính nó?


5
Không có gì sai khi gọi một phương thức công khai từ chính nó. Nhưng, dựa trên tên phương thức của bạn, ReverseData () nghe có vẻ hơi nguy hiểm khi trở thành phương thức công khai nếu nó đảo ngược dữ liệu nội bộ. Điều gì sẽ xảy ra nếu ReverseData () được gọi bên ngoài đối tượng và sau đó được gọi lại với RoutTransmission ().
Kaan

@Kaan Đây không phải là tên thật của các phương thức của tôi, nhưng có liên quan chặt chẽ. Trong thực tế, "dữ liệu ngược" chỉ đảo ngược 8 bit của tổng số từ và được thực hiện khi chúng tôi nhận và truyền.
Snoop

Câu hỏi vẫn đứng vững. Điều gì xảy ra nếu 8 bit đó bị đảo ngược hai lần trước khi truyền được lên lịch? Điều này cảm thấy như một lỗ hổng lớn trong giao diện công cộng. Suy nghĩ về giao diện công cộng như tôi đề cập trong câu trả lời của tôi có thể giúp đưa vấn đề này ra ánh sáng.
Daniel T.

2
@StevieV Tôi tin rằng điều này chỉ khuếch đại mối quan tâm Kaan tăng lên. Nghe có vẻ như bạn đang phơi bày một phương thức thay đổi trạng thái của đối tượng và trạng thái phụ thuộc chủ yếu vào số lần phương thức được gọi. Điều này tạo ra một cơn ác mộng trong việc cố gắng theo dõi trạng thái của đối tượng trong suốt mã của bạn. Tôi nghe có vẻ giống như bạn sẽ được hưởng lợi từ các loại dữ liệu riêng biệt từ các trạng thái khái niệm này, vì vậy bạn có thể biết nó là gì trong bất kỳ đoạn mã nào mà không phải lo lắng về nó.
jpmc26

2
@StevieV một cái gì đó như Dữ liệu và Nối tiếpData. Dữ liệu là những gì mã nhìn thấy và serializedData là những gì được gửi qua mạng. Sau đó thực hiện chuyển đổi dữ liệu ngược (hoặc thay vì tuần tự hóa / giải tuần tự hóa dữ liệu) từ loại này sang loại khác.
csiz 15/03/2016

Câu trả lời:


33

Tôi sẽ nói rằng nó không chỉ được chấp nhận mà còn được khuyến khích đặc biệt nếu bạn có kế hoạch cho phép tiện ích mở rộng. Để hỗ trợ các tiện ích mở rộng cho lớp trong C #, bạn cần gắn cờ phương thức là ảo theo các nhận xét bên dưới. Tuy nhiên, bạn có thể muốn ghi lại điều này để ai đó không ngạc nhiên khi ghi đè ReverseData () thay đổi cách thức hoạt động của Lịch trình ().

Nó thực sự đi xuống thiết kế của lớp. ReverseData () nghe có vẻ như là một hành vi cơ bản của lớp bạn. Nếu bạn cần sử dụng hành vi này ở những nơi khác, có lẽ bạn không muốn có các phiên bản khác của hành vi đó. Bạn chỉ cần cẩn thận rằng bạn không để chi tiết cụ thể về rò rỉ lên lịch trình () vào ReverseData (). Điều đó sẽ tạo ra vấn đề. Nhưng vì bạn đã sử dụng cái này bên ngoài lớp học, có lẽ bạn đã nghĩ đến điều đó rồi.


Wow, dường như thực sự kỳ lạ với tôi nhưng điều này giúp. Cũng muốn xem những gì một số người khác nói. Cảm ơn bạn.
Snoop

2
"để ai đó không ngạc nhiên khi ghi đè ReverseData () thay đổi cách thức hoạt động của Lịch trình () : " không, không thực sự. Ít nhất là không có trong C # (lưu ý rằng câu hỏi có thẻ C #). Trừ khi ReverseData()là trừu tượng, bất kể bạn làm gì trong lớp con, nó luôn là phương thức ban đầu sẽ được gọi.
Arseni Mourzenko 14/03/2016

2
@MainMa Hoặc virtual... Và nếu bạn ghi đè phương thức, thì theo định nghĩa, nó cần phải virtualhoặc abstract.
Pokechu22

1
@ Pokechu22: thực sự, virtualhoạt động quá. Trong mọi trường hợp, chữ ký, theo OP, là public void ReverseData()vậy, nên một phần của câu trả lời về công cụ ghi đè là hơi sai lệch.
Arseni Mourzenko 14/03/2016

@MainMa bạn đang nói rằng nếu một phương thức lớp cơ sở gọi một phương thức ảo thì nó không hoạt động theo cách ảo thông thường trừ khi đó là abstract? Tôi đã tìm kiếm câu trả lời trên abstractvs virtualvà không thấy điều đó được đề cập: khá trừu tượng chỉ có nghĩa là không có phiên bản nào được đưa ra.
JDługosz 15/03/2016

20

Chắc chắn rồi.

Khả năng hiển thị của một phương thức có một mục đích duy nhất là cho phép hoặc từ chối truy cập vào một phương thức bên ngoài lớp hoặc trong một lớp con; phương thức công khai, được bảo vệ và riêng tư luôn có thể được gọi trong chính lớp đó.

Không có gì sai khi gọi các phương thức công cộng. Hình minh họa trong câu hỏi của bạn là ví dụ hoàn hảo về tình huống có hai phương thức công khai chính xác là những gì bạn nên làm.

Tuy nhiên, bạn có thể cẩn thận theo dõi các mẫu sau:

  1. Một phương thức Hello()gọi World(string, int, int)như thế này:

    Hello()
    {
        this.World("Some magic value here", 0, 100);
    }

    Tránh mô hình này. Thay vào đó, sử dụng các đối số tùy chọn . Các đối số tùy chọn làm cho khả năng khám phá dễ dàng hơn: người gọi loại Hello(không nhất thiết phải biết rằng có một phương thức cho phép gọi phương thức đó với các giá trị mặc định. Đối số tùy chọn cũng là tài liệu tự. World()không hiển thị cho người gọi các giá trị mặc định thực tế là gì.

  2. Một phương thức Hello(ComplexEntity)gọi World(string, int, int)như thế này:

    Hello(ComplexEntity entity)
    {
        this.World(entity.Name, entity.Start, entity.Finish);
    }

    Thay vào đó, sử dụng quá tải . Lý do tương tự: khám phá tốt hơn. Người gọi có thể thấy ngay lập tức tất cả các tình trạng quá tải thông qua IntelliSense và chọn đúng.

  3. Một phương thức chỉ đơn giản gọi các phương thức công khai khác mà không cần thêm bất kỳ giá trị đáng kể nào.

    Nghĩ cho kỹ. Bạn có thực sự cần phương pháp này? Hoặc bạn nên loại bỏ nó và để cho người gọi gọi các phương thức khác nhau? Một gợi ý: nếu tên của phương thức có vẻ không đúng hoặc khó tìm, có lẽ bạn nên xóa nó.

  4. Một phương thức xác nhận đầu vào và sau đó gọi phương thức khác:

    Hello(string name, int start, int end)
    {
        if (name == null) throw new ArgumentNullException(...);
        if (start < 0) throw new OutOfRangeException(...);
        ...
        if (end < start) throw new ArgumentException(...);
    
        this.World(name, start, end);
    }

    Thay vào đó, Worldnên xác nhận các tham số của chính nó, hoặc là riêng tư.


Liệu những suy nghĩ tương tự có được áp dụng nếu ReverseData thực sự là nội bộ và được sử dụng trong toàn bộ không gian tên chứa các thể hiện của "đối tượng" không?
Snoop

1
@StevieV: vâng, tất nhiên rồi.
Arseni Mourzenko 14/03/2016

@MainMa Tôi không hiểu lý do đằng sau điểm 12
Kapol 15/03/2016

@Kapol: cảm ơn bạn đã phản hồi. Tôi chỉnh sửa câu trả lời của mình bằng cách thêm lời giải thích về hai điểm đầu tiên.
Arseni Mourzenko

Ngay cả trong trường hợp 1, tôi thường thích quá tải hơn là các đối số tùy chọn. Nếu các tùy chọn sao cho việc sử dụng quá tải không phải là một tùy chọn hoặc tạo ra số lượng quá tải đặc biệt lớn, tôi sẽ tạo một loại tùy chỉnh và sử dụng làm đối số (như trong gợi ý 2) hoặc cấu trúc lại mã.
Brian

8

Nếu một cái gì đó là công khai, nó có thể được gọi bất cứ lúc nào bởi bất kỳ hệ thống. Không có lý do gì bạn không thể là một trong những hệ thống đó!

Trong các thư viện được tối ưu hóa cao, bạn muốn sao chép thành ngữ đưa ra java.util.ArrayList#ensureCapacity(int).

Đảm bảo năng lực

  • ensureCapacity là công khai
  • ensureCapacity có tất cả các giới hạn cần thiết kiểm tra và giá trị mặc định, vv
  • ensureCapacity cuộc gọi ensureExplicitCapacity

Đảm bảoCapacityI Internal

  • ensureCapacityInternal là riêng tư
  • ensureCapacityInternal kiểm tra lỗi tối thiểu vì tất cả các đầu vào đến từ bên trong lớp
  • ensureCapacityInternal CSONG gọi ensureExplicitCapacity

Đảm bảoExplicitCapacity

  • ensureExplicitCapacity là riêng tư
  • ensureExplicitCapacitykhông kiểm tra lỗi
  • ensureExplicitCapacity thực tế làm việc
  • ensureExplicitCapacitykhông được gọi từ bất cứ đâu ngoại trừ bởi ensureCapacityensureCapacityInternal

Theo cách này, mã mà bạn tin tưởng sẽ có quyền truy cập đặc quyền (và nhanh hơn!) Vì bạn biết đầu vào của nó là tốt. Mã bạn không tin tưởng sẽ thông qua sự nghiêm ngặt nhất định để xác minh tính toàn vẹn và đánh bom của nó hoặc cung cấp mặc định hoặc xử lý các đầu vào xấu. Cả hai kênh đều đến nơi thực sự hoạt động.

Tuy nhiên, điều này được sử dụng trong ArrayList, một trong những lớp được sử dụng nhiều nhất trong JDK. Rất có thể trường hợp của bạn không đòi hỏi mức độ phức tạp và nghiêm ngặt đó. Tóm lại, nếu tất cả các ensureCapacityInternalcuộc gọi được thay thế bằng ensureCapacitycác cuộc gọi, hiệu suất vẫn sẽ thực sự, thực sự tốt. Đây là một vi mô hóa có lẽ chỉ được thực hiện sau khi xem xét rộng rãi.


3

Một số câu trả lời tốt đã được đưa ra và tôi cũng đồng ý với họ rằng có, một đối tượng có thể gọi các phương thức công khai của nó từ các phương thức khác. Tuy nhiên, có một cảnh báo thiết kế nhỏ mà bạn sẽ phải coi chừng.

Các phương thức công khai thường có hợp đồng "đưa đối tượng ở trạng thái nhất quán, làm điều gì đó hợp lý, để đối tượng ở trạng thái nhất quán (có thể khác nhau)". Ở đây, "phù hợp" có thể có nghĩa, ví dụ, rằng Lengthmột List<T>là không lớn hơn các sản phẩm Capacityvà tham khảo các yếu tố tại chỉ số từ 0đến Length-1sẽ không ném.

Nhưng bên trong các phương thức của đối tượng, đối tượng có thể ở trạng thái không nhất quán, vì vậy khi bạn gọi một trong các phương thức công khai của mình, nó có thể làm một điều rất sai, bởi vì nó không được viết với khả năng như vậy trong tâm trí. Vì vậy, nếu bạn có kế hoạch gọi các phương thức công khai của mình từ các phương thức khác của mình, hãy đảm bảo rằng hợp đồng của họ là "đưa đối tượng ở trạng thái nào đó, làm điều gì đó hợp lý, để đối tượng ở một (có thể khác) (chỉ có thể không nhất quán nhưng chỉ khi ban đầu nhà nước không nhất quán) nhà nước ".


1

Một ví dụ khác khi gọi phương thức công khai bên trong một phương thức công khai khác là hoàn toàn tốt là cách tiếp cận CanExecute / Execute. Tôi sử dụng nó khi tôi cần cả xác nhận và bảo tồn bất biến .

Nhưng nói chung tôi luôn thận trọng về điều đó. Nếu một phương thức a()được gọi là phương thức bên trong b(), điều đó có nghĩa là phương thức đó a()là một chi tiết triển khai b(). Rất thường nó chỉ ra rằng chúng thuộc các mức độ trừu tượng khác nhau. Và việc cả hai đều công khai khiến tôi tự hỏi liệu đó có phải là vi phạm nguyên tắc một trách nhiệm hay không.


-5

Xin lỗi nhưng tôi sẽ không đồng ý với hầu hết các câu trả lời 'có bạn có thể' khác và nói rằng:

Tôi sẽ không khuyến khích một lớp gọi một phương thức công khai từ một phương thức khác

Có một vài vấn đề tiềm năng với thực tiễn này.

1: Vòng lặp vô hạn trong lớp vô định

Vì vậy, lớp cơ sở của bạn gọi phương thức1 từ phương thức 2 nhưng sau đó bạn hoặc ai đó, kế thừa từ nó và ẩn phương thức1 bằng một phương thức mới gọi phương thức 2.

2: Sự kiện, Ghi nhật ký, v.v.

ví dụ: tôi có một phương thức Add1 thực hiện một sự kiện 'thêm 1!' Tôi có lẽ không muốn phương thức Add10 nâng cao sự kiện đó, ghi vào nhật ký hoặc bất cứ điều gì, mười lần.

3: phân luồng và các bế tắc khác

Ví dụ: insertComplexData mở kết nối db, bắt đầu giao dịch, khóa bảng, sau đó gọi insertSimpleData, với mở kết nối, bắt đầu giao dịch, chờ mở khóa bảng ....

Tôi chắc chắn có nhiều lý do hơn, một trong những câu trả lời khác đã chạm vào 'bạn chỉnh sửa phương thức1 và ngạc nhiên khi phương thức 2 bắt đầu hành xử khác đi'

Tạo ra nếu bạn có hai phương thức chung chia sẻ mã, tốt hơn là làm cho cả hai gọi một phương thức riêng tư thay vì gọi một phương thức khác.

Chỉnh sửa ----

Cho phép mở rộng về trường hợp cụ thể trong OP.

chúng tôi không có nhiều chi tiết nhưng chúng tôi biết rằng ReverseData được gọi bởi một bộ xử lý sự kiện thuộc loại nào đó cũng như phương thức lên lịch trình.

Tôi cho rằng dữ liệu ngược cũng thay đổi trạng thái bên trong của đối tượng

Với trường hợp này, tôi sẽ nghĩ rằng saftey chủ đề sẽ quan trọng và do đó sự phản đối thứ ba của tôi đối với thực tiễn được áp dụng.

Để làm cho chủ đề ReverseData an toàn, bạn có thể thêm một khóa. Nhưng nếu lên lịch trình cũng cần phải an toàn cho chủ đề, bạn sẽ muốn chia sẻ cùng một khóa.

Cách dễ nhất để làm điều này là chuyển mã ReverseData thành một phương thức riêng tư và có cả hai phương thức công khai gọi nó. Sau đó, bạn có thể đặt câu lệnh khóa trong Phương thức chung và chia sẻ đối tượng khóa.

Rõ ràng bạn có thể lập luận "điều đó sẽ không bao giờ xảy ra!" hoặc "Tôi có thể lập trình khóa theo cách khác" nhưng quan điểm về thực hành mã hóa tốt là cấu trúc mã của bạn tốt ngay từ đầu.

Về mặt học thuật tôi sẽ nói điều này vi phạm chữ L một cách vững chắc. Các phương pháp công khai không chỉ đơn thuần là công khai. Họ cũng có thể sửa đổi bởi những người thừa kế của nó. Mã của bạn nên được đóng lại để sửa đổi, điều đó có nghĩa là bạn phải suy nghĩ về những gì bạn làm trong cả phương pháp công khai và được bảo vệ.

Đây là một điều khác: Bạn cũng có khả năng vi phạm DDD. Nếu đối tượng của bạn là một đối tượng miền thì các phương thức công khai của nó phải là thuật ngữ Miền có ý nghĩa gì đó đối với doanh nghiệp. Trong trường hợp này, rất khó có khả năng 'mua một tá trứng' giống như 'mua 1 quả trứng 12 lần' ngay cả khi nó bắt đầu theo cách đó.


3
Những vấn đề này nghe có vẻ giống như kiến ​​trúc suy nghĩ kém hơn là một sự lên án chung của nguyên tắc thiết kế. (Có thể gọi "1 thêm" là tốt mỗi lần. Nếu không, chúng ta nên lập trình khác nhau. Có thể nếu hai phương pháp đang cố gắng độc quyền khóa cùng một tài nguyên, chúng không nên gọi cho nhau hoặc chúng tôi đã viết chúng kém .) Thay vì trình bày các triển khai xấu, bạn có thể muốn tập trung vào các đối số vững chắc hơn để giải quyết việc truy cập các phương thức công khai như một nguyên tắc phổ quát. Ví dụ, được cung cấp mã gọn gàng tốt, tại sao nó xấu?
doppelgreener 15/03/2016

Chính xác, nếu không, bạn không có nơi để đặt sự kiện. Tương tự như vậy với # 3, mọi thứ đều ổn cho đến khi một trong các phương thức của bạn xuống chuỗi cần giao dịch, thì bạn sẽ bị lừa
Ewan

Tất cả các vấn đề này phản ánh nhiều hơn bản thân mã có chất lượng kém hơn so với nguyên tắc chung bị thiếu sót. Cả ba trường hợp chỉ là mã xấu. Họ có thể được giải quyết bằng một số phiên bản của "không làm điều đó, làm điều này thay vì" (có lẽ với yêu cầu "những gì làm bạn muốn chính xác?"), Và không gọi vào câu hỏi nguyên tắc chung của một lớp gọi cộng đồng riêng của mình phương pháp. Tiềm năng mơ hồ cho một vòng lặp vô hạn là người duy nhất ở đây coi vấn đề là nguyên tắc, và thậm chí sau đó không được giải quyết triệt để.
doppelgreener 15/03/2016

chỉ có 'mã' trong các ví dụ của tôi là một phương thức công khai gọi một phương thức khác. Nếu bạn nghĩ rằng 'mã xấu' tốt! Đó là sự ganh đua của tôi
Ewan

Rằng bạn có thể dùng búa để bẻ tay mình không có nghĩa là búa rất tệ. Nó có nghĩa là bạn không cần phải làm những điều với nó sẽ làm gãy tay bạn.
doppelgreener 15/03/2016
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.