Search code examples
oopmodulefortran

OOP Fortran : How to create a factory with a variable number of arguments in the initialisation routine?


A suggested in the answer of Federico Perini(https://stackoverflow.com/a/72998466/7462275), I tried to write a factory to hide away all of the complexity in a separate module.
But, I do not manage to put in the module the init routine because the number of arguments depends of the object type (line, rectangle or box). This is the code that I wrote but the I still have a select type in the main. And, I did not manage to put it in the module.

module myModule
    implicit none
    
    type CFactory
        class(shape_t), pointer :: shape_type
    contains
        procedure :: create_shape
    end type CFactory

    type, abstract :: shape_t
    contains
        ! Put init in a generic interface
        procedure(abstract_init), nopass, deferred, private  :: shared_init
        generic :: init => shared_init

        procedure(abstract_print), deferred :: print_size
    end type shape_t

    abstract interface
        subroutine abstract_init()
        end subroutine abstract_init

        subroutine abstract_print(this)
            import shape_t
            class(shape_t), intent(in) :: this
        end subroutine abstract_print
    end interface

    type, extends(shape_t) :: line_t
        real :: length
    contains
        procedure, nopass, private :: shared_init => init_line
        procedure :: init_line_1d
        generic   :: init => init_line_1d
        procedure :: print_size => print_linesize
    end type line_t

     type, extends(shape_t) :: rectangle_t
        real :: length,width
    contains
        procedure, nopass, private :: shared_init => init_rectangle
        procedure :: init_rectangle_2d
        generic   :: init => init_rectangle_2d
        procedure :: print_size => print_rectanglesize
    end type rectangle_t

    type, extends(shape_t) :: box_t
        real :: length,width,height
    contains
        procedure, nopass, private :: shared_init => init_box
        procedure :: init_box_3d
        generic   :: init => init_box_3d
        procedure :: print_size => print_boxsize
    end type box_t

contains
    subroutine create_shape(this,choice)
        class(CFactory), intent(inout) :: this
        integer, intent(in) :: choice
        
        select case (choice)
            case(1)
                allocate(line_t::this%shape_type)
            case(2)
                allocate(rectangle_t::this%shape_type)
            case(3)
                allocate(box_t::this%shape_type)
            case default
                print *,'not permitted'
                stop
        end select
        
    end subroutine create_shape
    
    subroutine init_line_1d(this,length1)
        class(line_t), intent(inout) :: this
        real, intent(in) :: length1
        this%length = length1
    end subroutine init_line_1d
    subroutine print_linesize(this)
        class(line_t), intent(in) :: this
        print*,'Line size',this%length,'meter'
    end subroutine print_linesize

    subroutine init_rectangle_2d(this,length1,length2)
        class(rectangle_t), intent(inout) :: this
        real, intent(in) :: length1,length2
        this%length = length1
        this%width = length2
    end subroutine init_rectangle_2d   
    subroutine print_rectanglesize(this)
        class(rectangle_t), intent(in) :: this
        print*,'Rectangle area',this%length*this%width,'meter^2'
    end subroutine print_rectanglesize

    subroutine init_box_3d(this,length1,length2,length3)
        class(box_t), intent(inout) :: this
        real, intent(in) :: length1,length2,length3
        this%length = length1
        this%width = length2
        this%height = length3
    end subroutine init_box_3d
    subroutine print_boxsize(this)
        class(box_t), intent(in) :: this
        print*,'Box volume',this%length*this%width*this%height,'meter^3'
    end subroutine print_boxsize

    subroutine init_rectangle() ; end subroutine init_rectangle
    subroutine init_line() ; end subroutine init_line
    subroutine init_box() ; end subroutine init_box
    
end module myModule

program main

    use myModule

    implicit none

    type(CFactory) :: factory
    integer :: choice
    
    print *,'choice (1:line, 2:rectangle or 3:box)'
    read(*,'(i1)') choice
    call factory%create_shape(choice)
    
    select type (ptr => factory%shape_type)
        type is (line_t)
            call ptr%init(4.0)
        type is (rectangle_t)
            call ptr%init(4.0,3.0)
        type is (box_t)
            call ptr%init(4.0,3.0,2.0)
    end select
    
    call factory%shape_type%print_size()
    
end program main

I tried many things to have only in the main call factory%shape_type%someinitprocedure with one, or two or three arguments (for line, rectangular or box). Of course, a check will be performed in this subroutine to be sure that the number of arguments agrees with the shape_type.
(n.b. : I am discovering the factory concept, so my strategy is not the good one to hide away the complexity in a module) Thanks for answer


Solution

  • In your example, shape sizes (width, length, depth) are a property of the instantiated object. You're not initializing a generic box or a generic line (that is what a derived type represents) but THAT box, with that width, length, depth (assume they can never change, for now).

    So you want to have them set once and forall in the factory, where you can also do all your nice checks. Think something like:

    module shape_factory
       use shape_types ! where the class definitions are 
       implicit none 
    
       type, public :: shape_creator
    
          ! Configuration variables
          real :: max_width = blabla
          real :: min_depth = etcetc
    
          ! Some stats
          integer :: created_sofar = 0
    
          contains
    
             procedure :: create => init_shape
    
       end type shape_creator
    
       contains
    
       subroutine init_shape(this, the_shape, which_type, width, length, depth, radius)
          class(shape_creator), intent(inout) :: this
          class(shape_t), allocatable, intent(out) :: the_shape
          integer, intent(in) :: which
          real, optional, intent(in) :: width, length, depth, radius
    
          select case (which_type)
             case (1); allocate(box_t :: the_shape)
                       if (.not.present(width)) stop 'width is mandatory for box_t'
                       if (.not.present(depth)) stop 'depth is mandatory for box_t'
                       if (.not.present(length)) stop 'length is mandatory for box_t'
                       the_shape = box_t(width, depth,length)
             case (2); allocate(circle_t :: the_shape)
                       if (.not.present(radius)) stop 'radius is mandatory for circle_t'
                       the_shape = circle_t(radius)
             case default
                 stop 'shape factory: invalid shape type requested '
          end select
    
          ! Keep counter
          this%created_sofar = this%created_sofar + 1
       end subroutine init_shape 
    
          
    end module shape_factory
    
    program create_shapes
       use shape_factory
       use shape_types
    
       type(shape_creator) :: factory
       class(shape_t), allocatable :: shape
    
       call factory%create(shape,2,radius=0.5); call shape%print()
       call factory%create(shape,1,width=1.0); ! Error! not enough arguments
    
       ! etc.
    
    
    end program