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:
¿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í!
¿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
.
2 respuestas
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.
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).
Nuevas preguntas
oop
La programación orientada a objetos es un paradigma de programación que utiliza "objetos": estructuras de datos que consisten en campos y métodos de datos junto con sus interacciones.