JavaDB:Exercises - Introduction to layered architectures

From Juneday education
Jump to: navigation, search

Work in progress

This chapter is a work in progress. Remove this section when the page i production-ready.

Introduction

These exercises serve to give you a basic understanding of how Java interfaces could be used to create layers of abstraction within a Java application. Layers of abstraction is an important part of making your application immune to changes, and makes modular re-use easier.

The basic idea is that rather than programming using concrete classes, you should use interface types instead. Using object references of interface type, of course hides the fact of the runtime type of the objects and thus creates less dependencies between your code and any particular one concrete class.

A small example of that principle could be to use lists of type List rather than e.g. ArrayList. Programming against an interface type is actually easier than programming against a concrete class (most of the time), because you only have to care about the methods listed in the interface. The methods in the interface are abstract and described in very general terms, which helps you focus on the job they should do, and not about how they do the job.

The exercises below will show you a simple application for looking up synonyms for a Swedish word. The synonyms are loaded from a file, and the application reads the file and creates a Map with words as keys and lists of synonyms as the values.

The first version is a stand-alone single class with just a main method and a load method for reading in the file with synonyms.

This version exists to show you the basic principles of what the application should do, but also to make you think about the challenge of changing this application to use some other means for looking up the synonyms.

Next, you'll see a slightly more Object oriented example. In this example, the main class is protected from changes since it doesn't handle (or know about) the way synonyms are loaded and looked up. Instead, the main method creates an object of type SynonymEntry and asks the object for synonyms of some word provided as an argument to the application.

The SynonymEntry class delegates the looking up of the synonyms to a static method in a helper class, Synonyms.

The Synonyms class, also delegates, using an object of class SynonymsLoader. The SynonymsLoader class does the dirty work of reading the file and creating a Map with all the synonyms from the file.

The call-chain from main in this slightly more OO example is thus:

new SynonymEntry().getSynonyms(word) -> The SynonymEntry uses Synonyms.getSynonyms(word) -> the Synonyms.getSynonyms(word) static method uses a new SynonymsLoader: SynonymsLoader().getDictionary().get(word)

Now, if we want to add capability of asking a server for the synonyms, rather than using a Map we have read from a file, the semantics changes. We'll have to change the Synonyms class a lot.

The next version adds network capabilities to the system to show how we must change the classes to use a network instead of reading from file.

Finally, we show a slightly layered architecture which supports both reading from file and from a server.

In the last example, the user decides whether to use a server or a file, providing a flag (a property) on the command line. The main still uses a SynonymEntry object and looks like before. But the SynonymEntry class has now changed. It now gets a SynonymsOracle object (of interface type) from a factory class. Based on the user's preferences, it always gets the right type of Oracle (or one that uses a file as the default backup type).

The point of the last example is that we can now add more types of Oracles (objects which knows something we can ask for) by simply adding more classes which implement the interface (and update the factory accordingly). From this point, only the Factory is depending on changes, all other classes are immune.

We can think of the SynonymsOracle interface as a layer protecting and shielding other parts of the system from implementation details and changes. Where the concrete Oracle gets its data from is not a concern for the rest of the system! It is a rather low-level decision which the designers of the other classes shouldn't care about.

Whenever we need an oracle, we ask the factory for one. The factory checks what the user wanted (if anything) and returns a reference to an instance of the right type, either a SynonymsFileOracle or a SynonymsNetworkOracle (for instance). But what do we care? We are programming against the SynonymsOracle interface, so we don't have to know the actual type!

For the most part, these exercises are about reading code and understanding what it does. You should try to implement parts of the system yourself, you want more hands-on exercises about layers. You should carefully read the source code and try to understand how each version of the system works. You don't have to understand the networking part for the versions where we ask a server for synonyms instead of reading them from file. But it's there for reference (as a very simple single-threaded client-server implementation).

Exercises

Task 0 - download the source code

Clone or download the files from our java database repo into a fresh new folder where you'll do your work for this exercise. The directory in the repo for these exercises is in this directory. If you don't want to clone the whole java database repo, you can download a zip file of the exercises directory using this web page (open it in a browser for the download to work).

Here's what the directory tree looks like (inside the exercises directory):

.
|-- 01-standalone
|   |-- files
|   |   `-- synonyms.csv
|   `-- org
|       `-- flat
|           `-- main
|               `-- GetSynonyms.java
|-- 02-more-oo
|   |-- files
|   |   `-- synonyms.csv
|   `-- org
|       `-- flat
|           |-- main
|           |   `-- GetSynonyms.java
|           `-- text
|               |-- SynonymEntry.java
|               |-- Synonyms.java
|               `-- SynonymsLoader.java
|-- 03-networked
|   |-- files
|   |   `-- synonyms.csv
|   |-- org
|   |   `-- flat
|   |       |-- main
|   |       |   `-- GetSynonyms.java
|   |       `-- text
|   |           |-- SynonymEntry.java
|   |           |-- Synonyms.java
|   |           |-- SynonymsLoader.java
|   |           `-- SynonymsNetworkLoader.java
|   `-- server
|       `-- SynonymServer.java
`-- 04-layered
    |-- files
    |   `-- synonyms.csv
    |-- org
    |   `-- flat
    |       |-- main
    |       |   `-- GetSynonyms.java
    |       `-- text
    |           |-- SynonymEntry.java
    |           |-- SynonymsFileOracle.java
    |           |-- SynonymsNetworkOracle.java
    |           |-- SynonymsOracleFactory.java
    |           `-- SynonymsOracle.java
    `-- server
        `-- SynonymServer.java

25 directories, 22 files

As you can see, there are four version of the same application, and we'll investigate them one-by-one.

The first part will focus on the 01-standalone tree.

Task 1

Enter the 01-standalone directory, and compile and run the application, giving a Swedish word like "snäll" as the argument to the application. The main class is org.flat.main.GetSynonyms.

Expand using link to the right to see a hint.

The synonyms for "snäll" are: gullig, ljuv, snäll, trevlig, vänlig, älskvärd

$ javac org/*/*/*.java && java org.flat.main.GetSynonyms snäll

What were the synonyms for "snäll"?

Expand using link to the right to see a hint.

$ javac org/*/*/*.java && java org.flat.main.GetSynonyms snäll
[gullig, ljuv, snäll, trevlig, vänlig, älskvärd]

Open the main class source code and read the code carefully. Note how the list of potential synonyms is obtained via a call to a static method in the same class. While this is convenient for a small proof-of-concept like this tiny application, it is not very typical for a Java system. Applications tend to grow and change a lot over time, and we don't want to maintain growing code in one huge source code file. Static methods are also very limited, since they are tied to a class rather than to objects so we can't get the benefits of polymorphism either.

If you want to, you can try to understand how the Map of synonyms are built from the file. The file consists of pairs on each line on the form:

abakus;kulram
abbreviera;förkorta
abdikera;avgå
abdikera;avsäga sig tronen
abnorm;missbildad
abnorm;onaturlig
abnorm;onormal
abnormitet;avvikelse
abonnent;prenumerant
abonnera;boka

As you see, the first word is sometimes repeated, meaning that it has more than one synonym (one per repetition). So we need to read lines from the file and split them up. We'll use the first word as the key in a Map and the second word as a synonym in a list. This means that we have to create a new entry in the list if the first word isn't already there. The value for a new word is then a list with only one element - the second word. But if the first word already exists, it has a list to which we should add the second word. An example:

We find "abbreviera" and "förkorta". The first word "abbreviera" isn't in the map, so we add it as key with the value of a new list with only one element, "förkorta". Then we find "abdikera" and "avgå". Now, "abdikera" isn't in the list, so we do the same thing again - add "abdikera" as a key with the value of a new list with "avgå" as the only element. Then we find "abdikera" again, with the second word "avsäga sig tronen". Since "abdikera" already is in the map, we add "avsäga sig tronen" to its value list.

Task 2

OK, let's leave the stand-alone version and look at one version which is a little more object oriented (at least it creates and uses objects in the main method, rather than calling static methods, yey!).

The main purpose of this task is, again, to try and understand the code and what goes on when we run it.

Enter the 02-more-oo directory and open the main class org/flat/main/GetSynonyms.java

First of all, look at the import statements. There are no longer fifty seven (OK, it was eight in the previous class) import statements for the main class. This is a clear sign of delegation! Some one else will have to import the low-level stuff.

The main method in this version uses an object to do the work:

      SynonymEntry syn = new SynonymEntry(word);
      StringBuilder result = new StringBuilder("Synonyms for " + word + ":");
      for(String s : syn.synonyms()){
        result.append(" " + s);
      }

In order to understand the above we must know the idea behind the SynonymEntry class. It is supposed to represent a word and its corresponding list of synonyms (if any). From the code above, we can see that all we have to do is to create a new instance of class SynonymEntry and provide the word as an argument. The we can then ask the object via the reference to it, to give us a reference to its synonyms, if any.

So, where does the object get the list of synonyms from? Open the source code for the SynonymEntry class. Identify where the object of this class gets the list of synonyms from.

Expand using link to the right to see a hint.

It calls a static method in a helper class called Synonyms.

synonyms = Synonyms.getSynonyms(word);

Open the class org/flat/text/Synonyms.java and investigate it.

It turns out that this class, in its static convenience method getSynonyms() uses an object of class SynonymsLoader. So, let's investigate also this class. Open the source code and look at it. As you see, here's where all the import statements have gone. This is now the place where all the action is - the loading of the file and creation of the map with the dictionary of synonyms.

OK, so this is perhaps fine then? We've moved away all the low level stuff from the main method, so it doesn't depend on implementation details any more. It's an improvement, for sure. But what happens if we want to add more alternative ways to look up synonyms?

In the next version, we'll add networking to the application and use a server to look up synonyms one word at the time.

But first, compile and run this version and look up the synonyms for "beroende". What was the result?

Expand using link to the right to see a hint.

Synonyms for beroende: avhängig bundenhet inte fri missbruk osjälvständighet pundare torsk

$ javac org/*/*/*.java && java org.flat.main.GetSynonyms beroende
Synonyms for beroende: avhängig bundenhet inte fri missbruk osjälvständighet pundare torsk

Task 3

Change directory to 03-networked and look at the main class. It hasn't changed, so the new stuff must reside somewhere else! SynonymEntry hasn't changed either (check it out, if you don't believe us!), so it's got to be the Synonyms helper class!

Open org/flat/text/Synonyms.java and figure out what has changed. Can you describe the change from the last version?

Now, the class uses a different type of object to get the synonyms. It uses a SynonymsNetworkLoader, and it no longer gets the whole map in order to get the result for one specific word. This is due to the fact that the SynonymsNetworkLoader class (we agree, the name of the class is far from perfect!) only looks up one word at the time, and it queries a server to do so.

If you look at the new class, SynonymsNetworkLoader, you'll see how it asks a server for the synonyms given a word as an argument to the getSynonyms() method.

In order to try this version out, you need to open a second terminal, navigate to the 03-networked directory, compile and run the server:

$ cd 03-networked/
$ javac server/*.java && java server.SynonymServer files/synonyms.csv 
16270 words loaded.
Listening on ServerSocket[addr=0.0.0.0/0.0.0.0,localport=1234]

Now, the application can get the synonyms from the server rather than from file (let's ingore the fact that the server actually reads the same file as the previous version of the application did, use your imagination and pretend this server is on a different computer). The server listens for requests and reads a word from the client. Then the server replies with a list of synonyms, or an error message if no synonyms were found.

This means that the logic of the application has changed. Instead of reading all the synonyms into the memory (stored in a map), it now gets each list of synonyms from a server.

Compile and run the application with the server still running in the other terminal. Get the list of synonyms for "orörlig". What was printed in the server terminal? What was printed by the application on the client side?

Expand using link to the right to see a hint.

To compile the client application, you can do the following:

$ javac org/*/*/*.java && java org.flat.main.GetSynonyms orörlig

On the server-side (in the terminal running the server), you should see the following:

client said: orörlig
word synonyms: [fixerad, lam, orubblig, statisk, stel, stilla, stillastående]

That's just log messages and doesn't have to be what was sent to the client. So what did the client application get in terms of synonyms for "orörligt"?

$ javac org/*/*/*.java && java org.flat.main.GetSynonyms orörlig
Synonyms for orörlig: fixerad lam orubblig statisk stel stilla stillastående

Wasn't this fun? OK, fair enough. It wasn't. But at least it worked.

Next, we'll take things up a notch and see how we could re-write the application so that the user doesn't only supply the word as an argument, but also decides whether the synonyms should be looked up from file or from the network (server).

Task 4 - Introducing an interface to serve as a layer

For this task, you should park yourself in the 04-layered directory. A quick glance at the main class GetSynonyms tells us that it hasn't changed. We'll have to follow the chain of calls to see what's going on in this version. Open SynonymEntry and investigate how it works. In this version, it doesn't use the utility class method Synonyms.getSynonyms() anymore. Instead, this class uses a reference to an object of reference type SynonymsOracle. What kind of type is that?

Expand using link to the right to see a hint.

If you open the source code for SynonymsOracle, you'll see that it is an interface.

How is the SynonymsOracle reference variable initialized?

Expand using link to the right to see a hint.

In SynonymEntry, the SynonymsOracle is initialized via a call to SynonymsOracleFactory.getSynonymsOracle(). This is a pattern used when we want to encapsulate the creation of an instance and typically used for allowing the use of interface types. We have no idea (from the SynoymEntry class's perspective) what the concrete type is, just that it is some kind of SynonymsOracle.

Open the source code for SynonymsOracleFactory. If you wonder about the name oracle, it has nothing to do with the company or the database. We just use it as the name for an object which knows something we can ask it.

This class offers a public static method for getting a reference to an object of a class which implements the SynonymsOracle interface. It chooses concrete class based on a System property.

Do you recall how the user can set a System property on the command line when running a Java app?

Expand using link to the right to see a hint.

You use the -D flag and a key-value pair directly after it. If you want to set the property key=value you can do this:

$ java -Dkey=value SomeClass

From Java, you can then get the value of the property key like this: String key = System.getProperty("key"); or even provide a default value if it wasn't set: String key = System.getProperty("key", "Default value if key wasn't set");

Start the server in the other terminal and run the application and ask for the synonyms to "flexibel". Do it without setting the oracle.type property. Did anything happen in the terminal running the server?

Expand using link to the right to see a hint.

Nothing should have been printed from the server. Without the property, it should default to the SynonymsFileOracle implementation.

Now run the same query but set the property oracle.type to network. Was there any activity in the server terminal?

Expand using link to the right to see a hint.

This time, the server was used! You should see output logged in the server terminal similar to this:

client said: flexibel
word synonyms: [anpasslig, anpassningsbar, böjlig, reglerbar, rörlig, smidig, töjbar, öppen]

Task 5 - Summary

What was the point of the last version?

The point we wanted to show was that we could make the SynonymEntry class totally independent from the implementation of the object used to get the synonyms for a word. Whatever way is used to get the synonyms, the SynonymEntry class is insulated from knowing too much about it.

In order to achieve a barrier between the SynonymEntry class and the class used for getting the synonyms, we used an interface called SynonymsOracle.

Since the SynonymEntry class only uses a reference of type SynonymsOracle (the interface), and get the reference via the SynonymsOracleFactory class's factory method, the SynonymEntry class is protected from changes in the way synonyms are retrieved.

Imagine that the next step would be to use a database to store synonyms. The user now can choose between getting the synonyms data from a file, a server or a database. We don't have to change (or recompile) the SynonymEntry class.

The only classes affected by this change would be the SynonymsOracleFactory class (and the creation of a new class, SynonymsDatabaseOracle).

We could say that we have created a low level layer in our design, starting with the SynonymsOracle interface. All calls to low-level stuff like getting the synonyms from a file, network service, database or whatever, is now effectively hidden from the higher-level code dealing with the user interaction and presentation.

Note also that thanks to our "layered architecture", we don't have to recompile the application between running it first using the file, and then again using the server.

In a later lecture/chapter we'll see how we could make the whole loading of new classes dynamic so that we don't even have to change and recompile the factory if we add a new implementation.

The weakness of the example in this exercise is that we still have to change and re-compile the factory if we add, e.g. a SynonymsDatabaseOracle implementation.

Note that there is also a term called "tiered architecture" where the term "tier" is used instead of "layer". Usually, a "tier" refers to an abstraction running on its own server (or at least in its own process). If you like, you could think of our server-version as using a "tier". The basic idea is the same. Divide your application in parts protected from eachother.

Files

Source code

  • The whole repository for the Java Database chapters on github
  • A web page where you can download the exercises directory for this chapter's exercises can be found here

Chapter links

previous | next (to-be-decided)