Estoy leyendo C # Design Pattern Essentials . Actualmente estoy leyendo sobre el patrón iterador.
Entiendo completamente cómo implementar, pero no entiendo la importancia o veo un caso de uso. En el libro se da un ejemplo donde alguien necesita obtener una lista de objetos. Podrían haber hecho esto exponiendo una propiedad pública, como IList<T>
o una Array
.
El libro escribe
El problema con esto es que la representación interna en ambas clases ha sido expuesta a proyectos externos.
¿Qué es la representación interna? El hecho de que es un array
o IList<T>
? Realmente no entiendo por qué esto es algo malo para el consumidor (el programador lo llama) para saber ...
El libro luego dice que este patrón funciona al exponer su GetEnumerator
función, por lo que podemos llamar GetEnumerator()
y exponer la 'lista' de esta manera.
Supongo que estos patrones tienen un lugar (como todos) en ciertas situaciones, pero no veo dónde y cuándo.
fuente
F
que me da conocimiento (métodos) dea
,b
yc
. Todo está bien, puede haber muchas cosas diferentes pero soloF
para mí. Piense en el método como "restricciones" o cláusulas de un contrato queF
obliga a las clases a hacer. Supongamos que agregamos und
, porque lo necesitamos. Esto agrega una restricción adicional, cada vez que hacemos esto, imponemos más y más en elF
s. Eventualmente (en el peor de los casos), solo una cosa puede ser,F
por lo que bien podríamos no tenerla. YF
limita tanto que solo hay una forma de hacerlo.Respuestas:
El software es un juego de promesas y privilegios. Nunca es una buena idea prometer más de lo que puede cumplir, o más de lo que su colaborador necesita.
Esto se aplica particularmente a los tipos. El punto de escribir una colección iterable es que su usuario puede iterar sobre ella, ni más ni menos. Exponer el tipo concreto
Array
generalmente crea muchas promesas adicionales, por ejemplo, que puede ordenar la colección por una función de su elección, sin mencionar el hecho de que una normalArray
probablemente le permitirá al colaborador cambiar los datos que están almacenados dentro de ella.Incluso si crees que esto es algo bueno ("Si el renderizador nota que falta la nueva opción de exportación, ¡solo puede parchearlo! ¡Genial!"), En general, esto disminuye la coherencia de la base del código, lo que hace que sea más difícil razonar sobre, y hacer que el código sea fácil de razonar es el objetivo principal de la ingeniería de software.
Ahora, si su colaborador necesita acceso a una serie de cosas para garantizar que no se pierda ninguna de ellas, implemente una
Iterable
interfaz y exponga solo los métodos que esta interfaz declara. De esa manera, el próximo año, cuando aparezca una estructura de datos enormemente mejor y más eficiente en su biblioteca estándar, podrá cambiar el código subyacente y beneficiarse de él sin corregir su código de cliente en todas partes . Hay otros beneficios de no prometer más de lo necesario, pero este solo es tan grande que, en la práctica, no se necesitan otros.fuente
Exposing the concrete type Array usually creates many additional promises, e.g. that you can sort the collection by a function of your own choosing...
- Brillante Simplemente no lo pensé. ¡Sí, trae de vuelta una 'iteración' y solo, una iteración!Ocultar la implementación es un principio central de OOP y una buena idea en todos los paradigmas, pero es especialmente importante para los iteradores (o como se los llame en ese lenguaje específico) en los idiomas que admiten la iteración diferida.
El problema con exponer el tipo concreto de iterables, o incluso interfaces como
IList<T>
, no está en los objetos que los exponen, sino en los métodos que los usan. Por ejemplo, supongamos que tiene una función para imprimir una lista deFoo
s:Solo puede usar esa función para imprimir listas de foo, pero solo si implementan
IList<Foo>
Pero, ¿qué pasa si desea imprimir cada elemento indexado de la lista? Deberá crear una nueva lista:
Esto es bastante largo, pero con LINQ podemos hacerlo de una sola vez, lo que en realidad es más legible:
Ahora, ¿te diste cuenta
.ToList()
al final? Esto convierte la consulta diferida en una lista, para que podamos pasarlaPrintFoos
. Esto requiere una asignación de una segunda lista y dos pases en los elementos (uno en la primera lista para crear la segunda lista y otro en la segunda lista para imprimirlo). Además, ¿qué pasa si tenemos esto?¿Qué pasa si
foos
tiene miles de entradas? Tendremos que revisarlos todos y asignar una lista enorme solo para imprimir 6 de ellos.Ingrese Enumerators: la versión de C # de The Iterator Pattern. En lugar de que nuestra función acepte una lista, hacemos que acepte
Enumerable
:Ahora
Print6Foos
puede iterar perezosamente sobre los primeros 6 elementos de la lista, y no necesitamos tocar el resto.No exponer la representación interna es la clave aquí. Cuando
Print6Foos
aceptamos una lista, tuvimos que darle una lista, algo que admite el acceso aleatorio, y por lo tanto tuvimos que asignar una lista, porque la firma no garantiza que solo se repita. Al ocultar la implementación, podemos crear fácilmente unEnumerable
objeto más eficiente que solo admita lo que la función realmente necesita.fuente
Exponer la representación interna casi nunca es una buena idea. No solo hace que el código sea más difícil de razonar, sino que también es más difícil de mantener. Imagine que ha elegido cualquier representación interna, digamos una
IList<T>
, y expuso esta interna. Cualquier persona que use su código puede acceder a la Lista y el código puede confiar en que la representación interna sea una Lista.Por alguna razón, está decidiendo cambiar la representación interna
IAmWhatever<T>
en algún momento posterior. En lugar de simplemente cambiar las partes internas de su clase, tendrá que cambiar cada línea de código y método dependiendo de que la representación interna sea de tipoIList<T>
. Esto es engorroso y propenso a errores en el mejor de los casos, cuando usted es el único que usa la clase, pero puede romper el código de otras personas usando su clase. Si acaba de exponer un conjunto de métodos públicos sin exponer ninguna parte interna, podría haber cambiado la representación interna sin ninguna línea de código fuera de su clase tomando nota, trabajando como si nada hubiera cambiado.Es por eso que la encapsulación es uno de los aspectos más importantes de cualquier diseño de software no trivial.
fuente
Cuanto menos diga, menos tendrá que seguir diciendo.
Cuanto menos pidas, menos te tienen que decir.
Si su código solo se expone
IEnumerable<T>
, lo que solo admiteGetEnumrator()
puede ser reemplazado por cualquier otro código que pueda admitirIEnumerable<T>
. Esto agrega flexibilidad.Si su código solo lo usa
IEnumerable<T>
, puede ser compatible con cualquier código que implementeIEnumerable<T>
. Nuevamente, hay flexibilidad adicional.Todos los linq-to-objects, por ejemplo, solo dependen de
IEnumerable<T>
. Si bien realiza algunas implementaciones particulares de manera rápida,IEnumerable<T>
lo hace de una manera de prueba y uso que siempre puede recurrir al usoGetEnumerator()
y laIEnumerator<T>
implementación que regresa. Esto le da mucha más flexibilidad que si se construyera sobre matrices o listas.fuente
Una frase que me gusta usar refleja el comentario de @CaptainMan: "Está bien que un consumidor conozca los detalles internos. Es malo que un cliente necesite conocer los detalles internos".
Si un cliente tiene que escribir
array
oIList<T>
en su código, cuando esos tipos eran detalles internos, el consumidor debe acertarlos. Si están equivocados, el consumidor puede no compilar, o incluso obtener resultados incorrectos. Esto produce los mismos comportamientos que las otras respuestas de "siempre oculta los detalles de la implementación porque no debe exponerlos", pero comienza a investigar las ventajas de no exponer. Si nunca expone un detalle de implementación, ¡su cliente nunca podrá ponerse en apuros al usarlo!Me gusta pensarlo desde esta perspectiva porque abre el concepto de ocultar la implementación a matices de significado, en lugar de un draconiano "siempre oculta los detalles de su implementación". Hay muchas situaciones en las que exponer la implementación es totalmente válido. Considere el caso de los controladores de dispositivos en tiempo real, donde la capacidad de saltar capas pasadas de abstracción y meter bytes en los lugares correctos puede ser la diferencia entre hacer el tiempo o no. O considere un entorno de desarrollo en el que no tenga tiempo para hacer "buenas API" y necesite usar las cosas de inmediato. A menudo, exponer las API subyacentes en dichos entornos puede ser la diferencia entre hacer una fecha límite o no.
Sin embargo, a medida que deja atrás esos casos especiales y observa los casos más generales, comienza a ser cada vez más importante ocultar la implementación. Exponer detalles de implementación es como una cuerda. Utilizado correctamente, puede hacer todo tipo de cosas con él, como escalar montañas altas. Se usa incorrectamente y puede atarte en una soga. Cuanto menos sepa sobre cómo se usará su producto, más cuidadoso deberá ser.
El caso de estudio que veo una y otra vez es uno en el que un desarrollador crea una API que no tiene mucho cuidado al ocultar los detalles de la implementación. Un segundo desarrollador, usando esa biblioteca, descubre que a la API le faltan algunas características clave que le gustaría. En lugar de escribir una solicitud de función (que podría tener un tiempo de respuesta de meses), deciden abusar de su acceso a los detalles ocultos de implementación (lo que lleva segundos). Esto no es malo en sí mismo, pero a menudo el desarrollador de la API nunca planeó que eso fuera parte de la API. Más tarde, cuando mejoran la API y cambian ese detalle subyacente, descubren que están encadenados a la implementación anterior, porque alguien la usó como parte de la API. La peor versión de esto es cuando alguien usó su API para proporcionar una función centrada en el cliente, y ahora su empresa ' ¡El cheque de pago está vinculado a esta API defectuosa! ¡Simplemente exponiendo los detalles de implementación cuando no sabía lo suficiente sobre sus clientes demostró ser suficiente cuerda para atar un lazo alrededor de su API!
fuente
No necesariamente sabe cuál es la representación interna de sus datos, o puede llegar a ser en el futuro.
Suponga que tiene una fuente de datos que representa como una matriz, puede iterar sobre ella con un ciclo for regular.
Ahora di que quieres refactorizar. Su jefe ha pedido que la fuente de datos sea un objeto de base de datos. Además, le gustaría tener la opción de extraer datos de una API, o un mapa hash, o una lista vinculada, o un tambor magnético giratorio, o un micrófono, o un recuento en tiempo real de virutas de yak encontradas en Mongolia Exterior.
Bueno, si solo tiene uno para el bucle, puede modificar su código fácilmente para acceder al objeto de datos de la forma en que espera que se acceda.
Si tiene 1000 clases intentando acceder al objeto de datos, ahora tiene un gran problema de refactorización.
Un iterador es una forma estándar de acceder a una fuente de datos basada en listas. Es una interfaz común. Usarlo hará que su fuente de datos de código sea independiente.
fuente