Listopad 14, 2018

Po cholere interfejsy w Javie?

Interfejsy…

…z pewnością przerabiając nie jeden nawet niezbyt skomplikowany kurs Javy natknąłeś się na interfejsy. Często autorzy kursu – szczególnie, gdy pokazują proces budowania aplikacji – zachęcają do tworzenia interfejsów. Robię tak samo w swoim darmowym kursie Javy od podstawy. Wytłumaczę i pokażę Ci na konkretnym przykładzie dlaczego stosowanie interfejsów jest takie ważne, choć na samym początku programistycznej przygody mogą wydawać się bez sensu.

Otóż nie, nie są bez sensu czego się zaraz przekonasz. 😉

Szybkie przypomnienie – czym są interfejsy?

Interfejsy są pewną abstrakcją, w której możemy definiować co ma robić implementującą go klasa, a nie dokładnie jak. Jest on wspólnym typem dla wielu klas. Dzięki temu mamy pewność, że przeróżne implementacje mają właśnie te metody, które zawiera interfejs. Bez żadnych wątpliwości możemy z nich korzystać.

Mały przykład: BeepService:

public interface BeepService {
  void beep();
}

Implementacja przy użyciu printa:

public class BeepServicePrint implements BeepService {
  public void beep() {
    System.out.println("Beeeep!");
  }
}

Oraz implementacja przy użyciu dźwięku:

public class BeepServiceSound implements BeepService {
  public void beep() {
    Sound.play("beep.mp3");
  }
}

Sound.play(„beep.mp3”); jest moim małym wymysłem, aby pokazać Ci różnicę, że implementacje mogą się znacznie różnić. 😉

Mając stworzony taki interfejs mamy 100% pewność, że obie implementacje posiadają metodę beep.

import dao.CommentDao;
import dao.CommentDaoDatabase;
import dao.CommentDaoFile;
import dao.CommentDaoLocalStorage;
import model.Comment;
import service.CommentService;
import service.CommentServiceImpl;

public class Main {
    public static void main(String[] args) {      
        BeepService beepServicePrint = new BeepServicePrint();
        BeepService beepServiceSound = new BeepServiceSound();
        
        beepServicePrint.beep();
        beepServiceSound.beep();
    }
}

I tyle wiedzy w zupełności wystarczy Ci, aby w całości pochłonąć mój poniższy przykład. 😛

Boilerplate

Oczywiście, potrzebujemy stworzyć sobie małe środowisko, w którym będziemy pracować – w tym celu potrzebujemy modelu projektu Maven z takim oto pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>pl.maniaq</groupId>
    <artifactId>what-give-interfaces</artifactId>
    <version>1.0-SNAPSHOT</version>


    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>2.23.0</version>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>2.4</version>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>Main</mainClass>
                            <addClasspath>true</addClasspath>
                        </manifest>
                    </archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Model komentarza – Comment:

package model;

import java.util.Objects;

public class Comment {
    private Long id;
    private String owner;
    private String text;

    public Comment(Long id, String owner, String text) {
        this.id = id;
        this.owner = owner;
        this.text = text;
    }

    public Long getId() {
        return id;
    }

    public String getOwner() {
        return owner;
    }

    public String getText() {
        return text;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Comment comment = (Comment) o;
        return Objects.equals(id, comment.id) &&
                Objects.equals(owner, comment.owner) &&
                Objects.equals(text, comment.text);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, owner, text);
    }

    @Override
    public String toString() {
        return id + "|" + owner + "|" + text;
    }
}

I jeszcze CommentService – oczywiście bez interfejsu ani rusz!

package service;

import model.Comment;

public interface CommentService {
    void addComment(Comment comment);
    void removeComment(Comment comment);
    void editComment(Comment comment);
}

Oraz jego implementacja korzystającą CommentDao

package service;

import dao.CommentDao;
import model.Comment;

public class CommentServiceImpl implements CommentService {
    private CommentDao commentDao;

    public CommentServiceImpl(CommentDao commentDao) {
        this.commentDao = commentDao;
    }


    @Override
    public void addComment(Comment comment) {
        commentDao.save(comment);
    }

    @Override
    public void removeComment(Comment comment) {
        commentDao.deleteCommentById(comment.getId());
    }

    @Override
    public void editComment(Comment comment) {
        commentDao.update(comment);
    }
}

I tutaj zaczyna się nasza przygoda…

CommentDao

Naszym królikiem doświadczalnym będzie CommentDao, czyli interfejs, którym będziemy się bawić. Wygląda on dokładnie tak:

package dao;

import model.Comment;

import java.util.Collection;
import java.util.Optional;

public interface CommentDao {
    Comment save(Comment comment);

    Collection<Comment> getAllComments();
    Optional<Comment> getCommentById(Long id);
    Collection<Comment> getCommentsByOwner(String owner);

    void deleteCommentById(Long id);
    void deleteCommentsByOwner(String owner);

    Comment update(Comment comment);
}

Co warto zauważyć już na samym początku?

Interfejs zawiera większość metod, które powinny zawierać obiekty DAO (Data Access Object) – takie jak: dodawanie, pobieranie, edycja i usuwanie elementów.

Niby nic skomplikowanego, prosty interfejs, a dzięki niemu możemy uwspólnić wiele implementacji klas DAO. Możesz zapytać – ale jak to wiele? A no tak, w końcu klasa DAO nie oznacza od razu operację na bazie danych – czasami może to być pamięć, innym razem pliki, aż w końcu można skorzystać z baz danych.

Z baz danych, których jest wiele – nie tylko jedna. Możesz używać MySQL, Oracle, Postgres – albo innej, jaką sobie tylko wyobrazisz.

Jednak jakiej bazy byś nie wybrał lub w ogóle zmienił formę przetrzymywania danych – chociażby na plik – to interfejs naszego DAO powinien zostać zawsze taki sam! W końcu np. mnie jako programistę software dla klienta nie interesuje, gdzie on dokładnie będzie trzymał dane – ważne, aby interfejs, z którego korzysta aplikacja był zawsze spójny, a przecież implementacja zawsze może być podmieniona np. w kolejnej wersji projektu decydujemy się na zapis do bazy zamiast katować biedne pliki. 😉

W poprzednim akapicie padły bardzo ważne słowa:

Ważne, aby interfejs, z którego korzysta aplikacja był zawsze spójny, a przecież implementacja zawsze może być podmieniona

I właśnie doszliśmy do bardzo ważnego momentu. Dzięki interfejsom zawsze korzystamy z tych samych metod, nie zawsze interesuje nas implementacja metod, z których korzystamy. Zazwyczaj chcemy tylko, aby działały poprawnie. 😉

O ile dużo powiedziałem o tworzeniu interfejsu w celu podmiany implementacji (kiedykolwiek) to warto się zatrzymać trochę nad samym tworzeniem interfejsu. Choć nie jest to skomplikowany proces, to warto zapamiętać jedną zasadę – i nie tylko podczas tworzenia interfejsów.

Jako typ danych zawsze korzystaj z interfejsów, zamiast z konkretnych implementacji. Korzystaj z jak największego poziomu abstrakcji.

Co mam na myśli?

O ile mój interfejs wygląda jak wygląda, to w sumie mógłby wyglądać równie dobrze tak:

package dao;

import model.Comment;

import java.util.LinkedList;
import java.util.Optional;
import java.util.Set;

public interface CommentDao {
    Comment save(Comment comment);

    LinkedList<Comment> getAllComments();
    Optional<Comment> getCommentById(Long id);
    Set<Comment> getCommentsByOwner(String owner);

    void deleteCommentById(Long id);
    void deleteCommentsByOwner(String owner);

    Comment update(Comment comment);
}

Co się zmieniło? – zamieniłem interfejs Collection dokładnie na LinkedList oraz Set. Czy to dużo zmienia?

A no niby nie, w końcu nadal korzystamy z kolekcji z pakietu java.util – to jednak tym razem zawężamy krąg używanych kolekcji, a czasami nawet go w ogóle niszczymy (LinkedList).

Interfejs powinien zostać jak najbardziej generyczny, czyli powinien korzystać z jak najwyższego poziomu abstracji – czemu już w interfejsie mamy ograniczać się do konkretnej implementacji – w tym przypadku LinkedList – zamiast użyć interfejsu List lub Collection, aby konkretną implementację mógł wybrać programista tworzący kod klasy?

O ile użycie interfejsu Set może mieć swoje uzasadanienie – chcemy unikalne elementy – to użycie typu LinkedList jest bardzo złą praktyką. Kto wie, czy w pewnej implementacji tego interfejsu bardziej wydajną opcją nie będzie użycie ArrayList – no kto wie? 😉

Ostatecznie nasz interfejs zostawimy w najbardziej abstrakcyjnej formie:

package dao;

import model.Comment;

import java.util.Collection;
import java.util.Optional;

public interface CommentDao {
    Comment save(Comment comment);

    Collection<Comment> getAllComments();
    Optional<Comment> getCommentById(Long id);
    Collection<Comment> getCommentsByOwner(String owner);

    void deleteCommentById(Long id);
    void deleteCommentsByOwner(String owner);

    Comment update(Comment comment);
}

Dosyć tego gadania – pomyślisz, przejdźmy w końcu do obiecanych przykładów!

Już pędzimy, przygotowałem dla Ciebie przykład użycia wyżej stworzonego interfejsu – gotowy na dalszą część przygody?

Interfejs w akcji

Wspominałem, że nasz interfejs może być wykorzystany na milion sposobów – no przesadziłem, na pewno na trzy. 😉

  • Wykorzystanie bazy danych,
  • Wykorzystanie pliku,
  • Wykorzystanie pamięci.

I właśnie te trzy sposoby zaimplementujemy, a one będą implementować interfejs CommentDao. 😉

Na wstępie uprzedzę, że nie będę się wczuwał w tłumaczenie implementacji, nie o to tutaj chodzi. 

Local storage

Pierwsza implementacja jakiej się podejmiemy to localStorage, która będzie korzystała po prostu z pamięci.

public class CommentDaoLocalStorage implements CommentDao {
    private List<Comment> comments = new LinkedList<>();
    
    @Override
    public Comment save(Comment comment) {
        return null;
    }

    @Override
    public Collection<Comment> getAllComments() {
        return null;
    }

    @Override
    public Optional<Comment> getCommentById(Long id) {
        return Optional.empty();
    }

    @Override
    public Collection<Comment> getCommentsByOwner(String owner) {
        return null;
    }

    @Override
    public void deleteCommentById(Long id) {

    }

    @Override
    public void deleteCommentsByOwner(String owner) {

    }

    @Override
    public Comment update(Comment comment) {
        return null;
    }
}

Czyli wszystkie operacje wykonujemy po prostu na liście. 😉

package dao;

import model.Comment;

import java.util.*;
import java.util.stream.Collectors;


public class CommentDaoLocalStorage implements CommentDao {
    private List<Comment> comments = new LinkedList<>();

    @Override
    public Comment save(Comment comment) {
        comments.add(comment);
        return comment;
    }

    @Override
    public Collection<Comment> getAllComments() {
        return Collections.emptyList();
    }

    @Override
    public Optional<Comment> getCommentById(Long id) {
        return comments.stream()
                .filter(comment -> comment.getId().equals(id))
                .findFirst();
    }

    @Override
    public Collection<Comment> getCommentsByOwner(String owner) {
        return comments.stream()
                .filter(comment -> comment.getOwner().equals(owner))
                .collect(Collectors.toList());
    }

    @Override
    public void deleteCommentById(Long id) {
        comments.removeIf(comment -> comment.getId().equals(id));
    }

    @Override
    public void deleteCommentsByOwner(String owner) {
        comments.removeIf(comment -> comment.getOwner().equals(owner));

    }

    @Override
    public Comment update(Comment comment) {
        deleteCommentById(comment.getId());
        save(comment);
        return comment;
    }
}

stream(), filter, collect oraz removeIf weszły w życie wraz z Java 8 – wszystkie te metody korzystają z wyrażeń lambda.

File

Kolejna implementacja będzie korzystała z pliku jako naszego miejsca do składowania danych. 😉

package dao;

import model.Comment;

import java.io.*;
import java.util.*;
import java.util.stream.Collectors;

public class CommentDaoFile implements CommentDao {
    private final static String FILE_NAME = "comments.txt";

    public CommentDaoFile() {
        File file = new File(FILE_NAME);
        try {
            file.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void clearFile() throws FileNotFoundException {
        PrintWriter pw = new PrintWriter(FILE_NAME);
        pw.close();
    }

    private void save(Collection<Comment> comments) {


    }

    @Override
    public Comment save(Comment comment) {

    }

    @Override
    public Collection<Comment> getAllComments() {
        
    }

    @Override
    public Optional<Comment> getCommentById(Long id) {
        
    }

    @Override
    public Collection<Comment> getCommentsByOwner(String owner) {
        
    }

    @Override
    public void deleteCommentById(Long id) {
       
    }

    @Override
    public void deleteCommentsByOwner(String owner) {
        
    }

    @Override
    public Comment update(Comment comment) {
        
    }
}

Implementacja będzie podobna do poprzedniej – tutaj zawsze cały plik będzie wczytywany do listy, będą wykonywane na niej operacje i z powrotem zostanie zapisana do pliku.

package dao;

import model.Comment;

import java.io.*;
import java.util.*;
import java.util.stream.Collectors;

public class CommentDaoFile implements CommentDao {
    private final static String FILE_NAME = "comments.txt";

    public CommentDaoFile() {
        File file = new File(FILE_NAME);
        try {
            file.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void clearFile() throws FileNotFoundException {
        PrintWriter pw = new PrintWriter(FILE_NAME);
        pw.close();
    }

    private void save(Collection<Comment> comments) {
        try {
            clearFile();
            PrintWriter printWriter = new PrintWriter(new FileOutputStream(FILE_NAME, true));
            for(Comment comment : comments) {
                printWriter.write(comment.toString() + "\n");
            }
            printWriter.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

    }

    @Override
    public Comment save(Comment comment) {
        List<Comment> commentList = new LinkedList<>(getAllComments());
        commentList.add(comment);
        save(commentList);
        return comment;
    }

    @Override
    public Collection<Comment> getAllComments() {
        List<Comment> commentList = new ArrayList<>();

        try {
            BufferedReader  bufferedReader = new BufferedReader(new FileReader(FILE_NAME));
            String readLine = bufferedReader.readLine();

            while(readLine != null) {
                String [] values = readLine.split("|");
                Comment comment = new Comment(Long.parseLong(values[0]), values[1], values[2]);
                commentList.add(comment);

                readLine = bufferedReader.readLine();
            }
            bufferedReader.close();

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }


        return commentList;
    }

    @Override
    public Optional<Comment> getCommentById(Long id) {
        return getAllComments().stream()
                .filter(comment -> comment.getId().equals(id))
                .findFirst();
    }

    @Override
    public Collection<Comment> getCommentsByOwner(String owner) {
        return getAllComments().stream()
                .filter(comment -> comment.getOwner().equals(owner))
                .collect(Collectors.toList());
    }

    @Override
    public void deleteCommentById(Long id) {
        List<Comment> commentList = new LinkedList<>(getAllComments());
        commentList.removeIf(comment -> comment.getId().equals(id));
        save(commentList);
    }

    @Override
    public void deleteCommentsByOwner(String owner) {
        List<Comment> commentList = new LinkedList<>(getAllComments());
        commentList.removeIf(comment -> comment.getOwner().equals(owner));
        save(commentList);
    }

    @Override
    public Comment update(Comment comment) {
        deleteCommentById(comment.getId());
        List<Comment> commentList = new LinkedList<>(getAllComments());
        commentList.add(comment);
        save(commentList);
        return comment;
    }
}

Podobnie jak w implementacji LocalStorage skorzystałem z wyrażeń lambda i strumienii.

Database

Ostatni sposób, który jest najbardziej sexy to użycie bazy danych – ja skorzystam z MySQL, dlatego w pliku pom.xml był dodany driver właśnie do tej bazy danych. 😉

package dao;

import model.Comment;

import java.sql.*;
import java.util.*;

public class CommentDaoDatabase implements CommentDao {

    private Connection connection;
    private final String databaseName = "blog";
    private final String tableName = "comments";
    private final String user = "root";
    private final String password = "Admin!000!";

    public CommentDaoDatabase() {
        init();
    }

    private void init() {
        try {
            Class.forName("com.mysql.jdbc.Driver");
            connection = DriverManager.getConnection("jdbc:mysql://localhost/"+databaseName+"?useSSL=false", user, password);
        } catch(Exception e) {
            e.printStackTrace();
        }
    }



    @Override
    public Comment save(Comment comment) {
        PreparedStatement statement;
        try {
            String query = "insert into " + tableName + " (owner, text) values(?, ?)";
            statement = connection.prepareStatement(query);

            statement.setString(1, comment.getOwner());
            statement.setString(2, comment.getText());

            statement.execute();
            statement.close();

        } catch (SQLException e) {
            e.printStackTrace();
        }

        return comment;
    }

    @Override
    public Collection<Comment> getAllComments() {
        List<Comment> commentList = new LinkedList<>();
        Statement statement = null;
        try {
            statement = connection.createStatement();
            String query = "select * from " + tableName;
            ResultSet resultSet = statement.executeQuery(query);

            while (resultSet.next()) {
                Long id = resultSet.getLong("id");
                String text = resultSet.getString("text");
                String owner = resultSet.getString("owner");

                Comment comment = new Comment(id, owner, text);
                commentList.add(comment);
            }
            statement.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }

        return commentList;
    }

    @Override
    public Optional<Comment> getCommentById(Long id) {
        List<Comment> commentList = new LinkedList<>();
        Statement statement = null;
        try {
            statement = connection.createStatement();
            String query = "select * from " + tableName + " where id='" + id + "'";
            ResultSet resultSet = statement.executeQuery(query);

            while (resultSet.next()) {
                String ownerDatabase = resultSet.getString("owner");
                String text = resultSet.getString("text");

                Comment comment = new Comment(id, ownerDatabase, text);
                commentList.add(comment);
            }
            statement.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }

        return Optional.of(commentList.get(0));
    }

    @Override
    public Collection<Comment> getCommentsByOwner(String owner) {
        List<Comment> commentList = new LinkedList<>();
        Statement statement = null;
        try {
            statement = connection.createStatement();
            String query = "select * from " + tableName + " where owner='" + owner + "'";
            ResultSet resultSet = statement.executeQuery(query);

            while (resultSet.next()) {
                Long id = resultSet.getLong("id");
                String text = resultSet.getString("text");

                Comment comment = new Comment(id, owner, text);
                commentList.add(comment);
            }
            statement.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }

        return commentList;
    }

    @Override
    public void deleteCommentById(Long id) {
        PreparedStatement statement;
        try {
            String query = "delete from " + tableName + " where id=?";
            statement = connection.prepareStatement(query);

            statement.setLong(1, id);

            statement.execute();
            statement.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void deleteCommentsByOwner(String owner) {
        PreparedStatement statement;
        try {
            String query = "delete from " + tableName + " where owner=?";
            statement = connection.prepareStatement(query);

            statement.setString(1, owner);

            statement.execute();
            statement.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    @Override
    public Comment update(Comment comment) {
        PreparedStatement statement;
        try {
            String query = "update " + tableName + " set owner = ?, text = ? where id=?";
            statement = connection.prepareStatement(query);

            statement.setString(1, comment.getOwner());
            statement.setString(2, comment.getText());
            statement.setLong(3, comment.getId());

            statement.execute();
            statement.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }

        return comment;
    }
}

 

Mamy stworzone już trzy implementacje, skorzystajmy z nich w klasie Main. Skorzystamy z wcześniej stworzonego serwisu, do którego będziemy podawać konkretną implementację DAO poprzez konstruktor parametrowy.

import dao.CommentDao;
import dao.CommentDaoDatabase;
import dao.CommentDaoFile;
import dao.CommentDaoLocalStorage;
import model.Comment;
import service.CommentService;
import service.CommentServiceImpl;

public class Main {
    public static void main(String[] args) {
        CommentDao databaseDao = new CommentDaoDatabase();
        CommentDao fileDao = new CommentDaoFile();
        CommentDao localStorage = new CommentDaoLocalStorage();


        CommentService commentServiceUsingDatabase = new CommentServiceImpl(databaseDao);
        CommentService commentServiceUsingFile = new CommentServiceImpl(fileDao);
        CommentService commentServiceUsingLocalStorage = new CommentServiceImpl(localStorage);
   }
}

Zatrzymajmy się na chwilę nad powyższym kodem. Zauważ, że niezależnie z jakiej implementacji korzystamy dzięki interfejsowi możemy przypisać je do typu CommentDao.

Jakie korzyści nam to daje?

Spójrzmy na konstruktor parametrowy klasy CommentServiceImpl

public CommentServiceImpl(CommentDao commentDao) {
    this.commentDao = commentDao;
}

Jak widzisz oczekuje on implementacji CommentDao, ale w ogóle nie interesuje go jak robi to ta klasa. Korzysta on po prostu z takiej implementacji jaką mu dostarczymy tak jak robimy to w tej części kodu:

CommentService commentServiceUsingDatabase = new CommentServiceImpl(databaseDao);
CommentService commentServiceUsingFile = new CommentServiceImpl(fileDao);
CommentService commentServiceUsingLocalStorage = new CommentServiceImpl(localStorage);

Stwórzmy na koniec sobie jeszcze obiekt Comment i dodajmy go do trzech różnych DAO, korzystając z odpowiednich serwisów.

Comment comment = new Comment(1L, "Pablo", "Interfaces are awesome!!!");

commentServiceUsingDatabase.addComment(comment);
commentServiceUsingFile.addComment(comment);
commentServiceUsingLocalStorage.addComment(comment);

Szybkie wnioski po tym przykładzie?

Zauważ, że stworzyliśmy tylko jeden serwis – JEDEN, który potrafi m.in. zapisywać komentarze na trzy sposoby:

  • do bazy
  • do pliku
  • do pamięci

To jest niesamowite jak interfejsy mogą być pomocne w tworzeniu czystego, reużywalnego kodu. 😉

Testy

Nie bez powodu na początku w pliku pom.xml podałem w dependency junit oraz mockito. Pokażę Ci teraz wykorzystanie interfejsów podczas pisania testów.

Stwórzmy sobie prosty test serwisu:

public class TestCommentServiceImpl {
    CommentDao commentDao;
    CommentService commentService;

}

Dzięki temu, że serwis jako argument przyjmuje obiekt CommentDao to na czas testów możemy zmockować obiekt CommentDao – czyli zasymulować go bez zbędnego wchodzenia w implementację.

@Before
public void setup() {
    commentDao = Mockito.mock(CommentDao.class);
    commentService = new CommentServiceImpl(commentDao);
}

Dzięki operacji Mockito.mock został stworzony mock – czyli taki pusty model o typie CommentDao – czyli ma w sobie wszystkie potrzebne metody wykorzystywane w serwisie. Po udany zmockowaniu możemy napisać np. test, który sprawdzi ile razy wywoła się metoda save z obiektu CommentDao po wywołaniu metody addComment.

@Test
public void testAddComment() {
    //given
    Comment comment = new Comment(1l, "Pablo", "Default");

    //when
    commentService.addComment(comment);

    //expected
    Mockito.verify(commentDao, Mockito.times(1)).save(comment);
}

Test przejdzie poprawnie, ponieważ serwis wywołuje dokładnie raz metodę save z obiektu CommentDao.

O ile tutaj zmockowaliśmy nasz obiekt na podstawie interfejsu CommentDao, to nie stoi nic na przeszkodzi, aby również tworzyć bardziej skomplikowany testy (np. integracyjne) przy użyciu innej implementacji CommentDao i wstrzykiwać ją do serwisu, który testujemy. 😉

Podsumowanie

Cieszę się, że dotarłeś aż tutaj – poznałeś całe zastosowanie interfejsów oraz namiastkę wzorca projektowego Dependency Injection, która w skrócie polega na wstrzykiwaniu konkretnych implementacji. A to wszystko dzięki magii interfejsów.

Kiedy to my wstrzyknęliśmy swoją implementację? – a no właśnie wtedy:

CommentService commentServiceUsingDatabase = new CommentServiceImpl(databaseDao);
CommentService commentServiceUsingFile = new CommentServiceImpl(fileDao);
CommentService commentServiceUsingLocalStorage = new CommentServiceImpl(localStorage);

Do każdego serwisu wstrzyknęliśmy inną implementację DAO co pozwoli na inne zachowanie aplikacji. A to sprowadza się do kolejnego wzorca projektowego: Inversion Of Control, który oznacza wstrzykiwanie odpowiednich zależności poprzez framework – np. Spring.

Co można rozumieć poprzez IOC? 

Poprzez kilka profili (konfiguracji), które definiują kiedy ma być wstrzykiwana konkretna implementacja.

Uruchamiamy testy integracyjne? Podepnijmy wtedy jakąs lokalną bazę danych lub skorzystajmy z pamięci.

Releasujemy naszą aplikację? Podepnijmy bazę, z której będzie korzystał faktycznie końcowy produkt.

Ot co, trochę teorii interfejsów, która pozwoliła Ci zrozumieć podstawy wzorców projektowych takich jak DI oraz IOC, z których korzysta popularny framework Spring.

O którym w krótce będę pisał, a na koniec pamiętaj:

Interfaces are awesome. 😉

 

Cały kod z artykułu możesz znaleźć tutaj.