วิธีการนำคุณสมบัติไปใช้กับคลาส A ซึ่งอ้างถึงคุณสมบัติของอ็อบเจ็กต์ลูกของคลาส A


9

เรามีรหัสนี้ซึ่งเมื่อทำให้ง่ายขึ้นจะมีลักษณะเช่นนี้:

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; }
}

ตอนนี้เรามีสามมุมมอง

1) นี่เป็นรหัสที่ดีเพราะClientควรตั้งค่าคุณสมบัติ (เช่นไม่ใช่โมฆะ) ดังนั้นClient == nullจะไม่เกิดขึ้นและค่ารหัส0แสดงถึงรหัสเท็จอย่างไรก็ตาม (นี่คือความเห็นของผู้เขียนรหัส ;-))

2) คุณไม่สามารถพึ่งพาผู้เรียกได้ว่า0เป็นค่าเท็จIdและเมื่อ Clientควรตั้งค่าคุณสมบัติคุณควรโยนexceptionในgetเมื่อClientคุณสมบัติเกิดขึ้นเป็นโมฆะ

3) เมื่อClientควรตั้งค่าคุณสมบัติคุณเพียงแค่ส่งคืนClient.Idและปล่อยให้โค้ดโยนNullRefข้อยกเว้นเมื่อClientคุณสมบัติเป็นโมฆะ

ข้อใดถูกต้องที่สุด หรือมีความเป็นไปได้ที่สี่?


5
ไม่ใช่คำตอบ แต่เป็นข้อควรระวัง: อย่าโยนข้อยกเว้นจากผู้ได้รับทรัพย์สิน
แบรนดอน

3
หากต้องการอธิบายรายละเอียดเกี่ยวกับประเด็นของ Brandon: stackoverflow.com/a/1488488/569777
MetaFight

1
แน่นอน: แนวคิดทั่วไป ( msdn.microsoft.com/en-us/library/… , stackoverflow.com/questions/1488472/… , อื่น ๆ ) เป็นคุณสมบัติที่ควรทำให้น้อยที่สุดเท่าที่จะทำได้เบาและควรเป็นตัวแทนของความสะอาด สถานะสาธารณะของวัตถุของคุณโดยดัชนีถูกยกเว้นเล็กน้อย นอกจากนี้ยังมีพฤติกรรมที่ไม่คาดคิดในการดูข้อยกเว้นในหน้าต่างการดูสำหรับคุณสมบัติในระหว่างการดีบัก
แบรนดอน

1
คุณไม่ควร (ต้อง) เขียนรหัสที่ส่งไปยังตัวรับทรัพย์สิน ไม่ได้หมายความว่าคุณจะต้องซ่อนและกลืนข้อยกเว้นใด ๆ ที่เกิดขึ้นเนื่องจากวัตถุเข้าสู่สถานะที่ไม่รองรับ
Ben

4
ทำไมคุณไม่ทำ Room.Client.Id ไม่ได้? ทำไมคุณต้องการห่อสิ่งนั้นลงใน Room.ClientId ถ้ามันจะตรวจสอบลูกค้าแล้วทำไมไม่เหมือน Room.HasClient {รับ {return ลูกค้า == null; }} ที่ตรงไปตรงมามากขึ้นและยังคงให้คนทำ Room.Client.I ปกติเมื่อพวกเขาต้องการรหัสจริงหรือไม่
James

คำตอบ:


25

มันมีกลิ่นเหมือนว่าคุณควร จำกัด จำนวนสถานะที่Roomคลาสของคุณสามารถเข้าได้

ความจริงที่ว่าคุณกำลังถามเกี่ยวกับสิ่งที่ต้องทำเมื่อClientว่างคือคำใบ้ที่ว่าRoomพื้นที่รัฐมีขนาดใหญ่เกินไป

เพื่อให้ง่ายขึ้นฉันจะไม่อนุญาตให้ClientคุณสมบัติของRoomอินสแตนซ์ใด ๆเป็นโมฆะ นั่นหมายความว่ารหัสภายในRoomสามารถถือว่าไม่มีทางClientเป็นโมฆะ

หากด้วยเหตุผลบางอย่างในอนาคตClientจะnullต่อต้านความอยากสนับสนุนรัฐนั้น การทำเช่นนั้นจะเพิ่มความซับซ้อนในการบำรุงรักษาของคุณ

อนุญาตให้รหัสล้มเหลวและล้มเหลวอย่างรวดเร็วแทน ท้ายที่สุดนี่ไม่ใช่สถานะที่ได้รับการสนับสนุน หากแอปพลิเคชันเข้าสู่สถานะนี้คุณได้ข้ามเส้นที่ไม่มีการส่งคืนแล้ว สิ่งเดียวที่สมเหตุสมผลที่ต้องทำคือปิดแอปพลิเคชัน

สิ่งนี้อาจเกิดขึ้น (ค่อนข้างเป็นธรรมชาติ) ซึ่งเป็นผลมาจากข้อยกเว้นการอ้างอิง null ที่ไม่สามารถจัดการได้


2
ดี ฉันชอบความคิดที่ว่า "เพื่อให้สิ่งต่าง ๆ เรียบง่ายฉันจะไม่อนุญาตให้คุณสมบัติของไคลเอ็นต์ของอินสแตนซ์ของห้องใด ๆ เป็นโมฆะ" ดีกว่าการถามว่าจะทำอย่างไรเมื่อว่าง
Michel

@Michel หากคุณตัดสินใจเปลี่ยนข้อกำหนดนั้นในภายหลังคุณสามารถแทนที่nullค่าด้วยNullObject(หรือมากกว่าNullClient) ซึ่งจะยังคงทำงานกับรหัสที่มีอยู่ของคุณและอนุญาตให้คุณกำหนดพฤติกรรมสำหรับไคลเอ็นต์ที่ไม่มีอยู่หากต้องการ คำถามคือว่ากรณี "ไม่มีลูกค้า" เป็นส่วนหนึ่งของตรรกะทางธุรกิจหรือไม่
null

3
ถูกต้องทั้งหมด อย่าเขียนตัวอักษรรหัสเดียวเพื่อสนับสนุนสถานะที่ไม่สนับสนุน ปล่อยให้มันทิ้ง NullRef
เบ็น

@Ben ไม่เห็นด้วย; หลักเกณฑ์ของ MS ระบุไว้อย่างชัดเจนว่าผู้ได้รับทรัพย์สินไม่ควรโยนข้อยกเว้น และปล่อยให้การอ้างอิงแบบโมฆะถูกโยนเมื่อข้อยกเว้นที่มีความหมายมากกว่านั้นถูกโยนทิ้งแทนที่จะขี้เกียจ
Andy

1
@ เข้าใจเหตุผลของแนวทางที่จะช่วยให้คุณรู้ว่ามันไม่ได้ใช้เมื่อ
Ben

21

ข้อควรพิจารณาบางประการ:

a) ทำไมถึงมีผู้ทะเยอทะยานเป็นพิเศษสำหรับ ClientId เมื่อมีผู้ทะเยอทะยานสาธารณะสำหรับอินสแตนซ์ของลูกค้าเอง ฉันไม่เห็นว่าทำไมข้อมูลที่ลูกค้าต้องlongมีการแกะสลักด้วยหินในลายเซ็นของห้อง

ข) Invalid_Client_Idความเห็นเกี่ยวกับการที่สองคุณสามารถแนะนำอย่างต่อเนื่อง

c) เกี่ยวกับความคิดเห็นที่หนึ่งและสาม (และเป็นประเด็นหลักของฉัน): ห้องต้องมีลูกค้าเสมอ? อาจเป็นเพียงความหมาย แต่สิ่งนี้ไม่ถูกต้อง อาจเป็นการเหมาะสมที่จะแยกชั้นเรียนสำหรับห้องพักและลูกค้าและชั้นเรียนอื่นที่เชื่อมโยงพวกเขาเข้าด้วยกันอย่างสมบูรณ์ อาจจะได้รับการแต่งตั้งการจองการเข้าพัก? (ขึ้นอยู่กับสิ่งที่คุณทำจริง ๆ ) และในชั้นเรียนนั้นคุณสามารถบังคับใช้ข้อ จำกัด "ต้องมีห้อง" และ "ต้องมีลูกค้า"


5
+1 สำหรับ "ฉันไม่เห็นเช่นทำไมข้อมูลที่ลูกค้าต้องใช้เวลานานจะต้องถูกแกะสลักเป็นหินลงในลายเซ็นของห้อง"
Michel

ภาษาอังกฤษของคุณดี ;) เพียงแค่อ่านคำตอบนี้ฉันจะไม่ทราบว่าภาษาแม่ของคุณไม่ใช่ภาษาอังกฤษหากคุณไม่พูดเช่นนั้น
jpmc26

คะแนน B & C นั้นดี; RE: a, เป็นวิธีการอำนวยความสะดวกและความจริงที่ว่าห้องมีการอ้างอิงถึงลูกค้าอาจเป็นเพียงรายละเอียดการดำเนินการ (อาจเป็นที่ถกเถียงกันอยู่ว่าลูกค้าไม่ควรเปิดเผยต่อสาธารณะ)
Andy

17

ฉันไม่เห็นด้วยกับความคิดเห็นทั้งสาม หากClientไม่สามารถnull, แล้วไม่ได้ทำให้มันเป็นไปได้เพื่อให้เป็นnull !

  1. กำหนดค่าของClientในตัวสร้าง
  2. โยนArgumentNullExceptionในตัวสร้าง

ดังนั้นรหัสของคุณจะเป็นดังนี้:

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; }
}

สิ่งนี้ไม่เกี่ยวข้องกับรหัสของคุณ แต่คุณน่าจะดีกว่าถ้าใช้รุ่นที่ไม่เปลี่ยนรูปRoomโดยการทำtheClient readonlyแล้วถ้าลูกค้าเปลี่ยนให้สร้างห้องใหม่ สิ่งนี้จะปรับปรุงความปลอดภัยของเธรดของโค้ดของคุณนอกเหนือจากความปลอดภัยในด้านอื่น ๆ ของคำตอบของฉัน ดูการสนทนานี้เกี่ยวกับความไม่แน่นอนและไม่เปลี่ยนรูป


1
สิ่งเหล่านี้ไม่ควรArgumentNullExceptionใช่มั้ย
Mathieu Guindon


2
@Phap ฉันเป็นโปรแกรมเมอร์ Java ที่พยายามเขียนโค้ด C # ฉันคิดว่าฉันทำผิดพลาดแบบนั้น
durron597

@ durron597 ฉันควรจะเดาได้ว่าจากการใช้คำว่า 'final' (ในกรณีนี้คีย์เวิร์ด C # ที่เกี่ยวข้องคือreadonly) ฉันเพิ่งจะแก้ไข แต่ฉันรู้สึกว่าความคิดเห็นจะให้บริการเพื่ออธิบายว่าทำไมดีกว่า (สำหรับผู้อ่านในอนาคต) หากคุณยังไม่ได้รับคำตอบนี้ฉันจะให้ทางออกที่แน่นอน การเปลี่ยนไม่ได้เป็นความคิดที่ดี แต่ก็ไม่ง่ายอย่างที่คิด
Pharap

2
คุณสมบัติทั่วไปไม่ควรโยนข้อยกเว้น; แทนที่จะเป็นตัวตั้งค่าที่พ่น ArgumentNullException ให้ระบุวิธี SetClient แทน มีคนโพสต์ลิงก์ไปยังคำตอบเกี่ยวกับสิ่งนี้ในคำถาม
Andy

5

ตัวเลือกที่สองและสามควรหลีกเลี่ยง - ผู้เรียกไม่ควรตบผู้โทรด้วยข้อยกเว้นที่พวกเขาไม่สามารถควบคุมได้

คุณควรตัดสินใจว่าลูกค้าสามารถเป็นโมฆะได้หรือไม่ ถ้าเป็นเช่นนั้นคุณควรให้วิธีการสำหรับผู้โทรเพื่อตรวจสอบว่ามันเป็นโมฆะก่อนที่จะเข้าถึงมัน (เช่นคุณสมบัติ Bool ClientIsNull)

หากคุณตัดสินใจว่าไคลเอ็นต์ไม่สามารถเป็นโมฆะได้ให้กำหนดพารามิเตอร์ให้กับตัวสร้างและโยนข้อยกเว้นที่นั่นหากมีการส่งผ่านค่าว่าง

ในที่สุดตัวเลือกแรกยังมีกลิ่นรหัส คุณควรให้ลูกค้าจัดการกับคุณสมบัติ ID ของตัวเอง ดูเหมือนว่า overkill จะเขียนรหัสทะเยอทะยานในคลาส container ที่เรียกแค่ getter บนคลาสที่มีอยู่ เพียงแค่ให้ลูกค้าเห็นว่าเป็นคุณสมบัติ (มิฉะนั้นคุณจะต้องทำซ้ำทุกสิ่งที่ลูกค้ามีอยู่แล้ว )

long clientId = room.Client.Id;

หากลูกค้าเป็นโมฆะอย่างน้อยคุณจะต้องรับผิดชอบต่อผู้โทร:

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

1
ฉันคิดว่า"คุณจะทำซ้ำทุกสิ่งที่ลูกค้ามีอยู่แล้ว"ต้องเป็นตัวหนาหรือตัวเอียงอย่างน้อย
Pharap

2

ถ้าคุณสมบัติ null Client เป็นรัฐสนับสนุนพิจารณาใช้NullObject

แต่ส่วนใหญ่แล้วนี่เป็นสถานะพิเศษดังนั้นคุณควรทำให้เป็นไปไม่ได้ (หรือไม่สะดวกมาก) ในการจบด้วย 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;
}

หากไม่รองรับอย่างไรก็ตามอย่าเสียเวลากับการแก้ปัญหาแบบเบคครึ่ง ในกรณีนี้:

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

คุณได้ 'แก้ไข' NullReferenceException ที่นี่ แต่มีค่าใช้จ่ายมาก! สิ่งนี้จะบังคับให้ผู้โทรทั้งหมดRoom.ClientIdตรวจสอบ 0:

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

ข้อผิดพลาดที่แปลกสามารถเกิดขึ้นกับ devs อื่น ๆ (หรือตัวคุณเอง!) ลืมที่จะตรวจสอบค่าตอบแทน "ErrorCode" ในบางช่วงเวลา
เล่นอย่างปลอดภัยและล้มเหลวอย่างรวดเร็ว อนุญาตให้มีการโยน NullReferenceException และ "อย่างสง่างาม" จับที่ใดที่หนึ่งที่สูงกว่าค่าสแต็กการโทรแทนการอนุญาตให้ผู้โทรทำสิ่งต่าง ๆ ได้มากขึ้น ...

เมื่อทราบที่แตกต่างกันถ้าคุณมีความกระตือรือร้นมากที่จะเปิดเผยClientและยังClientIdคิดเกี่ยวกับTellDontAsk หากคุณถามมากเกินไปจากวัตถุแทนที่จะบอกพวกเขาว่าคุณต้องการบรรลุอะไรคุณอาจสิ้นสุดการเชื่อมต่อทุกอย่างเข้าด้วยกันทำให้เปลี่ยนแปลงได้ยากขึ้นในภายหลัง


นั่นNotSupportedExceptionคือการใช้ผิดวัตถุประสงค์คุณควรใช้ArgumentNullExceptionแทน
Pharap

1

ในฐานะของ c # 6.0 ซึ่งวางจำหน่ายแล้วคุณควรทำสิ่งนี้

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; }
}

การเพิ่มคุณสมบัติของClientto Roomเป็นการละเมิด encapsulation และหลักการ DRY ที่ชัดเจน

หากคุณต้องการเข้าถึงIdสิ่งClientที่Roomคุณสามารถทำได้

var clientId = room.Client?.Id;

หมายเหตุการใช้ของNull เงื่อนไข Operator , clientIdจะเป็นlong?ถ้าClientเป็นnull, clientIdจะnullมิฉะนั้นจะมีค่าของclientIdClient.Id


แต่ประเภทของclientidเป็นโมฆะนานแล้ว?
มิเชล

@ มิเชลถ้าห้องต้องมีลูกค้าให้ใส่เช็คว่างใน ctor จากนั้นจะมีทั้งความปลอดภัยและvar clientId = room.Client.Id long
Jodrell
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.