Contoh arsitektur heksagonal di Jawa

Terjemahan artikel disiapkan khusus untuk siswa kursus Java Developer .





Sebagai pengembang, kita sering harus berurusan dengan kode lawas, yang sulit dipertahankan. Anda tahu betapa sulitnya memahami logika sederhana dalam kode spaghetti yang besar dan berbelit-belit. Meningkatkan kode atau mengembangkan fungsionalitas baru menjadi mimpi buruk bagi pengembang.

Salah satu tujuan utama dari desain perangkat lunak adalah kemudahan perawatan. Kode yang dikelola dengan buruk menjadi sulit dikelola. Tidak hanya sulit untuk skala, tetapi menjadi masalah untuk menarik pengembang baru.

Di dunia TI, banyak hal yang bergerak cepat. Jika Anda diminta untuk segera mengimplementasikan fungsionalitas baru atau jika Anda ingin beralih dari database relasional ke NoSQL, apa yang akan menjadi reaksi pertama Anda?



Cakupan pengujian yang baik meningkatkan kepercayaan pengembang bahwa tidak akan ada masalah dengan rilis baru. Namun, jika logika bisnis Anda terkait dengan logika infrastruktur, maka mungkin ada masalah dengan pengujiannya.


Mengapa saya

Tetapi ada cukup banyak pembicaraan kosong, mari kita lihat arsitektur heksagonal. Menggunakan templat ini akan membantu Anda meningkatkan rawatan, kemampuan uji, dan manfaat lainnya.

Pengantar Arsitektur Heksagonal


Istilah Arsitektur Heksagonal (arsitektur heksagonal, heksagonal) diciptakan pada tahun 2006 oleh Alistair Cockburn. Gaya arsitektur ini juga dikenal sebagai Arsitektur Ports and Adapters . Dengan kata sederhana, komponen aplikasi Anda berinteraksi melalui banyak titik akhir (port). Untuk memproses permintaan, Anda harus memiliki adaptor yang cocok dengan porta.

Di sini Anda dapat menggambar analogi dengan port USB di komputer. Anda dapat menggunakannya jika Anda memiliki adaptor yang kompatibel (charger atau flash drive).



Arsitektur ini dapat secara skematis direpresentasikan sebagai segi enam dengan logika bisnis di pusat (di dalam inti), dikelilingi oleh objek yang berinteraksi, dan komponen yang mengendalikannya, menyediakan data input.

Dalam kehidupan nyata, pengguna, panggilan API, skrip otomatis, dan pengujian unit berinteraksi dan memberikan input ke aplikasi Anda. Jika logika bisnis Anda dicampur dengan logika antarmuka pengguna, maka Anda akan menghadapi banyak masalah. Misalnya, akan sulit untuk mengalihkan entri data dari antarmuka pengguna ke unit test.

Aplikasi ini juga berinteraksi dengan objek eksternal seperti database, antrian pesan, server web (melalui panggilan API HTTP), dll. Jika perlu, migrasikan database atau unggah data ke file, Anda harus dapat melakukan ini tanpa mempengaruhi bisnis. logika.

Seperti namanya, " port dan adapter ", ada "port" di mana interaksi terjadi dan "adapter" adalah komponen yang memproses input pengguna dan mengubahnya menjadi "bahasa" domain. Adaptor merangkum logika interaksi dengan sistem eksternal, seperti basis data, antrian pesan, dll., Dan memfasilitasi komunikasi antara logika bisnis dan objek eksternal.

Diagram di bawah ini menunjukkan lapisan ke mana aplikasi dibagi.



Arsitektur heksagonal membedakan tiga lapisan dalam aplikasi: domain, aplikasi, dan infrastruktur:

  • Domain . Lapisan berisi logika bisnis inti. Dia tidak perlu mengetahui detail implementasi lapisan luar.
  • Aplikasi . Lapisan bertindak sebagai jembatan antara lapisan domain dan infrastruktur.
  • Infrastruktur . Implementasi interaksi domain dengan dunia luar. Lapisan dalam terlihat seperti kotak hitam baginya.

Menurut arsitektur ini, dua jenis peserta berinteraksi dengan aplikasi: primer (driver) dan sekunder (didorong). Aktor utama mengirim permintaan dan mengelola aplikasi (misalnya, pengguna atau tes otomatis). Yang sekunder menyediakan infrastruktur untuk komunikasi dengan dunia luar (ini adalah adaptor basis data, klien TCP atau HTTP).
Ini dapat direpresentasikan sebagai berikut: Sisi



kiri segi enam terdiri dari komponen yang memberikan input untuk domain (mereka "mengontrol" aplikasi), dan sisi kanan terdiri dari komponen yang dikendalikan oleh aplikasi kita.

Contoh


Mari kita merancang aplikasi yang akan menyimpan ulasan film. Pengguna harus dapat mengirim permintaan dengan nama film dan mendapatkan lima ulasan acak.

Untuk mempermudah, kami akan membuat aplikasi konsol dengan penyimpanan data dalam RAM. Respons terhadap pengguna akan ditampilkan di konsol.

Kami memiliki pengguna (Pengguna) yang mengirim permintaan ke aplikasi. Dengan demikian, pengguna menjadi "manajer" (driver). Aplikasi harus dapat menerima data dari semua jenis penyimpanan dan menampilkan hasilnya ke konsol atau ke file. Objek yang dikelola (digerakkan) akan menjadi "gudang data" ( IFetchMovieReviews) dan "printer respons" ( IPrintMovieReviews).

Gambar berikut menunjukkan komponen utama aplikasi kita.



Di sebelah kiri adalah komponen yang menyediakan entri data ke dalam aplikasi. Di sebelah kanan adalah komponen yang memungkinkan Anda untuk berinteraksi dengan database dan konsol.

Mari kita lihat kode aplikasi.

Port kontrol

public interface IUserInput {
    public void handleUserInput(Object userCommand);
}

Port yang dikelola

public interface IFetchMovieReviews {
    public List<MovieReview> fetchMovieReviews(MovieSearchRequest movieSearchRequest);
}

public interface IPrintMovieReviews {
    public void writeMovieReviews(List<MovieReview> movieReviewList);
}

Adaptor port terkelola

Film akan diperoleh dari repositori film (MovieReviewsRepo). Ulasan ulasan film pada konsol akan menjadi kelas ConsolePrinter. Mari kita terapkan dua antarmuka di atas.

public class ConsolePrinter implements IPrintMovieReviews {
    @Override
    public void writeMovieReviews(List<MovieReview> movieReviewList) {
        movieReviewList.forEach(movieReview -> {
            System.out.println(movieReview.toString());
        });
    }
}

public class MovieReviewsRepo implements IFetchMovieReviews {
    private Map<String, List<MovieReview>> movieReviewMap;

    public MovieReviewsRepo() {
        initialize();
    }

    public List<MovieReview> fetchMovieReviews(MovieSearchRequest movieSearchRequest) {

        return Optional.ofNullable(movieReviewMap.get(movieSearchRequest.getMovieName()))
            .orElse(new ArrayList<>());
    }

    private void initialize() {
        this.movieReviewMap = new HashMap<>();
        movieReviewMap.put("StarWars", Collections.singletonList(new MovieReview("1", 7.5, "Good")));
        movieReviewMap.put("StarTreck", Arrays.asList(new MovieReview("1", 9.5, "Excellent"), new MovieReview("1", 8.5, "Good")));
    }
}

Domain


Tugas utama aplikasi kami adalah memproses permintaan pengguna. Anda perlu mendapatkan film, memprosesnya dan mentransfer hasilnya ke "printer". Saat ini, kami hanya memiliki satu fungsi - pencarian film. Untuk memproses permintaan pengguna, kami akan menggunakan antarmuka standar Consumer.

Mari kita lihat kelas utama MovieApp.

public class MovieApp implements Consumer<MovieSearchRequest> {
    private IFetchMovieReviews fetchMovieReviews;
    private IPrintMovieReviews printMovieReviews;
    private static Random rand = new Random();

    public MovieApp(IFetchMovieReviews fetchMovieReviews, IPrintMovieReviews printMovieReviews) {
        this.fetchMovieReviews = fetchMovieReviews;
        this.printMovieReviews = printMovieReviews;
    }

    private List<MovieReview> filterRandomReviews(List<MovieReview> movieReviewList) {
        List<MovieReview> result = new ArrayList<MovieReview>();
        // logic to return random reviews
        for (int index = 0; index < 5; ++index) {
            if (movieReviewList.size() < 1)
                break;
            int randomIndex = getRandomElement(movieReviewList.size());
            MovieReview movieReview = movieReviewList.get(randomIndex);
            movieReviewList.remove(movieReview);
            result.add(movieReview);
        }
        return result;
    }

    private int getRandomElement(int size) {
        return rand.nextInt(size);
    }

    public void accept(MovieSearchRequest movieSearchRequest) {
        List<MovieReview> movieReviewList = fetchMovieReviews.fetchMovieReviews(movieSearchRequest);
        List<MovieReview> randomReviews = filterRandomReviews(new ArrayList<>(movieReviewList));
        printMovieReviews.writeMovieReviews(randomReviews);
    }
}

Sekarang kita mendefinisikan kelas CommandMapperModelyang akan memetakan perintah ke penangan.

public class CommandMapperModel {
    private static final Class<MovieSearchRequest> searchMovies = MovieSearchRequest.class;

    public static Model build(Consumer<MovieSearchRequest> displayMovies) {
        Model model = Model.builder()
            .user(searchMovies)
            .system(displayMovies)
            .build();

        return model;
    }
}

Kontrol Port Adapters


Pengguna akan berinteraksi dengan sistem kami melalui antarmuka IUserInput. Implementasi akan menggunakan ModelRunnerdan mendelegasikan eksekusi.

public class UserCommandBoundary implements IUserInput {
    private Model model;

    public UserCommandBoundary(IFetchMovieReviews fetchMovieReviews, IPrintMovieReviews printMovieReviews) {
        MovieApp movieApp = new MovieApp(fetchMovieReviews, printMovieReviews);
        model = CommandMapperModel.build(movieApp);
    }

    public void handleUserInput(Object userCommand) {
        new ModelRunner().run(model)
            .reactTo(userCommand);
    }
}

Sekarang mari kita lihat pengguna yang menggunakan antarmuka di atas.

public class MovieUser {
    private IUserInput userInputDriverPort;

    public MovieUser(IUserInput userInputDriverPort) {
        this.userInputDriverPort = userInputDriverPort;
    }

    public void processInput(MovieSearchRequest movieSearchRequest) {
        userInputDriverPort.handleUserInput(movieSearchRequest);
    }
}

aplikasi


Selanjutnya, buat aplikasi konsol. Adaptor yang dikelola ditambahkan sebagai dependensi. Pengguna akan membuat dan mengirim permintaan ke aplikasi. Aplikasi akan menerima data, memproses dan menampilkan respons ke konsol.

public class Main {

    public static void main(String[] args) {
        IFetchMovieReviews fetchMovieReviews = new MovieReviewsRepo();
        IPrintMovieReviews printMovieReviews = new ConsolePrinter();
        IUserInput userCommandBoundary = new UserCommandBoundary(fetchMovieReviews, printMovieReviews);
        MovieUser movieUser = new MovieUser(userCommandBoundary);
        MovieSearchRequest starWarsRequest = new MovieSearchRequest("StarWars");
        MovieSearchRequest starTreckRequest = new MovieSearchRequest("StarTreck");

        System.out.println("Displaying reviews for movie " + starTreckRequest.getMovieName());
        movieUser.processInput(starTreckRequest);
        System.out.println("Displaying reviews for movie " + starWarsRequest.getMovieName());
        movieUser.processInput(starWarsRequest);
    }

}

Apa yang bisa diperbaiki, diubah


  • Dalam implementasi kami, Anda dapat dengan mudah beralih dari satu penyimpanan data ke yang lain. Implementasi penyimpanan dapat disuntikkan ke dalam kode tanpa mengubah logika bisnis. Misalnya, Anda dapat mentransfer data dari memori ke database dengan menulis adaptor basis data.
  • Alih-alih output ke konsol, Anda dapat menerapkan "printer", yang akan menulis data ke file. Dalam aplikasi multi-layer, menjadi lebih mudah untuk menambahkan fungsionalitas dan memperbaiki bug.
  • Untuk menguji logika bisnis, Anda dapat menulis tes kompleks. Adaptor dapat diuji secara terpisah. Dengan demikian, dimungkinkan untuk meningkatkan cakupan tes secara keseluruhan.

Kesimpulan


Keuntungan-keuntungan berikut dari arsitektur heksagonal dapat dicatat:

  • Pengiring - lapisan longgar dan independen. Menjadi mudah untuk menambahkan fitur baru ke satu lapisan tanpa mempengaruhi lapisan lainnya.
  • Testability - unit test ditulis sederhana dan cepat dilakukan. Anda dapat menulis tes untuk setiap lapisan menggunakan objek rintisan yang mensimulasikan dependensi. Misalnya, kita bisa menghapus ketergantungan pada database dengan membuat data warehouse di memori.
  • Kemampuan beradaptasi - logika bisnis utama menjadi independen dari perubahan objek eksternal. Misalnya, jika Anda perlu bermigrasi ke database lain, maka kami tidak perlu melakukan perubahan pada domain. Kita dapat membuat adaptor yang sesuai untuk basis data.

Referensi



Itu saja. Sampai jumpa di lapangan !

All Articles