Search code examples
c#domain-driven-designfactory-pattern

Where should factories live in a project?


I have a solution with a project called MudEngine.Core. This project contains some essential classes and then all of the interfaces that my domain objects are abstracted behind. Interfaces like IWorld and IRealm.

Example domain interface

public interface IWorld : IGameComponent, ICloneableComponent<IWorld>
{
    /// <summary>
    /// Gets how many hours it takes to complete one full day in this world.
    /// </summary>
    int HoursPerDay { get; }

    /// <summary>
    /// Gets or sets the game day to real hour ratio.
    /// </summary>
    double GameDayToRealHourRatio { get; set; }

    /// <summary>
    /// Adds a collection of realms to world, initializing them as they are added.
    /// </summary>
    /// <param name="realms">The realms.</param>
    /// <returns>
    /// Returns an awaitable Task
    /// </returns>
    IRealm[] GetRealmsInWorld();

    /// <summary>
    /// Initializes and then adds the given realm to this world instance.
    /// </summary>
    /// <param name="realm">The realm to add.</param>
    /// <returns>Returns an awaitable Task</returns>
    Task AddRealmToWorld(IRealm realm);

    /// <summary>
    /// Creates and initializes a new instance of a realm.
    /// </summary>
    /// <param name="name">The name of the realm.</param>
    /// <param name="owner">The world that owns this realm.</param>
    /// <returns>Returns an initialized instance of IRealm</returns>
    Task<IRealm> CreateRealm(string name, IWorld owner);

    /// <summary>
    /// Adds a collection of realms to world, initializing them as they are added.
    /// </summary>
    /// <param name="realms">The realms.</param>
    /// <returns>Returns an awaitable Task</returns>
    Task AddRealmsToWorld(IEnumerable<IRealm> realms);

    /// <summary>
    /// Removes the given realm from this world instance, deleting the realm in the process.
    /// If it must be reused, you may clone the realm and add the clone to another world.
    /// </summary>
    /// <param name="realm">The realm to remove.</param>
    /// <returns>Returns an awaitable Task</returns>
    Task RemoveRealmFromWorld(IRealm realm);

    /// <summary>
    /// Removes a collection of realms from this world instance.
    /// If any of the realms don't exist in the world, they will be ignored.
    /// The realms will be deleted during the process.
    /// If they must be reused, you may clone the realm and add the clone to another world.
    /// </summary>
    /// <param name="realms">The realms collection.</param>
    /// <returns>Returns an awaitable Task</returns>
    Task RemoveRealmsFromWorld(IEnumerable<IRealm> realms);
}

I also have a project called MudEngine.Mud which is the default implementations for all of the interfaces.

The MudEngine.Core project includes interfaces for my factories. Factories like IWorldFactory and IRealmFactory. The IWorld interface has a creation method, that uses a given IRealmFactory to create realms and return them.

Example factory interface

/// <summary>
/// Provides methods for creating an instance of an IRealm implementation
/// </summary>
public interface IRealmFactory
{
    /// <summary>
    /// Creates and initializes a new instance of a realm.
    /// </summary>
    /// <param name="name">The name of the realm.</param>
    /// <param name="owner">The world that owns this realm.</param>
    /// <returns>Returns an initialized instance of IRealm</returns>
    Task<IRealm> CreateRealm(string name, IWorld owner);

    /// <summary>
    /// Creates and initializes a new instance of a realm.
    /// </summary>
    /// <param name="name">The name of the realm.</param>
    /// <param name="owner">The world that owns this realm.</param>
    /// <param name="timeZoneOffset">The time zone offset to apply to the realm.</param>
    /// <returns>Returns an initialized instance of IRealm</returns>
    Task<IRealm> CreateRealm(string name, IWorld owner, ITimeOfDay timeZoneOffset);

    /// <summary>
    /// Creates and initializes a new instance of a realm.
    /// All of the children zones will be initialized prior to being added to the realm.
    /// </summary>
    /// <param name="name">The name of the realm.</param>
    /// <param name="owner">The world that owns this realm.</param>
    /// <param name="zones">A collection of zones that will be initialized and added to the realm.</param>
    /// <returns>Returns an initialized instance of IRealm</returns>
    Task<IRealm> CreateRealm(string name, IWorld owner, IEnumerable<IZone> zones);

    /// <summary>
    /// Creates and initializes a new instance of a realm.
    /// All of the children zones will be initialized prior to being added to the realm.
    /// </summary>
    /// <param name="name">The name of the realm.</param>
    /// <param name="owner">The world that owns this realm.</param>
    /// <param name="timeZoneOffset">The time zone offset to apply to the realm.</param>
    /// <param name="zones">A collection of zones that will be initialized and added to the realm.</param>
    /// <returns>Returns an initialized instance of IRealm</returns>
    Task<IRealm> CreateRealm(string name, IWorld owner, ITimeOfDay timeZoneOffset, IEnumerable<IZone> zones);
}

When I call CreateRealm on the IWorld interface, the implementation uses an IRealmFactory to create it. The factory is passed in through the IWorld implementation's constructor.

My question now is, where should the factory implementation exist? Is it common for the same project that contains the domain interface implementations to provide an implementation for the factory or should the consuming layer (such as the presentation/unit test projects) be responsible for implementing the factories and using them?

The intent is that these components can be swapped out depending on the kind of text-based game you are building. So I was leaning towards each package implementing the interfaces having their own factory. What I am concerned with though is the DI setup. The IoC container further up in the layering (server/client apps) would need to know which factory in each package it should use, as opposed to just using one that is defined in the consuming layer that the IoC container is a part of.

Is there any industry standard guidance on this?


Solution

  • It seems you're trying to build a domain model with abstracted subdomains (I guess you can call them subdomains because they are full fledged game systems), i.e. the user can dynamically pick a subdomain at runtime which triggers some subdomain-agnostic behavior, then do subdomain-specific stuff with it for some period of time and potentially jump to another one when they're finished.

    Though in my experience you rarely encounter this in "normal" line of business projects, here's what I'd do :

    • Create an overarching Bounded Context for generic World and Realm management. Put IWorld and IRealmFactory there.

    • Create a separate Bounded Context for each game system subdomain. They can be whole projects or just namespaces. Write implementations of IRealmFactory in these.

    • This is I think one of the few instances when using Container.Resolve(...) can be legit. Create named registrations in IoC configuration and resolve one of them at any moment to wire up the object subgraph corresponding to one game system.