Liigu peamise sisu juurde

Data Access Object (DAO)

Sissejuhatus

Enamus rakendusi peavad ühel või teisel viisil kirjutama ja lugema andmeid kusagile, olgu selleks siis failid, andmebaasid või välised API-d. Üks viis selle saavutamiseks oleks kirjutada antud loogika sinna, kus seda kasutatakse. See tähendaks, et äriloogika tegeleks siis faili avamisega, sisu töötlemisega ja tulemuste tagastamisega.

See töötaks, kuid tulemusena tekiks tihe sidusus mitme erineva kihi vahel. Samuti ei oleks see tulevikukindel ehk kui mingil põhjusel peaks andmesalvestusformaat muutuma - näiteks failipõhisest andmebaasipõhiseks - siis sellest muudatusest on järsku suur osa koodist mõjutatud. Ning lisaks see läheks vastuollu kõikide asjadega, mida eelmised artiklid on üritanud õpetada (disainimustrid, SOLID jne.)...

Selle probleemi lahendamiseks tutvume Data Access Object (DAO) mustriga, kus andmetele ligipääsemiseks tekitatakse eraldi kiht rakenduse arhitektuuri.

Probleemist lähemalt

Toome näiteks rakenduse, mille ülesanne on raamatukogus järge hoida raamatutest. Ilma DAO mustrita peaks kõik vajalik loogika asetsema klassis, mis seda vajab, reeglina teenuskihis:

public class BookService {
public Optional<Book> findByIsbn(String isbn) throws IOException {
List<String> lines = Files.readAllLines(Path.of("books.txt"));
for (String line : lines) {
String[] parts = line.split(",");
if (parts[1].equals(isbn)) {
return Optional.of(new Book(
Integer.parseInt(parts[0]),
parts[1],
parts[2],
parts[3]
));
}
}
return Optional.empty();
}
}

Sellisel lähenemisel on mitu probleemi:

  • Segunenud vastusalad. BookService teab nüüd nii ärireegleid kui ka failivormingut. Vormingu muutmine (nt üleminek CSV-le koos tsiteeritud väljadega) nõuab äriloogika muutmist.
  • Asendatavuse puudumine. Puudub võimalus see asendada andmebaasi või mälusisese lahendusega ilma BookService-it ümber kirjutamata.
  • Raske testida. Iga test, mis otseselt või kaudselt kasutab BookService-it, eeldab, et kettal on vajalik fail olemas.

DAO muster

DAO mustri mõte on tõsta kõik, mis on seotud andmete ligipääsemisega, ühise liidese taha. Antud liides kirjeldab, mis operatsioone on võimalik antud andmeallikaga läbi viia. Klassid, mis teostavad seda liidest, otsustavad kuidas neid tegevusi läbi viia, sõltuvalt andmehoidla tüübist.

public interface BookDao {
List<Book> readAll();
Optional<Book> findById(int id);
Optional<Book> findByIsbn(String isbn);
void save(Book book);
void remove(int id);
}
hoiatus

findById on salvestustaseme operatsioon - kirje leidmine primaarvõtme alusel on iga andmesalvestuse põhifunktsioon, seega kuulub see DAO-sse.

findByIsbn on vaieldavam. ISBN on domeenikontseptsioon, mis tähendab, et mustri range tõlgenduse järgi peaks see päring kuuluma hoopis repository kihti. Praktikas hoitakse seda siiski siin, kuna ISBN toimib unikaalse võtmena ja otsingut saab andmesalvestuse tasemel tõhusalt teostada.

Kui märkad, et sinu DAO liidesesse koguneb üha rohkem domeenispetsiifilisi päringumeetodeid, on see märk, et need kuuluvad repository kihti.

Toome näiteks failipõhise DAO, mis hoiab andmeid tavalises .txt failis:

public class FileBookDao implements BookDao {
private final Path filePath;

public FileBookDao(Path filePath) {
this.filePath = filePath;
ensureFileExists();
}

private void ensureFileExists() {
// If file doesn't exist, create file
// ...
}

@Override
public List<Book> readAll() {
// Read all lines, parse each line, map them to object, collect to list
// ...
}

@Override
public Optional<Book> findByIsbn(String isbn) {
return readAll().stream()
.filter(book -> book.getIsbn().equals(isbn))
.findFirst();
}

@Override
public void save(Book book) {
List<Book> books = new ArrayList<>(readAll());
books.removeIf(b -> b.getId() == book.getId());
books.add(book);
writeAll(books);
}

// And so on...
}

BookService nüüd sõltub ainult DAO liidesest:

public class BookService {
private final BookDao dao;

public BookService(BookDao dao) {
this.dao = dao;
}

public Optional<Book> findByIsbn(String isbn) {
return dao.findByIsbn(isbn);
}
}

Teostuste välja vahetamine

Kuna BookService sõltub ainult BookDao liidesest, siis andmehoidlate välja vahetamiseks piisab koodis ainult paari rea muutmisest. Ükski nendest muudatustest teenust ennast ei puuduta.

Täpsemalt, kui luua näiteks andmebaasipõhine teostus DAO liidesele:

public class DatabaseBookDao implements BookDao {
// uses JDBC or similar to query a database
// same interface, completely different internals
}

siis koodi käivituskohas saame FileBookDao hõlpsalt välja vahetada:

// Using a file
BookDao dao = new FileBookDao(Path.of("books.txt"));
BookService service = new BookService(dao);

// Switching to a database — service is unchanged
BookDao dao = new DatabaseBookDao(connection);
BookService service = new BookService(dao);

Teisisõnu siin näete kuidas Sõltuvuste süstimist praktikas kasutatakse. Otsus millist DAO-d kasutada lasub koodi väljakutsujale, mitte teenuskihile.

Seos kolmekihilise arhitektuuriga

Kolmekihilises arhitektuuris asub repository-kiht vahetult teenuskihi all. Süsteemides, kus see kiht peab toetama mitut andmesalvestuse lahendust, lisatakse DAO repository alla:

Repository kasutab DAO liidest teadmata, millise konkreetse teostusega see parasjagu töötab. See muudab lihtsalt kolmanda teostuse lisamise hiljem, ilma et peaks midagi DAO-kihist kõrgemal muutma.