c# design-patterns encapsulation value-type solid-principles

c# - Patrón para crear un tipo de valor simple y eficiente



design-patterns encapsulation (4)

¿Hay una manera de hacer que MinValue / MaxValue const en lugar de solo lectura?

No. Sin embargo, el BCL tampoco hace esto. Por ejemplo, DateTime.MinValue es static readonly . Su enfoque actual, para MinValue y MaxValue es apropiado.

En cuanto a sus otras dos preguntas: la usabilidad y el patrón en sí.

Personalmente, evitaría las conversiones automáticas (operadores de conversión implícita) para un tipo de "temperatura" como este. Una temperatura no es un valor entero (de hecho, si fuera a hacer esto, diría que debería ser un punto flotante: 93.2 grados C es perfectamente válido). Tratar la temperatura como un número entero, y especialmente tratar cualquier valor entero implícitamente como una temperatura parece inapropiada y una posible causa de errores.

Encuentro que las estructuras con conversión implícita a menudo causan más problemas de usabilidad de los que abordan. Obligando a un usuario a escribir:

Celsius c = new Celcius(41);

Realmente no es mucho más difícil que la conversión implícita de un entero. Sin embargo, es mucho más claro.

Motivación:

Al leer el blog de Mark Seemann en Code Smell: Automatic Property , dice cerca del final:

La conclusión es que las propiedades automáticas rara vez son apropiadas. De hecho, solo son apropiados cuando el tipo de la propiedad es un tipo de valor y todos los valores concebibles están permitidos.

Da int Temperature como un ejemplo de mal olor y sugiere que la mejor solución es el tipo de valor específico de la unidad, como Celsius. Así que decidí intentar escribir un tipo de valor Celsius personalizado que encapsule todos los límites y la lógica de conversión de tipos como un ejercicio para ser más SOLID .

Requerimientos básicos:

  1. Imposible tener un valor inválido
  2. Encapsula operaciones de conversión
  3. Copia eficiente (equivalente a la int su sustitución)
  4. Tan intuitivo de usar como sea posible (intentando la semántica de un int)

Implementación:

[System.Diagnostics.DebuggerDisplay("{m_value}")] public struct Celsius // : IComparable, IFormattable, etc... { private int m_value; public static readonly Celsius MinValue = new Celsius() { m_value = -273 }; // absolute zero public static readonly Celsius MaxValue = new Celsius() { m_value = int.MaxValue }; private Celsius(int temp) { if (temp < Celsius.MinValue) throw new ArgumentOutOfRangeException("temp", "Value cannot be less then Celsius.MinValue (absolute zero)"); if (temp > Celsius.MaxValue) throw new ArgumentOutOfRangeException("temp", "Value cannot be more then Celsius.MaxValue"); m_value = temp; } public static implicit operator Celsius(int temp) { return new Celsius(temp); } public static implicit operator int(Celsius c) { return c.m_value; } // operators for other numeric types... public override string ToString() { return m_value.ToString(); } // override Equals, HashCode, etc... }

Pruebas:

[TestClass] public class TestCelsius { [TestMethod] public void QuickTest() { Celsius c = 41; Celsius c2 = c; int temp = c2; Assert.AreEqual(41, temp); Assert.AreEqual("41", c.ToString()); } [TestMethod] public void OutOfRangeTest() { try { Celsius c = -300; Assert.Fail("Should not be able to assign -300"); } catch (ArgumentOutOfRangeException) { // pass } catch (Exception) { Assert.Fail("Threw wrong exception"); } } }

Preguntas:

  • ¿Hay una manera de hacer que MinValue / MaxValue const en lugar de solo lectura? Observando el BCL, me gusta cómo la definición de metadatos de int establece claramente que MaxValue y MinValue son constantes de tiempo de compilación. ¿Cómo puedo imitar eso? No veo una manera de crear un objeto Celsius sin llamar al constructor o exponer los detalles de la implementación que Celsius almacena un int.
  • ¿Me faltan características de usabilidad?
  • ¿Hay un mejor patrón para crear un tipo de valor de campo único personalizado?

Creo que desde el punto de vista de la usabilidad optaría por un tipo de Temperature lugar de Celsius . Celsius es solo una unidad de medida, mientras que una Temperature representaría una medida real. Entonces tu tipo podría soportar múltiples unidades como Celsius, Fahrenheit y Kelvin. También optaría por decimal como almacenamiento de respaldo.

Algo a lo largo de estas líneas:

public struct Temperature { private decimal m_value; private const decimal CelsiusToKelvinOffset = 273.15m; public static readonly Temperature MinValue = Temperature.FromKelvin(0); public static readonly Temperature MaxValue = Temperature.FromKelvin(Decimal.MaxValue); public decimal Celsius { get { return m_value - CelsiusToKelvinOffset; } } public decimal Kelvin { get { return m_value; } } private Temperature(decimal temp) { if (temp < Temperature.MinValue.Kelvin) throw new ArgumentOutOfRangeException("temp", "Value {0} is less than Temperature.MinValue ({1})", temp, Temperature.MinValue); if (temp > Temperature.MaxValue.Kelvin) throw new ArgumentOutOfRangeException("temp", "Value {0} is greater than Temperature.MaxValue ({1})", temp, Temperature.MaxValue); m_value = temp; } public static Temperature FromKelvin(decimal temp) { return new Temperature(temp); } public static Temperature FromCelsius(decimal temp) { return new Temperature(temp + CelsiusToKelvinOffset); } .... }

Evitaría la conversión implícita ya que Reed afirma que hace las cosas menos obvias. Sin embargo, sobrecargaría a los operadores (<,>, ==, +, -, *, /), ya que en este caso tendría sentido realizar este tipo de operaciones. Y quién sabe, en alguna versión futura de .net podríamos incluso especificar restricciones de operador y finalmente poder escribir estructuras de datos más reutilizables (imagine una clase de estadísticas que puede calcular estadísticas para cualquier tipo que admita +, -, *, /).


Creo que este es un patrón de implementación perfectamente fino para los tipos de valor. He hecho cosas similares en el pasado que han funcionado bien.

Solo una cosa, ya que Celsius es implícitamente convertible a / desde int todos modos, puedes definir los límites de esta manera:

public const int MinValue = -273; public const int MaxValue = int.MaxValue;

Sin embargo, en realidad no hay una diferencia práctica entre static readonly y la const .


DebuggerDisplay es un toque útil. Agregaría la unidad de medidas "{m_value} C" para que pueda ver el tipo inmediatamente.

Dependiendo del uso de destino, es posible que también desee tener un marco de conversión genérico a / desde unidades base además de clases concretas. Es decir, almacenar valores en unidades SI, pero ser capaz de mostrar / editar en función de la cultura como (grados C, km, kg) vs. (grados F, mi, lb).

También puede consultar las unidades de medida F # para obtener ideas adicionales ( http://msdn.microsoft.com/en-us/library/dd233243.aspx ): tenga en cuenta que es una construcción de tiempo de compilación.