Tôi có nên luôn luôn đóng gói một cấu trúc dữ liệu nội bộ hoàn toàn không?


11

Vui lòng xem xét lớp học này:

class ClassA{

    private Thing[] things; // stores data

    // stuff omitted

    public Thing[] getThings(){
        return things;
    }

}

Lớp này hiển thị mảng mà nó sử dụng để lưu trữ dữ liệu, cho bất kỳ mã máy khách nào quan tâm.

Tôi đã làm điều này trong một ứng dụng tôi đang làm việc. Tôi đã có một ChordProgressionlớp lưu trữ một chuỗi Chords (và thực hiện một số thứ khác). Nó có một Chord[] getChords()phương thức trả về mảng hợp âm. Khi cấu trúc dữ liệu phải thay đổi (từ một mảng thành ArrayList), tất cả mã máy khách đã bị hỏng.

Điều này làm tôi suy nghĩ - có thể cách tiếp cận sau là tốt hơn:

class ClassA{

    private Thing[] things; // stores data

    // stuff omitted

    public Thing[] getThing(int index){
        return things[index];
    }

    public int getDataSize(){
        return things.length;
    }

    public void setThing(int index, Thing thing){
        things[index] = thing;
    }

}

Thay vì phơi bày cấu trúc dữ liệu, tất cả các hoạt động được cung cấp bởi cấu trúc dữ liệu hiện được cung cấp trực tiếp bởi lớp kèm theo nó, sử dụng các phương thức công khai ủy quyền cho cấu trúc dữ liệu.

Khi cấu trúc dữ liệu thay đổi, chỉ các phương thức này phải thay đổi - nhưng sau khi thực hiện, tất cả mã máy khách vẫn hoạt động.

Lưu ý rằng các bộ sưu tập phức tạp hơn các mảng có thể yêu cầu lớp kèm theo thực hiện thậm chí nhiều hơn ba phương thức chỉ để truy cập cấu trúc dữ liệu nội bộ.


Là phương pháp này phổ biến? Bạn nghĩ gì về điều này? Những nhược điểm nào nó có khác? Có hợp lý không khi lớp kèm theo thực hiện ít nhất ba phương thức công khai chỉ để ủy quyền cho cấu trúc dữ liệu bên trong?

Câu trả lời:


14

Mã như:

   public Thing[] getThings(){
        return things;
    }

Không có ý nghĩa gì nhiều, vì phương thức truy cập của bạn không làm gì ngoài việc trả lại trực tiếp cấu trúc dữ liệu nội bộ. Bạn cũng có thể chỉ cần tuyên bố Thing[] thingspublic. Ý tưởng đằng sau một phương thức truy cập là tạo ra một giao diện cách ly khách hàng khỏi những thay đổi bên trong và ngăn không cho họ thao túng cấu trúc dữ liệu thực tế trừ những cách kín đáo như giao diện cho phép. Như bạn đã phát hiện ra khi tất cả mã khách hàng của bạn bị hỏng, phương thức truy cập của bạn đã không làm điều đó - đó chỉ là sự lãng phí mã. Tôi nghĩ rằng rất nhiều lập trình viên có xu hướng viết mã như vậy bởi vì họ đã học được ở đâu đó rằng mọi thứ cần phải được gói gọn với các phương thức truy cập - nhưng đó là lý do tôi giải thích. Làm điều đó chỉ để "làm theo mẫu" khi phương thức truy cập không phục vụ bất kỳ mục đích nào chỉ là tiếng ồn.

Tôi chắc chắn sẽ đề xuất giải pháp đề xuất của bạn, thực hiện một số mục tiêu quan trọng nhất của việc đóng gói: Cung cấp cho khách hàng một giao diện mạnh mẽ, kín đáo, cách ly họ khỏi các chi tiết triển khai bên trong của lớp của bạn và không cho phép họ chạm vào cấu trúc dữ liệu nội bộ mong đợi theo những cách mà bạn quyết định là phù hợp - "luật đặc quyền ít cần thiết nhất". Nếu bạn nhìn vào các khung OOP phổ biến lớn, chẳng hạn như CLR, STL, VCL, thì mẫu mà bạn đề xuất là phổ biến, vì chính xác lý do đó.

Bạn có nên luôn luôn làm điều đó? Không cần thiết. Ví dụ: nếu bạn có các lớp người trợ giúp hoặc bạn bè về cơ bản là một thành phần của lớp nhân viên chính của bạn và không phải là "mặt trước", thì không cần thiết - đó là một sự quá mức cần thiết để thêm nhiều mã không cần thiết. Và trong trường hợp đó, tôi sẽ không sử dụng một phương thức truy cập nào cả - nó vô nghĩa, như đã giải thích. Chỉ cần khai báo cấu trúc dữ liệu theo cách chỉ nằm trong lớp chính sử dụng nó - hầu hết các ngôn ngữ đều hỗ trợ các cách thực hiện điều đó - friendhoặc khai báo nó trong cùng một tệp với lớp worker chính, v.v.

Nhược điểm duy nhất tôi có thể thấy trong đề xuất của bạn đó là viết mã nhiều công việc hơn (và bây giờ bạn sẽ phải mã hóa lại các lớp tiêu dùng của mình - nhưng dù sao bạn cũng phải / phải làm điều đó.) Nhưng đó không thực sự là nhược điểm - bạn cần phải làm điều đó đúng, và đôi khi điều đó đòi hỏi nhiều công việc hơn.

Một trong những điều làm cho một lập trình viên giỏi trở nên tốt là họ biết khi nào công việc làm thêm có giá trị và khi nào thì không. Về lâu dài, việc đưa thêm vào bây giờ sẽ trả hết với cổ tức lớn trong tương lai - nếu không phải trong dự án này, sau đó cho những người khác. Học cách viết mã đúng cách và sử dụng đầu của bạn về nó, không chỉ là robot tuân theo các hình thức quy định.

Lưu ý rằng các bộ sưu tập phức tạp hơn các mảng có thể yêu cầu lớp kèm theo thực hiện thậm chí nhiều hơn ba phương thức chỉ để truy cập cấu trúc dữ liệu nội bộ.

Nếu bạn đang hiển thị toàn bộ cấu trúc dữ liệu thông qua một lớp chứa, IMO bạn cần suy nghĩ về lý do tại sao lớp đó được gói gọn, nếu không chỉ đơn giản là cung cấp một giao diện an toàn hơn - một "lớp bao bọc". Bạn đang nói rằng lớp chứa không tồn tại cho mục đích đó - vì vậy có lẽ có điều gì đó không đúng về thiết kế của bạn. Xem xét chia các lớp học của bạn thành các mô-đun kín đáo hơn và xếp lớp chúng.

Một lớp nên có một mục đích rõ ràng và kín đáo, và cung cấp một giao diện để hỗ trợ chức năng đó - không còn nữa. Bạn có thể đang cố gắng kết hợp những thứ không thuộc về nhau. Khi bạn làm điều đó, mọi thứ sẽ bị phá vỡ mỗi khi bạn phải thực hiện một thay đổi. Các lớp học của bạn càng nhỏ và càng kín đáo thì càng dễ thay đổi mọi thứ xung quanh: Hãy nghĩ về LEGO.


1
Cảm ơn đã trả lời. Một câu hỏi: Thế còn nếu cấu trúc dữ liệu nội bộ có, có thể, 5 phương thức công khai - tất cả cần phải được đặc trưng bởi giao diện chung của lớp tôi? Ví dụ, một ArrayList Java có các phương pháp sau: get(index), add(), size(), remove(index), và remove(Object). Sử dụng kỹ thuật được đề xuất, lớp chứa ArrayList này phải có năm phương thức công khai chỉ để ủy quyền cho bộ sưu tập bên trong. Và mục đích của lớp này trong chương trình rất có thể không gói gọn ArrayList này, mà là làm một cái gì đó khác. ArrayList chỉ là một chi tiết. [...]
Manila Cohn

Cấu trúc dữ liệu bên trong chỉ là một thành viên bình thường, sử dụng kỹ thuật trên - yêu cầu lớp chứa nó có thêm năm phương thức công khai. Theo bạn - điều này có hợp lý không? Và cũng - điều này là phổ biến?
Manila Cohn

@Prog - Còn về cấu trúc dữ liệu nội bộ, có thể, 5 phương thức công khai ... IMO nếu bạn thấy bạn cần bọc toàn bộ một lớp trợ giúp bên trong lớp chính của mình và phơi bày theo cách đó, bạn cần nghĩ lại rằng thiết kế - lớp công khai của bạn đang làm quá nhiều / hoặc không trình bày giao diện thích hợp. Một lớp nên có một vai trò rất kín đáo và được xác định rõ ràng, và giao diện của nó sẽ hỗ trợ vai trò đó và chỉ vai trò đó. Hãy suy nghĩ về việc chia tay và xếp lớp của bạn. Một lớp không nên là một "bồn rửa nhà bếp" có chứa tất cả các loại đối tượng trong tên của sự đóng gói.
Vector

Nếu bạn đang hiển thị toàn bộ cấu trúc dữ liệu thông qua lớp trình bao bọc, IMO bạn cần suy nghĩ về lý do tại sao lớp đó được gói gọn nếu không chỉ đơn giản là cung cấp giao diện an toàn hơn. Bạn đang nói rằng lớp chứa không tồn tại cho mục đích đó - vì vậy có gì đó không đúng về thiết kế này.
Vector

1
@Phoshi - Chỉ đọc là từ khóa - Tôi có thể đồng ý với điều đó. Nhưng OP không nói về chỉ đọc. ví dụ removekhông chỉ đọc Hiểu biết của tôi là OP muốn công khai mọi thứ - như trong mã gốc trước khi thay đổi được đề xuất public Thing[] getThings(){return things;}Đó là điều tôi không thích.
Vector

2

Bạn hỏi: Tôi có nên luôn gói gọn một cấu trúc dữ liệu nội bộ không?

Trả lời ngắn gọn: Có, hầu hết thời gian nhưng không phải lúc nào cũng vậy .

Câu trả lời dài: Tôi nghĩ rằng các lớp học theo các loại sau:

  1. Các lớp đóng gói dữ liệu đơn giản. Ví dụ: Điểm 2D. Thật dễ dàng để tạo các hàm công khai cung cấp khả năng nhận / đặt tọa độ X và Y nhưng bạn có thể ẩn dữ liệu nội bộ một cách dễ dàng mà không gặp quá nhiều khó khăn. Đối với các lớp như vậy, việc tiết lộ các chi tiết cấu trúc dữ liệu nội bộ là không được phép.

  2. Các lớp container đóng gói các bộ sưu tập. STL có các lớp container cổ điển. Tôi xem xét std::stringstd::wstringtrong số những người quá. Họ cung cấp một giao diện phong phú để đối phó với các khái niệm trừu tượng nhưng std::vector, std::stringstd::wstringcũng cung cấp khả năng để có được quyền truy cập vào các dữ liệu thô. Tôi sẽ không vội vàng gọi họ là những lớp học được thiết kế kém. Tôi không biết sự biện minh cho các lớp này phơi bày dữ liệu thô của họ. Tuy nhiên, trong công việc của tôi, tôi thấy cần phải phơi bày dữ liệu thô khi xử lý hàng triệu nút lưới và dữ liệu trên các nút lưới đó vì lý do hiệu suất.

    Điều quan trọng về việc phơi bày cấu trúc bên trong của một lớp là bạn phải suy nghĩ lâu dài và chăm chỉ trước khi đưa ra tín hiệu xanh. Nếu giao diện là nội bộ của một dự án, sẽ rất tốn kém để thay đổi nó trong tương lai nhưng không phải là không thể. Nếu giao diện bên ngoài dự án (chẳng hạn như khi bạn đang phát triển thư viện sẽ được sử dụng bởi các nhà phát triển ứng dụng khác), có thể không thể thay đổi giao diện mà không mất khách hàng của bạn.

  3. Các lớp học chủ yếu là chức năng trong tự nhiên. Ví dụ: std::istream,, std::ostreamiterators của STL container. Thật ngu ngốc khi để lộ các chi tiết nội bộ của các lớp này.

  4. Các lớp lai. Đây là các lớp đóng gói một số cấu trúc dữ liệu nhưng cũng cung cấp chức năng thuật toán. Cá nhân, tôi nghĩ rằng đây là kết quả của thiết kế suy nghĩ kém. Tuy nhiên, nếu bạn tìm thấy chúng, bạn phải quyết định xem có hợp lý khi để lộ dữ liệu nội bộ của họ theo từng trường hợp hay không.

Tóm lại: Lần duy nhất tôi thấy cần phải phơi bày cấu trúc dữ liệu nội bộ của một lớp là khi nó trở thành nút cổ chai hiệu năng.


Tôi nghĩ lý do quan trọng nhất mà STL tiết lộ dữ liệu nội bộ của họ là khả năng tương thích với tất cả các chức năng mong đợi con trỏ, rất nhiều.
Siyuan Ren

0

Thay vì trả lại dữ liệu thô trực tiếp, hãy thử một cái gì đó như thế này

class ClassA {
  private Things[] things;
  ...
  public Things[] asArray() { return things; }
  public List<Thing> asList() { ... }
  ...
}

Vì vậy, về cơ bản, bạn đang cung cấp một bộ sưu tập tùy chỉnh thể hiện bất kỳ khuôn mặt nào với thế giới mong muốn. Trong triển khai mới của bạn sau đó,

class ClassA {
  private List<Thing> things;
  ...
  public Things[] asArray() { return things.asArray(); }
  public List<Thing> asList() { return things; }
  ...
}

Bây giờ bạn đã đóng gói thích hợp, ẩn các chi tiết triển khai và cung cấp khả năng tương thích ngược (với chi phí).


Ý tưởng thông minh cho khả năng tương thích ngược. Nhưng: Bây giờ bạn đã đóng gói thích hợp, ẩn các chi tiết thực hiện - không thực sự. Khách hàng vẫn phải đối phó với các sắc thái của List. Các phương thức truy cập chỉ đơn giản là trả về các thành viên dữ liệu, ngay cả với một nhóm để làm cho mọi thứ mạnh mẽ hơn, không thực sự đóng gói IMO. Lớp worker nên xử lý tất cả, không phải client. "Dumber" một khách hàng phải là, nó sẽ mạnh mẽ hơn. Ở một bên, tôi không chắc bạn đã trả lời câu hỏi ...
Vector

1
@Vector - bạn đúng rồi. Cấu trúc dữ liệu được trả về vẫn có thể thay đổi và tác dụng phụ sẽ giết chết thông tin ẩn.
BobDalgleish

Cấu trúc dữ liệu được trả về vẫn có thể thay đổi và các tác dụng phụ sẽ giết chết thông tin ẩn - vâng, điều đó cũng vậy - nó nguy hiểm. Tôi chỉ đơn giản là suy nghĩ về những gì được yêu cầu từ khách hàng, đó là trọng tâm của câu hỏi.
Vector

@BobDalgleish: tại sao không trả lại một bản sao của bộ sưu tập gốc?
Giorgio

1
@BobDalgleish: Trừ khi có lý do hiệu suất tốt, tôi sẽ xem xét trả lại tham chiếu cho cấu trúc dữ liệu nội bộ để cho phép người dùng thay đổi chúng như một quyết định thiết kế rất tệ. Trạng thái bên trong của một đối tượng chỉ nên được thay đổi thông qua các phương thức công khai thích hợp.
Giorgio

0

Bạn nên sử dụng giao diện cho những thứ đó. Sẽ không giúp ích gì trong trường hợp của bạn, vì mảng của Java không triển khai các giao diện đó, nhưng bạn nên làm điều đó từ bây giờ:

class ClassA{

    public ClassA(){
        things = new ArrayList<Thing>();
    }

    private List<Thing> things; // stores data

    // stuff omitted

    public List<Thing> getThings(){
        return things;
    }

}

Bằng cách đó, bạn có thể thay đổi ArrayListthành LinkedListhoặc bất cứ điều gì khác và bạn sẽ không phá vỡ bất kỳ mã nào vì tất cả các bộ sưu tập Java (bên cạnh mảng) có quyền truy cập ngẫu nhiên (giả?) Có thể sẽ thực hiện List.

Bạn cũng có thể sử dụng Collection, cung cấp ít phương thức hơn Listnhưng có thể hỗ trợ các bộ sưu tập mà không cần truy cập ngẫu nhiên hoặc Iterablethậm chí có thể hỗ trợ các luồng nhưng không cung cấp nhiều về phương thức truy cập ...


-1 - thỏa hiệp kém và IMO không an toàn đặc biệt: Bạn đang phơi bày cấu trúc dữ liệu nội bộ cho máy khách, chỉ che giấu nó và hy vọng điều tốt nhất vì "các bộ sưu tập Java ... có thể sẽ triển khai Danh sách". Nếu giải pháp của bạn thực sự dựa trên đa hình / kế thừa - rằng tất cả các bộ sưu tập luôn luôn thực hiện Listnhư các lớp dẫn xuất, nó sẽ có ý nghĩa hơn, nhưng chỉ "hy vọng điều tốt nhất" không phải là một ý tưởng tốt. "Một lập trình viên giỏi nhìn cả hai chiều trên đường một chiều".
Vector

@Vector Có, tôi giả sử các bộ sưu tập Java trong tương lai sẽ triển khai List(hoặc Collection, hoặc ít nhất Iterable). Đó là toàn bộ quan điểm của các giao diện này và thật xấu hổ khi Mảng Java không triển khai chúng, nhưng chúng là giao diện chính thức cho các bộ sưu tập trong Java nên sẽ không quá xa khi cho rằng bất kỳ bộ sưu tập Java nào sẽ triển khai chúng - trừ khi bộ sưu tập đó cũ hơn hơn Listvà trong trường hợp đó, rất dễ dàng để bọc nó bằng AbstractList .
Idan Arye

Bạn đang nói rằng giả định của bạn hầu như được đảm bảo là đúng, vì vậy OK - Tôi sẽ xóa bỏ phiếu bầu (khi tôi được phép) vì bạn đủ tử tế để giải thích và tôi không phải là người Java ngoại trừ bằng thẩm thấu. Tuy nhiên, tôi không ủng hộ ý tưởng phơi bày cấu trúc dữ liệu nội bộ này, bất kể nó được thực hiện như thế nào và bạn chưa trả lời trực tiếp câu hỏi của OP, đây thực sự là về đóng gói. tức là hạn chế quyền truy cập vào cấu trúc dữ liệu nội bộ.
Vector

1
@Vector Có, người dùng có thể cast Listvào ArrayList, nhưng nó không giống như việc thực hiện có thể là 100% bảo vệ - bạn luôn có thể sử dụng phản ánh để truy cập các lĩnh vực tư nhân. Làm như vậy là nhăn mặt, nhưng đúc cũng được nhăn mặt (mặc dù không nhiều). Điểm đóng gói không phải là ngăn chặn hack độc hại - thay vào đó, nó là để ngăn người dùng phụ thuộc vào chi tiết triển khai mà bạn có thể muốn thay đổi. Sử dụng Listgiao diện thực hiện chính xác điều đó - người dùng của lớp có thể phụ thuộc vào Listgiao diện thay vì ArrayListlớp cụ thể có thể thay đổi.
Idan Arye

bạn luôn có thể sử dụng sự phản chiếu để truy cập các trường riêng tư - nếu ai đó muốn viết mã xấu và lật đổ một thiết kế, họ có thể làm như vậy. thay vào đó, đó là để ngăn chặn người dùng ... - đó là một lý do cho việc đóng gói. Một cách khác là đảm bảo tính toàn vẹn và nhất quán của trạng thái nội bộ của lớp bạn. Vấn đề không phải là "hack độc hại", mà là tổ chức kém dẫn đến những lỗi khó chịu. "Luật đặc quyền ít nhất cần thiết" - chỉ dành cho người tiêu dùng của lớp bạn những gì bắt buộc - không còn nữa. Nếu bắt buộc bạn phải công khai toàn bộ cấu trúc dữ liệu nội bộ, bạn đã gặp sự cố thiết kế.
Vector

-2

Điều này là khá phổ biến để ẩn cấu trúc dữ liệu nội bộ của bạn với thế giới bên ngoài. Đôi khi nó quá mức đặc biệt trong DTO. Tôi khuyên bạn nên điều này cho mô hình miền. Nếu ở tất cả các yêu cầu của nó để lộ, trả lại bản sao bất biến. Cùng với điều này, tôi khuyên bạn nên tạo một giao diện có các phương thức này như get, set, remove, v.v.


1
điều này dường như không cung cấp bất cứ điều gì đáng kể hơn 3 câu trả lời trước
gnat
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.