Đây có phải là một mẫu chống nếu một thuộc tính lớp tạo và trả về một thể hiện mới của một lớp không?


24

Tôi có một lớp được gọi là Headingthực hiện một vài điều, nhưng nó cũng có thể trả về giá trị ngược lại của giá trị tiêu đề hiện tại, cuối cùng phải được sử dụng thông qua việc tạo một thể hiện mới của Headingchính lớp đó.

Tôi có thể có một thuộc tính đơn giản được gọi reciprocalđể trả về tiêu đề ngược lại của giá trị hiện tại và sau đó tạo thủ công một thể hiện mới của lớp Heading hoặc tôi có thể tạo một phương thức như createReciprocalHeading()để tự động tạo một thể hiện mới của lớp Heading và trả nó về người sử dụng.

Tuy nhiên, một trong những đồng nghiệp của tôi đề nghị tôi để chỉ tạo ra một lớp tài sản gọi reciprocalđó trả về một thể hiện mới của lớp mình thông qua phương pháp getter của nó.

Câu hỏi của tôi là: Không phải đó là một mô hình chống đối một thuộc tính của một lớp để hành xử như vậy sao?

Tôi đặc biệt thấy điều này ít trực quan hơn bởi vì:

  1. Trong tâm trí của tôi, một thuộc tính của một lớp không nên trả về một thể hiện mới của một lớp và
  2. Tên của tài sản, reciprocalkhông giúp nhà phát triển hiểu đầy đủ hành vi của nó, mà không nhận được sự trợ giúp từ IDE hoặc kiểm tra chữ ký getter.

Tôi có quá khắt khe về những gì một tài sản hạng nên làm hay đó là một mối quan tâm hợp lệ? Tôi đã luôn cố gắng quản lý trạng thái của lớp thông qua các trường và thuộc tính của nó và hành vi của nó thông qua các phương thức của nó và tôi không thấy cách này phù hợp với định nghĩa của thuộc tính lớp.


6
Như @Ewan nói trong đoạn cuối của câu trả lời của anh ấy, không phải là một kiểu chống đối, có Headingmột kiểu bất biến và reciprocaltrả lại một cái mới Headinglà "hố thành công". (với lời cảnh báo ở hai cuộc gọi reciprocalsẽ trả về "cùng một thứ", tức là họ sẽ vượt qua bài kiểm tra về đẳng thức.)
David Arno

20
"Lớp học của tôi không phải là bất biến". Có mô hình chống thực sự của bạn, ngay tại đó. ;)
David Arno

3
@DavidArno Chà, đôi khi có một hình phạt hiệu suất rất lớn khi khiến một số thứ trở nên bất biến, đặc biệt là nếu ngôn ngữ không hỗ trợ nó một cách tự nhiên, nhưng nếu không thì tôi đồng ý rằng bất biến là một cách thực hành tốt trong nhiều trường hợp.
53777A

2
@dcorking lớp được triển khai cả trong C # và TypeScript nhưng tôi không giữ ngôn ngữ này.
53777A

2
@Kaiserludi nghe giống như một câu trả lời (một câu trả lời tuyệt vời) - ý kiến ​​là để yêu cầu sự rõ ràng
dcorking

Câu trả lời:


22

Không có gì lạ khi có những thứ như Copy () hoặc Clone () nhưng vâng tôi nghĩ bạn có quyền lo lắng về điều này.

lấy ví dụ:

h2 = h1.reciprocal
h2.Name = "hello world"
h1.reciprocal.Name = ?

Sẽ rất tốt nếu có một số cảnh báo rằng tài sản là một đối tượng mới mỗi lần.

bạn cũng có thể cho rằng:

h2.reciprocal == h1

Tuy nhiên. Nếu lớp tiêu đề của bạn là một loại giá trị bất biến thì bạn có thể thực hiện các mối quan hệ này và đối ứng có thể là một tên hay cho hoạt động


1
Chỉ cần lưu ý rằng Copy()Clone()là phương pháp, không phải thuộc tính. Tôi tin rằng OP đang đề cập đến những người thu hút tài sản trả lại các trường hợp mới.
Lynn

6
tôi biết. nhưng các thuộc tính là (sắp xếp) cũng là các phương thức trong c #
Ewan

4
Trong trường hợp đó, C # có nhiều hơn nữa để cung cấp: String.Substring(), Array.Reverse(), Int32.GetHashCode()vv
Lynn

If your heading class was an immutable value type- Tôi nghĩ bạn nên thêm các setters thuộc tính đó vào các đối tượng bất biến sẽ luôn trả về các thể hiện mới (hoặc ít nhất là nếu giá trị mới không giống với giá trị cũ), vì đó là định nghĩa của các đối tượng bất biến. :)
Ivan Kolmychek

17

Một giao diện của một lớp dẫn người dùng của lớp đưa ra các giả định về cách thức hoạt động của nó.

Nếu nhiều trong số các giả định này là đúng và ít có sai, giao diện là tốt.

Nếu nhiều trong số các giả định này là sai và một số ít là đúng, giao diện là rác.

Một giả định phổ biến về Properties là việc gọi hàm get là rẻ. Một giả định phổ biến khác về các thuộc tính là việc gọi hàm get hai lần liên tiếp sẽ trả về cùng một thứ.


Bạn có thể giải quyết vấn đề này bằng cách sử dụng tính nhất quán để thay đổi kỳ vọng. Ví dụ, đối với một thư viện 3D nhỏ, nơi bạn cần Vector, Ray Matrix, vv bạn có thể làm cho nó như vậy mà thu khí như Vector.normalMatrix.inverseđược khá nhiều luôn đắt. Thật không may, ngay cả khi các giao diện của bạn luôn sử dụng các thuộc tính đắt tiền, các giao diện tốt nhất sẽ trực quan như giao diện sử dụng Vector.create_normal()Matrix.create_inverse()- nhưng tôi không biết có thể tranh luận rằng có thể sử dụng các thuộc tính tạo ra giao diện trực quan hơn, ngay cả sau khi thay đổi kỳ vọng.


10

Các thuộc tính sẽ trả về rất nhanh, trả về cùng một giá trị với các cuộc gọi lặp lại và nhận giá trị của chúng sẽ không có tác dụng phụ. Những gì bạn mô tả ở đây không nên được thực hiện như một tài sản.

Trực giác của bạn là chính xác rộng rãi. Một phương thức nhà máy không trả về cùng một giá trị với các cuộc gọi lặp lại. Nó trả về một thể hiện mới mỗi lần. Điều này khá nhiều không đủ điều kiện nó là một tài sản. Sẽ không nhanh nếu phát triển sau này tăng thêm trọng lượng cho việc tạo cá thể, ví dụ như phụ thuộc mạng.

Các thuộc tính thường là các thao tác rất đơn giản có được / thiết lập một phần trạng thái hiện tại của lớp. Các hoạt động giống như nhà máy không đáp ứng tiêu chí này.


1
Các DateTime.Nowtài sản thường được sử dụng và đề cập đến một đối tượng mới. Tôi nghĩ rằng tên của một tài sản có thể giúp loại bỏ sự mơ hồ về việc liệu cùng một đối tượng được trả lại mỗi lần. Đó là tất cả trong tên và cách nó được sử dụng.
Greg Burghardt

3
DateTime.Now được coi là một sai lầm. Xem stackoverflow.com/questions/5437972/ Mạnh
Brad Thomas

@GregBurghardt Tác dụng phụ của một đối tượng mới có thể không phải là vấn đề trong chính nó, vì DateTimelà một loại giá trị và không có khái niệm về nhận dạng đối tượng. Nếu tôi hiểu chính xác, vấn đề là nó không mang tính quyết định và trả về một giá trị mới mỗi lần.
Zev Spitz

1
@GregBurghardt DateTime.Newlà tĩnh và do đó bạn không thể mong đợi nó đại diện cho trạng thái của một đối tượng.
Tsahi Asher

5

Tôi không nghĩ rằng có một câu trả lời bất khả tri về ngôn ngữ này vì câu hỏi cấu thành nên một tài sản ở thế giới là một câu hỏi dành riêng cho ngôn ngữ và những gì người gọi của một tài sản của người Hồi giáo cũng mong đợi là một câu hỏi dành riêng cho ngôn ngữ. Tôi nghĩ rằng cách hiệu quả nhất để nghĩ về điều này là suy nghĩ về những gì nó trông giống như từ quan điểm của người gọi.

Trong C #, các thuộc tính đặc biệt ở chỗ chúng (được quy ước) viết hoa (giống như các phương thức) nhưng thiếu dấu ngoặc đơn (như các biến thể hiện công khai). Nếu bạn thấy mã sau đây, tài liệu vắng mặt, bạn mong đợi điều gì?

var reciprocalHeading = myHeading.Reciprocal;

Là người mới làm quen với C #, nhưng một người đã đọc Nguyên tắc sử dụng tài sản của Microsoft , tôi sẽ mong đợi Reciprocal, trong số những điều khác:

  1. là thành viên dữ liệu logic của Headinglớp
  2. không tốn kém để gọi, vì vậy tôi không cần phải lưu trữ giá trị
  3. thiếu tác dụng phụ có thể quan sát
  4. tạo ra kết quả tương tự nếu được gọi hai lần liên tiếp
  5. (có thể) cung cấp một ReciprocalChangedsự kiện

Trong số các giả định này, (3) và (4) có thể đúng (giả sử Headinglà loại giá trị bất biến, như trong câu trả lời của Ewan ), (1) không thể tranh cãi, (2) không rõ nhưng cũng không thể tranh cãi, và (5) không chắc có ý nghĩa ngữ nghĩa (mặc dù bất cứ điều gì một tiêu đề có lẽ nên có một HeadingChangedsự kiện). Điều này gợi ý cho tôi rằng trong API C #, không nên thực hiện hoặc tính toán đối ứng với nhau như một tài sản, mà đặc biệt là nếu phép tính rẻ và Headingkhông thay đổi, đó là trường hợp đường biên.

(Tuy nhiên, xin lưu ý rằng không có bất kỳ mối quan tâm nào trong số những mối quan tâm này có liên quan đến việc gọi tài sản có tạo ra một thể hiện mới hay không, thậm chí (2). Tạo các đối tượng trong CLR, nói chung, là khá rẻ.)

Trong Java, các thuộc tính là một quy ước đặt tên phương thức. Nếu tôi thấy

Heading reciprocalHeading = myHeading.getReciprocal();

những kỳ vọng của tôi tương tự như những điều ở trên (nếu ít được đặt ra rõ ràng): Tôi hy vọng cuộc gọi sẽ rẻ, bình thường và thiếu tác dụng phụ. Tuy nhiên, bên ngoài khung công tác JavaBeans, khái niệm về một thuộc tính của Hồi không phải là tất cả có ý nghĩa trong Java và đặc biệt khi xem xét một thuộc tính bất biến không có tương ứng setReciprocal(), getXXX()quy ước hiện có phần lỗi thời. Từ Java hiệu quả , phiên bản thứ hai (đã hơn tám năm tuổi):

Các phương thức trả về một booleanhàm không thuộc tính hoặc thuộc tính của đối tượng mà chúng được gọi thường được đặt tên bằng một danh từ, cụm danh từ hoặc cụm động từ bắt đầu bằng động từ get. Có một đội ngũ phát biểu tuyên bố rằng chỉ có hình thức thứ ba (bắt đầu bằng get) là có thể chấp nhận được, nhưng có rất ít cơ sở cho tuyên bố này. Hai hình thức đầu tiên thường dẫn đến mã dễ đọc hơn (trang 239)

Trong một API hiện đại, trôi chảy hơn, tôi sẽ thấy

Heading reciprocalHeading = myHeading.reciprocal();

- điều này một lần nữa gợi ý rằng cuộc gọi rẻ tiền, bình thường và thiếu tác dụng phụ, nhưng sẽ không nói gì về việc liệu một phép tính mới được thực hiện hay một đối tượng mới được tạo ra. Điều này là tốt; trong một API tốt, tôi không nên quan tâm.

Trong Ruby, không có thứ gọi là tài sản. Có những thuộc tính của người Viking, nhưng nếu tôi thấy

reciprocalHeading = my_heading.reciprocal

Tôi không có cách nào ngay lập tức để biết liệu tôi đang truy cập một biến đối tượng @reciprocalthông qua một attr_readerphương thức truy cập đơn giản hay liệu tôi đang gọi một phương thức thực hiện một phép tính đắt tiền. Thực tế, tên phương thức là một danh từ đơn giản, tuy nhiên, thay vì nói calcReciprocal, một lần nữa, gợi ý rằng cuộc gọi ít nhất là rẻ và có lẽ không có tác dụng phụ.

Trong Scala, quy ước đặt tên là các phương thức có tác dụng phụ có dấu ngoặc đơn và phương thức mà không có chúng, nhưng

val reciprocal = heading.reciprocal

có thể là bất kỳ:

// immutable public value initialized at creation time
val reciprocal: Heading = … 

// immutable public value initialized on first use
lazy val reciprocal: Heading = … 

// public method, probably recalculating on each invocation
def reciprocal: Heading = …

// as above, with parentheses that, by convention, the caller
// should only omit if they know the method has no side effects
def reciprocal(): Heading = …

(Lưu ý rằng Scala cho phép nhiều thứ mà dù sao cũng không được hướng dẫn bởi phong cách . Đây là một trong những phiền toái lớn của tôi với Scala.)

Việc thiếu dấu ngoặc đơn cho tôi biết cuộc gọi không có tác dụng phụ; Tên, một lần nữa, gợi ý rằng cuộc gọi nên tương đối rẻ. Ngoài ra, tôi không quan tâm làm thế nào nó mang lại cho tôi giá trị.

Tóm lại: Biết ngôn ngữ bạn đang sử dụng và biết những kỳ vọng mà các lập trình viên khác sẽ mang lại cho API của bạn. Mọi thứ khác là một chi tiết thực hiện.


1
+1 một trong những câu trả lời yêu thích của tôi, cảm ơn vì đã dành thời gian của bạn để viết bài này.
53777A

2

Như những người khác đã nói, đó là một mô hình khá phổ biến để trả về các thể hiện của cùng một lớp.

Việc đặt tên phải đi đôi với quy ước đặt tên của ngôn ngữ.

Ví dụ, trong Java, tôi có thể mong đợi nó được gọi getReciprocal();

Điều đó đang được nói, tôi sẽ xem xét các đối tượng có thể thay đổi và bất biến .

Với những thứ không thay đổi , mọi thứ khá dễ dàng và sẽ không bị tổn thương nếu bạn trả lại cùng một đối tượng.

Với những người đột biến , điều này có thể trở nên rất đáng sợ.

b = a.reciprocal
a += 1
b = what?

Bây giờ b đang đề cập đến cái gì? Đối ứng của giá trị ban đầu của a? Hoặc đối ứng của một trong những thay đổi? Đây có thể là một ví dụ quá ngắn, nhưng bạn có được điểm.

Trong những trường hợp đó, hãy tìm một cách đặt tên tốt hơn để truyền đạt những gì xảy ra, ví dụ createReciprocal()có thể là một lựa chọn tốt hơn.

Nhưng nó thực sự phụ thuộc vào bối cảnh là tốt.


Ý bạn là đột biến ?
Carsten S

@CarstenS Vâng, tất nhiên, cảm ơn. Không biết đầu tôi đang ở đâu. :)
Eiko

Không đồng ý một chút getReciprocal(), vì "âm thanh" đó giống như một trình thu thập kiểu JavaBeans "bình thường". Thích "createdXXX ()" hoặc có thể "tínhXXX ()" mà theo chỉ dẫn hoặc gợi ý cho các lập trình viên khác rằng có điều gì đó khác biệt đang xảy ra
user949300

@ user949300 Tôi đồng ý phần nào với những lo lắng của bạn - và đề cập đến việc tạo ... tên tiếp tục xuống. Có những nhược điểm, mặc dù. tạo ... ngụ ý các trường hợp mới có thể không phải luôn luôn như vậy (nghĩ về các đối tượng dành riêng cho một số trường hợp đặc biệt), tính toán ... loại chi tiết thực hiện rò rỉ - có thể khuyến khích các giả định sai về thẻ giá.
Eiko

1

Vì lợi ích của tính đơn lẻ và rõ ràng, tôi sẽ có ReverseHeadFactory lấy đối tượng và trả về mặt sau của nó. Điều này sẽ làm rõ hơn rằng một đối tượng mới đã được trả về và có nghĩa là mã để tạo ra sự đảo ngược được gói gọn khỏi mã khác.


1
+1 cho tên rõ ràng, nhưng SRP có gì sai trong các giải pháp khác?
53777A

3
Tại sao bạn đề xuất một lớp nhà máy hơn một phương thức nhà máy? (Theo tôi, trách nhiệm hữu của một đối tượng thường là các đối tượng Emit rằng cũng giống như chính nó, chẳng hạn như iterator.next Có vi phạm nhẹ này của SRP nguyên nhân vấn đề kỹ thuật phần mềm khác.?)
dcorking

2
@dcorking Một lớp nhà máy so với phương thức (theo ý kiến ​​của tôi) thường là cùng một loại câu hỏi như "Đối tượng có thể so sánh được không, hay tôi nên làm một bộ so sánh?" Luôn luôn ổn khi tạo một bộ so sánh, nhưng chỉ ổn khi làm cho chúng có thể so sánh được nếu đó là một cách so sánh chúng khá phổ biến. (Ví dụ: số lớn hơn lớn hơn số nhỏ hơn, điều này tốt cho so sánh, nhưng bạn có thể nói tất cả các evens đều lớn hơn tất cả các tỷ lệ cược, tôi sẽ biến nó thành một công cụ so sánh) - Vì vậy, tôi nghĩ rằng chỉ có một cách để đảo ngược một tiêu đề, vì vậy tôi sẽ nghiêng về việc tạo một phương thức để tránh một lớp học thêm.
Thuyền trưởng Man

0

Nó phụ thuộc. Tôi thường mong đợi các thuộc tính trả về những thứ là một phần của một thể hiện, vì vậy việc trả về một thể hiện khác của cùng một lớp sẽ là một chút bất thường.

Nhưng ví dụ, một lớp chuỗi có thể có các thuộc tính "firstWord", "lastWord" hoặc nếu nó xử lý Unicode "firstLetter", "lastLetter" sẽ là các đối tượng chuỗi đầy đủ - thường chỉ là các đối tượng chuỗi nhỏ hơn.


0

Vì các câu trả lời khác bao gồm phần Tài sản, tôi sẽ chỉ giải quyết số 2 của bạn về reciprocal:

Không sử dụng bất cứ thứ gì khác ngoài reciprocal. Đây là thuật ngữ chính xác duy nhất cho những gì bạn mô tả (và đó là thuật ngữ chính thức). Không viết phần mềm không chính xác để cứu nhà phát triển của bạn khỏi việc tìm hiểu tên miền mà họ đang làm việc. Trong điều hướng, các thuật ngữ có ý nghĩa rất cụ thể và sử dụng một cái gì đó dường như vô hại reversehoặc oppositecó thể dẫn đến nhầm lẫn trong một số bối cảnh nhất định.


0

Theo logic, không, nó không phải là một tài sản của một tiêu đề. Nếu đúng như vậy, bạn cũng có thể nói rằng số âm của một số là tài sản của số đó, và thẳng thắn sẽ làm cho "tài sản" mất hết ý nghĩa, gần như mọi thứ sẽ là tài sản.

Reciprocal là một hàm thuần túy của một tiêu đề, giống như âm của một số là một hàm thuần túy của số đó.

Theo nguyên tắc thông thường, nếu cài đặt nó không có ý nghĩa, có lẽ nó không nên là một tài sản. Tất nhiên, việc thiết lập nó vẫn có thể không được phép, nhưng việc thêm một setter nên về mặt lý thuyết là có thể và có ý nghĩa.

Bây giờ, trong một số ngôn ngữ, có thể có ý nghĩa để có nó như một tài sản như được chỉ định trong ngữ cảnh của ngôn ngữ đó, nếu nó mang lại một số lợi thế do cơ học của ngôn ngữ đó. Ví dụ: nếu bạn muốn lưu trữ nó vào cơ sở dữ liệu, có thể hợp lý để biến nó thành tài sản nếu nó cho phép xử lý tự động theo khung ORM. Hoặc có thể đó là ngôn ngữ không có sự phân biệt giữa thuộc tính và hàm thành viên không tham số (Tôi không biết ngôn ngữ đó, nhưng tôi chắc chắn có một số ngôn ngữ). Sau đó, tùy thuộc vào trình thiết kế API và documenter để phân biệt giữa các hàm và thuộc tính.


Chỉnh sửa: Đối với C # cụ thể, tôi sẽ xem xét các lớp hiện có, đặc biệt là các lớp của Thư viện chuẩn.

  • BigIntegerComplexcấu trúc. Nhưng chúng là các cấu trúc và chỉ có các hàm tĩnh và tất cả các thuộc tính thuộc loại khác nhau. Vì vậy, không có nhiều thiết kế giúp đỡ cho bạn Headinglớp.
  • Math.NET Numerics có Vectorlớp , có rất ít thuộc tính và dường như khá gần với của bạn Heading. Nếu bạn làm điều này, thì bạn sẽ có một chức năng đối ứng, không phải là một tài sản.

Bạn có thể muốn xem các thư viện thực sự được sử dụng bởi mã của bạn hoặc các lớp tương tự hiện có. Cố gắng duy trì sự nhất quán, đó thường là tốt nhất, nếu một cách rõ ràng không đúng và một cách khác sai.


-1

Câu hỏi rất thú vị thực sự. Tôi thấy không có vi phạm RẮN + DRY + KISS trong những gì bạn đang đề xuất, nhưng, vẫn có mùi khó chịu.

Một phương thức trả về một thể hiện của lớp được gọi là constructor , phải không? vì vậy, bạn đang tạo một thể hiện mới từ một phương thức không có hàm tạo. Đó không phải là ngày tận thế, nhưng với tư cách là một khách hàng, tôi sẽ không mong đợi điều đó. Điều này thường được thực hiện trong các trường hợp cụ thể: một nhà máy hoặc, đôi khi, một phương thức tĩnh của cùng một lớp (hữu ích cho các singletons và cho ... lập trình viên không hướng đối tượng?)

Plus: điều gì xảy ra nếu bạn gọi getReciprocal()đối tượng được trả về? bạn có được một thể hiện khác của lớp, đó rất có thể là một bản sao chính xác của lớp đầu tiên! Và nếu bạn viện dẫn getReciprocal()trên THAT? Đối tượng ngổn ngang ai?

Một lần nữa: nếu kẻ xâm lược cần giá trị đối ứng (ý tôi là vô hướng) thì sao? getReciprocal()->getValue()? chúng tôi đang vi phạm Luật Demeter vì lợi ích nhỏ.


Tôi sẵn sàng chấp nhận các đối số phản đối từ downvoter, cảm ơn!
Tiến sĩ Gianluigi Zane Zanettini

1
Tôi đã không downvote nhưng tôi nghi ngờ lý do có thể là thực tế là nó không thực sự bổ sung nhiều vào phần còn lại của câu trả lời, nhưng tôi có thể sai.
53777A

2
RẮN và KISS thường đối lập nhau.
whatsisname
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.