Today marks the completion of a project I’ve been working on for over three months now: Sockets.cpp. So, I figured this would be a good chance to look back on my project and reflect on the various things I learned.
I orginally started this project because of a homework assignment. The assignment was to create a networked application in C++. Yet, prior to the class I built an HTTP server in C++. I figured that I could make the assignment really easy by copying the socket API abstraction I built for my HTTP server to the homework assignment. Of course, that wasn’t the case. I built and tested the HTTP server on Windows only, and the assignment was supposed to compile and run on an unknown UNIX system. Needless to say, there were adaptability pains. So after the assignment was finished I’d figured I’d take my abstraction and make it into a real library.
This was an interesting project because it was the first time I made something that wasn’t “throwaway” – it wasn’t to learn some new framework or language, nor was it for a one-time event. This was something that I planned on maybe using again in the future and maybe sharing it with other people who have similar use cases. I really had to think about the API I was making, and how I as a client, or other people, would want to use it. Or how they would expect to use it.
Lesson 1: Don’t make your own containers
When I first started out by writing my own containers.
ByteBuffer was supposed to be a fixed-sized array and
ByteString was supposed to be an immutable analog to that. I spent a while trying to figure out the best way to write and use these containers to be efficient. But when I actually started to think about it, I realized this was pointless.
Making my own containers would just fustrate anyone who used my library. Imagine being a developer who gets data from
MyAwesomeContainer in one component, and then has to copy it to a
CoolContainer in order to use it with another with another component. Imagine if the difference between these two containers was just method names and slightly different behaviors like
CoolContainer has iterators while
MyAwesomeContainer doesn’t. Pretty annoying isn’t it? Even more annoying when you realize that if both libraries had just used the STL, you could pass data by reference and avoid unnecessary copies.
And writing my own containers put more work on me, the developer, too. I had to write the containers, and I had to test them. Thouroghly. Why do that when the STL is well-tested already? In the end,
ByteBuffer was just a watered-down
ByteString was just
const std::array, so I switched to that.
I would change the the sub-title to “Don’t use containers at all” because iterators are significantly more powerful than containers for the purpose of recievers(and, in my opinion, a case where C++ shines). But in order to for a method to recieve iterators, it must be templated. And in order to use templates, the implementation can’t be separated from the declaration. This makes developing for multiple platforms inconvienient.
Lesson 2: State changes that change behavior imply change in type
This is something that I wish I learned sooner, especially a few years ago when I was doing things in Java. If certain states of your object causes methods to throw something like a
InvalidStateError then those special states should be represented with a change of type. This kind of design provides a clearer way to perform a task. It’s not immediately obvious that you need to
listen() on a socket before
accept()-ing new connections, unless you have experience. But if
write() exist on a
Connection object and
ServerSocker::accept() returns a
Connection, it’s much easier to connect the pieces together.
Lesson 3: Use CMake
CMake is great. Not really much more to be said on that. Point it to your header files. Point it to your implementation files. Tell it you want a library. Done. No need to fiddle with scripts or run
make 400 times.
Lesson 4: Breaks
This is really a more personal issue than an engineering one. But, take breaks. I’d spend 10 hours doing nothing but writing and testing code, the last few hours consisting of deleted and reworking the same part of code over and over again. Eventually I’d give up, go to sleep, wake up, take a fresh look at my code and come up with a completely obvious and perfect solution.