Цель
Очень повторяющийся дизайн, который я хотел бы реализовать на C#, следующий: Класс, который владеет несколькими экземплярами другого класса.
Для ясности возьмем пример, скажем, «автомобиль», имеющий четыре «колеса».
Вот ограничения, которые я хочу выполнить:
Я хочу, чтобы машина показывала свои колеса, а не прятала их. => В классе автомобиля должно быть что-то вроде
public Wheel GetWheel(Left, Rear)
Я хочу, чтобы машина могла каким-то образом обновить свои колеса. Например, в классе автомобиля может быть такой метод:
public void ChangeTires(TireType)
Я не хочу, чтобы кто-то еще мог обновлять колеса. Например, не должно быть возможности запустить что-то вроде:
aCar.GetWheel(Left, Rear).SetTire(someTireType); // Does not compile
Это очень повторяющаяся схема: у сети есть узлы, у стула есть спинка, у пользователя есть учетные данные, у сокета есть IP-адрес, у банковского счета есть владелец...
Я читал форумы и спрашивал коллег, как они это делают. Мне было дано несколько ответов. Вот они.
Мои вопросы:
Попытка решения 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
Кроме того, это не распространяется на случаи, когда сборы не производятся.
Вместо wheel.setTire есть WheelMachine.SetTire(car, location, tire)... В этом случае вы можете использовать внутреннюю логику настройки шины, автомобиля, шины и колеса — все общедоступные.
@RodrigoRodrigues: решение 3. Означает ли это, что все ваши классы используются через общедоступные интерфейсы. Это тяжело каждый день?
@Т.С. Я понимаю и мне нравится идея выделения функции SetTire в отдельный класс. Как запретить классу, который не является WheelMachine, менять шины?
почему вы позволяете кому-либо, кроме колесной машины, менять шину?
Не могли бы вы объяснить, почему интерфейсы только для чтения (Решение 3) каким-то образом вызывают дублирование кода?
Решение 3 — это подход, который я бы выбрал для решения этой проблемы. Вы хотите предоставить доступную только для чтения версию Wheel из вашего автомобиля — сохраните частное свойство как изменяемое колесо и откройте IWheel, в котором есть методы только для чтения.
@AlexeiLevenkov, я только что сделал это в основной части вопроса.





Я не уверен, что понимаю проблему, но попытаюсь обобщить то, что понимаю:
Car надо подержать немного WheelsCar должен иметь 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 объектов, но никакие другие классы не могут.
Вот что я бы сделал: 1) имел бы интерфейс IWheel в качестве возвращаемого значения для Car.getWheel без setTire в интерфейсе. 2) сделать Wheel структурой, реализующей IWheel