Several business applications need to react to external stimuli—such as network traffic—asynchronously. An example of such software might be a desktop application that allows us to track a company's share prices in the stock market.
We will build this application first using a more traditional approach. In doing so, we will:
Our stock market program will consist of three main components:
We'll start by creating a new leiningen project, where the source code for our application will live. Type the following on the command line and then switch into the newly created directory:
lein new stock-market-monitor cd stock-market-monitor
As we'll be building a GUI for this application, go ahead and add a dependency on Seesaw to the dependencies section of your project.clj
:
[seesaw "1.4.4"]
Next, create a src/stock_market_monitor/core.clj
file in your favorite editor. Let's create and configure our application's UI components:
(ns stock-market-monitor.core (:require [seesaw.core :refer :all]) (:import (java.util.concurrent ScheduledThreadPoolExecutor TimeUnit))) (native!) (def main-frame (frame :title "Stock price monitor" :width 200 :height 100 :on-close :exit)) (def price-label (label "Price: -")) (config! main-frame :content price-label)
As you can see, the UI is fairly simple. It consists of a single label that will display a company's share price. We also imported two Java classes, ScheduledThreadPoolExecutor
and TimeUnit
, which we will use shortly.
The next thing we need is our polling machinery so that we can invoke the pricing service on a given schedule. We'll implement this via a thread pool so as not to block the main thread:
User interface SDKs such as swing have the concept of a main—or UI—thread. This is the thread used by the SDK to render the UI components to the screen. As such, if we have blocking—or even simply slow running— operations execute in this thread, the user experience will be severely affected, hence the use of a thread pool to offload expensive function calls.
(def pool (atom nil)) (defn init-scheduler [num-threads] (reset! pool (ScheduledThreadPoolExecutor. num-threads))) (defn run-every [pool millis f] (.scheduleWithFixedDelay pool f 0 millis TimeUnit/MILLISECONDS)) (defn shutdown [pool] (println "Shutting down scheduler...") (.shutdown pool))
The init-scheduler
function creates ScheduledThreadPoolExecutor
with the given number of threads. That's the thread pool in which our periodic function will run. The run-every
function schedules a function f
in the given pool
to run at the interval specified by millis
. Finally, shutdown
is a function that will be called on program termination and shutdown the thread pool gracefully.
The rest of the program puts all these parts together:
(defn share-price [company-code] (Thread/sleep 200) (rand-int 1000)) (defn -main [& args] (show! main-frame) (.addShutdownHook (Runtime/getRuntime) (Thread. #(shutdown @pool))) (init-scheduler 1) (run-every @pool 500 #(->> (str "Price: " (share-price "XYZ")) (text! price-label) invoke-now)))
The share-price
function sleeps for 200 milliseconds to simulate network latency and returns a random integer between 0 and 1,000 representing the stock's price.
The first line of our -main
function adds a shutdown hook to the runtime. This allows our program to intercept termination—such as pressing Ctrl + C in a terminal window—and gives us the opportunity to shutdown the thread pool.
Next, we initialize the scheduler with a single thread and schedule a function to be executed every 500 milliseconds. This function asks the share-price
function for XYZ's current price and updates the label.
Desktop applications require all rendering to be done in the UI thread. However, our periodic function runs on a separate thread and needs to update the price label. This is why we use invoke-now
, which is a Seesaw function that schedules its body to be executed in the UI thread as soon as possible.
Let's run the program by typing the following command in the project's root directory:
lein trampoline run -m stock-market-monitor.core
A window like the one shown in the following screenshot will be displayed, with the values on it being updated as per the schedule implemented earlier:
This is a fine solution. The code is relatively straightforward and satisfies our original requirements. However, if we look at the big picture, there is a fair bit of noise in our program. Most of its lines of code are dealing with creating and managing a thread pool, which, while necessary, isn't central to the problem we're solving—it's an implementation detail.
We'll keep things as they are for the moment and add a new requirement: rolling averages.
18.226.172.200