| @@ -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<T> | |||
| { | |||
| public ulong Id { get; } | |||
| } | |||
| shape SGuildChannel<T> : SChannel<T> | |||
| { | |||
| public ulong GuildId { get; } | |||
| public int? Position { get; } | |||
| public IEnumerable<PermissionOverwrite> PermissionOverwrites { get; } | |||
| public string Name { get; } | |||
| } | |||
| shape STextChannel<T> : SChannel<T> | |||
| { | |||
| public ulong? LastMessageId { get; } | |||
| public DateTimeOffset LastPinTimestamp { get; } | |||
| } | |||
| shape SGuildTextChannel<T> : SGuildChannel<T>, STextChannel<T> | |||
| { | |||
| public string? Topic { get; } | |||
| public bool IsNsfw { get; } | |||
| public int? RateLimit { get; } | |||
| } | |||
| public ValueTask<bool> DeleteLastMessageAsync<T>(T channel) | |||
| where T : STextChannel<T> | |||
| { | |||
| return channel.LastMessageId switch { | |||
| null => new ValueTask<bool>(false), | |||
| 0 => new ValueTask<bool>(false), | |||
| _ => new ValueTask<bool>(DeleteMessageAsync(channel.LastMessageId)) | |||
| }; | |||
| static async Task<bool> 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<T>` 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 | |||