Recientemente tuve un problema con la legibilidad de mi código.
Tenía una función que hizo una operación y devolvió una cadena que representa la ID de esta operación para referencia futura (un poco como OpenFile en Windows que devuelve un identificador). El usuario usaría esta ID más tarde para comenzar la operación y monitorear su finalización.
La identificación tenía que ser una cadena aleatoria debido a problemas de interoperabilidad. Esto creó un método que tenía una firma muy poco clara como esta:
public string CreateNewThing()
Esto hace que la intención del tipo de devolución no esté clara. Pensé envolver esta cadena en otro tipo que aclara su significado de esta manera:
public OperationIdentifier CreateNewThing()
El tipo contendrá solo la cadena y se usará siempre que se use esta cadena.
Es obvio que la ventaja de esta forma de operación es una mayor seguridad de tipo y una intención más clara, pero también crea mucho más código y código que no es muy idiomático. Por un lado, me gusta la seguridad adicional, pero también crea mucho desorden.
¿Crees que es una buena práctica incluir tipos simples en una clase por razones de seguridad?
fuente
Respuestas:
Las primitivas, como
string
oint
, no tienen significado en un dominio comercial. Una consecuencia directa de esto es que puede usar una URL por error cuando se espera un ID de producto, o usar la cantidad cuando espera el precio .Esta es también la razón por la cual el desafío Object Calisthenics presenta el primitivo envoltorio como una de sus reglas:
El mismo documento explica que hay un beneficio adicional:
De hecho, cuando se usan primitivas, generalmente es extremadamente difícil rastrear la ubicación exacta del código relacionado con esos tipos, lo que a menudo conduce a una duplicación severa del código . Si hay
Price: Money
clase, es natural encontrar el rango comprobando dentro. Si, en cambio, se utiliza unint
(peor, adouble
) para almacenar los precios de los productos, ¿quién debería validar el rango? ¿El producto? El descuento? ¿El carro?Finalmente, un tercer beneficio no mencionado en el documento es la capacidad de cambiar relativamente fácilmente el tipo subyacente. Si hoy
ProductId
tiene mishort
tipo subyacente y luego necesito usarloint
en su lugar, es probable que el código que cambie no abarque toda la base de código.El inconveniente, y el mismo argumento se aplica a todas las reglas del ejercicio de Calistenia de Objetos, es que si rápidamente se vuelve demasiado abrumador para crear una clase para todo . Si
Product
contieneProductPrice
qué hereda dePositivePrice
qué hereda de quién aPrice
su vez heredaMoney
, esto no es una arquitectura limpia, sino más bien un desastre completo donde, para encontrar una sola cosa, un mantenedor debe abrir algunas docenas de archivos cada vez.Otro punto a considerar es el costo (en términos de líneas de código) de crear clases adicionales. Si los envoltorios son inmutables (como deberían ser, por lo general), significa que, si tomamos C #, debe tener, al menos en el envoltorio:
ToString()
,Equals
y unaGetHashCode
anulación (también una gran cantidad de LOC).y eventualmente, cuando sea relevante:
==
y!=
operadores,Cien LOC para un envoltorio simple lo hacen bastante prohibitivo, por lo que también puede estar completamente seguro de la rentabilidad a largo plazo de dicho envoltorio. La noción de alcance explicada por Thomas Junk es particularmente relevante aquí. Escribir cien LOC para representar un
ProductId
usado en toda su base de código parece bastante útil. Escribir una clase de este tamaño para un fragmento de código que forma tres líneas dentro de un solo método es mucho más cuestionable.Conclusión:
Envuelva primitivas en clases que tengan un significado en un dominio comercial de la aplicación cuando (1) ayuda a reducir errores, (2) reduce el riesgo de duplicación de código o (3) ayuda a cambiar el tipo subyacente más adelante.
No envuelva automáticamente cada primitivo que encuentre en su código: hay muchos casos en los que usar
string
oint
está perfectamente bien.En la práctica,
public string CreateNewThing()
devolver una instancia deThingId
clase en lugar destring
podría ayudar, pero también puede:Devuelve una instancia de
Id<string>
clase, que es un objeto de tipo genérico que indica que el tipo subyacente es una cadena. Tiene el beneficio de la legibilidad, sin el inconveniente de tener que mantener muchos tipos.Devuelve una instancia de
Thing
clase. Si el usuario solo necesita la ID, esto se puede hacer fácilmente con:fuente
Usaría el alcance como regla general: cuanto más estrecho sea el alcance de generar y consumir
values
, menos probable es que tenga que crear un objeto que represente este valor.Digamos que tiene el siguiente pseudocódigo
entonces el alcance es muy limitado y no tendría sentido convertirlo en un tipo. Pero supongamos que genera este valor en una capa y lo pasa a otra capa (o incluso a otro objeto), entonces tendría mucho sentido crear un tipo para eso.
fuente
doFancyOtherstuff()
en una subrutina, puede pensar que lajob.do(id)
referencia no es lo suficientemente local como para mantenerla como una cadena simple.Los sistemas de tipo estático tienen que ver con la prevención del uso incorrecto de los datos.
Hay ejemplos obvios de tipos que hacen esto:
Hay ejemplos más sutiles.
Podemos sentir la tentación de usar
double
tanto por precio como por duración, o usarstring
tanto para un nombre como para una URL. Pero hacer eso subvierte nuestro increíble sistema de tipos y permite que estos abusos pasen las comprobaciones estáticas del lenguaje.Confundir libra segundos con Newton segundos puede tener malos resultados en tiempo de ejecución .
Esto es particularmente un problema con las cadenas. Con frecuencia se convierten en el "tipo de datos universal".
Estamos acostumbrados principalmente a las interfaces de texto con las computadoras, y a menudo ampliamos estas interfaces humanas (UI) a las interfaces de programación (API). Pensamos en 34.25 como los personajes 34.25 . Pensamos en una fecha como los personajes 05-03-2015 . Pensamos en un UUID como los caracteres 75e945ee-f1e9-11e4-b9b2-1697f925ec7b .
Pero este modelo mental daña la abstracción API.
Del mismo modo, las representaciones textuales no deberían desempeñar ningún papel en el diseño de tipos y API. Cuidado con el
string
! (y otros tipos "primitivos" demasiado genéricos)Los tipos comunican "qué operaciones tienen sentido".
Por ejemplo, una vez trabajé en un cliente para una API REST HTTP. REST, hecho correctamente, utiliza entidades hipermedia, que tienen hipervínculos que apuntan a entidades relacionadas. En este cliente, no solo se escribieron las entidades (por ejemplo, Usuario, Cuenta, Suscripción), también se escribieron los enlaces a esas entidades (UserLink, AccountLink, SubscriptionLink). Los enlaces eran poco más que envoltorios
Uri
, pero los tipos separados hacían imposible intentar usar un AccountLink para buscar un Usuario. Si todo hubiera sido sencilloUri
, o incluso peor,string
estos errores solo se encontrarían en tiempo de ejecución.Del mismo modo, en su situación, tiene datos que se utilizan para un solo propósito: identificar un
Operation
. No debería usarse para otra cosa, y no deberíamos tratar de identificarOperation
s con cadenas aleatorias que hemos inventado. Crear una clase separada agrega legibilidad y seguridad a su código.Por supuesto, todas las cosas buenas se pueden usar en exceso. Considerar
cuánta claridad agrega a su código
con qué frecuencia se usa
Si se usa con frecuencia un "tipo" de datos (en sentido abstracto), para propósitos distintos y entre interfaces de código, es un muy buen candidato para ser una clase separada, a expensas de la verbosidad.
fuente
05-03-2015
significa cuando se interpreta como una cita ?A veces.
Este es uno de esos casos en los que necesita sopesar los problemas que pueden surgir al usar un en
string
lugar de uno más concretoOperationIdentifier
. ¿Cuáles son su gravedad? ¿Cuál es su probabilidad?Entonces debe considerar el costo de usar el otro tipo. ¿Qué tan doloroso es usar? ¿Cuánto trabajo es hacer?
En algunos casos, se ahorrará tiempo y esfuerzo al tener un buen tipo concreto para trabajar. En otros, no valdrá la pena.
En general, creo que este tipo de cosas debería hacerse más de lo que es hoy. Si tiene una entidad que significa algo en su dominio, es bueno tener eso como su propio tipo, ya que es más probable que esa entidad cambie / crezca con el negocio.
fuente
Generalmente estoy de acuerdo en que muchas veces debes crear un tipo para primitivas y cadenas, pero debido a que las respuestas anteriores recomiendan crear un tipo en la mayoría de los casos, enumeraré algunas razones por las cuales / cuándo no:
fuente
short
(por ejemplo) tiene el mismo costo envuelto o no. (?)No, no debe definir tipos (clases) para "todo".
Pero, como dicen otras respuestas, a menudo es útil hacerlo. Debe desarrollar, conscientemente, si es posible, un sentimiento o sensación de demasiada fricción debido a la ausencia de un tipo o clase adecuada, mientras escribe, prueba y mantiene su código. Para mí, el inicio de demasiada fricción es cuando quiero consolidar múltiples valores primitivos como un solo valor o cuando necesito validar los valores (es decir, determinar cuál de todos los valores posibles del tipo primitivo corresponde a valores válidos de 'tipo implícito').
He encontrado consideraciones como lo que has planteado en tu pregunta para ser responsable de demasiado diseño en mi código. He desarrollado el hábito de evitar deliberadamente escribir más código del necesario. Esperemos que esté escribiendo buenas pruebas (automatizadas) para su código; si lo está, puede refactorizar fácilmente su código y agregar tipos o clases si hacerlo proporciona beneficios netos para el desarrollo y mantenimiento continuos de su código .
La respuesta de Telastyn y la respuesta de Thomas no deseado ambos hacen un muy buen punto sobre el contexto y el uso del código en cuestión. Si está utilizando un valor dentro de un solo bloque de código (por ejemplo, método, bucle,
using
bloque), entonces está bien usar un tipo primitivo. Incluso está bien usar un tipo primitivo si está usando un conjunto de valores repetidamente y en muchos otros lugares. Pero cuanto más frecuente y ampliamente esté utilizando un conjunto de valores, y cuanto menos se corresponda con ese conjunto de valores a los valores representados por el tipo primitivo, más debería considerar encapsular los valores en una clase o tipo.fuente
En la superficie, parece que todo lo que necesita hacer es identificar una operación.
Además, usted dice lo que se supone que debe hacer una operación:
Lo dice de una manera como si esto fuera "simplemente cómo se usa la identificación", pero yo diría que estas son propiedades que describen una operación. Eso me parece una definición de tipo, incluso hay un patrón llamado "patrón de comando" que se ajusta muy bien. .
Dice
Creo que esto es muy similar en comparación con lo que quieres hacer con tus operaciones. (Compare las frases que escribí en negrita en ambas citas) En lugar de devolver una cadena, devuelva un identificador para un
Operation
en sentido abstracto, un puntero a un objeto de esa clase en oop, por ejemplo.Sobre el comentario
No, no lo haría. Recuerde que los patrones son muy abstractos , tan abstractos de hecho que son algo meta. Es decir, a menudo abstraen la programación en sí y no un concepto del mundo real. El patrón de comando es una abstracción de una llamada de función. (o método) Como si alguien pulsara el botón de pausa justo después de pasar los valores de los parámetros y justo antes de la ejecución, para reanudarlo más tarde.
Lo siguiente es considerar la opresión, pero la motivación detrás de esto debería ser cierta para cualquier paradigma. Me gusta dar algunas razones por las cuales colocar la lógica en el comando puede considerarse algo malo.
Para recapitular: el patrón de comando le permite ajustar la funcionalidad en un objeto, para ser ejecutado más tarde. En aras de la modularidad (la funcionalidad existe independientemente de que se ejecute a través de un comando o no), la capacidad de prueba (la funcionalidad debe ser comprobable sin comando) y todas esas otras palabras de moda que esencialmente expresan la demanda de escribir un buen código, no pondría la lógica real en el comando.
Debido a que los patrones son abstractos, puede ser difícil encontrar buenas metáforas del mundo real. Aquí hay un intento:
"Hola abuela, ¿podrías presionar el botón de grabación en el televisor a las 12 para no perderme a los Simpson en el canal 1?"
Mi abuela no sabe qué sucede técnicamente cuando presiona ese botón de grabación. La lógica está en otra parte (en la televisión). Y eso es algo bueno. La funcionalidad está encapsulada y la información está oculta del comando, es un usuario de la API, no necesariamente parte de la lógica ... oh cielos, estoy balbuceando palabras de moda nuevamente, mejor termino esta edición ahora.
fuente
Idea detrás de envolver tipos primitivos,
Obviamente, sería muy difícil y no práctico hacerlo en todas partes, pero es importante ajustar los tipos donde sea necesario,
Por ejemplo, si tiene la clase Order,
Las propiedades importantes para buscar un pedido son principalmente OrderId y InvoiceNumber. Y Amount y CurrencyCode están estrechamente relacionados, donde si alguien cambia el CurrencyCode sin cambiar el Importe, el Pedido ya no puede considerarse válido.
Por lo tanto, solo el ajuste de OrderId, InvoiceNumber y la introducción de un compuesto para la moneda tiene sentido en este escenario y probablemente la descripción de ajuste no tiene sentido. Por lo tanto, el resultado preferido podría ser,
Entonces no hay razón para envolver todo, sino cosas que realmente importan.
fuente
Opinión impopular:
¡Usualmente no deberías definir un nuevo tipo!
La definición de un nuevo tipo para envolver una clase primitiva o básica a veces se denomina definición de un pseudotipo. IBM describe esto como una mala práctica aquí (se centran específicamente en el mal uso de genéricos en este caso).
Los pseudo tipos hacen que la funcionalidad de la biblioteca común sea inútil.
Las funciones matemáticas de Java pueden funcionar con todas las primitivas numéricas. Pero si define un nuevo Porcentaje de clase (envolviendo un doble que puede estar en el rango 0 ~ 1), todas estas funciones son inútiles y deben envolverse en clases que (aún peor) necesitan saber sobre la representación interna de la clase de Porcentaje .
Los pseudo tipos se vuelven virales
Al hacer varias bibliotecas, a menudo encontrará que estos pseudotipos se vuelven virales. Si usa la clase de Porcentaje mencionada anteriormente en una biblioteca, deberá convertirla en el límite de la biblioteca (perdiendo toda la seguridad / significado / lógica / otra razón que tuvo para crear ese tipo) o tendrá que hacer estas clases accesible a la otra biblioteca también. Infectar la nueva biblioteca con sus tipos donde un simple doble podría haber sido suficiente.
Quitar mensaje
Siempre y cuando el tipo que ajuste no necesite mucha lógica de negocios, le aconsejaría que no lo ajuste en una pseudo clase. Solo debe ajustar una clase si existen restricciones comerciales serias. En otros casos, nombrar sus variables correctamente debería ser muy útil para transmitir el significado.
Un ejemplo:
A
uint
puede representar perfectamente un UserId, podemos seguir usando los operadores integrados de Java parauint
s (como ==) y no necesitamos lógica de negocios para proteger el 'estado interno' del ID de usuario.fuente
Al igual que con todos los consejos, saber cuándo aplicar las reglas es la habilidad. Si crea sus propios tipos en un lenguaje basado en tipos, obtiene la verificación de tipos. Por lo general, ese será un buen plan.
NamingConvention es clave para la legibilidad. Los dos combinados pueden transmitir la intención claramente.
Pero /// sigue siendo útil.
Entonces sí, diría que crean muchos tipos propios cuando su vida útil está más allá de un límite de clase. Considere también el uso de ambos
Struct
yClass
, no siempre de la clase.fuente
///
significa