diff --git a/docs/design/interfaces.md b/docs/design/interfaces.md new file mode 100644 index 000000000..79895c8cb --- /dev/null +++ b/docs/design/interfaces.md @@ -0,0 +1,94 @@ +# Discord.Net 3.0 Interface Design # + +This is mostly just a collection of notes for interface design for 3.0. As +such, don't expect them to be comprehensive or up to date. + +## Why no `ITextChannel`, `IGuildChannel`, etc? ## + +Usually, it's better to design types in terms of the fields they have, rather +than using a discriminated union like the current `IChannel` interface. + +However, the design chosen in 1.0 led to [traits](wiki-traits), which in C# was +*notoriously* hard to work with, debug and expand. Most notably, in +`1.0.0-beta2`, the internal implementation details were exposed in form of +`SocketUserMessage`, `SocketTextChannel` etc. simply to make consumption +easier. The effect of this has been that writing portable, re-usable code is +very hard, and has also caused us to be much slower when implementing updates +from Discord. + +To simplify this, the current design has gone back to a centralised `IChannel` +type, a la [D#+'s DiscordChannel](dsharpplus-channel) implementation. However, +we are going to be using [Nullable Reference Types](nrts) to help reduce some +of the inevitable errors which will come around with a discriminated union +approach. Furthermore, this design allows in the future a traits-like system to +be implemented on top of it, once the design flaws have been worked out. + +## Future Plans ## + +Ideally, [shapes] will become a thing, making the previous design *much* easier +to implement and maintain, on top of the current design. To give a small +example: + +```cs +shape SChannel +{ + public ulong Id { get; } +} + +shape SGuildChannel : SChannel +{ + public ulong GuildId { get; } + public int? Position { get; } + public IEnumerable PermissionOverwrites { get; } + public string Name { get; } +} + +shape STextChannel : SChannel +{ + public ulong? LastMessageId { get; } + public DateTimeOffset LastPinTimestamp { get; } +} + +shape SGuildTextChannel : SGuildChannel, STextChannel +{ + public string? Topic { get; } + public bool IsNsfw { get; } + public int? RateLimit { get; } +} + +public ValueTask DeleteLastMessageAsync(T channel) + where T : STextChannel +{ + return channel.LastMessageId switch { + null => new ValueTask(false), + 0 => new ValueTask(false), + _ => new ValueTask(DeleteMessageAsync(channel.LastMessageId)) + }; + + static async Task DeleteMessage(ulong messageId) + { + var message = await GetMessageAsync(messageId); + + if (message == null) + return false; + + await message?.DeleteAsync(); + return true; + } +} +``` + +Internally, `DeleteLastMessageAsync` will work on any channel, but as long +as that channel exposes the correct APIs, the above method will function as the +user expects. + +Additionally, this provides a huge advantage in that the above code is +extremely easy to unit test; any type which fulfils the contract of +`STextChannel` can be used, even if it doesn't implement the `IChannel` +interface. + + +[wiki-traits]: https://en.wikipedia.org/wiki/Trait_(computer_programming) +[dsharpplus-channel]: https://dsharpplus.emzi0767.com/api/DSharpPlus.Entities.DiscordChannel.html +[nrts]: https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references +[shapes]: https://github.com/dotnet/csharplang/issues/164