Pull to refresh

Полное сокрытие полей свойствами в C#

Reading time5 min
Views13K
Сперва я подумал, что стоит начать статью с описания основного назначения свойств в языке C#, но потом понял, что с этим можно на самом деле “развернуться” на целую статью. Поэтому, чтобы не затягивать со вступительной частью, я начну сразу с конкретной задачи.

Постановка задачи


Как известно, в подавляющем большинстве случаев свойства применяются, чтобы скрыть private или protected поле класса. То есть свойства в данном случае помогают реализовать инкапсуляцию данных и методов работы с ними. Рассмотрим простой пример.
  1. class Car
  2. {
  3.   const double MINIMAL_SPEED = 0d;
  4.  
  5.   const double MAX_KNOWN_CAR_SPEED = 1229.78d;
  6.  
  7.   private double maxSpeed;
  8.  
  9.   public double MaxSpeed
  10.   {
  11.     get { return maxSpeed; }
  12.     set
  13.     {
  14.       if (value < MINIMAL_SPEED || value > MAX_KNOWN_CAR_SPEED)
  15.         throw new ArgumentOutOfRangeException("MaxSpeed");
  16.       maxSpeed = value;
  17.     }
  18.   }
  19. }
* This source code was highlighted with Source Code Highlighter.

В данном примере определён класс Car, который накладывает ограничение на поле maxSpeed с тем, чтобы оно имело значение лишь в определённом диапазоне. У этого кода есть довольно существенная проблема. К сожалению, свойство MaxSpeed защищает поле maxSpeed только от присвоения некорректной скорости извне класса Car. Другие методы и свойства класса Car могут осуществить присвоение произвольного значения полю maxSpeed. Иногда это нормально, а иногда это опасно. Посмотрите на следующий (опасный) фабричный метод в классе Car:
  1. class Car
  2. {
  3.   // ...
  4.   public static Car CreateRandomCar()
  5.   {
  6.     return new Car()
  7.     {
  8.       maxSpeed = (new Random()).NextDouble() * double.MaxValue,
  9.     };
  10.   }
  11. }
* This source code was highlighted with Source Code Highlighter.

Очевидно, этот метод может создать машину с максимальной скоростью большей, чем значение константы MAX_KNOWN_CAR_SPEED. Таким образом, задача состоит в том, чтобы переменную maxSpeed скрыть как от кода внешнего по отношению к Car, так и от самого класса Car. Пусть даже сам класс осуществляет доступ к этому полю, используя метод. А уже метод обеспечит гарантии корректности присваимового значения. Печально, но заставить всех участников проекта использовать свойство MaxSpeed вместо поля maxSpeed невозможно. Да и сам я замечал, что забываю о такого рода проблемных полях.

Решение


Люди, практикующие тру-ООП программирование скажут, что задача не стоила рассмотрения, потому что решение её простое — инкапсулировать maxSpeed в отдельный класс Speed и всего делов. Однако, на мой взгляд, в части случаев это напоминает стрельбу из пушки по воробьям и заводить отдельный класс на каждое подобное свойство это стиль на любителя. Хотя, я согласен, что идеологически такой подход наиболее чистый.

Мы же с коллегой нашли другое решение. У решения есть свои недостатки и некоторые ограничения. Итак, ниже представлен класс, который позволит легко описывать свойства, инкапсулирующие такие “проблемные” поля:
  1. public class HidingProperty<T>
  2. {
  3.   public delegate T1 Getter<T1>(ref T1 currentValue);
  4.   public delegate void Setter<T2>(ref T2 currentValue, T2 newValue);
  5.  
  6.   private T _storedValue;
  7.   private Getter<T> _getter;
  8.   private Setter<T> _setter;
  9.     
  10.   public HidingProperty(Getter<T> getter, Setter<T> setter)
  11.     : this(default(T), getter, setter) { }
  12.     
  13.   public HidingProperty(T initialValue, Getter<T> getter, Setter<T> setter)
  14.   {
  15.     _storedValue = initialValue;
  16.     _getter = getter;
  17.     _setter = setter;
  18.   }
  19.  
  20.   public void Set(T newValue)
  21.   {
  22.     _setter(ref _storedValue, newValue);
  23.   }
  24.   public T Get()
  25.   {
  26.     return _getter(ref _storedValue);
  27.   }
  28. }
* This source code was highlighted with Source Code Highlighter.

А вот и пример описания проблемного поля и свойства:
  1. private HidingProperty<double> NewMaxSpeed = new HidingProperty<double>(
  2.   (ref double currentValue) => { return currentValue; },
  3.   (ref double currentValue, double newValue) =>
  4.   {
  5.     if (newValue < MINIMAL_SPEED || newValue > MAX_KNOWN_CAR_SPEED)
  6.       throw new ArgumentOutOfRangeException("NewMaxSpeed");
  7.     currentValue = newValue;
  8.   }
  9. );
* This source code was highlighted with Source Code Highlighter.

Работать с новым полем придётся, используя методы Get() и Set():
var currentSpeed = mazda.NewMaxSpeed.Get()
mazda.NewMaxSpeed.Set(currentSpeed + 10d);

Да, коллеги. Это, конечно, не супер сексуальный код. Пример работы с полем напоминает мне програмиирование на Java. Однако, основная задача решена, причём относительно малой кровью. Теперь при обращению к NewMaxSpeed как извне Car, так и внутри него, будет осуществляться одна и та же проверка на вхождения нового значения в заданный диапазон.

Недостатки, ограничения, побочные эффекты


Признаюсь, мне бы хотелось видеть возможность полного сокрытия поля свойством в самом языке. Мой коллега видит себе примерно такой синтаксис для этого дела:
  1. public double Speed
  2. {
  3.   double speed;
  4.   get { // Код геттера }
  5.   set { // Код сеттера }
  6. }
* This source code was highlighted with Source Code Highlighter.

Тот же вариант, что был приведён мной, заставляет работать с полем через противные методы Get, Set, не допуская прямых присвоений. Преодолеть этого мне так и не удалось. Если описать оператор неявного приведения, позволящий писать double x = car.MaxSpeed; довольно просто, то вот реализовать возможность использования car.MaxSpeed = 10d; оказалось невозможно.

Ещё одна гадость, связанная с предложенным решением заключается в невозможности указания разных модификаторов доступа для геттера и сеттера, что, разумеется, является большим минусом.

Вопрос сериализации таких полей/свойств я даже не рассматривал. Полагаю, тут могут возникнуть сложности. Хотя не исключаю, что простого “навешивание” атрибута Serializable на класс HidingProperty может оказаться достаточно.

Выводы


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

Спасибо за внимание! Буду признателен за критику решения и новые мысли.
Tags:
Hubs:
Total votes 11: ↑6 and ↓5+1
Comments37

Articles