Reddit.st — In 10 Cool Pharo Classes

Sven VC
Sven VC
Sep 1, 2014 · 16 min read

This is a tutorial showing how to implement a small but non-trivial web application in Pharo using Seaside, Glorp (an ORM) and PostgreSQL.

Reddit is a web application where users can post interesting links that get voted up or down. The idea is that the ‘best’ links automatically end up with the most points. Many other websites exist in the area of social bookmarking, like Delicious, Digg and Hacker News.

Reddit.st adds persistency in a relational database, unit tests as well as web application components to the mix.

The 10 main sections of this article follow the development of the 10 classes making up the application. The focus is not so much on the smaller size or the higher developer productivity in Pharo, but more on the fact that we can cover so much ground using such powerful frameworks, as well as the natural development flow from model over tests and persistence to web GUI.

The appendix explains how to get the source code discussed in this article.

1.

RedditLink

The central object of our application will be RedditLink, representing an interesting URL with a title, a created timestamp and a number of points. It thus has the following properties:

  • id
  • url
  • title
  • created
  • points

These will become the instance variables of our class. Create a new Object subclass by editing the class template.

Next, we use the class refactoring tool to automatically generate accessors (getters and setters) for all our instance variables. With these implemented we can write our initialize and printOn: methods.

We also added a age method that will return the Duration of time the link now exists. We will need that when rendering links. To make it a little bit easier for others to create new instances of us, we add a class method called withUrl:title:.

Apart from creating and displaying RedditLinks, we want to be able to vote them up and down. So lets add two action methods, voteUp and voteDown.

The core of the RedditLink object is now finished. Everything is ready to make instances and use them.

2.

RedditLinkTests

Units tests are very important, not so much in small examples like this one, but especially in larger applications. Having a good set of unit tests with decent coverage helps protect the code during changes. At the same time, unit tests function as working documentation. Instead of writing scratch test code in some workspace, you can just as well write a unit test. We make a TestCase subclass named RedditLinkTests and add 3 test methods.

After creating a RedditLink object, and/or manipulating it, these methods assert that certain conditions hold. To improve code sharing (we’ll need it again in section 5) we add a method called assertContractUsing: to RedditLink that checks the basic contract of the receiver, using assert: on an arbitrary object. For completeness, we also implement a general validate method.

Pharo has integrated tools to quickly run these tests. There is a separate Test Runner, but you can run tests directly from a Browser as well. The browser even has a permanent indication for successful and failed tests.

3.

RedditSchema

To make our application less trivial, we are going to make our collection of links persistent in a relation database. For this we are going to use Glorp, an object-relational mapping tool. Glorp will take care of all the SQL. To do its magic, Glorp needs a DescriptorSystem that tells it 3 things: the class models involved, the tables involved and the way the two map (which it calls a descriptor).

In this simple case we are just mapping one object into one table, so the descriptor system might look a bit verbose. Just remember that Glorp can do much, much more advanced things. Furthermore, there exist extensions to Glorp that implement ActiveRecord style automatic descriptors.

RedditSchema subclasses from DescriptorSystem.

So first we tell Glorp about our class model, RedditLink, and which instance variable are to be persistent attributes. Instead of some XML description, a much more powerful object model is being built. Conventions in methods names are being used to glue things together. This forms in effect a Domain Specific Language (DSL).

The second step is to describe which tables are involved. So here we list all the fields (columns) of our table REDDIT_LINKS. Glorp shields us from the differences between different SQL dialects. Note how id is designated to be a primary key. The serial type will result in an SQL sequence being used.

The third and final step is the actual mapping between the class model RedditLink and the table REDDIT_LINKS. In this simple case, direct mappings are used between attributes and fields. As we will see in the next sections, Glorp is now ready to do its work.

4.

RedditDatabaseResource

Let’s assume you installed and configured PostgreSQL on some machine and that you created a database there. On Mac OS X, Postgres.app is the easiest and fastest way to get do this.

We now have to specify how Glorp has to connect to PostgreSQL. We do this using an Object subclass called RedditDatabaseResource, where we add some class methods (as well as a class variable called DefaultLogin).

The username and password speak for themselves, the connectString contains the hostname, port number and database name (after an underscore). The encodingStrategy should match the encoding of the database.

Glorp accesses a database through sessions. A session is most easily started from a descriptor system given a login as argument, that’s what we do in the session class method.

Glorp can help us to create our REDDIT_LINKS table, that is what the createTables class method does. So we truely don’t have to use any SQL ! The flow should be familiar: get a session, login, do some work in a transaction and logout. By setting logging to true, the generated SQL statements will be printed on the Transcript (comment this out for production use).

5.

RedditDatabaseTests

With our RedditSchema descriptor system and our RedditLinksDatabaseResource we are now ready to test the persistency of our model. We create another TestCase subclass named RedditDatabaseTests. These tests need an instance variable called session to hold the Glorp session, as well as setUp and teardown methods to manage this test fixture.

The tests themselves do some basic CRUD operations.

Our first test reads all RedditLinks from the database, making sure they are valid and of the expected type. Querying doesn’t have to be done in a unit of work or transaction.

The second test creates a new RedditLink and then registers it with the session inside a unit of work. This will effectively save the object in the database. The id of the RedditLink will have a value afterwards. Next we reset the session and query the RedditLink with the known id. After making sure that what we put in got out of the database we delete the object.

The third test checks if updating an existing persistent object works as expected. Note that the actual modification, the voteUp, has to be done inside a unit of work to a registered object for it to be picked up by Glorp.

6.

RedditSession

We are almost ready to start writing the GUI of our actual web application. Seaside web applications always have a session object that keeps the application’s state during the user’s interaction with it. We need to extend that session with a database session. Our WASession subclass RedditSession has an instance variable called glorpSession to hold a Glorp session to the database.

Note how we are using lazy initialization in the glorpSession accessor. In newGlorpSession we’re making use of our RedditDatabaseResource. The unregistered is a hook called by Seaside whenever a session expires, we use it clean up our Glorp session by doing a log out.

7.

RedditWebApp

We can now start with the web app itself. There are 4 sections in this single page app: a header or title section, some action links, a list of some of the highest or top ranking links and a list of some of the latest or most recent links.

We create a WAComponent subclass called RedditWebApp. This will become our central or root web app component. We start by writing the rendering methods.

Starting with the main renderContentOn: method, the rendering of each section is delegated to its own method. Note how renderLink:on: is used 2 times. For now, we’re not yet implementing the ‘New Link’ action. We are generating HTML using a DSL.

Our rendering methods depend on 5 extra methods: highestRankingLinks, latestLinks, the class method durationString: and the actions methods voteUp: and voteDown:. Only the first three are needed to render the page itself.

In highestRankingLinks and latestLinks, we explicitely build up and execute Query objects with some more advanced options. For duration string conversion we use the powerful pluralize method. With these in place we can already render the page. Since we did not yet add any CSS styling, the result will look rather dull.

Voting links up or down is trivial, like with our database test, we only have to make sure to do the object modifications inside a Glorp unit of work and the actual SQL update will be done automatically.

8. RedditFileLibrary

To style our web app, we’ll be using CSS. This CSS code references one small GIF for its background gradient. We need to make sure our application makes use of the CSS file and that we serve the actual files. Seaside can serve these files in a couple of ways, we’ll be using the FileLibrary approach. This is a class where each resource served is implemented as a method.

Our WAFileLibrary subclass RedditFileLibrary will thus have 2 methods: mainCss and bgGif, returning a string and bytes respectively. These are long methods which are not our main focus so we do not list them here. Based on some naming conventions, Seaside will figure out what mime types to use.

By implementing the updateRoot: hook method on RedditWebApp, we can set our page title and CSS. Note again how everything happens in Pharo. To install a Seaside application, a class side initialize method is typically used.

We register our application under the handler ‘reddit’ so its URL will become something like http://localhost:8080/reddit. Then we tell it to use our custom session class and finally add our file library. We now have a nicely styled, working web app.

9.

RedditLinkEditor

One of Seaside’s main advantages over other web application frameworks is its support for components. Especially for large and complex projects this makes a huge difference. We’ll be introducing a new component to allow the user to enter the necessary information when adding a new link.

Consider the difference between first and second screenshot of our web app in action: when the user clicks the ‘New Link’ anchor, we’ll add an editor just below (while hiding the ‘New Link’ anchor). The editor will have its own ‘Save’ and ‘Cancel’ buttons. Both of these will dismiss the editor, saving or cancelling the new link.

How is Seaside’s component model powerful ? As we will see next, the component is written without any knowledge of where it will be used. Its validation logic is independent. It is used just by embedding it and by wiring it to its user in a simple way. The subcomponent functions indepedently from its embedding parent while each keeps its own state: whether the component is visible or not, you can keep on voting links up or down, and doing so will not alter the contents of the component.

To prove our point, we’ll be using yet another component inside our link editor: a simple CAPTCHA component. This will be implemented in the final section, but is used here as a black box.

The first step is to make the necessary additions and modifications to RedditWebApp to accommodate the link editor component. We add an instance variable called linkEditor with and its accessors.

There are 2 possible states: either we have a link editor subcomponent visible or not. So the main renderContentOn: method conditionally asks the link editor to render itself. Likewise, in renderActionsOn: the ‘New Link’ anchor is only rendered when there is no link editor yet.

In the showNewLinkEditor action method we instanciate our subcomponent and hook it up. We could have reused just one instance, creating a new one is easier and clearer. The wiring is done by supplying a block to onAnswer:. A component can answer a value, in our case true or false for save or cancel respectively. So when the link editor answers true, we save a new link object and hide the editor.

In Seaside, the children method is a hook method that has to be implemented to list all subcomponents. Again this happens conditionally.

We can now implement the component itself: RedditLinkEditor is a subclass of WAComponent with 3 instances variables and their accessors: url, title and captcha.

Most of the code should be familiar by now. New is how cancel and save use answer: to return to whoever called upon this component. Before save returns successfully, a number of validation tests are done. When one of these tests fails, a message is shown and the operation is aborted. The isUrlValid method actually tries to resolve the URL. Finally, createLink instantiates a new RedditLink instance based on the valid fields entered by the user. Note how the CAPTCHA is used as a true black box component.

10.

RedditCaptcha

The last and simplest web component is a CAPTCHA that presents a simple addition in words. This component does not need answer logic. RedditCaptcha is again a WAComponent subclass with the following instance variables and accessors: x, y and sum.

Each time the CAPTCHA is rendered, x and y get a new random value between 1 and 10. Next, the addition is presented in words. The isSolved method checks if the user answered correctly.

Appendix

The source code discussed in this article is available from SmalltalkHub in a project called Reddit. It was written for Pharo 3.0. You should load the code using its Metacello configuration, because Seaside and Glorp have to be loaded as well. These are both heavy packages that take a while to load and compile.

You will have to configure the connection to your PostgreSQL instance. One way to do so it to edit the method RedditDatabaseResource class>>#createLogin. After you have done so, make sure to clear the cached version by doing RedditDatabaseResource resetLogin.

Alternatively, you can download a prebuilt image containing everything as the latest successful build artifact from the Pharo Contribution CI job called Reddit.

Concerning Pharo

Articles about software development using a pure dynamic…

Concerning Pharo

Articles about software development using a pure dynamic object oriented language in an immersive live environment focused on simplicity and feedback

Sven VC

Written by

Sven VC

loves high-level, dynamic and interactive object-oriented systems

Concerning Pharo

Articles about software development using a pure dynamic object oriented language in an immersive live environment focused on simplicity and feedback

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store