How to structure a mid-sized OOP-like project physically and logically - Types.jl, Submodules or "deep Hierarchies"?

Hey there Julians!

I’m still a bloody beginner with Julia, but want to start with a medium size OOP-structured project. (I’m also a CS student, so I’m still learning how to engineer software the right way)

I started writing a prototype of a project(a board game), but I started running in so many issues related to project structure, usage of modules and submodules etc… and tools - I want autocomplete and the linter(VSCode) to work! :smiley:

I haven’t found any good resource in the last 2 weeks, where someone outlines a good strategy in a detailed way. I’ve read many threads, which answered many questions, but still not all.
I’ve picked up all the Julia Packt Books, but I haven’t found something specific regarding this topic. In one book (Best Practices and … Julia Packt - something like this) the author suggested in a very short side note to use “types.jl” file to define all your types.

I’m not sure which way is the best practice (in a OOP-like style/way) in Julia. I think I’ve found kind of 3 ways how I could handle this. If there are other ways you are welcome to share them!

Side Note: I would love to be able to use autocomplete and linter, I think this can’t work at the moment for some strategies - for example splitting the code in files (but not submodules) and include the files in the main package file. I think here the linter and autocompleter wouldn’t work - correct me if I’m wrong!

In the following I’m showing my project in a very simplified way. I’ve written it only with this editor, so probably this code isn’t runnable and has some mistakes, but I think you get the differences and my resulting question out of it.

The main problem is, that some structs contain/use other structs as DataTypes. The Character is on the Node position. The Game Board is built of many Nodes. The GameSystem is moving a Character to a different Node. Even more that’s not included in this example: A character has tickets to move from node to node. Node connections require specific tickets to travel a character from one to the other. Maybe just my software design is completely screwed up? :smiley: (Are these Circular Dependencies?)

Please share any resources you have on this topic - especially best practices.
Or are there any lightweight, easy understandable example projects, where I could learn how to build mid scale projects in a OOP-like way in Julia?

Using only Submodules - OOP like way (Feels wrong, too cumbersome and dangerous for a bigger project)
GameBoard.jl

module GameBoard

using ..GameNode

export Board, z

struct Board
gameNodes::Vector{Node}
end

function z()
...
end

end

GameCharacter.jl

module GameCharacter

using ..GameNode

export Character, y

struct Character
positionOnBoard::Node
end

function y()
...
end

end

GameNode.jl

module GameNode

export Node, x

struct Node
name::String
index::Int64
end

function x()
...
end

end

GameSystem.jl

module GameSystem

include("GameNode.jl")
using .GameNode # Probably not necessary
include("GameBoard.jl")
using .GameBoard
include("GameCharacter.jl")
using .GameCharacter

struct System
board::Board
characters::Vector{Character}
end

function init()
...
end

end

Using a file with all defined types/“Classes” (Feels just not right and not too readable and not enough OOP-like)
Types.jl

module Types

export Node, Character, Board, System

struct Node
name::String
index::Int64
end

struct Character
positionOnBoard::Node
end

struct Board
gameNodes::Vector{Node}
end

struct System
board::Board
characters::Vector{Character}
end

end

GameBoard.jl

module GameBoard

using ..GameNode

export z

function z()
...
end

end

GameCharacter.jl

module GameCharacter

using ..GameNode

export y

function y()
...
end

end

GameNode.jl

module GameNode

export x

function x()
...
end

end

GameSystem.jl

module GameSystem

include("Types.jl")
using .Types

include("GameNode.jl")
using .GameNode # Probably not necessary

include("GameBoard.jl")
using .GameBoard

include("GameCharacter.jl")
using .GameCharacter

function init()
...
end

end

Using a different hierarchy (Feels completely confusing and not applicable for real world use - big projects)
GameBoard.jl

module GameBoard

include("GameNode.jl")
using .GameNode

export Board, z

struct Board
gameNodes::Vector{Node}
end

function z()
...
end

end

GameCharacter.jl

module GameCharacter

using ..GameBoard.GameNode

export Character, y
# any other export for GamenNode needed?

struct Character
positionOnBoard::Node
end

function y()
...
end

end

GameNode.jl

module GameNode

export Node, x

struct Node
name::String
index::Int64
end

function x()
...
end

end

GameSystem.jl

module GameSystem

include("GameBoard.jl")
using .GameBoard
# using .GameBoard.GameNode # Explicit using needed?

include("GameCharacter.jl")
using .GameCharacter

struct System
board::Board
characters::Vector{Character}
end

function init()
...
end

end

Julia developers typically do not structure code to follow classic OOP design patterns. Julia does not have interfaces or property inheritance which most of these patterns rely upon. Instead Julia leverages principals such as dynamic dispatch and composition. See Composition and inheritance: the Julian way. Also, in regards to the code you have so far, while you are free to structure the code however you want and I think the way you have divided up the code into multiple files is fine, but generally I would try to avoid using sub-modules as much as possible. Ideally there would be one main module for your package. Im not sure why the linter would not work with just one module.

Edit: Here are some other threads on this topic

4 Likes

Can you expand on why you would not use submodules? I use submodules kinda of extensively and saw no reason until now to avoid them.

1 Like

Its just my personal preference to try to reduce the complexity of the module hierarchy as much as possible especially for small packages or in the early stages of development. Specifically here is a good example of the excessive use of modules because of the high amount of coupling between these types. This causes a perfusion of using statements, one for almost every type, often with two dots. Of course as the size of your project grows, modules can help reduce complexity by separating concerns.

4 Likes