Celebrating 10 Years!

profile picture

How To Write IRC: Part 3

August 23, 2017 - Roundwall Software

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:

Image of my notebook drawing

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