Baru-baru ini, sebuah
artikel kolega muncul di Habré, yang menggambarkan pendekatan yang agak menarik untuk menggabungkan kemampuan Generik dan Musim Semi. Dia mengingatkan saya pada satu pendekatan yang saya gunakan untuk menulis layanan microser, dan itulah yang saya putuskan untuk dibagikan kepada pembaca.

Pada output, kita mendapatkan sistem transportasi, untuk menambahkan entitas baru yang mana kita perlu membatasi diri kita untuk menginisialisasi satu kacang di setiap elemen bundel repositori-layanan-pengontrol.
Sumber daya segera.
Cabang, seperti yang saya tidak lakukan:
standart_version .
Pendekatan yang dijelaskan dalam artikel ini di cabang
abstract_version .
Saya mengumpulkan proyek melalui
Spring Initializr , menambahkan kerangka kerja JPA, Web, dan H2. Gradle, Spring Boot 2.0.5. Itu sudah cukup.

Untuk memulai, pertimbangkan versi klasik transport dari controller ke repositori dan sebaliknya, tanpa logika tambahan. Jika Anda ingin pergi ke esensi dari pendekatan, gulir ke bawah ke versi abstrak. Tapi, bagaimanapun, saya sarankan membaca artikel selengkapnya.
Versi klasik.
Sumber
daya dari contoh menyediakan beberapa entitas dan metode untuk mereka, tetapi dalam artikel ini mari kita memiliki hanya satu entitas Pengguna dan hanya satu metode save (), yang akan kita seret dari repositori melalui layanan ke controller. Dalam sumber daya, ada 7 dari mereka, tetapi secara umum Spring CRUD / JPA Repository memungkinkan Anda untuk menggunakan sekitar selusin metode menyimpan / menerima / menghapus, ditambah Anda dapat menggunakan, misalnya,
beberapa yang universal . Juga, kami tidak akan terganggu oleh hal-hal yang diperlukan seperti validasi, pemetaan dto dan sebagainya. Anda dapat menambahkannya sendiri atau belajar
di artikel Habr lainnya .
Domain:
@Entity public class User implements Serializable { private Long id; private String name; private String phone; @Id @GeneratedValue public Long getId() { return id; } public void setId(Long id) { this.id = id; } @Column(nullable = false) public String getName() { return name; } public void setName(String name) { this.name = name; } @Column public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; }
Repositori:
@Repository public interface UserRepository extends CrudRepository<User, Long> { }
Layanan:
public interface UserService { Optional<User> save(User user); }
Layanan (implementasi):
@Service public class UserServiceImpl implements UserService { private final UserRepository userRepository; @Autowired public UserServiceImpl(UserRepository userRepository) { this.userRepository = userRepository; } @Override public Optional<User> save(User user) { return Optional.of(userRepository.save(user)); } }
Pengendali:
@RestController @RequestMapping("/user") public class UserController { private final UserService service; @Autowired public UserController(UserService service) { this.service = service; } @PostMapping public ResponseEntity<User> save(@RequestBody User user) { return service.save(user).map(u -> new ResponseEntity<>(u, HttpStatus.OK)) .orElseThrow(() -> new UserException( String.format(ErrorType.USER_NOT_SAVED.getDescription(), user.toString()) )); } }
Kami mendapat serangkaian kelas dependen tertentu yang akan membantu kami beroperasi pada entitas Pengguna di tingkat CRUD. Dalam contoh kami, ini adalah salah satu metode, ada lebih banyak sumber daya. Lapisan penulisan versi abstrak ini sama sekali tidak disajikan dalam cabang
standart_version .
Misalkan kita perlu menambahkan entitas lain, katakanlah, Mobil. Kami tidak akan menghasilkan uang di tingkat entitas satu sama lain (jika Anda mau, Anda bisa memetakannya).
Pertama, buat entitas.
@Entity public class Car implements Serializable { private Long id; private String brand; private String model; @Id @GeneratedValue public Long getId() { return id; } public void setId(Long id) { this.id = id; }
Kemudian buat repositori.
public interface CarRepository extends CrudRepository<Car, Long> { }
Kemudian layanan ...
public interface CarService { Optional<Car> save(Car car); List<Car> saveAll(List<Car> cars); Optional<Car> update(Car car); Optional<Car> get(Long id); List<Car> getAll(); Boolean deleteById(Long id); Boolean deleteAll(); }
Kemudian implementasi layanan ....... Pengendali ...........
Ya, Anda cukup menyalin dan menempelkan metode yang sama (universal di sini) dari kelas Pengguna, lalu ubah Pengguna ke Mobil, lalu lakukan hal yang sama dengan implementasinya, dengan pengontrolnya, kemudian entitas berikutnya ada di baris berikutnya, dan di sana mereka sudah melihat lebih banyak lagi ... Biasanya Anda bosan dengan yang kedua, penciptaan arsitektur layanan untuk beberapa lusinan entitas (salin-tempel, ganti nama entitas, tempat yang keliru, disegel di suatu tempat ...) mengarah pada siksaan yang disebabkan oleh pekerjaan yang monoton. Cobalah untuk meresepkan dua puluh entitas entah bagaimana di waktu luang Anda dan Anda akan mengerti apa yang saya maksud.
Jadi, pada satu titik, ketika saya hanya tertarik pada parameter generik dan tipikal, saya sadar bahwa prosesnya bisa menjadi jauh lebih tidak rutin.
Jadi, abstraksi didasarkan pada parameter tipikal.
Arti dari pendekatan ini adalah untuk membawa semua logika ke dalam abstraksi, mengikat abstraksi ke parameter khas antarmuka, dan menyuntikkan tempat sampah lainnya ke dalam tempat sampah. Dan itu dia. Tidak ada logika dalam kacang. Hanya suntikan kacang lainnya. Pendekatan ini melibatkan penulisan arsitektur dan logika sekali dan tidak menduplikasi ketika menambahkan entitas baru.
Mari kita mulai dengan landasan abstraksi kita - entitas abstrak. Dari dia, rantai ketergantungan abstrak akan dimulai, yang akan berfungsi sebagai kerangka kerja layanan.
Semua entitas memiliki setidaknya satu bidang yang sama (biasanya lebih). Ini adalah ID. Kami membawa bidang ini ke entitas abstrak yang terpisah dan mewarisi Pengguna dan Mobil darinya.
AbstractEntity:
@MappedSuperclass public abstract class AbstractEntity implements Serializable { private Long id; @Id @GeneratedValue public Long getId() { return id; } public void setId(Long id) { this.id = id; } }
Ingatlah untuk menandai abstraksi dengan penjelasan @MappedSuperclass - Hibernate juga harus tahu bahwa itu adalah abstraksi.
Pengguna:
@Entity public class User extends AbstractEntity { private String name; private String phone;
Dengan Mobil, sesuai, sama.
Di setiap lapisan, selain tempat sampah, kita akan memiliki satu antarmuka dengan parameter khas dan satu kelas abstrak dengan logika. Selain repositori - berkat spesifikasi dari Spring Data JPA, semuanya akan jauh lebih sederhana di sini.
Hal pertama yang kita butuhkan dalam repositori adalah repositori bersama.
Gudang Umum:
@NoRepositoryBean public interface CommonRepository<E extends AbstractEntity> extends CrudRepository<E, Long> { }
Dalam repositori ini, kami menetapkan aturan umum untuk seluruh rantai: semua entitas yang berpartisipasi di dalamnya akan mewarisi dari abstrak. Selanjutnya, untuk setiap entitas, kita harus menulis antarmuka-repositori kita sendiri, di mana kita mengindikasikan entitas mana yang akan bekerja dengan rantai repositori-layanan-pengontrol ini.
Gudang Pengguna:
@Repository public interface UserRepository extends CommonRepository<User> { }
Berkat fitur Spring Data JPA, pengaturan repositori berakhir di sini - semuanya akan berfungsi seperti itu. Berikutnya adalah layanan. Kita perlu membuat antarmuka umum, abstraksi, dan kacang.
Layanan Umum:
public interface CommonService<E extends AbstractEntity> { { Optional<E> save(E entity);
Layanan Abstrak:
public abstract class AbstractService<E extends AbstractEntity, R extends CommonRepository<E>> implements CommonService<E> { protected final R repository; @Autowired public AbstractService(R repository) { this.repository = repository; }
Di sini kita mendefinisikan kembali semua metode, dan juga, membuat konstruktor parameter untuk repositori masa depan, yang kita definisikan ulang di dalam kacang. Jadi, kita sudah menggunakan repositori yang belum kita definisikan. Kami belum tahu entitas mana yang akan diproses dalam abstraksi ini dan repositori mana yang akan kami butuhkan.
Layanan Pengguna:
@Service public class UserService extends AbstractService<User, UserRepository> { public UserService(UserRepository repository) { super(repository); } }
Dalam nampan, kita melakukan hal terakhir - kita secara eksplisit mendefinisikan repositori yang kita butuhkan, yang kemudian disebut dalam konstruktor abstraksi. Dan itu dia.
Menggunakan antarmuka dan abstraksi, kami menciptakan jalan raya yang akan digunakan untuk mengarahkan semua entitas. Di tempat sampah, kami membawa ruang tunggu ke jalan raya, di mana kami akan menampilkan entitas yang kami butuhkan di jalan raya.
Controller dibangun dengan prinsip yang sama: antarmuka, abstraksi, bin.
CommonController:
public interface CommonController<E extends AbstractEntity> { @PostMapping ResponseEntity<E> save(@RequestBody E entity);
AbstractController:
public abstract class AbstractController<E extends AbstractEntity, S extends CommonService<E>> implements CommonController<E> { private final S service; @Autowired protected AbstractController(S service) { this.service = service; } @Override public ResponseEntity<E> save(@RequestBody E entity) { return service.save(entity).map(ResponseEntity::ok) .orElseThrow(() -> new SampleException( String.format(ErrorType.ENTITY_NOT_SAVED.getDescription(), entity.toString()) )); }
UserController:
@RestController @RequestMapping("/user") public class UserController extends AbstractController<User, UserService> { public UserController(UserService service) { super(service); } }
Ini adalah keseluruhan struktur. Itu ditulis sekali.
Apa selanjutnya
Dan sekarang mari kita bayangkan bahwa kita memiliki entitas baru yang telah kita warisi dari AbstractEntity, dan kita perlu menulis rantai yang sama untuk itu. Kami butuh satu menit. Dan tidak ada copy-paste dan koreksi.
Ambil sudah diwarisi dari AbstractEntity Car.
Tempat Penyimpanan:
@Repository public interface CarRepository extends CommonRepository<Car> { }
CarService:
@Service public class CarService extends AbstractService<Car, CarRepository> { public CarService(CarRepository repository) { super(repository); } }
CarController:
@RestController @RequestMapping("/car") public class CarController extends AbstractController<Car, CarService> { public CarController(CarService service) { super(service); } }
Seperti yang bisa kita lihat, menyalin logika yang sama hanya terdiri dari menambahkan kacang. Tidak perlu menulis ulang logika di setiap bin dengan perubahan parameter dan tanda tangan. Mereka ditulis sekali dan bekerja dalam setiap kasus selanjutnya.
Kesimpulan
Tentu saja, contoh menggambarkan semacam situasi bola di mana CRUD untuk setiap entitas memiliki logika yang sama. Itu tidak terjadi - Anda masih harus mendefinisikan kembali beberapa metode dalam nampan atau menambahkan yang baru. Tetapi ini akan datang dari kebutuhan khusus pemrosesan entitas. Nah, jika 60 persen dari jumlah total metode CRUD akan tetap abstraksi. Dan ini akan menjadi hasil yang baik, karena semakin banyak kita menghasilkan kode berlebihan secara manual, semakin banyak waktu yang kita habiskan untuk pekerjaan yang monoton dan semakin tinggi risiko kesalahan atau kesalahan ketik.
Semoga artikel ini bermanfaat, terima kasih atas perhatiannya.
UPD
Berkat proposal tersebut,
aleksandy berhasil mendapatkan inisialisasi kacang ke dalam konstruktor dan dengan demikian secara signifikan meningkatkan pendekatan. Jika Anda melihat bagaimana lagi Anda bisa meningkatkan contoh, tulis di komentar, dan mungkin saran Anda akan dikirimkan.