Объект C# возвращает внутренние компоненты

Цель

Очень повторяющийся дизайн, который я хотел бы реализовать на C#, следующий: Класс, который владеет несколькими экземплярами другого класса.

Для ясности возьмем пример, скажем, «автомобиль», имеющий четыре «колеса».

Вот ограничения, которые я хочу выполнить:

  • Я хочу, чтобы машина показывала свои колеса, а не прятала их. => В классе автомобиля должно быть что-то вроде

    public Wheel GetWheel(Left, Rear)
    
  • Я хочу, чтобы машина могла каким-то образом обновить свои колеса. Например, в классе автомобиля может быть такой метод:

    public void ChangeTires(TireType)
    
  • Я не хочу, чтобы кто-то еще мог обновлять колеса. Например, не должно быть возможности запустить что-то вроде:

    aCar.GetWheel(Left, Rear).SetTire(someTireType); // Does not compile
    

Это очень повторяющаяся схема: у сети есть узлы, у стула есть спинка, у пользователя есть учетные данные, у сокета есть IP-адрес, у банковского счета есть владелец...

Я читал форумы и спрашивал коллег, как они это делают. Мне было дано несколько ответов. Вот они.

Мои вопросы:

  • Есть ли другой способ достижения этой цели, не указанный выше?
  • Существует ли общий консенсус относительно «хорошего» способа действий?
  • Есть ли где-нибудь документированное размышление на эту тему? Блестящие люди, которые создали C# или широко его используют, вероятно, уже давно решили эту тему.

Попытка решения 1: использование internal

Некоторые советуют ограничить видимость метода SetTire текущей сборкой:

class Wheel
{
   internal void setTire(TireType someTireType);
}

Обновлять его (менять шины) могут только "файлы в той же сборке", что и Колесо.

Но это подразумевает, что Автомобиль и Колесо определены в одной и той же сборке, что в силу транзитивности заставляет все определяться в одной и той же сборке.

Кроме того, недостаточно ограничиться одной и той же сборкой, я хочу ограничиться Car.

Попытка решения 2: вернуть копию

Некоторые люди советуют, чтобы метод GetWheel возвращал копию:

class Car
{
   public Wheel LeftRearWheel;

   public Wheel GetWheel(LeftOrRight, FrontOrRear)
   {
       return LeftRearWheel.DeepCopy();
   }
}

Благодаря этому решению ничто не сообщает клиентскому коду, что колесо является копией. Это означает, что каждый раз, когда клиентский программист записывает метод get, он не знает, может ли он обновить возвращаемый объект или нет.

Кроме того, глубокое копирование всего, что вы используете в своем приложении, может снизить производительность.

Другими словами: следующий код компилируется, но ничего не делает:

aCar.GetWheel(Left, Rear).SetTire(someTireType); // Compiles but does nothing

Попытка решения 3: создать неизменяемое колесо

Некоторые советуют сделать это:

public class ReadonlyWheel
{
    public Tire getTire();
}

internal class Wheel : ReadonlyWheel
{
    internal void setTire(Tire);
}

public class Car
{
    public Wheel LeftRearWheel;

    public ReadonlyWheel GetWheel(LeftOrRight, FrontOrRear)
    {
        return LeftRearWheel;
    }
}

Вариант — объявить интерфейс

public interface IWheel
{
    TireType getTire();
}

internal class Wheel: IWheel
{
    public TireType getTire();
    internal void setTire(Tire);
}

public class Car
{
    public Wheel LeftRearWheel;

    public IWheel(LeftOrRight, FrontOrRear)
    {
        return LeftRearWheel;
    }
}

Это решение действительно достигает цели, но предполагает значительное дублирование кода и дополнительную сложность. Поскольку это очень повторяющийся шаблон, я бы хотел избежать дублирования кода.

[Редактировать]

По просьбе добавляю подробности о недостатках этого решения:

Это решение подразумевает, что для каждого класса, принадлежащего другому (то есть большинства классов), нам необходимо создать интерфейс. Это подразумевает объявление каждого немодифицирующего публичного метода дважды: один раз в интерфейсе, один раз в классе.

[/Редактировать]

Попытка решения 4: все равно

Некоторые люди утверждают, что сам вопрос выходит за рамки «образа мышления C#».

По мнению некоторых, меня не заботит обеспечение того, чтобы мои пользователи не делали «глупых» вещей с моей моделью.

Вариант такой: «если такое происходит, значит, ваша модель плохая». Колесо должно быть либо полностью частным, либо полностью публичным.

Я не могу заставить себя так думать. Возможно, кто-нибудь мог бы указать на любую документацию, объясняющую, как «образ мышления C#» обеспечивает такого рода моделирование.

Попытка решения 5: использование ReadOnlyCollection

Если объект, который я хочу показать, является контейнером, то существует класс с именем ReadOnlyCollection, который, насколько я понимаю, объединяет попытку решения 2 и 3: он делает копию и встраивает ее в интерфейс только для чтения:

class Car
{
    private List<Wheel> myWheels;

    public ReadOnlyCollection<Wheel> GetWheels()
    {
        return myWheels.AsReadOnly();
    }
}

Частично это достигает цели: предупреждает пользователя, что ему не следует пытаться обновить коллекцию. Но это не может предотвратить обновление. Другими словами, следующий код компилируется и ничего не делает.

aCar.GetWheels()[0].SetTire(someTireType); // Compile but does nothing

Кроме того, это не распространяется на случаи, когда сборы не производятся.

Вот что я бы сделал: 1) имел бы интерфейс IWheel в качестве возвращаемого значения для Car.getWheel без setTire в интерфейсе. 2) сделать Wheel структурой, реализующей IWheel

Rodrigo Rodrigues 22.09.2023 19:29

Вместо wheel.setTire есть WheelMachine.SetTire(car, location, tire)... В этом случае вы можете использовать внутреннюю логику настройки шины, автомобиля, шины и колеса — все общедоступные.

T.S. 22.09.2023 19:37

@RodrigoRodrigues: решение 3. Означает ли это, что все ваши классы используются через общедоступные интерфейсы. Это тяжело каждый день?

mgueydan 22.09.2023 19:41

@Т.С. Я понимаю и мне нравится идея выделения функции SetTire в отдельный класс. Как запретить классу, который не является WheelMachine, менять шины?

mgueydan 22.09.2023 19:43

почему вы позволяете кому-либо, кроме колесной машины, менять шину?

T.S. 22.09.2023 19:53

Не могли бы вы объяснить, почему интерфейсы только для чтения (Решение 3) каким-то образом вызывают дублирование кода?

Alexei Levenkov 22.09.2023 20:22

Решение 3 — это подход, который я бы выбрал для решения этой проблемы. Вы хотите предоставить доступную только для чтения версию Wheel из вашего автомобиля — сохраните частное свойство как изменяемое колесо и откройте IWheel, в котором есть методы только для чтения.

E. Moffat 22.09.2023 22:43

@AlexeiLevenkov, я только что сделал это в основной части вопроса.

mgueydan 23.09.2023 23:27
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
8
61
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я не уверен, что понимаю проблему, но попытаюсь обобщить то, что понимаю:

  • Car надо подержать немного Wheels
  • Car должен иметь ChangeTires метод
  • Вызывающие абоненты не должны иметь возможность звонить ChangeTires на Wheel — только через Car.

Если да, то один из способов сделать это — отделить публичный интерфейс от реализации.

Сначала определите общедоступный API, который вы хотите предоставить клиентскому коду:

public interface IWheel
{
    // Put some members here that DON'T include changing tires...
    // Current pressure, size, etc.
}

Теперь создайте класс Car и сделайте реализацию IWheel частным вложенным классом внутри Car:

public sealed class Car
{
    private readonly Wheel leftRearWheel = new Wheel();

    public IWheel LeftRearWheel { get { return leftRearWheel; } }
    // Other wheels here...

    public void ChangeTires(TireType tireType)
    {
        leftRearWheel.ChangeTire(tireType);
        // Other wheels here...
    }

    private sealed class Wheel : IWheel
    {
        public void ChangeTire(TireType tireType)
        {
            // Change the tire
        }
    }
}

Хотя метод вложенного Wheel класса ChangeTire помечен public, он виден только классу Car, поскольку класс Wheel является классом private.

Таким образом, Car может вызывать ChangeTire для всех Wheel объектов, но никакие другие классы не могут.

Другие вопросы по теме