Hola compañeros:
Estoy retomando la Programación Orientada a Objetos, y conociendo ya las bases y demás, me surje una duda a la hora de encarar un proyecto.
Imaginemos que tengo una empresa con distintos puestos, cada uno de los cuales trabaja con unas referencias, que se almacenan en unas estanterías, y cada puesto cuenta con sus propios empleados.
La cosa es, si yo cargo un objeto del puesto 1, ¿debería cargar todos los objetos relacionados?. Si mi clase Puesto tiene un campo que es una lista de objetos tipo Referencias, ¿al cargar el puesto 1 debería cargar en esa lista todas las Referencias de ese puesto?. Y lo mismo para el resto de cosas, si la clase Puesto tiene un campo que es una lista de trabajadores... ¿Tendría que cargar la lista de trabajadores, cargando a su vez los posibles objetos relacionados de la clase Trabajador?. Direcciones por ejemplo.
No sé si es una manera correcta de trabajar, o es mejor cargar los Puestos y según necesite ir cargando y descargando las listas. Si quisiera cargar todos los puestos, tendría en memoría muchísimos datos, y no sé si eso está bien.
Gracias.
No importa lo complejo de un sistema, la cuestión es que solo se 'carga' lo que se necesita usar. Por ello el diseño debe ser eficiente.
Las dependencias deben ser realistas... si es objetiva y realmente dependiente, se hace dependiente, si no, no.
Una rueda tiene atributos, como numero de radios, diámetro, ancho, número de tornillos, marca, etc... luego es razonable que todos esos atributos sean propiedades de las ruedas. Luego la cargar una rueda (crearla, referenciarla) todos sus atributos estarán disponibles. Y todo vehículo tiene una cierta cantidad de ruedas, luego las ruedas será algún tipo de colección, que tienen todos los vehículos, pero que cada implementación de vehículo tiene un valor diferente. Sin embargo si cambias una rueda, deberá del mismo tamaño que el resto, esto es debe encajar... así que debiera haber constructores específicos para cada tipo de vehículo.
Piensa con cosas cotidianas, manejables que puedas fácilmente entender el porqué de ello.
Un simple ejemplo:
Interfaz Vehículo
atributo clase Motor
atributo coleccion Ruedas
atributo coleccion Puertas
atributo Motorizado
atributo TienePuertas
atributo Velocidad
atributo EstaEnMarcha
atributo NumeroRuedas
atributo VelocidadMaxima
atributo Color
metodo Acelerar
metodo Frenar
metodo Parar
metodo Arrancar
Fin interfaz
Una interfaz (es una clase que) define una plantilla básica y que luego cada implementación puede particularizar.
Clase Bicicleta Implementa Vehículo
atributo clase Motor
Read = Nada
Mensaje "El vehículo bicicleta no tiene motor"
fin atributo
atributo coleccion Ruedas
Read(i)
Si ((i=0) ó (i=1)) luego
Devolver Ruedas(i)
Sino
Mensaje "El vehículo bicicleta solo tiene 2 ruedas, no puede proporcionarse la rueda" + i.Tostring
fin si
fin Read
fin atributo
atributo coleccion Puertas
Read = Nada
Mensaje "El vehículo bicicleta no tiene puertas"
fin atributo
atributo Motorizado
Read = p_Motorizado
fin atributo
atributo TienePuertas
Read = p_TienePuertas
fin atributo
atributo Velocidad
Read = p_Velocidad
fin atributo
atributo EstaEnMarcha
Read = p_EstaEnMarcha // ó: (p_velocidad>0)
fin atributo
atributo NumeroRuedas
Read = p_NumeroRuedas
fin atributo
atributo VelocidadMaxima
Read = p_VelocidadMaxima
fin atributo
atributo Color
Read
devolver p_Color
fin read
Write (c)
p_color = c
fin write
fin atributo
metodo Acelerar
Si (p_velocidad = 0) luego p_EnMarcha = TRUE
p_velocidad +=1
fin metodo
metodo Frenar
Si (p_Velocidad >0) luego
p_Velocidad -=1
Si (p_Velocidad = 0) luego p_EnMarcha = FALSE
fin si
fin metodo
metodo Parar
p_Velocidad = 0
p_EnMarcha = FALSE
fin metodo
metodo Arrancar
Si (p_EnMarcha = FALSE) luego
p_Velocidad = 20 //por ejemplo...
p_EnMarcha = TRUE
fin si
fin metodo
metodo Nueva(color) // Crear
p_VelocidadMaxima = 65
p_NumeroRuedas = 2
p_Motorizado = FALSE
p_TienePuertas = FALSE
p_Color = Color
Ruedas = nueva coleccion
r = nueva Rueda(diametro=650, radios=50, ancho=5, ...etc..)
r.Situacion = Delantera
Ruedas.añadir(r)
r = nueva Rueda(diametro=650, radios=50, ancho=5, ...etc..)
r.Situacion = Trasera
Ruedas.añadir(r)
...etc...
fin metodo
atributo NumeroDePiñones
atributo NumeroDePlatos
fin clase
Como se puede ver algo simple, pero fácil de entender y seguir el ejemplo... tiene varios atributos de solo lectura, y tan solo el color es lectura y escritura. Como es una bicie, no tiene motor ni puertas, además fuera de la interfaz que implementa se han añadido dos atributos aparte, NúmeroDePiñones y NumeroDePlatos... faltaría a la interfaz un método 'cambiarDeMarcha' y añadir un atributo 'NumeroDeMarchas'... la bicileta también tiene...
Como ves cuando se 'carga' un vehículo, con él se carga todo lo necesario, porque forma un 'objeto', todo englobado en la clase, un único objeto engloba a todo, pero puede estar formado por varios objetos, un motor podría ser otro objeto en sí mismo, lo mismo que las ruedas y las puertas... básicamente cualquier cosa que sea más compleja que una mera propiedad. Fíjate como aunque no hemos definido una clase Rueda, al crearla sin embargo le estamos pasando algunos atributos necesarios para crearla, y tras ello, incluso modificamos un atributo (donde está situada la rueda), que no se incluye en el 'constructor'... en cambio al crear el objeto bicicleta, solo se reclama el atributo color, el número de ruedas es prefijado por el diseño propio de la bici, lo mismo sucede con 'motorizado' y 'tienepuertas'. Un diseño mejor sería tener un atributo llamado NumeroDePuertas, en sustitución de 'TienePuertas', ya que 'número identifica cuantas son, y al caso '0', es lo mismo que 'no tiene'. Es decir una primera aproximación se define y luego de repensarlo haces cambios y ajustes...
Como ves no es necesario cargar nada más que lo que respecta al vehículo... pero eso está en su propio diseño. si ahora creas un objeto ciudad, tendrás por un lado objetos calle, objetos edificios y objetos vehículos, incluso objetos garaje.
Cada uno de ellos sería una colección, pero incluso así, una ciudad tendría atributos como 'nombre', 'extension', 'ubicacion', etc... pero es necesario cargar todos los vehículos de la ciudad?... No, tan solo los de la calle actual.
Siendo calles una colección de la clase 'Calle', y poseyendo cada calle una colección vehículos (que circulan por ella), bastaría cargas solo esos. A su vez cada vehículo podría tener un atributo 'calle', para referenciar al objeto calle en el que se encuentra.
Cuando se cambia de calle (otra distinta), se descarga la previa para cargar la nueva...
Intenta diseñar un objeto calle, edificio y ciudad.
Una colección es una lista de objetos que puede ser tanto homogéneos como heterogéneos, el diseño estbalece lo deseable y al caso, suelen abundar las colecciones cuyo contenido son del mismo tipo "Colección de tipo x"... una colección tiene métodos tales como: añadir (al final), eliminar (del final), insertar (en cierta posición), Borrar (de cierta posición), vaciar (por completo), listar (desde x hasta y), enumerar (todos), Devolver (el enésimo objeto en la colección), y algunos más complejos como 'UnirAOtraColeccion, Clonar, Copiar, etc...
y suele tener atributos del tipo: Vacia, Cantidad, Tipo para señalar si la colección está vacía, cuantos objetos contiene y el tipo de objetos que aloja. Incluso puede hacerse uno como actual y tener un atributo IndiceActual, que puede ser leído y cambiado...
Según la necesidad se crea o no, por ejemplo en una bicicleta, la colección ruedas, no precisa un atributo 'indiceActual', además siendo sólo 2 es fácl acceder a una y saber cual de ellas es (trasera, delantera), pero (por ejemplo) en una ciudad saber el nombre de la calle actual seleccionada, si resulta conveniente... ya que por ejemplo podría ser deseable 'conducir' un vehículo a dicho destino.
Así, un método Conducir (a la calle x)... movería un vehívulo hacia dicha calle, haciéndolo más complejo, debería encontrar el trazado desde su ubicación actual hasta el destino, y al llegar allí detenerse y avisar...
Piensa en objetos de la vida real y trata de 'visualizarlo' todo desde esa perspectiva de la manipulación, propiedades que tienen y lo que se puede hacer con ello...
Cita de: Devilkeeper en 8 Enero 2018, 21:35 PMNo sé si es una manera correcta de trabajar, o es mejor cargar los Puestos y según necesite ir cargando y descargando las listas. Si quisiera cargar todos los puestos, tendría en memoría muchísimos datos, y no sé si eso está bien.
Es precisamente por esa necesidad que existe la interfáz
IEnumerable<T> (o
IEnumerable(Of T) en VB.NET) de evaluación vaga y el tipo
Lazy<T> (o
Lazy(Of T) en VB.NET) para la inicialización/instanciación vaga de objetos.
La interfáz
IEnumerable, o colección enumerable, provee lo que se conoce como un enumerador, que es un mecanismo para obtener el elemento actual en la colección enumerable, mover al siguiente elemento, y reiniciar. El enumerador solamente debe preocuparse por saber como obtener el siguiente elemento en la colección enumerable, por lo que no es necesario que la colección entera esté alojada en memoria ni saber cuantos elementos hay en total; este comportamiento o implementación se traduce en una optimización de varios aspectos, empezando por el más obvio: el consumo de memoria.
Practicamente todas las colecciones disponibles en los espacios de nombre de la librería de
.NET Framework implementan la la interfáz
IEnumerable, como los tipos
List,
Dictionary,
Array,
Stack,
Collection y etcétera, pero ojo, eso no quiere decir que todos los tipos de colecciones sean de evaluación vaga, cada tipo tiene una implementación distinta para servir a un propósito específico, así que si quieres asegurarte de beneficiarte de la evaluación vaga entonces usa directamente la interfáz
IEnumerable.
El tipo
Lazy<T>, por lo general lo usarías en escenarios de programación asincrónica y para prevenir la inicialización de una instancia que sea bastante pesada... hasta que realmente necesites acceder/leer ese objeto.
Te recomiendo unas lecturas importantes para comprender los conceptos básicos:
- Lazy Evaluation | Wikipedia (https://en.wikipedia.org/wiki/Lazy_evaluation)
- Eager evaluation | Wikipedia (https://en.wikipedia.org/wiki/Eager_evaluation)
- Lazy Initialization | docs.microsoft.com (https://docs.microsoft.com/en-us/dotnet/framework/performance/lazy-initialization)
- How to: Perform Lazy Initialization of Objects | docs.microsoft.com[/url (https://docs.microsoft.com/en-us/dotnet/framework/performance/how-to-perform-lazy-initialization-of-objects)
CitarLazy initialization of an object means that its creation is deferred until it is first used. (For this topic, the terms lazy initialization and lazy instantiation are synonymous.) Lazy initialization is primarily used to improve performance, avoid wasteful computation, and reduce program memory requirements.
CitarBy initializing objects in a lazy manner, you can avoid having to create them at all if they are never needed, or you can postpone their initialization until they are first accessed
Bien, ahora que ya he explicado que es cada cosa, te mostraré como puedes reproducir las diferencias a través de varios ejemplos (básicos) basados en el escenario o problema que propones...
Primero de todo, para estos ejemplos crearemos un tipo como éste para representar la información básica de un cliente:
Public NotInheritable Class Customer
Public Property Name As String
Public Property Surname As String
Public Property Buffer As Byte()
Public ReadOnly Property InstanceId As Guid
Get
If (Me.instanceIdB = Guid.Empty) Then
Me.instanceIdB = Guid.NewGuid()
End If
Return Me.instanceIdB
End Get
End Property
Private instanceIdB As Guid
Private Sub New()
End Sub
Public Sub New(name As String, surname As String)
Me.Name = name
Me.Surname = surname
Me.Buffer = New Byte(4096) {}
End Sub
Public Overrides Function ToString() As String
Return Me.InstanceId.ToString()
End Function
End Class
...es solo un ejemplo cualquiera, mi intención al escribir este ejemplo ha sido que una instancia del tipo
Customer ocupe bastantes bytes.
Bien, por lo general, podriamos decidir crear una colección genérica de tipo
List<T> (o
List(Of T) en VB.NET) para almacenar varias instancias de la clase
Customer, e iterar la colección para mostrar cualquier tipo de información de cada
Customer, quedando algo parecido a esto:
Imports System
Imports System.Collections.Generic
Imports System.Threading
Public Module Module1
Private customers As List(Of Customer)
Private customerCount As Integer
Private Sub BuildCustomers(ByRef collection As List(Of Customer), ByVal amount As Integer)
If (collection Is Nothing) Then
collection = New List(Of Customer)
ElseIf (collection.Any) Then
collection.Clear()
End If
For i As Integer = 0 To (amount - 1)
collection.Add(New Customer(String.Empty, String.Empty))
Next i
End Sub
Public Sub Main()
' Construimos la lista de clientes.
Module1.BuildCustomers(Module1.customers, 100000)
' En este punto, la instanciación de todos los objetos en la colección
' habrá alcanzado un consumo de memoria sobre los 500 megabytes aprox.
' Mostramos la cantidad de elementos en la lista
' (la propiedad Count debe realizar una iteración completa, así que puede tardar unos segundos).
Console.WriteLine(String.Format("Customers Count: {0}", Module1.customers.Count))
' Iteramos cada elemento en la lista.
For Each c As Customer In Module1.customers
Console.WriteLine(String.Format("Customer {0}; Unique Id: {1}",
Interlocked.Increment(Module1.customerCount), c.ToString()))
Next c
' Ya no necesitamos la lista, así que liberarariamos recursos administrados innecesarios,
' y forzamos una recolección del GarbageCollector para un efecto inmediato.
Module1.customers.Clear()
Module1.customers = Nothing
GC.Collect()
GC.WaitForPendingFinalizers()
GC.WaitForFullGCApproach()
GC.WaitForFullGCComplete()
' En este punto, la carga de objetos en memoria se reduce al máximo posible,
' en mi caso el consumo total es de 80 mb aprox.
Console.WriteLine("Press any key to exit...")
Console.ReadKey(intercept:=True)
Environment.Exit(0)
End Sub
End Module
¿Qué ocurre con esto?, pues que todos los elementos de la colección se inicializan, se asigna un espacio en el bloque de memoria para cada elemento de la colección, y evidentemente si tenemos en cuenta el tamaño en bytes de una única instancia de la classe
Customer pues... 100.000 instancias simultaneas resultan en un gran consumo de memoria...
(https://i.imgur.com/BbrdH2T.png)
Cuando un elemento
Customer ha sido inicializado y ya hemos trabajado con él, no necesitamos que siga albergando espacio en memoria, queremos desechar ese consumo adicional que ya no necesitamos para nada, y aquí es donde
.NET Framework nos ofrece la solución al problema:
IEnumerable<T>.
Este código de aquí abajo es practicamente lo mismo que el anterior con el uso de
List<T>, solo que ha sido adaptado para el uso de
IEnumerable<T> mediante una función iteradora y así demostrar el beneficio que nos interesa obtener...
Imports System
Imports System.Collections.Generic
Imports System.Threading
Public Module Module1
Private customers As IEnumerable(Of Customer)
Private customerCount As Integer
Private Iterator Function BuildCustomers(ByVal amount As Integer) As IEnumerable(Of Customer)
For i As Integer = 0 To amount
Yield New Customer(String.Empty, String.Empty)
Next i
End Function
Public Sub Main()
' Construimos la colección de clientes.
Module1.customers = Module1.BuildCustomers(100000)
' En este punto, el consumo de memoria es mínimo, alcanzando los 12 mb aprox.
' Mostramos la cantidad de elementos en la colección
Console.WriteLine(String.Format("Customers Count: {0}", Module1.customers.Count))
' Iteramos cada elemento en la colección.
For Each c As Customer In Module1.customers
Console.WriteLine(String.Format("Customer {0}; Unique Id: {1}",
Interlocked.Increment(Module1.customerCount), c.ToString()))
Next c
' En este punto, el consumo de memoria es mínimo, sin cambios, puesto que al iterar, los elementos se han evaluado de forma vaga.
Module1.customers = Nothing
Console.WriteLine("Press any key to exit...")
Console.ReadKey(intercept:=True)
Environment.Exit(0)
End Sub
End Module
El beneficio en la optimización del consumo de memoria queda bastante claro:
(https://i.imgur.com/q7CjIa3.png)
Te muestro un ejemplo para el uso del tipo
Lazy:
Imports System
Imports System.Collections.Generic
Imports System.Threading
Public Module Module1
Private lazyCustomers As List(Of Lazy(Of Customer))
Private customerCount As Integer
Private Function BuildCustomers(ByVal amount As Integer) As List(Of Lazy(Of Customer))
Dim collection As New List(Of Lazy(Of Customer))
For i As Integer = 0 To amount
collection.Add(New Lazy(Of Customer)(
Function() As Customer
Return New Customer(String.Empty, String.Empty)
End Function))
Next i
Return collection
End Function
Public Sub Main()
' Construimos la instancia de inicialización vaga con la colección de clientes.
Module1.lazyCustomers = Module1.BuildCustomers(100000)
' En este punto, el consumo de memoria es mínimo, alcanzando los 12 mb aprox,
' puesto que todavía NO hemos inicializado ningún elemento de la colección.
Console.WriteLine(String.Format("Lazy Customers Count: {0}", Module1.lazyCustomers.Count))
For Each lz As Lazy(Of Customer) In Module1.lazyCustomers
Console.WriteLine(String.Format("Customer {0}; Unique Id: {1}",
Interlocked.Increment(Module1.customerCount), lz.Value.ToString()))
Next lz
' Cuando accedemos a la propiedad 'Lazy(Of T).Value' es cuando inicializamos el objecto de inicialización vaga,
' así que al terminar la iteración hemos inicializaco todos los elementos, por lo que en este punto el consumo de memoria rondará los 500 megabytes aprox, como en el primer ejemplo.
' Ya no necesitamos la colección, así que liberarariamos recursos administrados innecesarios,
' y forzamos una recolección del GarbageCollector para un efecto inmediato.
Module1.lazyCustomers.Clear()
Module1.lazyCustomers = Nothing
GC.Collect()
GC.WaitForPendingFinalizers()
GC.WaitForFullGCApproach()
GC.WaitForFullGCComplete()
' En este punto, la carga de objetos en memoria se reduce al máximo posible,
' en mi caso el consumo total es de 20 mb aprox.
Console.WriteLine("Press any key to exit...")
Console.ReadKey(intercept:=True)
Environment.Exit(0)
End Sub
End Module
Por supuesto podemos combinar el tipo
Lazy<T> e
IEnumerable<T>:
Imports System
Imports System.Collections.Generic
Imports System.Threading
Public Module Module1
Private lazyCustomers As IEnumerable(Of Lazy(Of Customer))
Private customerCount As Integer
Private Iterator Function BuildCustomers(ByVal amount As Integer) As IEnumerable(Of Lazy(Of Customer))
For i As Integer = 0 To amount
Yield New Lazy(Of Customer)(
Function() As Customer
Return New Customer(String.Empty, String.Empty)
End Function)
Next i
End Function
Public Sub Main()
' Construimos la instancia de inicialización vaga con la colección de clientes.
Module1.lazyCustomers = Module1.BuildCustomers(100000)
' En este punto, el consumo de memoria es mínimo, alcanzando los 12 mb aprox,
' puesto que todavía NO hemos inicializado ningún elemento de la colección.
Console.WriteLine(String.Format("Lazy Customers Count: {0}", Module1.lazyCustomers.Count))
For Each lz As Lazy(Of Customer) In Module1.lazyCustomers
Console.WriteLine(String.Format("Customer {0}; Unique Id: {1}",
Interlocked.Increment(Module1.customerCount), lz.Value.ToString()))
' Cuando accedemos a la propiedad 'Lazy(Of T).Value' es cuando inicializamos el objecto de inicialización vaga.
Next lz
' Al terminar la iteración, en este punto no hay cambios en el consumo de memoria,
' puesto que hemos utilizado una colección enumerable.
Module1.lazyCustomers = Nothing
Console.WriteLine("Press any key to exit...")
Console.ReadKey(intercept:=True)
Environment.Exit(0)
End Sub
End Module
IEnumerable y
Lazy son dos propuestas para fines distintos, aunque combinables para un mismo fin, y yo solo te he mostado ejemplos básicos de su utilización en escenarios sincrónicos; al final la solución más óptima y adecuada a tu problema siempre dependerá de lo que realmente quieras hacer y como sea realmente necesario hacerlo...
Espero que esto haya servido de ayuda, al menos.
Saludos.
Muchísimas gracias a los dos por vuestras respuestas tan elaboradas.
Aunque en mi caso no voy a cargar ni 100 registros, voy a implementar el Lazy Loading para ir aprendiendo su uso. Ya iré contando que tal, he encontrado otros recursos para seguir documentándome. Muchas veces buscas algo que tiene un nombre concreto, pero como no lo conoces, no encuentras nada de informacion.
Un saludo.