Me he estado golpeando la cabeza contra la pared toda la tarde tratando de resolver este problema, así que espero que alguien pueda ayudarme.

Tengo una clase base abstracta, llamada base_model (digamos), que en Fortran2003 se ve así:

type, abstract :: base_model
contains
  procedure(initMe), pass(this), deferred :: init ! constructor
  procedure(delMe), pass(this), deferred :: delete ! destructor
  procedure(solveMe), pass(this), deferred :: solve
end type base_model

Donde, obviamente, los procedimientos abstractos initMe, delMe y solveMe se definen usando un bloque de interfaz abstracto. Luego tengo tres clases derivadas, llamadas model1, model2 y model3 (digamos):

type, extends(base_model) :: model1
  double precision :: x,y
contains
  procedure :: init => init_model1
  procedure :: delete => delete_model1
  procedure :: solve => solve_model1
end type model1

type, extends(base_model) :: model2
contains
  procedure :: init => init_model2
  procedure :: delete => delete_model2
  procedure :: solve => solve_model2
end type model2

type, extends(base_model) :: model3
contains
  procedure :: init => init_model3
  procedure :: delete => delete_model3
  procedure :: solve => solve_model3
end type model3

Entonces tengo un objeto "controlador", llamado control (digamos), que extiende un resumen base_control:

type, abstract :: base_control
  class(base_model), allocatable :: m1
  class(base_model), allocatable :: m2
  class(base_model), allocatable :: m3
contains
  procedure(initMe), pass(self), deferred :: init
  procedure(delMe), pass(self), deferred :: delete
  procedure(runMe), pass(self), deferred :: run
end type base_control

type, extends(base_control) :: control
contains
  procedure :: init => init_control
  procedure :: delete => delete_control
  procedure :: run => run_control
end type control

Los objetos m1, m2 y m3 pueden asignarse a cualquiera de los modelos: model1, model2 o model3, y están "resueltos "en cualquier orden en particular dependiendo de qué" control "solicite el usuario.

Los tres objetos asignables (m1, m2 y m3) necesitan pasar datos entre ellos. Dado que son miembros de un objeto "controlador", puedo definir un "captador" para cada modelo que luego pasa los datos requeridos a cada modelo. Sin embargo, los modelos específicos no se conocen en el momento de la compilación y, por lo tanto, el objeto "control" no sabe qué datos obtener y, de hecho, ¡los modelos no saben qué datos recibir!

Por ejemplo, si yo allocate(model1::m1) (es decir, asigno m1 para que sea del tipo model1) entonces contendrá dos bits de datos double precision :: x,y. Entonces, si m2 se asigna para ser de tipo model2 (allocate(model2::m2)), podría requerir x pero si se asigna para ser de tipo model3 ({ {X8}}), entonces puede requerir y de m1. Por lo tanto, dado que el objeto "controlador" no puede saber qué tipo m2 está asignado, ¿cómo puede obtener los datos necesarios de m1 para pasar a m2?

Una complicación adicional es que las interacciones entre los modelos son, en general, circulares. Es decir, m1 requiere datos de m2, m2 requiere datos de m1 y así sucesivamente. Además, los datos requeridos son en general no solo específicos de los modelos, sino también variables tanto en tipo como en cantidad.

Desafortunadamente, los datos x y y no son miembros de base_model y, por lo tanto, pasar m1 a m2 como argumento tampoco funcionaría.

Entonces tengo las siguientes preguntas:

  1. ¿Existe una mejor manera de diseñar estos objetos para que pueda pasar datos entre ellos fácilmente? Mirando a su alrededor aquí, ha habido algunas sugerencias de que lo mejor que se puede hacer es rediseñar los objetos para que la interacción entre ellos no sea circular. Sin embargo, ¡eso es algo necesario aquí!

  2. ¿Tengo que escribir un "captador" para cada dato que podría compartir entre los objetos? Eso parece mucha codificación (tengo muchos datos que podrían compartirse). Sin embargo, eso también parece bastante complicado porque el "captador" (específico de un dato) también tendría que satisfacer una interfaz abstracta.

En lenguajes de nivel superior como Python, esto sería fácil, ya que simplemente podríamos crear un nuevo tipo de datos como un compuesto de los modelos, pero eso no es posible, hasta donde yo sé, en Fortran.

Gracias por adelantado. Cualquier ayuda es muy apreciada.

Editar: después de la discusión con francescalus a continuación, select type es una opción. De hecho, en el ejemplo simple que se dio arriba, select type sería una buena opción. Sin embargo, en mi código real, esto daría como resultado select type grandes y anidados, por lo que, si hay una manera de hacerlo sin usar select type, lo preferiría. Gracias a francescalus por señalar mi error con respecto a select type.

7
crispyninja 22 feb. 2018 a las 20:21

2 respuestas

La mejor respuesta

Para responder a sus dos preguntas:

¿Existe una forma mejor de diseñar?

No sé muy bien por qué tiene tantas restricciones en su diseño, pero en resumen, sí. Puede utilizar un administrador de contexto para sus modelos. Le recomiendo que consulte esta respuesta: patrón de clase de contexto

¿Tienes que escribir un método getter en cada modelo?

No exactamente, si usa una estrategia de contexto en este tema específico, lo único que necesita implementar en cada modelo es un método de establecimiento que compartirá los datos entre los modelos.

Implementé una solución funcional para este escenario en Python. El código habla más fuerte que las palabras. He evitado usar características especiales de Python para brindarle una comprensión clara de cómo usar un contexto en este caso.

from abc import ABC, abstractmethod
import random

class BaseModel(ABC):
    def __init__(self, ctx):
        super().__init__()
        self.ctx = ctx
        print("BaseModel initializing with context id:", ctx.getId())

    @abstractmethod
    def solveMe():
        pass

class BaseControl(object):
    # m1 - m3 could be replaced here with *args
    def __init__(self, m1, m2, m3):
        super().__init__()
        self.models = [m1, m2, m3]


class Control(BaseControl):
    def __init__(self, m1, m2, m3):
        super().__init__(m1, m2, m3)

    def run(self):
        print("Now Solving..")
        for m in self.models:
            print("Class: {} reports value: {}".format(type(m).__name__, m.solveMe()))


class Model1(BaseModel):
    def __init__(self, x, y, ctx):
        super().__init__(ctx)
        self.x = x
        self.y = y
        ctx.setVal("x", x)
        ctx.setVal("y", y)

    def solveMe(self):
        return self.x * self.y

class Model2(BaseModel):
    def __init__(self, z, ctx):
        super().__init__(ctx)
        self.z = z
        ctx.setVal("z", z)

    def solveMe(self):
        return self.z * self.ctx.getVal("x")

class Model3(BaseModel):
    def __init__(self, z, ctx):
        super().__init__(ctx)
        self.z = z
        ctx.setVal("z", z)

    def solveMe(self):
        return self.z * self.ctx.getVal("y")

class Context(object):
    def __init__(self):
        self.modelData = {}
        self.ctxId = random.getrandbits(32)

    def getVal(self, key):
        return self.modelData[key]

    def setVal(self, key, val):
        self.modelData[key] = val

    def getId(self):
        return self.ctxId


ctx = Context()

m1 = Model1(1,2, ctx)
m2 = Model2(4, ctx)
m3 = Model3(6, ctx)

# note that the order in the arguments to control defines behavior
control = Control(m1, m2, m3)
control.run()

Salida

python context.py
BaseModel initializing with context id: 1236512420
BaseModel initializing with context id: 1236512420
BaseModel initializing with context id: 1236512420
Now Solving..
Class: Model1 reports value: 2
Class: Model2 reports value: 4
Class: Model3 reports value: 12

Explicación

En resumen, creamos una clase de contexto que tiene un diccionario que se puede compartir entre los diferentes modelos. Esta implementación es muy específica para los tipos de datos primitivos que proporcionó (es decir, x, y, z). Si necesita calcular los datos antes de que se compartan entre los modelos, aún puede usar este patrón reemplazando la devolución de solveMe() con una promesa diferida.

3
Josue Alexander Ibarra 5 mar. 2018 a las 07:37

FWIW, a continuación, se muestra un intento similar de acceder a campos de otros objetos basados en clave / valores (*). Para simplificar, el programa principal obtiene un número entero de un objeto child1_t y establece un valor complejo en un objeto child2_t (ambos son tipos extendidos de parent_t).

Parent.f90:

module parent_m
    implicit none

    type, abstract :: parent_t   !(abstract is optional)
    contains
        procedure :: set
        procedure :: get
        procedure :: show
    endtype

    type composite_t
        class(parent_t), allocatable :: pa, pb
    endtype

contains
    subroutine set( this, key, val )  ! key-based setter
        class(parent_t), intent(inout) :: this
        character(*),   intent(in)     :: key
        integer,        intent(in)     :: val
    endsubroutine

    subroutine get( this, key, val )  ! key-based getter
        class(parent_t), intent(in)  :: this
        character(*),    intent(in)  :: key
        integer,         intent(out) :: val
    endsubroutine

    subroutine show( this )   ! print contents
        class(parent_t), intent(in) :: this
    endsubroutine
end module

Child.f90:

module child_m
    use parent_m, only: parent_t
    implicit none

    type, extends(parent_t) :: child1_t
        integer :: n1 = 777   ! some property
    contains
        procedure :: get  => child1_get
        procedure :: show => child1_show
    endtype

    type, extends(parent_t) :: child2_t
        complex :: c2 = ( 0.0, 0.0 )   ! another property
    contains
        procedure :: set  => child2_set
        procedure :: show => child2_show
    endtype

contains

    subroutine child1_get( this, key, val )
        class(child1_t), intent(in)  :: this
        character(*),    intent(in)  :: key
        integer,         intent(out) :: val

        select case( key )
            case ( "num", "n1" ) ; val = this % n1  ! get n1
            case default ; stop "invalid key"
        end select
    end subroutine

    subroutine child1_show( this )
        class(child1_t), intent(in) :: this
        print *, "[child1] ", this % n1
    endsubroutine

    subroutine child2_set( this, key, val )
        class(child2_t), intent(inout) :: this
        character(*),    intent(in)    :: key
        integer,         intent(in)    :: val

        select case( key )
            case ( "coeff", "c2" ) ; this % c2 = cmplx( real(val), 0.0 )  ! set c2
            case default ; stop "invalid key"
        end select
    end subroutine

    subroutine child2_show( this )
        class(child2_t), intent(in) :: this
        print *, "[child2] ", this % c2
    endsubroutine

end module

Main.f90:

program main
    use parent_m, only: composite_t
    use child_m,  only: child1_t, child2_t
    implicit none
    type(composite_t) :: c
    integer itmp

    allocate( child1_t :: c % pa )
    allocate( child2_t :: c % pb )

    print *, "initial state:"
    call c % pa % show()
    call c % pb % show()

    call c % pa % get( "num",  itmp )   ! get an integer value from pa
    call c % pb % set( "coeff", itmp )  ! set a complex value to pb

    print *, "modified state:"
    call c % pa % show()
    call c % pb % show()
end

Compilar y resultados:

 $ gfortran parent.f90 child.f90 main.f90

 initial state:
 [child1]          777
 [child2]              (0.00000000,0.00000000)
 modified state:
 [child1]          777
 [child2]              (777.000000,0.00000000)

Aunque el código anterior trata sobre la "transferencia de datos" solo para enteros y complejos, se pueden agregar otro tipo de datos de manera similar en la construcción select case (sin agregar nuevos métodos getter / setter para cada uno). Además, si es necesario, podríamos sobrecargar get() y set() para diferentes tipos de value (por ejemplo, set_int() y set_real()) a través de generic palabra clave ( en el tipo parent_t) y anótelos en los tipos extendidos. Lo mismo también es válido para value de tipo de matriz, tal vez ...

Si la transferencia (copia) de datos entre tipos extendidos es costosa (p. Ej., Matrices grandes), supongo que el captador podría devolverles punteros para que childX_t puedan comunicarse de una manera estrechamente acoplada (sin conocer su implementación ).

(Pero supongo que puede haber un enfoque más simple que hacer algo como lo anterior, incluyendo el punto 1 en la pregunta (por ejemplo, rediseñar el programa en sí). diccionarios (como en la otra respuesta) me parece más atractiva).

2
roygvib 5 mar. 2018 a las 10:01