I'm making an open-source voxel engine, and there's an architectural problem that I would like an answer to. I have come up with 2 different solutions, and would like your opinions on:
When I'm creating a new class for a feature that uses the voxel engine, should the new class only refer to a big re-director class called VoxelWorld
that redirects the calls to whatever component is responsible for it (solution 1), OR should the new class directly refer to only the classes it actually needs. Simply put, should the new class refer to the entire voxel engine, or just the parts it needs to?
This may be similar to a monolithic vs microservice problem?
Solution 1: All interactions go through a single entry point (I shall call it the VoxelWorld
class). It's a single class, which would redirect all calls to whichever component is responsible for it. Examples of those calls would be for example VoxelWorld.GetVoxelData(position)
or VoxelWorld.UnloadChunk(position)
. These calls would be coming from wherever, maybe a custom class made by the user such as a TerrainDeformer
.
The TerrainDeformer
would only have a reference to VoxelWorld
, and nothing else (except for all the parameters it needs to deform the terrain, such as the deformation range). The TerrainDeformer
would simply call VoxelWorld.EditTerrain(listOfModifications)
, and that's it. The VoxelWorld
would be responsible for redirecting the EditTerrain
call to whichever class is responsible for editing the terrain, for example, a VoxelWorldEditor
.
The VoxelWorld.UnloadChunk
call would be redirected to ChunkManager
, which is responsible for managing chunks. Here's a picture to better explain it:
The public VoxelWorld
would be the only public class in assembly A. Everything else is internal, meaning they are only visible for other classes inside assembly A. The assembly A is also like a black box, it is mostly (except for the VoxelWorld
) hidden from the outside project. It just works, that's not a good ideology, but sometimes a necessary one. Everything in assembly B is easily extensible and public to the user. It contains the code the user would be messing with.
VoxelWorld
would essentially by only a redirector of calls. It would have absolutely no logic, only redirections. A function in VoxelWorld.cs
could look like this:
public void LoadChunk(int x, int y, int z){
ChunkManager.LoadChunk(x, y, z);
}
Solution 2: Everything that the user wants to add refers to only the classes it really needs. Now, forget all that public/internal/redirecting stuff from solution 1, but keep in mind the different classes. This is kind of like the interface segregation principle, but just without the interface part.
The interface-segregation principle (ISP) states that no client [e.g.
TerrainDeformer
] should be forced to depend on methods it does not use [e.g.LoadChunk
]
In solution 1, everything depends on everything. That's the blessing and the curse of solution 1. In solution 2, that's not a problem because a class only depends on what it actually needs.
Solution 2 is more traditional and maybe cleaner -> easier to maintain. I feel like I like solution 2 more than solution 1, but both seem good. Here's the main benefits and disadvantages:
Solution 1:
+Easy to use API (VoxelWorld.DoWhatever()
)
-It contains a black box, so it's harder for the user to extend the very core features. (Of course, it wouldn't be an actual black box, this is open source, it's just something that the user shouldn't mess with)
Solution 2:
+Easier to maintain and extend
-More difficult API, everything is scattered so the user has to explicitly know if some feature already exists (in solution 2, the user can just scroll through the suggested functions for VoxelWorld.___()
)
So, which solution do you recommend, and why? Or is there some third solution that I haven't thought about?
This was quite a long question, but I hope it can help others who might have the same problem. I tried googling but didn't find anything related to this kind of problem, but I didn't really even know what to search for.