How To Write IRC: Part 3
August 23, 2017 -This is a continuation of the article published yesterday: How To Write IRC: Part 2
Defining the API
Before you run off and make a framework, it can be helpful to do a bit of brainstorming and plotting or scheming first. For this part I pulled out a sweet notebook and my fancy pencil. After some doodling and discussion with a friend of mine, I came up with this:
I wanted the idea of a channel to be a first-class object rather than an interface where you send along the channel name as a string and say "hey send this message to this channel". Directly having a channel object to send messages to sounded nicer to me.
With this vague idea in mind, I started writing tests. This way I could work out exactly what the API would look like by attempting to actually use the API.
After some debate with myself and discussion with a friend, I wrote a test to flush out the API I planned:
func testConnectingToServer() {
let user = IRCUser(username: "sgoodwin", realName: "Samuel Goodwin", nick: "mukman")
let server = IRCServer.connect("irc.freenode.org", port: 6667, user: user)
struct ServerDelegate: IRCServerDelegate {
}
let serverDelegate = ServerDelegate()
server.delegate = serverDelegate
let channel = server.join("clearlyafakechannel")
struct ChannelDelegate: IRCChannelDelegate {
}
let channelDelegate = ChannelDelegate()
channel.delegate = channelDelegate
channel.send("Hey sup everybody")
}
A user needs a few different pieces of identifying information, so I grouped them into an IRCUser
struct. Next I identified the two important conceptual pieces: a channel and a server. In most IRC apps, you're able to connect to multiple servers and join multiple channels. I thought treating each one as a separate object would be easier to work with than having a single global object and needing to use functions that say things like, "Send this text (a string) to this channel (also a string)" and to have delegate methods that say things like, "You got this message (a string) from this channel (also a string)". Whoever uses this library later would have to do more work to keep up with which channels and servers a user is connected to and I didn't like that so much. With separate objects, a developer could hand each channel and server object to a different controller responsible for displaying the information from that one object. It was also reassuring to know that this doesn't need to be permanent. I can change anything later and I'll have tests to be confident it still works.
Of course this test won't even compile right away. I needed to make the classes and protocols mentioned in the test so the compiler wasn't sad they don't exist.
struct IRCUser {
let username: String
let realName: String
let nick: String
}
class IRCChannel {
var delegate: IRCChannelDelegate?
func send(_ text: String) {
}
}
class IRCServer {
var delegate: IRCServerDelegate?
static func connect(_ hostname: String, port: Int, user: IRCUser) -> IRCServer {
return IRCServer()
}
func join(_ channelName: String) -> IRCChannel {
return IRCChannel()
}
}
protocol IRCServerDelegate {
}
protocol IRCChannelDelegate {
}
At this point I only filled in the minimum required bits. This wouldn't actually do anything, but the shell of a working thing was there. Now I just needed to cram some innards in there and make it walk.
The work continues in How To Write IRC: Part 4