samedi 25 avril 2020

Public interface with implementers having extra data?

With the public interface IGameTrack, an implementation actually needs more data than what is publicly exposed by this interface. I came up with a pattern that just seems to work, i.e. explicit implementation, Array.Find and casting, but I was wondering if there could be another approach for Game5.Install(IGameTrack):

public interface IGame
{
    [NotNull]
    [ItemNotNull]
    IReadOnlyList<IGameTrack> Tracks { get; }

    void Install([NotNull] IGameTrack track);
}

public interface IGameTrack
{
    [NotNull]
    string Name { get; } // nothing more needs to be exposed than this
}

internal sealed class GameTrack5 : IGameTrack // but custom implementation needs extra data
{
    public GameTrack5([NotNull] string name, int position)
    {
        Name = string.IsNullOrWhiteSpace(name)
            ? throw new ArgumentException("Value cannot be null or whitespace.", nameof(name))
            : name;

        Position = position < 0
            ? throw new ArgumentOutOfRangeException(nameof(position))
            : position;
    }

    public string Name { get; }

    public int Position { get; }

    string IGameTrack.Name => Name;
}

public sealed class Game5 : IGame
{
    [NotNull]
    [ItemNotNull]
    private IReadOnlyList<GameTrack5> Tracks { get; } = new[]
    {
        new GameTrack5("abcd", 1234)
    };

    IReadOnlyList<IGameTrack> IGame.Tracks => Tracks;

    public void Install(IGameTrack track) // works but is a bit weird
    {
        if (track == null)
            throw new ArgumentNullException(nameof(track));

        // TODO anything better than that ?

        if (!(Array.Find(Tracks.Cast<IGameTrack>().ToArray(), s => s == track) is GameTrack5 result))
            throw new ArgumentNullException(nameof(result));

        var name     = result.Name;
        var position = result.Position; // our extra data we'll need
    }
}

Question:

How to expose a public interface but its implementors requiring extra data ?

To make it clear, IGame.Install(IGameTrack) will receive an IGameTrack from which it should map to its specific implementation of IGameTrack, in this case it will be GameTrack5.

Edit:

Possible solution that does not expose implementations of IGameTrack:

using System;
using System.Collections.Generic;
using OpenAG.Annotations;

// ReSharper disable once CheckNamespace
namespace Whatever
{
    public interface IGame
    {
        string Name { get; }

        IReadOnlyList<IGameTrack> Tracks { get; }

        void Install([NotNull] IGameTrack track);
    }

    internal interface IGame<in T> : IGame where T : IGameTrack
    {
        void Install([NotNull] T track);
    }

    public interface IGameTrack
    {
        string Name { get; }
    }

    public class Game1 : IGame, IGame<GameTrack1>
    {
        public string Name { get; }

        public IReadOnlyList<IGameTrack> Tracks { get; }

        public void Install(IGameTrack track)
        {
            (this as IGame<GameTrack1>).Install(track as GameTrack1 ?? throw new InvalidOperationException());
        }

        void IGame<GameTrack1>.Install(GameTrack1 track)
        {
            if (track == null)
                throw new ArgumentNullException(nameof(track));

            throw new NotImplementedException();
        }
    }

    internal class GameTrack1 : IGameTrack
    {
        public string Name { get; }
    }
}

Aucun commentaire:

Enregistrer un commentaire