Duda carga de objetos POO

Iniciado por Devilkeeper, 8 Enero 2018, 21:35 PM

0 Miembros y 1 Visitante están viendo este tema.

Devilkeeper

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.

Serapis

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...

Eleкtro

#2
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:

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:

Código (vbnet) [Seleccionar]
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:

Código (vbnet) [Seleccionar]
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...






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...

Código (vbnet) [Seleccionar]
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:






Te muestro un ejemplo para el uso del tipo Lazy:

Código (vbnet) [Seleccionar]
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>:

Código (vbnet) [Seleccionar]
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.








Devilkeeper

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.