Tại sao các lớp không nên được thiết kế để mở ra?


44

Khi đọc các câu hỏi khác nhau về Stack Overflow và mã của người khác, sự đồng thuận chung về cách thiết kế các lớp được đóng lại. Điều này có nghĩa là theo mặc định trong Java và C #, mọi thứ đều riêng tư, các trường là cuối cùng, một số phương thức là cuối cùng và đôi khi các lớp thậm chí là cuối cùng .

Ý tưởng đằng sau điều này là để che giấu các chi tiết thực hiện, đó là một lý do rất tốt. Tuy nhiên, với sự tồn tại của protectedhầu hết các ngôn ngữ OOP và tính đa hình, điều này không hoạt động.

Mỗi lần tôi muốn thêm hoặc thay đổi chức năng cho một lớp tôi thường bị cản trở bởi sự riêng tư và cuối cùng được đặt ở mọi nơi. Ở đây chi tiết thực hiện vấn đề: bạn đang thực hiện và mở rộng nó, biết rõ hậu quả là gì. Tuy nhiên vì tôi không thể truy cập vào các trường và phương thức riêng tư và cuối cùng, tôi có ba tùy chọn:

  • Đừng mở rộng lớp, chỉ giải quyết vấn đề dẫn đến mã phức tạp hơn
  • Sao chép và dán cả lớp, giết chết mã tái sử dụng
  • Ngã ba dự án

Đó không phải là lựa chọn tốt. Tại sao không protectedđược sử dụng trong các dự án được viết bằng ngôn ngữ hỗ trợ nó? Tại sao một số dự án rõ ràng cấm kế thừa từ các lớp học của họ?


1
Tôi đồng ý, tôi đã gặp vấn đề này với các thành phần Java Swing, nó thực sự tồi tệ.
Jonas


3
Có một cơ hội tốt rằng nếu bạn gặp vấn đề này, các lớp được thiết kế kém ngay từ đầu - hoặc có lẽ bạn đang cố gắng sử dụng chúng không chính xác. Bạn không yêu cầu một lớp thông tin, bạn yêu cầu nó làm điều gì đó cho bạn - do đó bạn thường không cần dữ liệu đó. Mặc dù điều này không phải lúc nào cũng đúng, nhưng nếu bạn thấy bạn cần truy cập vào dữ liệu của lớp thì rất nhiều khả năng là có gì đó không ổn.
Bill K

2
"hầu hết các ngôn ngữ OOP?" Tôi biết nhiều hơn nữa nơi các lớp học không thể được đóng lại. Bạn bỏ qua tùy chọn thứ tư: thay đổi ngôn ngữ.
kevin cline

@Jonas Một trong những vấn đề lớn của Swing là nó phơi bày quá nhiều cách thực hiện. Điều đó thực sự giết chết sự phát triển.
Tom Hawtin - tackline

Câu trả lời:


55

Thiết kế các lớp để hoạt động đúng khi được mở rộng, đặc biệt là khi lập trình viên thực hiện việc mở rộng không hiểu đầy đủ về cách thức hoạt động của lớp, cần thêm nỗ lực đáng kể. Bạn không thể lấy mọi thứ riêng tư và công khai (hoặc được bảo vệ) và gọi đó là "mở". Nếu bạn cho phép người khác thay đổi giá trị của một biến, bạn phải xem xét tất cả các giá trị có thể sẽ ảnh hưởng đến lớp như thế nào. (Điều gì sẽ xảy ra nếu họ đặt biến thành null? Một mảng trống? Số âm?) Áp dụng tương tự khi cho phép người khác gọi một phương thức. Nó cần suy nghĩ cẩn thận.

Vì vậy, nó không quá nhiều đến nỗi các lớp học không nên mở, nhưng đôi khi nó không đáng để nỗ lực để làm cho chúng mở.

Tất nhiên, cũng có thể các tác giả thư viện chỉ lười biếng. Phụ thuộc vào thư viện mà bạn đang nói về. :-)


13
Vâng, đây là lý do tại sao các lớp được niêm phong theo mặc định. Bởi vì bạn không thể dự đoán nhiều cách mà khách hàng của bạn có thể mở rộng lớp, nên việc niêm phong nó sẽ an toàn hơn là cam kết hỗ trợ số lượng cách vô hạn mà lớp có thể được mở rộng.
Robert Harvey


18
Bạn không cần phải dự đoán lớp học của bạn sẽ bị ghi đè như thế nào, bạn chỉ cần giả định rằng mọi người mở rộng lớp của bạn biết họ đang làm gì. Nếu họ mở rộng lớp và đặt một cái gì đó thành null khi nó không phải là null thì đó là lỗi của họ, không phải của bạn. Nơi duy nhất mà lập luận này có ý nghĩa là trong các ứng dụng cực kỳ quan trọng, nơi một cái gì đó sẽ trở nên sai lầm khủng khiếp nếu một trường là null.
TheLQ

5
@TheLQ: Đó là một giả định khá lớn.
Robert Harvey

9
@TheLQ Lỗi của ai là một câu hỏi mang tính chủ quan cao. Nếu họ đặt một biến thành một giá trị có vẻ hợp lý và bạn đã không ghi lại sự thật rằng điều đó không được phép và mã của bạn đã không ném ArgumentException (hoặc tương đương), nhưng điều đó khiến máy chủ của bạn ngoại tuyến hoặc dẫn đối với một khai thác bảo mật, sau đó tôi sẽ nói đó là lỗi của bạn nhiều như của họ. Và nếu bạn đã ghi lại các giá trị hợp lệ và đặt kiểm tra đối số, thì bạn đã đưa ra chính xác loại nỗ lực mà tôi đang nói đến, và bạn phải tự hỏi liệu nỗ lực đó có thực sự sử dụng tốt thời gian của bạn không.
Aaron

24

Làm cho mọi thứ riêng tư theo mặc định nghe có vẻ khó khăn, nhưng nhìn từ phía bên kia: Khi mọi thứ đều mặc định là riêng tư, làm cho một cái gì đó công khai (hoặc được bảo vệ, gần như giống nhau) được coi là một lựa chọn có ý thức; đó là hợp đồng của tác giả lớp với bạn, người tiêu dùng, về cách sử dụng lớp. Đây là một thuận tiện cho cả hai bạn: Tác giả có thể tự do sửa đổi hoạt động bên trong của lớp, miễn là giao diện không thay đổi; và bạn biết chính xác những phần nào của lớp bạn có thể dựa vào và phần nào có thể thay đổi.

Ý tưởng cơ bản là 'khớp nối lỏng lẻo' (còn được gọi là 'giao diện hẹp'); giá trị của nó nằm ở việc giữ sự phức tạp xuống. Bằng cách giảm số cách mà các thành phần có thể tương tác, lượng phụ thuộc chéo giữa chúng cũng giảm đi; và phụ thuộc chéo là một trong những loại phức tạp tồi tệ nhất khi nói đến bảo trì và quản lý thay đổi.

Trong các thư viện được thiết kế tốt, các lớp có giá trị mở rộng thông qua kế thừa đã bảo vệ và các thành viên công cộng ở đúng nơi và ẩn mọi thứ khác.


Điều đó có ý nghĩa gì đó. Vẫn còn vấn đề khi bạn cần một cái gì đó rất giống nhau nhưng với 1 hoặc 2 thứ đã thay đổi trong quá trình thực hiện (hoạt động bên trong của lớp). Tôi cũng đang đấu tranh để hiểu rằng nếu bạn ghi đè lên lớp, bạn không phụ thuộc nhiều vào việc thực hiện nó sao?
TheLQ

5
+1 cho lợi ích của khớp nối lỏng lẻo. @TheLQ, đó là giao diện bạn nên phụ thuộc nhiều vào chứ không phải việc triển khai.
Karl Bielefeldt

19

Mọi thứ không riêng tư ít nhiều được cho là tồn tại với hành vi không thay đổi trong mọi phiên bản tương lai của lớp. Trên thực tế, nó có thể được coi là một phần của API, được ghi lại hoặc không. Do đó, việc phơi bày quá nhiều chi tiết có thể gây ra vấn đề tương thích sau này.

Về "cuối cùng" tôn trọng. Các lớp "kín", một trường hợp điển hình là các lớp bất biến. Nhiều phần của khung phụ thuộc vào chuỗi là bất biến. Nếu lớp String không phải là cuối cùng, sẽ dễ dàng (thậm chí hấp dẫn) để tạo một lớp con Chuỗi có thể thay đổi gây ra tất cả các loại lỗi trong nhiều lớp khác khi được sử dụng thay vì lớp Chuỗi không thay đổi.


4
Đây là câu trả lời thực sự. Các câu trả lời khác liên quan đến những điều quan trọng nhưng bit thực sự có liên quan là đây: mọi thứ không finalvà / hoặc privatebị khóa tại chỗ và không bao giờ có thể thay đổi .
Konrad Rudolph

1
@Konrad: Cho đến khi các nhà phát triển quyết định tân trang lại mọi thứ và không tương thích ngược.
JAB

Điều đó là đúng, nhưng đáng lưu ý rằng đó là một yêu cầu yếu hơn nhiều so với publiccác thành viên; protectedcác thành viên chỉ tạo thành một hợp đồng giữa các lớp phơi bày chúng và các lớp dẫn xuất ngay lập tức của nó; ngược lại, publiccác thành viên ký hợp đồng với tất cả người tiêu dùng thay mặt cho tất cả các lớp kế thừa hiện tại và tương lai.
supercat

14

Trong OO có hai cách để thêm chức năng cho mã hiện có.

Cái đầu tiên là do thừa kế: bạn lấy một lớp và xuất phát từ nó. Tuy nhiên, kế thừa nên được sử dụng cẩn thận. Bạn nên sử dụng kế thừa công khai chủ yếu khi bạn có mối quan hệ isA giữa cơ sở và lớp dẫn xuất (ví dụ: Hình chữ nhật Hình dạng). Thay vào đó, bạn nên tránh kế thừa công khai để sử dụng lại một triển khai hiện có. Về cơ bản, kế thừa công khai được sử dụng để đảm bảo lớp dẫn xuất có cùng giao diện với lớp cơ sở (hoặc lớp lớn hơn), do đó bạn có thể áp dụng nguyên tắc thay thế của Liskov .

Phương pháp khác để thêm chức năng là sử dụng ủy quyền hoặc thành phần. Bạn viết lớp riêng của bạn sử dụng lớp hiện có làm đối tượng bên trong và ủy thác một phần công việc thực hiện cho nó.

Tôi không chắc ý tưởng đằng sau tất cả những trận chung kết trong thư viện mà bạn đang cố gắng sử dụng là gì. Có lẽ các lập trình viên muốn đảm bảo rằng bạn sẽ không kế thừa một lớp mới từ lớp của họ, bởi vì đó không phải là cách tốt nhất để mở rộng mã của họ.


5
Điểm tuyệt vời với thành phần so với thừa kế.
Zsolt Török

+1, nhưng tôi không bao giờ thích ví dụ "là hình dạng" cho tính kế thừa. Một hình chữ nhật và một hình tròn có thể là hai thứ rất khác nhau. Tôi thích nhiều hơn để sử dụng, một hình vuông là một hình chữ nhật bị hạn chế. Hình chữ nhật có thể được sử dụng như Hình dạng.
tylermac

@tylermac: thật vậy. Trường hợp hình vuông / hình chữ nhật thực sự là một trong những ví dụ của LSP. Có lẽ, tôi nên bắt đầu nghĩ về một ví dụ hiệu quả hơn.
knulp

3
Hình vuông / Hình chữ nhật chỉ là một ví dụ mẫu nếu bạn cho phép sửa đổi.
starblue

11

Hầu hết các câu trả lời đều đúng: Các lớp nên được niêm phong (cuối cùng, NotOverridable, v.v.) khi đối tượng không được thiết kế hoặc dự định mở rộng.

Tuy nhiên, tôi sẽ không xem xét đơn giản là đóng mọi thứ thiết kế mã phù hợp. Chữ "O" trong RẮN là dành cho "Nguyên tắc đóng mở", nói rằng một lớp nên được "đóng" để sửa đổi, nhưng "mở" để mở rộng. Ý tưởng là, bạn có mã trong một đối tượng. Nó hoạt động tốt. Thêm chức năng không nên yêu cầu mở mã đó lên và thực hiện các thay đổi phẫu thuật có thể phá vỡ hành vi làm việc trước đó. Thay vào đó, người ta có thể sử dụng kế thừa hoặc tiêm phụ thuộc để "mở rộng" lớp để làm những việc bổ sung.

Ví dụ, một lớp "ConsoleWriter" có thể nhận văn bản và xuất nó ra bàn điều khiển. Nó làm điều này tốt. Tuy nhiên, trong một trường hợp nhất định, bạn C needNG cần cùng một đầu ra được ghi vào một tệp. Mở mã của ConsoleWriter, thay đổi giao diện bên ngoài của nó để thêm một tham số "WriteToFile" cho các chức năng chính và đặt mã viết tệp bổ sung bên cạnh mã viết bảng điều khiển nói chung sẽ bị coi là một điều xấu.

Thay vào đó, bạn có thể thực hiện một trong hai điều sau: bạn có thể xuất phát từ ConsoleWriter để tạo thành ConsoleAndFileWriter và mở rộng phương thức Write () để trước tiên gọi triển khai cơ sở, sau đó cũng ghi vào tệp. Hoặc, bạn có thể trích xuất một giao diện IWriter từ ConsoleWriter và thực hiện lại giao diện đó để tạo hai lớp mới; FileWriter và "MultiWriter" có thể được cung cấp cho các IWriters khác và sẽ "phát" bất kỳ cuộc gọi nào được thực hiện theo phương thức của nó tới tất cả các nhà văn đã cho. Cái nào bạn chọn phụ thuộc vào những gì bạn sẽ cần; dẫn xuất đơn giản là tốt, đơn giản hơn, nhưng nếu bạn có tầm nhìn xa về việc cuối cùng cần gửi tin nhắn đến máy khách mạng, ba tệp, hai ống có tên và bảng điều khiển, hãy tiếp tục và xử lý sự cố để trích xuất giao diện và tạo "Bộ chuyển đổi chữ Y"; nó '

Bây giờ, nếu hàm Write () chưa bao giờ được khai báo là ảo (hoặc nó đã bị niêm phong), thì bây giờ bạn sẽ gặp rắc rối nếu bạn không kiểm soát mã nguồn. Đôi khi ngay cả khi bạn làm. Đây thường là một vị trí không tốt và nó làm người dùng thất vọng về các API nguồn đóng hoặc nguồn giới hạn không có hồi kết khi nó xảy ra. Nhưng, có lý do chính đáng tại sao Write () hoặc toàn bộ lớp ConsoleWriter, bị niêm phong; có thể có thông tin nhạy cảm trong một trường được bảo vệ (lần lượt được ghi đè từ một lớp cơ sở để cung cấp thông tin đã nói). Có thể không đủ xác nhận; khi bạn viết một api, bạn phải cho rằng các lập trình viên sử dụng mã của bạn không thông minh hay nhân từ hơn "người dùng cuối" trung bình của hệ thống cuối cùng; theo cách đó bạn không bao giờ thất vọng.


Điểm tốt. Kế thừa có thể là tốt hoặc xấu, vì vậy thật sai lầm khi nói rằng mọi lớp nên được niêm phong. Nó thực sự phụ thuộc vào vấn đề trong tầm tay.
knulp

2

Tôi nghĩ rằng có lẽ từ tất cả những người sử dụng ngôn ngữ oo để lập trình c. Tôi đang nhìn bạn, java. Nếu cây búa của bạn có hình dạng như một vật thể, nhưng bạn muốn có một hệ thống thủ tục và / hoặc không thực sự hiểu các lớp, thì dự án của bạn sẽ cần phải được khóa.

Về cơ bản, nếu bạn định viết bằng ngôn ngữ oo, dự án của bạn cần tuân theo mô hình oo, bao gồm cả phần mở rộng. Tất nhiên, nếu ai đó đã viết một thư viện theo một mô hình khác, có thể bạn không nên mở rộng nó.


7
BTW, OO và thủ tục rõ ràng nhất là KHÔNG loại trừ lẫn nhau. Bạn không thể có OO mà không cần thủ tục. Ngoài ra, thành phần cũng giống như một khái niệm OO như phần mở rộng.
Michael K

@Michael tất nhiên là không. Tôi đã nói rằng bạn có thể muốn có một hệ thống thủ tục , nghĩa là một hệ thống không có khái niệm OO. Tôi đã thấy mọi người sử dụng Java để viết mã thủ tục đơn thuần và nó không hoạt động nếu bạn thử và tương tác với nó theo kiểu OO.
Spencer Rathbun

1
Điều đó có thể được giải quyết, nếu Java có các hàm lớp đầu tiên. Meh.
Michael K

Thật khó để dạy ngôn ngữ Java hoặc DOTNet cho các sinh viên mới. Tôi thường dạy thủ tục với Pascal thủ tục hoặc "Plain C", và sau đó, chuyển sang OO với Object Pascal hoặc "C ++". Và, để lại Java cho sau này. Dạy lập trình với một chương trình thủ tục trông giống như một đối tượng (singleton) hoạt động tốt.
umlcat

@Michael K: Trong OOP, hàm hạng nhất chỉ là một đối tượng với chính xác một phương thức.
Giorgio

2

Bởi vì họ không biết gì hơn.

Các tác giả ban đầu có lẽ đang bám vào sự hiểu lầm về các nguyên tắc RẮN bắt nguồn từ một thế giới C ++ rối rắm và phức tạp.

Tôi hy vọng bạn sẽ nhận thấy rằng thế giới ruby, trăn và perl không có vấn đề mà các câu trả lời ở đây tuyên bố là lý do để niêm phong. Lưu ý rằng nó trực giao để gõ động. Công cụ sửa đổi truy cập dễ dàng làm việc trong hầu hết các ngôn ngữ (tất cả?). Các trường C ++ có thể được xử lý bằng cách chuyển sang một số loại khác (C ++ yếu hơn). Java và C # có thể sử dụng sự phản chiếu. Công cụ sửa đổi truy cập làm cho mọi thứ đủ khó để ngăn bạn thực hiện trừ khi bạn THỰC SỰ muốn.

Niêm phong các lớp và đánh dấu bất kỳ thành viên riêng tư vi phạm rõ ràng nguyên tắc rằng những điều đơn giản nên là những điều đơn giản và khó khăn nên có thể. Đột nhiên mọi thứ nên đơn giản, không.

Tôi khuyến khích bạn cố gắng hiểu quan điểm của các tác giả ban đầu. Phần lớn là từ một ý tưởng hàn lâm về đóng gói chưa bao giờ chứng minh thành công tuyệt đối trong thế giới thực. Tôi chưa bao giờ thấy một khung hoặc thư viện nơi một số nhà phát triển ở đâu đó không muốn nó hoạt động hơi khác và không có lý do chính đáng để thay đổi nó. Có hai khả năng có thể khiến các nhà phát triển phần mềm ban đầu bịt kín và khiến các thành viên riêng tư.

  1. Sự kiêu ngạo - họ thực sự tin rằng họ đã mở rộng và đóng cửa để sửa đổi
  2. Khiếu nại - họ biết có thể có các trường hợp sử dụng khác nhưng đã quyết định không viết cho các trường hợp sử dụng đó

Tôi nghĩ trong khuôn khổ công ty đã nói, # 2 có lẽ là trường hợp. Các khung C ++, Java và .NET đó phải được "thực hiện" và chúng phải tuân theo các nguyên tắc nhất định. Những hướng dẫn đó thường có nghĩa là các loại được niêm phong trừ khi loại được thiết kế rõ ràng như là một phần của hệ thống phân cấp loại và các thành viên riêng cho nhiều thứ có thể hữu ích cho những người khác sử dụng .. nhưng vì chúng không liên quan trực tiếp đến lớp đó, nên chúng không bị lộ . Trích xuất một loại mới sẽ quá tốn kém để hỗ trợ, tài liệu, v.v ...

Toàn bộ ý tưởng đằng sau các sửa đổi truy cập là các lập trình viên nên được bảo vệ khỏi chính họ. "Lập trình C là xấu vì nó cho phép bạn tự bắn vào chân mình." Đó không phải là một triết lý mà tôi đồng ý với tư cách là một lập trình viên.

Tôi rất thích cách tiếp cận mang tên của python. Bạn có thể dễ dàng (dễ dàng hơn nhiều so với phản ánh) thay thế tư nhân nếu bạn cần. Một bài viết tuyệt vời về nó có sẵn ở đây: http://bytebaker.com/2009/03/31/python-properIES-vs-java-access-modifier/

Công cụ sửa đổi riêng tư của Ruby thực sự giống như được bảo vệ trong C # và không có công cụ sửa đổi C # riêng tư. Được bảo vệ là một chút khác nhau. Có một lời giải thích tuyệt vời ở đây: http://www.ruby-lang.org/en/documentation/ruby-from-other-lacular/

Hãy nhớ rằng, ngôn ngữ tĩnh của bạn không phải tuân theo các kiểu lỗi thời của mã writte trong ngôn ngữ đó.


5
"Đóng gói chưa bao giờ chứng minh thành công tuyệt đối". Tôi đã nghĩ rằng mọi người sẽ đồng ý rằng việc thiếu đóng gói đã gây ra rất nhiều rắc rối.
Joh

5
-1. Những kẻ ngốc vội vã ở nơi những thiên thần sợ hãi bước đi. Kỷ niệm khả năng thay đổi các biến riêng tư cho thấy một lỗ hổng nghiêm trọng trong phong cách mã hóa của bạn.
riwalk

2
Ngoài việc gây tranh cãi và có lẽ sai, bài đăng này cũng khá kiêu ngạo và xúc phạm. Hoàn thành tốt
Konrad Rudolph

1
Có hai trường phái suy nghĩ mà những người đề xướng dường như không hợp nhau. Dân gian gõ tĩnh muốn nó dễ dàng để lý luận về phần mềm, dân gian động muốn nó dễ dàng để thêm chức năng. Cái nào tốt hơn về cơ bản phụ thuộc vào tuổi thọ dự kiến ​​của dự án.
Joh

1

EDIT: Tôi tin rằng các lớp học nên được thiết kế để mở. Với một số hạn chế, nhưng không nên đóng cửa để thừa kế.

Tại sao: "Lớp cuối cùng" hoặc "lớp kín" có vẻ kỳ lạ đối với tôi. Tôi không bao giờ phải đánh dấu một trong các lớp của mình là "cuối cùng" ("niêm phong"), bởi vì tôi có thể phải kế thừa các lớp đó sau đó.

Tôi đã mua / tải xuống các thư viện của bên thứ ba với các lớp, (hầu hết các điều khiển trực quan) và thực sự biết ơn không có thư viện nào sử dụng "cuối cùng", ngay cả khi được hỗ trợ bởi ngôn ngữ lập trình, trong đó chúng được mã hóa, bởi vì tôi đã kết thúc việc mở rộng các lớp đó, ngay cả khi tôi đã biên dịch các thư viện mà không có mã nguồn.

Đôi khi, tôi phải đối phó với một số lớp "niêm phong", và kết thúc việc tạo ra một "lớp bọc" mới đối với lớp đã cho, có các thành viên tương tự.

class MyBaseWrapper {
  protected FinalClass _FinalObject;

  public virtual DoSomething()
  {
    // these method cannot be overriden,
    // its from a "final" class
    // the wrapper method can  be open and virtual:
    FinalObject.DoSomething();
  }
} // class MyBaseWrapper


class MyBaseWrapper: MyBaseWrapper {

  public override DoSomething()
  {
    // the wrapper method allows "overriding",
    // a method from a "final" class:
    DoSomethingNewBefore();
    FinalObject.DoSomething();
    DoSomethingNewAfter();
  }
} // class MyBaseWrapper

Bộ tách phạm vi cho phép "đóng dấu" một số tính năng mà không hạn chế tính kế thừa.

Khi tôi chuyển từ Progr có cấu trúc. với OOP, tôi bắt đầu sử dụng các thành viên của lớp tư nhân , nhưng, sau nhiều dự án, tôi đã kết thúc việc sử dụng được bảo vệ và cuối cùng, quảng bá các thuộc tính hoặc phương thức đó ra công chúng , nếu cần thiết.

PS Tôi không thích "cuối cùng" là từ khóa trong C #, tôi thích sử dụng "niêm phong" như Java hoặc "vô trùng", theo phép ẩn dụ kế thừa. Bên cạnh đó, "Final" là một từ ambiguos được sử dụng trong một số ngữ cảnh.


-1: Bạn nói rằng bạn thích thiết kế mở, nhưng điều này không trả lời câu hỏi, đó là lý do tại sao các lớp không nên được thiết kế để mở.
Joh

@Joh Xin lỗi, tôi đã không giải thích rõ về bản thân mình, tôi đã cố gắng bày tỏ ý kiến ​​ngược lại, không nên bị niêm phong. Cảm ơn đã mô tả lý do tại sao bạn không đồng ý.
umlcat
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.