Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.Sign up
Learnable software makes it easier to gain a solid understanding of how things works without having to rely too heavily on documentation. While this aspect of software quality manifests itself in various different ways depending on the context, establishing clear and consistent patterns is a big part of designing learnable tools. To illustrate this point, we can look at a change that was made in Prawn to make its graphics API more learnable.
When Prawn was first developed, it did not have a consistent API for its primitive drawing operations. While the data involved with drawing a rectangle, circle, and ellipse were somewhat similar, the naming conventions and arguments for these methods varied, as shown below.
Prawn::Document.generate("drawing.pdf") do stroke do rectangle [200,200], 100, 50 ellipse_at [200,200], 30, 10 circle_at [200,200], :radius => 20 end end
In the process of cleaning up and stabilizing our API, we noticed that while each of these API calls looked reasonable on their own, they needed to be learned and memorized independently. Our naming conventions and arguments were not helping with this task, but actively working against it. The naming convention
circle_at nicely indicate that the first argument is a point, but make it easier for users to misremember the
rectangle API as
rectangle_at. Similarly, the width and height arguments between
ellipse matched up with each other nicely, but the
circle_at method switched to keyword-like arguments, explicitly specifying a
:radius value. The fact that these methods were sort of similar to one another but had subtle differences was a sign of a problem, and we decided to resolve it by making them all consistent. The code below demonstrates what we replaced the old API with:
Prawn::Document.generate("drawing.pdf") do stroke do rectangle [200,200], 100, 50 ellipse [200,200], 30, 10 circle [200,200], 20 end end
In this code, the naming convention is simply the name of the shape you want to build, removing the somewhat superfluous
_at suffix. Additionally, all three methods ordered its arguments as a point followed by its dimensions. In the case of a circle, specifying two dimensional values does not make sense, so the third argument is simply omitted. This is a much smaller adjustment for the user to remember than it would be to ask them to memorize that
circle takes hash arguments while
ellipse does not.
Through this refactoring, we realized that reducing the number of special cases in our API design was a way to increase learnability. While it may result in individual API calls being slightly less aesthetically pleasing, the overall impact on the user is a net positive one.
The important thing to remember is that learnability and operability can occasionally be at odds with one another, and the relation between them is a balancing act. Don't forget the lesson that Clippy taught us.
Turn the page if you're taking the linear tour, or feel free to jump around via the sidebar.