How can I set up multiple modules so that they can interact?
I'm creating a plugin that has at least two modules. One will be responsible for managing data and the other will be responsible for the editor interface. Here is where I'm running into issues. I can't quite wrap my head around what the hierarchy should be as far as the module interfaces and class structure go.
Here is a quick break down of how I have things right now (I can provide more details if necessary):
Each module has it's own Build.cs and PrivatePCH.h file. The data module (the main module) has the following .h which is then included in the PrivateDependencyModuleNames array in the Editor's Build.cs file.
From looking at multiple examples it looks like it is ideal to have a basic Interface for each module that is a subclass of IModuleInterface, and then subclass that interface w/ a class that is private. So, that has been the approach I took.
Here enters the problem. I need to access some of the FDataEntityManager (my subclass of IDataEntityManager) functions from within the Editor module. The question I would like to ask, is what is the best way of doing that? Right now I can "see" the IDataEntityManager and get a shared pointer to it via a call to the function IDataModule::Get().GetDataEntityManager(), but that doesn't help me access to the FDataEntityManager's public functions, much less "see" them. What am I missing here?
Your directory hierarchy looks great, so the question really comes down to how to structure the public interface of your modules. The answer to this depends on what the role of each of the modules is and how much you want to decouple the module implementations from each other.
Most modules will fulfill one of two roles: they can be a plug-in that extends some existing functionality, or they can be a library that provides new functionality. The difference is subtle, but important. In the first case your module will be the consumer of public interfaces exposed by others (i.e. the Engine), and in the second case your module will provide public interfaces to be consumed by others. The direction of who provides and consumes the interfaces is reversed.
When a library module provides public interfaces to be consumed by others, there are different ways in which the public interface can be implemented, and these ways generally differ in the degree of coupling to the module's implementation details. The easiest option is to treat such a module as a static library that directly exports new types, such as FDataEntityManager. Other modules can add a module dependency to this module, include its public header file and then immediately create instances of FDataEntityManager. For many library modules this is sufficient.
The downside of such static libraries is that they strongly couple the consuming modules to a particular type of implementation, i.e. FDataEntityManager. This means that, if you ever wanted to swap out this class for another implementation in the future, i.e. FBetterDataEntityManager, then you would have to rewrite parts of the consuming module. If you anticipate that you may have different kinds of data entity managers down the road, then it would be advisable to expose this functionality with an interface class instead, i.e. IDataEntityManager.
Library modules that expose their implementations through interface classes have the advantage that particular implementations of these interface classes are no longer visible to the outside world, which means that they can be exchanged without affecting consumers (Liskov's Substitution Principle). In other words: interface classes loosely couple the consuming modules to the module providing the functionality. For example, the FDataEntityManager class would implement the public IDataEntityManager interface class in the Private part of DataModule, and EditorModule would consume the IDataEntityManager interface classes that are returned by the DataModule instead of instantiating FDataEntityManager instances itself.
When you make implementations available through interface classes then you need a mechanism to create actual instances of types that implement those interface classes. For example, if FDataEntityManager is a private implementation detail of DataModule, then EditorModule can no longer do something like DataManager = new FDataEntityManager(); because it cannot see the type FDataEntityManger. The most common mechanism used is the Factory Method Pattern in which the DataModule would expose a method in its interface that allows for creating instances of data entity managers, i.e. IDataModule::CreateDataEntityManager().
The module's public interface, i.e. IDataModule, is also an abstract interface class, so you may wonder how we get an actual instance of the module implementation. This can be accomplished by requesting the module instance from the ModuleManager, i.e. FModuleManager::LoadModuleChecked<IDataModule>("DataModule");
To make a long story short, there are different ways of exposing the functionality of library modules, the most common of which I have outlined above. You have to make a decision about the degree of coupling between your modules you would like to achieve. If you will ever only have one data entity manager implementation, then the easiest way would be to simply expose the FDataEntityManager type in the public interface of DataModule. In this case you do not need any interface classes at all.
If you want the implementation details of DataModule to be decoupled from EditorModule, then you should expose all types in the form of interface classes. This is more work, but will provide better flexibility down the road, if you need it. In the Unreal Engine code base we use both approaches, and sometimes we even mix and match, which I would strongly recommend against, however.
Last but not least, I would like to add that your EditorModule probably does not require a public interface at all, because it acts as a plug-in, not a library. But it looks like you already figured that out.
Follow this question
Once you sign in you will be able to subscribe for any updates here