Як реалізувати властивість класу 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ніколи не відбудеться, а значення 0Id все одно позначає помилковий ідентифікатор (така думка автора коду ;-))

2) Ви не можете розраховувати на абонента, який знає, що 0це помилкове значення, Idі коли Clientвластивість завжди має бути встановлено, вам слід кинути exceptionте, getколи Clientвластивість стане недійсною

3) Коли Clientвластивість завжди має бути встановлено, ви просто повернетесь Client.Idі дозвольте коду викинути NullRefвиняток, коли Clientвластивість буде недійсною.

Яке з них найбільш правильне? Або є четверта можливість?


5
Не відповідь, а лише зауваження: ніколи не кидайте винятки з власників нерухомості.
Брендон

3
Для того, щоб зупинитися на точці Брендона: stackoverflow.com/a/1488488/569777
MetaFight

1
Звичайно: Основна ідея ( msdn.microsoft.com/en-us/library / ... , stackoverflow.com/questions/1488472 / ... , серед інших) є те , що властивості повинні зробити якомога менше, бути легкими і повинні представляти чистий , стан, що стоїть перед громадськістю вашого об’єкта, причому індекси є незначним винятком. Також несподівана поведінка бачити винятки у вікні перегляду властивості під час налагодження.
Брендон

1
Вам не слід (потрібно) писати код, який передає особу, яка отримує майно. Це не означає, що вам потрібно приховувати та проковтувати будь-які винятки, які виникають через те, що об’єкт переходить у непідтримуваний стан.
Бен

4
Чому ви не можете просто зробити Room.Client.Id? Чому ви хочете зафіксувати це в Room.ClientId? Якщо його перевірити на клієнта, то чому б не щось на кшталт Room.HasClient {get {return client == null; }}, що пряміше вперед, і все ж дозволяють людям робити звичайний Room.Client.Id коли їм насправді потрібен ідентифікатор?
Джеймс

Відповіді:


25

Це пахне так, як ви повинні обмежити кількість штатів, у яких Roomможе бути ваш клас.

Сам факт, що ви запитуєте про те, що робити, коли Clientце недійсне, - це натяк на те Room, що простір станів занадто великий.

Щоб зробити все просто, я не дозволяв би Clientвластивості будь-якого Roomпримірника ніколи бути нульовим. Це означає, що код всередині Roomможе сміливо вважати, що Clientніколи не буває нульовим.

Якщо з якихось причин у майбутньому Clientстане nullпротистояти прагненню підтримати цю державу. Це зробить вашу складність обслуговування.

Натомість, дозвольте коду вийти з ладу і швидко вийти з ладу. Зрештою, це не підтримуваний стан. Якщо програма переходить у цей стан, ви вже переступили лінію повернення. Єдине розумне, що потрібно зробити, це закрити програму.

Це може статися (цілком закономірно) внаслідок невпорядкованого виключення з нульовою посиланням.


2
Приємно. Мені подобається думка "Щоб все було просто, я б не дозволив власністю Клієнта жодного екземпляра Кімнати ніколи не бути нульовим". Це дійсно краще, ніж ставити під сумнів, що робити, коли це недійсне
Мішель

@Michel, якщо пізніше ви вирішите змінити цю вимогу, ви можете замінити nullзначення на NullObject(а точніше NullClient), яке б потім все ще працювало з вашим існуючим кодом і дозволило вам визначити поведінку для неіснуючого клієнта, якщо це буде необхідно. Питання в тому, чи є справа "немає клієнта" частиною бізнес-логіки чи ні.
null

3
Цілком правильно. Не пишіть жодного символу коду для підтримки непідтримуваного стану. Просто нехай це кине NullRef.
Бен

@Ben Disagree; В керівництві MS чітко зазначено, що особи, які отримують власність, не повинні кидати винятки. І дозволити викидати нульові рефлексиви, коли замість них можна буде кинути більш значущий виняток, це просто лінь.
Енді,

1
@Анді розуміння причини настанови дозволить вам дізнатися, коли вона не застосовується.
Бен

21

Лише кілька міркувань:

a) Чому існує геттер спеціально для ClientId, коли вже існує загальнодоступна програма для самого екземпляра клієнта? Я не розумію, чому інформація про ClientId longповинна бути вирізана з каменю у підписі Кімнати.

б) Щодо другої думки, ви можете ввести константу Invalid_Client_Id.

c) Щодо думки першої та третьої (і це моя головна думка): У кімнаті завжди повинен бути клієнт? Можливо, це просто семантика, але це не звучить правильно. Можливо, було б доречніше мати абсолютно окремі класи для Кімнати та Клієнта та ще один клас, який пов’язує їх між собою. Можливо призначення, бронювання, зайнятість? (Це залежить від того, чим ви насправді займаєтесь.) І для цього класу ви можете застосувати обмеження "повинен мати номер" і "повинен мати клієнта".


5
+1 за "Я не бачу, наприклад, чому інформація про те, що ClientId триває, повинна бути вирізана в камінь у підпис кімнати"
Мішель

Ваша англійська чудово. ;) Щойно прочитавши цю відповідь, я б не знав, що ваша рідна мова - це не англійська, якщо ви цього не сказали.
jpmc26

Бали B & C хороші; RE: a, його метод зручності та те, що в кімнаті є посилання на Клієнта, може бути просто деталізацією впровадження (можна стверджувати, що Клієнт не повинен бути публічно видимим)
Енді,

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?
Матьє Гіндон,

4
Ні. Код програми ніколи не повинен кидати a NullReferenceException, він викидається .Net VM, "коли є спроба скинути нульову посилання на об'єкт" . Вам слід накидати ArgumentNullException, який викидається, "коли нульова посилання (Nothing у Visual Basic) передається методу, який не приймає його як дійсний аргумент".
Фарап

2
@Pharap Я програміст Java, який намагається написати код C #, я зрозумів, що я помиляюся.
durron597

@ durron597 Я повинен був здогадатися, що з використання терміна "остаточний" (у цьому випадку відповідне ключове слово C # є readonly). Я щойно відредагував, але відчув, що коментар послужить кращому поясненню чому (для майбутніх читачів). Якби ви ще не дали такої відповіді, я б дав саме таке рішення. Незмінюваність також є хорошою ідеєю, але це не завжди так просто, як можна сподіватися.
Фарап

2
Властивості, як правило, не повинні кидати винятки; замість сеттера, який викидає ArgumentNullException, надайте замість нього метод SetClient. Хтось опублікував посилання на відповідь щодо цього питання.
Енді,

5

Слід уникати другого та третього варіантів - одержувач не повинен прищурювати абонента, за винятком, над яким він не має контролю.

Ви повинні вирішити, чи може Клієнт коли-небудь бути недійсним. Якщо це так, вам слід надати спосіб, щоб абонент перевірив, чи є він нульовим, перш ніж отримати доступ до нього (наприклад, властивість bool ClientIsNull).

Якщо ви вирішите, що Клієнт ніколи не може бути нульовим, тоді введіть його конструктору необхідний параметр і перекиньте туди виняток, якщо нуль буде передано.

Нарешті, перший варіант також містить кодовий запах. Ви повинні дозволити Клієнту займатися власним ідентифікаційним майном. Здається, надмірне кодування getter у класі контейнерів, що просто викликає getter у класі, що міститься. Просто виставіть Клієнта як властивість (інакше ви в кінцевому підсумку дублюватиме все, що вже пропонує Клієнт ).

long clientId = room.Client.Id;

Якщо Клієнт може бути недійсним, ви, принаймні, відповідаєте за абонента:

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

1
Я думаю, що "ви в кінцевому підсумку дублюватимете все, що клієнт вже пропонує", потрібно писати жирним шрифтом або, принаймні, курсивом.
Фарап

2

Якщо властивість Null Client є підтримуваним станом, розгляньте можливість використання NullObject .

Але, швидше за все, це винятковий стан, тому вам слід унеможливити (або просто не дуже зручно) закінчитись з нулем 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);

Дивні помилки можуть арізуватись з іншими дисками (або самим собою!), Забуваючи перевірити повернення значення "ErrorCode" в якийсь момент часу.
Відіграйте це безпечно, невдало швидко. Дозвольте перекинути NullReferenceException і "витончено" зачепити його десь вище вгору до стека викликів, замість того, щоб дозволити абоненту зіпсувати речі ще більше ...

З іншого боку, якщо ви настільки прагнете викрити, Clientа також ClientIdподумати про TellDontAsk . Якщо ви запитуєте занадто багато предметів, а не кажете їм того, чого ви хочете досягти, ви можете закінчити з'єднання всього разом, а згодом зміни ускладнюються.


Це NotSupportedExceptionнеправильне використання, ви повинні використовувати ArgumentNullExceptionзамість цього.
Фарап

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

Підвищення властивості Clientдо Room- це очевидне порушення інкапсуляції та принципу DRY.

Якщо вам необхідно отримати доступ до Idз Clientз Roomви можете зробити,

var clientId = room.Client?.Id;

Зверніть увагу на використання Null умовного оператора , clientIdбуде long?, якщо Clientє null, clientIdбуде null, в іншому випадку clientIdбуде мати значення Client.Id.


але тип clientidє нульовим довгим тоді?
Мішель

@Michel добре, якщо в кімнаті повинен бути клієнт, покладіть нульовий чек у ctor. Тоді var clientId = room.Client.Idбуде і безпечно, і long.
Джодрелл
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.