Cách triển khai một thuộc tính trên lớp A trong đó đề cập đến một thuộc tính của một đối tượng con của lớp A


9

Chúng ta có mã này, khi được đơn giản hóa, trông như thế này:

public class Room
{
    public Client Client { get; set; }

    public long ClientId
    {
        get
        {
            return Client == null ? 0 : Client.Id;
        }
    }
}

public class Client 
{
    public long Id { get; set; }
}

Bây giờ chúng tôi có ba quan điểm.

1) Đây là mã tốt vì thuộc Clienttính phải luôn được đặt (không phải là null) vì vậy Client == nullsẽ không bao giờ xảy ra và giá trị Id 0biểu thị Id sai dù sao (đây là ý kiến ​​của người viết mã ;-))

2) Bạn không thể dựa vào người gọi để biết đó 0là giá trị sai Idvà khi Clienttài sản phải luôn được đặt, bạn nên ném exceptionvào getkhi Clienttài sản xảy ra là null

3) Khi thuộc Clienttính phải luôn được đặt, bạn chỉ cần trả về Client.Idvà để mã ném NullRefngoại lệ khi thuộc Clienttính không có giá trị.

Điều nào là đúng nhất? Hay là có khả năng thứ tư?


5
Không phải là một câu trả lời mà chỉ là một lưu ý: Đừng bao giờ ném ngoại lệ từ những người thu hút tài sản.
Brandon

3
Để giải thích về quan điểm của Brandon: stackoverflow.com/a/1488488/569777
MetaFight

1
Chắc chắn: Ý tưởng chung ( msdn.microsoft.com/en-us/library/... , stackoverflow.com/questions/1488472/... , trong số những người khác) là thuộc tính nên làm càng ít càng tốt, hãy nhẹ và nên đại diện cho sạch , trạng thái đối mặt công khai của đối tượng của bạn, với các bộ chỉ mục là một ngoại lệ nhỏ. Đây cũng là hành vi bất ngờ khi thấy ngoại lệ trong cửa sổ xem cho một thuộc tính trong quá trình gỡ lỗi.
Brandon

1
Bạn không nên (cần) viết mã ném vào một getter thuộc tính. Điều đó không có nghĩa là bạn được yêu cầu che giấu và nuốt bất kỳ trường hợp ngoại lệ nào xảy ra do một đối tượng rơi vào trạng thái không được hỗ trợ.
Bến

4
Tại sao bạn không thể làm Room.Client.Id? Tại sao bạn muốn bọc nó vào Room.ClientId? Nếu nó là để kiểm tra một khách hàng thì tại sao không phải là một cái gì đó như Room.HasClient {get {return client == null; }} đó là thẳng hơn và vẫn cho phép mọi người làm Room.Client.Id bình thường khi họ thực sự cần id?
James

Câu trả lời:


25

Có vẻ như bạn nên giới hạn số lượng trạng thái mà Roomlớp của bạn có thể ở.

Thực tế là bạn đang hỏi về việc phải làm gì khi Clientnull là một gợi ý rằng Roomkhông gian trạng thái quá lớn.

Để đơn giản, tôi sẽ không cho phép Clienttài sản của bất kỳ Roomtrường hợp nào là null. Điều đó có nghĩa là mã bên trong Roomcó thể giả định một cách an toàn Clientlà không bao giờ null.

Nếu vì một lý do nào đó trong tương lai Clienttrở nên nullchống lại sự thôi thúc ủng hộ trạng thái đó. Làm như vậy sẽ tăng sự phức tạp bảo trì của bạn.

Thay vào đó, cho phép mã thất bại và thất bại nhanh chóng. Rốt cuộc, đây không phải là một trạng thái được hỗ trợ. Nếu ứng dụng tự chuyển sang trạng thái này, bạn đã vượt qua một dòng không trả lại. Điều hợp lý duy nhất để làm sau đó là đóng ứng dụng.

Điều này có thể xảy ra (khá tự nhiên) là kết quả của một ngoại lệ tham chiếu null chưa được xử lý.


2
Đẹp. Tôi thích ý nghĩ "Để giữ mọi thứ đơn giản, tôi sẽ không cho phép thuộc tính Khách hàng của bất kỳ trường hợp Phòng nào là không có giá trị." Điều đó thực sự tốt hơn là đặt câu hỏi phải làm gì khi không có giá trị
Michel

@Michel nếu sau này bạn quyết định thay đổi yêu cầu đó, bạn có thể thay thế nullgiá trị bằng NullObject(hoặc đúng hơn NullClient) mà sau đó vẫn hoạt động với mã hiện tại của bạn và cho phép bạn xác định hành vi cho một khách hàng không tồn tại nếu cần. Câu hỏi là liệu trường hợp "không có khách hàng" có phải là một phần của logic kinh doanh hay không.
null

3
Hoàn toàn chính xác. Đừng viết một ký tự mã để hỗ trợ trạng thái không được hỗ trợ. Chỉ cần để nó ném một NullRef.
Bến

@Ben Không đồng ý; Hướng dẫn của MS nêu rõ rằng các nhân viên tài sản không nên ném ngoại lệ. Và cho phép null refs được ném khi một ngoại lệ có ý nghĩa hơn có thể được ném thay vào đó chỉ là sự lười biếng.
Andy

1
@Andy hiểu lý do của hướng dẫn sẽ cho phép bạn biết khi nào nó không áp dụng.
Ben

21

Chỉ cần một vài cân nhắc:

a) Tại sao lại có một getter dành riêng cho ClientId khi đã có một getter công khai cho chính đối tượng Client? Tôi không thấy lý do tại sao thông tin mà ClientId longphải được khắc trên đá trong chữ ký của Room.

b) Về ý kiến ​​thứ hai, bạn có thể đưa ra một hằng số Invalid_Client_Id.

c) Về ý kiến ​​một và ba (và là điểm chính của tôi): Một phòng phải luôn có khách hàng? Có thể đó chỉ là ngữ nghĩa nhưng điều này không đúng. Có lẽ sẽ phù hợp hơn khi có các lớp hoàn toàn riêng biệt cho Room và Client và một lớp khác gắn kết chúng lại với nhau. Có thể bổ nhiệm, đặt phòng, chiếm dụng? (Điều này phụ thuộc vào những gì bạn thực sự đang làm.) Và trên lớp đó, bạn có thể thực thi các ràng buộc "phải có phòng" và "phải có khách hàng".


5
+1 cho "Tôi không thấy, ví dụ tại sao thông tin mà ClientId tồn tại lâu phải được khắc thành đá thành chữ ký của Phòng"
Michel

Tiếng anh của bạn tốt. ;) Chỉ cần đọc câu trả lời này, tôi sẽ không biết rằng tiếng mẹ đẻ của bạn không phải là tiếng Anh nếu bạn không nói như vậy.
jpmc26

Điểm B & C là tốt; RE: a, đây là một phương thức tiện lợi và thực tế là phòng có tham chiếu đến Khách hàng có thể chỉ là một chi tiết triển khai (có thể lập luận rằng Khách hàng không nên hiển thị công khai)
Andy

17

Tôi không đồng ý với cả ba ý kiến. Nếu Clientkhông bao giờ có thể null, thì thậm chí đừng biến nó thành có thểnull !

  1. Đặt giá trị của Clienttrong hàm tạo
  2. Ném một ArgumentNullExceptiontrong các nhà xây dựng.

Vì vậy, mã của bạn sẽ là một cái gì đó như:

public class Room
{
    private Client theClient;

    public Room(Client client) {
        if(client == null) throw new ArgumentNullException();
        this.theClient = client;
    }

    public Client Client { 
        get { return theClient; }
        set 
        {
            if (value == null) 
                throw new ArgumentNullException();
            theClient = value;
        }
    }

    public long ClientId
    {
        get
        {
            return theClient.Id;
        }
    }
}
public class Client 
{
    public long Id { get; set; }
}

Điều này không liên quan đến mã của bạn, nhưng có lẽ bạn nên sử dụng một phiên bản bất biếnRoom bằng cách thực hiện theClient readonly, và sau đó nếu khách hàng thay đổi, tạo một phòng mới. Điều này sẽ cải thiện tính an toàn của chuỗi mã của bạn ngoài sự an toàn vô hiệu của các khía cạnh khác trong câu trả lời của tôi. Xem cuộc thảo luận này về mutable vs bất biến


1
Đây có nên là ArgumentNullExceptions?
Mathieu Guindon

4
Không. Mã chương trình không bao giờ được ném NullReferenceException, nó được ném bởi .Net VM "khi có một nỗ lực để hủy bỏ tham chiếu đối tượng null" . Bạn nên ném một cái ArgumentNullException, được ném "khi tham chiếu null (Không có gì trong Visual Basic) được truyền cho một phương thức không chấp nhận nó như một đối số hợp lệ."
Pharap

2
@Pharap Tôi là một lập trình viên Java đang cố viết mã C #, tôi đoán rằng tôi đã phạm sai lầm như thế.
durron597

@ durron597 Tôi đã đoán rằng từ việc sử dụng thuật ngữ 'cuối cùng' (trong trường hợp này là từ khóa C # tương ứng là readonly). Tôi sẽ chỉ chỉnh sửa nhưng tôi cảm thấy một bình luận sẽ phục vụ để giải thích rõ hơn tại sao (cho độc giả tương lai). Nếu bạn chưa đưa ra câu trả lời này, tôi sẽ đưa ra giải pháp chính xác tương tự. Bất biến cũng là một ý tưởng tốt, nhưng nó không phải lúc nào cũng dễ dàng như người ta hy vọng.
Pharap

2
Các thuộc tính thường không nên ném ngoại lệ; thay vì setter ném ArgumentNullException, hãy cung cấp phương thức SetClient thay thế. Ai đó đã đăng liên kết đến một câu trả lời liên quan đến vấn đề này.
Andy

5

Các tùy chọn thứ hai và thứ ba nên tránh - getter không nên đánh người gọi bằng một ngoại lệ mà họ không kiểm soát được.

Bạn nên quyết định xem Client có thể là null hay không. Nếu vậy, bạn nên cung cấp một cách để người gọi kiểm tra xem nó có phải là null hay không trước khi truy cập vào nó (ví dụ: thuộc tính bool ClientIsNull).

Nếu bạn quyết định rằng Client không bao giờ có thể là null, thì hãy biến nó thành một tham số bắt buộc cho hàm tạo và ném ngoại lệ vào đó nếu null được truyền vào.

Cuối cùng, tùy chọn đầu tiên cũng chứa mùi mã. Bạn nên để Khách hàng xử lý tài sản ID riêng của mình. Có vẻ như quá mức để mã hóa một getter trong một lớp container mà chỉ đơn giản gọi một getter trên lớp chứa. Chỉ cần hiển thị Khách hàng là tài sản (nếu không, cuối cùng bạn sẽ sao chép mọi thứ mà Khách hàng đã cung cấp ).

long clientId = room.Client.Id;

Nếu Khách hàng có thể là null, thì ít nhất bạn sẽ chịu trách nhiệm cho người gọi:

if (room.Client != null){
    long clientId = room.Client.Id;
    /* other code follows... */
}

1
Tôi nghĩ rằng "cuối cùng bạn sẽ sao chép mọi thứ mà khách hàng đã cung cấp" cần được in đậm hoặc ít nhất là in nghiêng.
Pharap

2

Nếu thuộc tính Client null là trạng thái được hỗ trợ, hãy xem xét sử dụng NullObject .

Nhưng rất có thể đây là một trạng thái đặc biệt, vì vậy bạn nên làm cho nó không thể (hoặc chỉ không thuận tiện lắm) để kết thúc bằng null Client:

public Client Client { get; private set; }

public Room(Client client)
{
    Client = client;
}

public void SetClient(Client client)
{
    if (client == null) throw new ArgumentNullException();
    Client = client;
}

Tuy nhiên, nếu nó không được hỗ trợ, đừng lãng phí thời gian cho các giải pháp nướng một nửa. Trong trường hợp này:

return Client == null ? 0 : Client.Id;

Bạn đã 'giải quyết' NullReferenceException ở đây nhưng với chi phí rất cao! Điều này sẽ buộc tất cả người gọi Room.ClientIdphải kiểm tra 0:

var aRoom = new Room(aClient);
var clientId = aRoom.ClientId;
if (clientId == 0) RestartRoombookingWizard();
else ContinueRoombooking(clientid);

Các lỗi lạ có thể xuất hiện với các nhà phát triển khác (hoặc chính bạn!) Quên kiểm tra giá trị trả về "ErrorCode" tại một số thời điểm.
Chơi nó an toàn thất bại nhanh. Cho phép ném NullReferenceException và "duyên dáng" bắt nó ở đâu đó cao hơn ngăn xếp cuộc gọi thay vì cho phép người gọi làm rối tung mọi thứ hơn nữa ...

Ở một lưu ý khác, nếu bạn rất muốn phơi bày Clientvà cũng là ClientIdsuy nghĩ về TellDontAsk . Nếu bạn yêu cầu quá nhiều từ các đối tượng thay vì nói với họ những gì bạn muốn đạt được, bạn có thể kết thúc việc ghép mọi thứ lại với nhau, khiến cho việc thay đổi trở nên khó khăn hơn sau này.


Điều đó NotSupportedExceptionđang được sử dụng sai, bạn nên sử dụng một ArgumentNullExceptionthay thế.
Pharap

1

Kể từ c # 6.0, hiện đã được phát hành, bạn chỉ nên làm điều này,

public class Room
{
    public Room(Client client)
    {
        this.Client = client;
    }

    public Client Client { get; }
}

public class Client
{
    public Client(long id)
    {
        this.Id = id;
    }

    public long Id { get; }
}

Nâng cao một tài sản của Clientđể Roomlà một sự vi phạm rõ ràng đóng gói và nguyên tắc DRY.

Nếu bạn cần truy cập vào Idmột Clienttrong những điều Roombạn có thể làm,

var clientId = room.Client?.Id;

Lưu ý việc sử dụng Toán tử có điều kiện Null , clientIdsẽ là long?, nếu Clientnull, clientIdsẽ có null, nếu không clientIdsẽ có giá trị là Client.Id.


nhưng loại clientidlà một nullable dài thì sao?
Michel

@Michel tốt, nếu một phòng phải có Khách hàng, hãy đặt một kiểm tra null trong ctor. Sau đó var clientId = room.Client.Idsẽ được an toàn và long.
Jodrell
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.