JAVA exPress > Archive > Issue 4 (2009-06-01) > Layering

Layering

Why layers (tiers)?

Almost every programmer has heard of multi-tier (two-tier, three-tier or n-tier) architecture. However, in many conversations concerning software development I often had a feeling that this element has little impact on everyday life of programmers’ work. Although this subject is strictly related to system architecture. No matter what your role in the project is, it can always affect the things that you are working on. The purpose of this article is to explain how to use the idea of multi-tier architecture in practice and how its understanding may improve the quality of your code. It also attempts to show some pragmatic approaches in this matter.

What is it all about?

The notion of layers, similarly to many other human inventions, was introduced in order to make our life easier. In this case, the main goal was to simplify computer systems development. To organize system structure it is advisable worth to determine logical parts of a system that are related to each other and share some common responsibilities. Therefore, in many systems we can distinguish, for instance:

  • user interface that is responsible for user interaction, in most cases by appropriate views or windows,
  • domain which could be defined by main system data, processing, algorithms, computations and system lifecycle,
  • communication with the outside world, which can be understood as ways of accessing data, writing and reading it persistently and cooperating with external systems.

That was only an example of splitting system features into layers. There might be more of them and they may be defined in various other ways.

The first determinant that arises from using layers is the division of a system into logical parts with clearly defined responsibilities. These parts are orthogonal to system functions.

The second determinant is defined by relations between layers. Similarly to onions, computer systems are built out of layers. The most outer layers are closer to the users of an application. Therefore, layers are ordered and serve functions between themselves. This dependency is shown below.

User Interface

Domain

Data Access

As you can see, the most outer layer is a user interface, which is responsible for communication – it deals with querying and showing data and with a logical views organisation. The actual processing in a system is delegated to the domain classes, since it is responsible for main application functions (separately from the UI). On the other hand, all write, read or external systems communication operations in the domain layer are delegated to the data access layer. Only the adjacent layers can communicate with each other directly; however, it is always the upper layer that calls the one below.

What are the main advantages of using layers (tiers)?

  • Layers are a way of dividing a system into high-level logical components – it is easier to manage and understand them because each of them has got clearly defined responsibility.
  • Each layer is uniquely constructed and provides a set of interfaces that should be implemented.
  • Tiers should be treated as autonomous entities which are to a large extent independent of each other.
  • Components in a layer may be reused in other applications with the same layered structure and this supports the idea of creating application frameworks.
  • Independent teams may independently work on the development of each system layer.
  • Components from different layers may be independently implemented, installed, maintained and upgraded.

Of course, there are also some drawbacks:

  • The existence of several layers may cause some serious changes in functions of a system and they often enforce cascade modifications in numerous layers.
  • Layers may affect performance of a system.

If you create your application working on your own or if you have influence on a system’s architecture, then layers will certainly help you to take control of a project and make its creation simpler – as the responsibilities in a system are clearly determined. If you come across situations when, after a couple of development days, your application becomes inconsistent – there are many iterations and you do not know how to separate the code responsible for database queries from the rest of the system, then you can overcome this obstacle by dividing the system into layers.

If you are a member of a larger team, then it is very likely that the system’s architecture has already been chosen and someone has decided that your system will follow the layers architecture schema. It may be defined differently in different technologies, but the basic idea always remains the same. Knowledge of layers allows you to understand easily the system you are developing and makes it significantly easier to get adjusted to its boundaries. You should then be aware what your classes are responsible for and what is beyond their scope. Layers are like continents on the map of the world – you know what can be found and where it is located.

Simple example with no layers

We will take a closer look at a straightforward console program. Even in such a simple application, it is possible to isolate layers. Of course, the important question that should be asked is "Is it worth to apply layers pattern?". For the needs of this article and to keep it simple we will deal with a console application.

Our simple application will provide users with a simple dictionary that would be able to translate words from English to Polish. It should make possible to:

  • add new words and their translations,
  • remove a word and its translation,
  • find a word and its equivalent,
  • show all the words and their translations:
    • in random order,
    • alphabetically sorted,
    • sorted according to dates when the words were added,
  • the application should persistently store its data.

One of the possible implementations of such a system may look like the one below:

    package bnsit.layers.wordbook;

    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.Comparator;
    import java.util.Date;
    import java.util.List;
    import java.util.Scanner;

    public class Wordbook {

        private static String FILENAME = "wordbook.dat";

        public static void main(String[] args)
                throws FileNotFoundException, IOException, ClassNotFoundException {

            List<DictionaryWord> words = loadData();

            boolean ok = true;
            Scanner s = new Scanner(System.in);

            System.out.println("Welcome to Wordbook.");

            while (ok) {
                System.out.print("dictionary> ");
                String line = s.nextLine();
                String [] arguments = line.split(" ");

                if ( line.startsWith( "search" ) ) {
                    if ( arguments.length != 2 ) {
                        System.out.println( "Command syntax: search <english_word>" );
                    } else {
                        String englishWord = arguments[1];
                        for (DictionaryWord word : words) {
                            if ( word.getEnglishWord().equals(englishWord) ) {
                                System.out.println( word );
                            }
                        }
                    }
                } else if ( line.startsWith( "add" ) ) {
                    if ( arguments.length != 3 ) {
                        System.out.println(
                            "Command syntax: add <english_word> <polish_word>" );
                    } else {
                        String englishWord = arguments[1];
                        String polishWord = arguments[2];
                        DictionaryWord dictionaryWord
                                = new DictionaryWord(
                                        englishWord, polishWord, new Date());
                        words.add( dictionaryWord );
                        System.out.println( "Word added: " + dictionaryWord );
                        writeData(words);
                    }
                } else if ( line.startsWith( "delete" ) ) {
                    if ( arguments.length != 2 ) {
                        System.out.println(
                                "Command syntax:delete <word_natural_number>");
                    } else {
                        int wordNumber = Integer.valueOf( arguments[1] );
                        words.remove( wordNumber - 1 );
                        writeData(words);
                    }
                } else if ( line.equals( "show" ) ) {
                    showList(words);
                } else if ( line.equals( "show sorted by name" ) ) {
                    showList(words, new Comparator<DictionaryWord>() {
                        @Override
                        public int compare(DictionaryWord o1, DictionaryWord o2) {
                            return o1.getEnglishWord()
                                    .compareTo(o2.getEnglishWord());
                        }
                    });
                } else if ( line.equals( "show sorted by date" ) ) {
                    showList(words, new Comparator<DictionaryWord>() {
                        @Override
                        public int compare(DictionaryWord o1, DictionaryWord o2) {
                            return o1.getDate().compareTo(o2.getDate());
                        }
                    });
                } else if ( line.equals( "exit" ) ) {
                    ok = false;
                } else {
                    System.out.println( "Command not found: '" + line + "'" );
                }
            }
            s.close();
        }

        private static void writeData(List<DictionaryWord> words)
                        throws IOException, FileNotFoundException {
            ObjectOutputStream objectOutputStream
                    = new ObjectOutputStream( new FileOutputStream( FILENAME ) );
            objectOutputStream.writeObject( words );
        }

        private static List<DictionaryWord> loadData()
                throws FileNotFoundException, IOException, ClassNotFoundException {

            List<DictionaryWord> result = new ArrayList<DictionaryWord>();
            File file = new File( FILENAME );
            if ( file.exists() ) {
                ObjectInputStream objectInputStream
                        = new ObjectInputStream( new FileInputStream( FILENAME ) );
                result = (List<DictionaryWord>) objectInputStream.readObject();
            }
            return result;
        }

        private static void showList(List<DictionaryWord> words) {
            int counter = 0;
            for (DictionaryWord word : words) {
                System.out.println( ++counter + " " + word );
            }
        }

        private static void showList(List<DictionaryWord> words,
                        Comparator<DictionaryWord> comparator) {

            List<DictionaryWord> wordsCopy = new ArrayList<DictionaryWord>(words);
            Collections.sort(wordsCopy, comparator);
            showList(wordsCopy);
        }
    }

This is a typical application with so-called flat architecture. Of course, because the system is so simple, this solution has got a lot of advantages – it is clear, concise and easy to navigate when you browse its source code. However, when the system is developed, this type of solution will be getting more and more in maintenance. There will be more repetitions, the structure of the source code will be getting more and more complicated and the elements of the user interface and the data access part will get mingled.

Let us introduce layers

Is it possible to distinguish layers in such a simple system? Of course, it is! By looking at the application, we can separate elements that are responsible for: user interface (such as a user data input or showing information on the screen) – Wordbook class data processing (sorting, adding new words) – WordbookService class data access (saving to and loading from a file) – WordbookDao class.

All of these are shown in the following picture:

Let us take a look at the classes in order to determine their main characteristics from each layer. We will start with the user interface class – Wordbook (the source code below). This class, compared to the code from the previous version, has got specifically identified responsibility which is the interaction with the user. Only functions related to handling console and delegating specific tasks to WordbookService has been left there, which in this case represents domain layer. Wordbook class:

  • retrieves data from the console,
  • validates and analyzes user input data,
  • shows appropriate messages,
  • delegates concrete operations.

Please note that there is no single line of code related to the internal logic of the system, there is only the user interface. Summing up, the class was reduced so that it plays a single role in the system.

    public class Wordbook {
        private WordbookService wordbookService = new WordbookService();

        public static void main(String[] args) {
            Wordbook wordbook = new Wordbook();
            wordbook.run();
        }

        public void run() {

            boolean ok = true;
            Scanner s = new Scanner(System.in);

            System.out.println("Welcome to Wordbook.");

            while (ok) {
                System.out.print("dictionary> ");
                String line = s.nextLine();
                String [] arguments = line.split(" ");

                if ( line.startsWith( "search" ) ) {
                    if ( arguments.length != 2 ) {
                        System.out.println(
                                        "Command syntax: search <english_word>" );
                    } else {
                        String englishWord = arguments[1];

                        List<DictionaryWord> words
                                = wordbookService.find( englishWord );
                        for (DictionaryWord word : words) {
                            System.out.println( word );
                        }
                    }
                } else if ( line.startsWith( "add" ) ) {
                    if ( arguments.length != 3 ) {
                        System.out.println( "Command syntax: "
                                        + "add <english_word> <polish_word>" );
                    } else {
                        String englishWord = arguments[1];
                        String polishWord = arguments[2];
                        DictionaryWord dictionaryWord
                            = wordbookService.createNewWord(
                                            englishWord, polishWord);
                        System.out.println( "Word added: " + dictionaryWord );
                    }
                } else if ( line.startsWith( "delete" ) ) {
                    if ( arguments.length != 2 ) {
                        System.out.println( "Command syntax: "
                                        + "delete <word_natural_number>" );
                    } else {
                        int wordNumber = Integer.valueOf( arguments[1] );
                        wordbookService.remove( wordNumber );
                    }
                } else if ( line.equals( "show" ) ) {
                    List<DictionaryWord> words = wordbookService.findAll();
                    showList(words);
                } else if ( line.equals( "show sorted by name" ) ) {
                    List<DictionaryWord> words
                        = wordbookService.findAllSortedByName();
                    showList(words);
                } else if ( line.equals( "show sorted by date" ) ) {
                    List<DictionaryWord> words
                        = wordbookService.findAllSortedByDate();
                    showList(words);
                } else if ( line.equals( "exit" ) ) {
                    ok = false;
                } else {
                    System.out.println( "Command not found: '" + line + "'" );
                }
            }
            s.close();
        }

        private void showList(List<DictionaryWord> words) {
            int counter = 0;
            for (DictionaryWord word : words) {
                System.out.println( ++counter + " " + word );
            }
        }
    }

Specific operations are delegated to WordbookService class which deals with the main tasks related to system functions. On the other hand, operations of persistent storing or data searches are delegated to another object – WordbookDao.

Let us now have a look at WordbookService class. What is important to notice?

1. The methods in this class respond to the system’s functions, e.g. find, delete, find all.

2. All the methods are short and concise.

3. The methods do not depend on the user interface, so they can cooperate with any user interface!

4. Operations that depend on a specific data source are delegated to WordbookDao class.

    public class WordbookService {

        private WordbookDao wordbookDao = new WordbookDao();

        public List<DictionaryWord> find(String englishWord) {
            return wordbookDao.find(englishWord);
        }

        public DictionaryWord createNewWord(String englishWord, String polishWord) {
            DictionaryWord dictionaryWord
                = new DictionaryWord( englishWord, polishWord, new Date());
            wordbookDao.save(dictionaryWord);
            return dictionaryWord;
        }

        public void remove(int wordNumber) {
            DictionaryWord dictionaryWord
                = wordbookDao.findByIndex( wordNumber - 1  );
            wordbookDao.remove(dictionaryWord);
        }

        public List<DictionaryWord> findAll() {
            return wordbookDao.findAll();
        }

        public List<DictionaryWord> findAllSortedByName() {
            List<DictionaryWord> words = wordbookDao.findAll();
            Collections.sort(words, new Comparator<DictionaryWord>() {
                @Override
                public int compare(DictionaryWord o1, DictionaryWord o2) {
                    return o1.getEnglishWord().compareTo(o2.getEnglishWord());
                }
            });
            return words;
        }

        public List<DictionaryWord> findAllSortedByDate() {
            List<DictionaryWord> words = wordbookDao.findAll();
            Collections.sort(words, new Comparator<DictionaryWord>() {
                @Override
                public int compare(DictionaryWord o1, DictionaryWord o2) {
                    return o1.getDate().compareTo(o2.getDate());
                }
            });
            return words;
        }
    }

Let us look at the Wordbook class. There are a few elements which require your attention:

1. This class is responsible for cooperation with data and the data source (in this case it is a file that contains serialized data).

2. Methods in this class represent basic operations related to the system data.

3. Methods are short, precise and their responsibilities are clearly defined.

4. Thanks to the fact that the data access has been encapsulated, it is easy to change the way the data is written (e.g. to a XML file) and it will not affect the rest of the application.

    public class WordbookDao {

        final private String FILENAME = "wordbook.dat";
        private List<DictionaryWord> words = null;

        public WordbookDao() {
            words = loadData();
        }

        public List<DictionaryWord> find(String englishWord) {
            List<DictionaryWord> result = new ArrayList<DictionaryWord>();
            for (DictionaryWord word : words) {
                if ( englishWord.equals(word.getEnglishWord()) ) {
                    result.add(word);
                }
            }
            return result;
        }

        public DictionaryWord findByIndex(int i) {
            return words.get( i );
        }

        public List<DictionaryWord> findAll() {
            return new ArrayList<DictionaryWord>(words);
        }

        public void save(DictionaryWord dictionaryWord) {
            words.add(dictionaryWord);
            writeData(words);
        }

        public void remove(DictionaryWord dictionaryWord) {
            words.remove( dictionaryWord );
            writeData(words);
        }

        private void writeData(List<DictionaryWord> words) {
            ObjectOutputStream objectOutputStream;
            try {
                objectOutputStream = new ObjectOutputStream(
                                new FileOutputStream(FILENAME));
                objectOutputStream.writeObject(words);
            } catch (Exception e) {
                throw new WordbookDaoException(e);
            }
        }

        private List<DictionaryWord> loadData() {
            List<DictionaryWord> result = new ArrayList<DictionaryWord>();
            File file = new File(FILENAME);
            if (file.exists()) {
                ObjectInputStream objectInputStream;
                try {
                    objectInputStream = new ObjectInputStream(
                                    new FileInputStream(FILENAME));
                    result = (List<DictionaryWord>) objectInputStream.readObject();
                } catch (Exception e) {
                    throw new WordbookDaoException(e);
                }
            }
            return result;
        }
    }

That is how we have managed to split the application into layers. What are the consequences of this step? Well, responsibilities are now clearly defined, the application is ready for further changes, the code has become easier to manage and better organized – it is clear where to look for the specific elements. On the other hand, the application structure is now more complex – there are three classes instead of one. Also, the application performance may be now worse. Well, there is no rose without a thorn. In simple systems, which are composed from less than twenty classes, such an approach would probably consume too much work. In bigger systems, however, it would make their structure clarified and would make navigation much more easy.

Layers switching

One of the main features of the layers concept is the possibility of switching layers almost in the real-time. This allows you to dynamically adjust your solution wrapped in one of your layers with a very little impact on the other parts of a system. And this is the magic of the layers concept.

In order to do this, we should make our system more flexible. At the moment the classes are strictly tied to each other. We will use two techniques to make them loosely coupled – these are: interfaces for the classes that are in a layer and Dependency Injection pattern. Both of these will simplify changing of the application dependencies. The system will look like this:

Thanks to interfaces, system elements are now loosely coupled and we can change their concrete implementations.

As you can see in the picture, Wordbook class implements WordbookService interface, which means that it is possible to insert any implementation in its place (that is based on POJO or EJB). To enable dependency injection, we have added getters and setters and put a code that builds related classes in the method main.

    public class Wordbook {
        private WordbookService wordbookService = null;

        public static void main(String[] args) {
            Wordbook wordbook = new Wordbook();

            PlainWordbookService plainWordbookService = new PlainWordbookService();
            plainWordbookService.setWordbookDao(new SerializationWordbookDao());
            wordbook.setWordbookService(plainWordbookService);

            wordbook.run();
        }
        // ...
        public WordbookService getWordbookService() {
            return wordbookService;
        }

        public void setWordbookService(WordbookService wordbookService) {
            this.wordbookService = wordbookService;
        }
    }

By analogy, we have corrected PlainWordbookService class which implements WordbookService interface.

    public interface WordbookService {

        public abstract List<DictionaryWord> find(String englishWord);

        public abstract DictionaryWord createNewWord(String englishWord,
                        String polishWord);

        public abstract void remove(int wordNumber);

        public abstract List<DictionaryWord> findAll();

        public abstract List<DictionaryWord> findAllSortedByName();

        public abstract List<DictionaryWord> findAllSortedByDate();

    }

    public class PlainWordbookService implements WordbookService {

        private WordbookDao wordbookDao = null;

        // ...

        public WordbookDao getWordbookDao() {
            return wordbookDao;
        }

        public void setWordbookDao(WordbookDao wordbookDao) {
            this.wordbookDao = wordbookDao;
        }

    }

In a similar way, WordbookDao class has been modified. You can see its fragment below. The whole source code for this article can be downloaded here: http://www.bnsit.pl/rozwarstwienie/.

    public interface WordbookDao {

        public abstract List<DictionaryWord> find(String englishWord);

        public abstract DictionaryWord findByIndex(int i);

        public abstract List<DictionaryWord> findAll();

        public abstract void save(DictionaryWord dictionaryWord);

        public abstract void remove(DictionaryWord dictionaryWord);

    }

    public class SerializationWordbookDao implements WordbookDao {

        final private String FILENAME = "wordbook.dat";
        private List<DictionaryWord> words = null;

        public SerializationWordbookDao() {
            words = loadData();
        }

        public List<DictionaryWord> find(String englishWord) {
            List<DictionaryWord> result = new ArrayList<DictionaryWord>();
            for (DictionaryWord word : words) {
                if ( englishWord.equals(word.getEnglishWord()) ) {
                        result.add(word);
                }
            }
            return result;
        }

        // ...

        private List<DictionaryWord> loadData() {
            List<DictionaryWord> result = new ArrayList<DictionaryWord>();
            File file = new File(FILENAME);
            if (file.exists()) {
                ObjectInputStream objectInputStream;
                try {
                    objectInputStream = new ObjectInputStream(
                                    new FileInputStream(FILENAME));
                    result = (List<DictionaryWord>) objectInputStream.readObject();
                } catch (Exception e) {
                    throw new WordbookDaoException(e);
                }
            }
            return result;
        }
    }

Now the data access layer in our sample application works with a file with serialized data. It is very easy to develop some other solution by implementing WordbookDao interface, for example, based on JDBC. Then, the same application, with no major changes in the user interface and domain layers, will work with database! You can treat it as an exercise, the correct answer can be found on the following website: http://www.bnsit.pl/rozwarstwienie/.

The approach described in this article may be applied to more complex systems.

Testing

The next profit from using layers can be noticed when we are testing our system. To spot that it is enough to compare the initial and the final version of the sample program. Which is easier to test? Perhaps the monolithic code that contains several alternative paths mixed together with user interface, domain and data access functions? Or maybe classes that contain small methods with clearly defined responsibilities? The unit testing becomes a pleasure.

Summary

Layers are not a cure-all drug, that will overcome all obstacles in software design. In complex systems they are indispensable if they are to be effectively developed. However, each layer constitutes an additional level of complexity. In the case of minor applications it is a decision of an individual, which is worth-taking if you want to develop your application in a structuralized way. At this stage it is up to you how many and what kind of layers you want to apply. Good luck with your experimentation!

Translation: Łukasz Baran

Nobody has commented it yet.

Only logged in users can write comments

Developers World