
На выходе мы получаем транспортную систему, для добавления в которую новой сущности нужно будет ограничиться инициализацией одного бина в каждом элементе связки репозиторий-сервис-контроллер.
Сразу ресурсы.
Ветка, как я не делаю: standart_version.
Подход, о котором рассказывается в статье, в ветке abstract_version.
Я собрал проект через Spring Initializr, добавив фреймворки JPA, Web и H2. Gradle, Spring Boot 2.0.5. Этого будет вполне достаточно.

Для начала, рассмотрим классический вариант транспорта от контроллера до репозитория и обратно, лишённый всякой дополнительной логики. Если же Вы хотите перейти к сути подхода, проматывайте вниз до абстрактного варианта. Но, всё же, рекомендую прочитать статью целиком.
Классический вариант.
В ресурсах примера представлены несколько сущностей и методов для них, но в статье пусть у нас будет только одна сущность User и только один метод save(), который мы протащим от репозитория через сервис до контроллера. В ресурсах же их 7, а вообще Spring CRUD / JPA Repository позволяют использовать около дюжины методов сохранения / получения / удаления плюс Вы можете пользоваться, к примеру, какими-то своими универсальными. Также, мы не будем отвлекаться на такие нужные вещи, как валидацию, мапинг dto и прочее. Это Вы сможете дописать сами или изучить в других статьях Хабра.
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; } //equals, hashcode, toString }
Repository:
@Repository public interface UserRepository extends CrudRepository<User, Long> { }
Service:
public interface UserService { Optional<User> save(User user); }
Service (имплементация):
@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)); } }
Controller:
@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()) )); } }
У нас получился некий набор зависимых классов, которые помогут нам оперировать сущностью User на уровне CRUD. Это в нашем примере по одному методу, в ресурсах их больше. Этот нисколько не абстрактный вариант написания слоёв представлен в ветке standart_version.
Допустим, нам нужно добавить ещё одну сущность, скажем, Car. Мапить на уровне сущностей мы их друг к другу не будем (если есть желание, можете замапить).
Для начала, создаём сущность.
@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; } //геттеры, сеттеры, equals, hashcode, toString }
Потом создаём репозиторий.
public interface CarRepository extends CrudRepository<Car, Long> { }
Потом сервис…
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(); }
Потом имплементация сервиса…… Контроллер………
Да, можно просто скопипастить те же методы (они же у нас универсальные) из класса User, потом поменять User на Car, потом проделать то же самое с имплементацией, с контроллером, далее на очереди очередная сущность, а там уже выглядывают ещё и ещё… Обычно устаёшь уже на второй, создание же служебной архитектуры для пары десятков сущностей (копипастинг, замена имени сущности, где-то ошибся, где-то опечатался…) приводит к мукам, которые вызывает любая монотонная работа. Попробуйте как-нибудь на досуге прописать двадцать сущностей и Вы поймёте, о чём я.
И вот, в один момент, когда я как раз увлекался дженериками и типовыми параметрами, меня осенило, что процесс можно сделать гораздо менее рутинным.
Итак, абстракции на основе типовых параметров.
Смысл данного подхода заключается в том, чтобы вынести всю логику в абстракцию, абстракцию привязать к типовым параметрам интерфейса, а в бины инжектить другие бины. И всё. Никакой логики в бинах. Только инжект других бинов. Этот подход предполагает написать архитектуру и логику один раз и не дублировать её при добавлении новых сущностей.
Начнём с краеугольного камня нашей абстракции — абстрактной сущности. Именно с неё начнётся цепочка абстрактных зависимостей, которая послужит каркасом сервиса.
У всех сущностей есть как минимум одно общее поле (обычно больше). Это ID. Вынесем это поле в отдельную абстрактную сущность и унаследуем от неё User и Car.
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; } }
Не забудьте пометить абстракцию аннотацией @MappedSuperclass — Hibernate тоже должен узнать, что это абстракция.
User:
@Entity public class User extends AbstractEntity { private String name; private String phone; //... }
С Car, соответственно, то же самое.
В каждом слое у нас, помимо бинов, будет один интерфейс с типовыми параметрами и один абстрактный класс с логикой. Кроме репозитория — благодаря специфике Spring Data JPA, здесь всё будет намного проще.
Первое, что нам потребуется в репозитории — общий репозиторий.
CommonRepository:
@NoRepositoryBean public interface CommonRepository<E extends AbstractEntity> extends CrudRepository<E, Long> { }
В этом репозитории мы задаём общие правила для всей цепочки: все сущности, участвующие в ней, будут наследоваться от абстрактной. Далее, для каждой сущности мы должны написать свой репозиторий-интерфейс, в котором обозначим, с какой именно сущностью будет работать эта цепочка репозиторий-сервис-контроллер.
UserRepository:
@Repository public interface UserRepository extends CommonRepository<User> { }
На этом, благодаря особенностям Spring Data JPA, настройка репозитория заканчивается — всё будет работать и так. Далее следует сервис. Мы должны создать общий интерфейс, абстракцию и бин.
CommonService:
public interface CommonService<E extends AbstractEntity, R extends CommonRepository<E>> { R getRepository(); //ключевой метод инициализации нужного нам репозитория Optional<E> save(E entity); //какое-то количество нужных нам методов }
AbstractService:
public abstract class AbstractService<E extends AbstractEntity, R extends CommonRepository<E>> implements CommonService<E, R> { @Override public Optional<E> save(E entity) { return Optional.of(getRepository().save(entity)); //здесь мы используем репозиторий, который заинжектим позже } //другие методы, переопределённые из интерфейса }
Здесь мы переопределяем все методы, кроме getRepository(). Таким образом, мы уже используем репозиторий, который мы ещё не определили. Мы пока не знаем, какая сущность будет обработана в этой абстракции и какой репозиторий нам потребуется.
UserService:
@Service public class UserService extends AbstractService<User, UserRepository> { private final UserRepository repository; @Autowired public UserService(UserRepository repository) { this.repository = repository; } @Override public UserRepository getRepository() { return repository; } }
В бине мы делаем две заключительные вещи: явно определяем сущность и инжектим необходимый для неё репозиторий.
При помощи интерфейса и абстракции мы создали магистраль, по которой будем гонять все сущности. В бине же мы подводим к магистрали развязку, по которой будем выводить нужную нам сущность на магистраль.
Контроллер строится по тому же принципу: интерфейс, абстракция, бин.
CommonController:
public interface CommonController< E extends AbstractEntity, R extends CommonRepository<E>, S extends CommonService<E, R>> { S getService(); @PostMapping ResponseEntity<E> save(@RequestBody E entity); //остальные методы }
AbstractController:
public abstract class AbstractController< E extends AbstractEntity, R extends CommonRepository<E>, S extends CommonService<E, R>> implements CommonController<E, R, S> { @Override public ResponseEntity<E> save(@RequestBody E entity) { return getService().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, UserRepository, UserService> { private final UserService service; @Autowired public UserController(UserService service) { this.service = service; } @Override public UserService getService() { return service; } }
Это вся структура. Она пишется один раз.
Что дальше?
И вот теперь давайте представим, что у нас появилась новая сущность, которую мы уже унаследовали от AbstractEntity, и нам нужно прописать для неё такую же цепочку. На это у нас уйдёт минута. И никаких копипаст и исправлений.
Возьмём уже унаследованный от AbstractEntity Car.
CarRepository:
@Repository public interface CarRepository extends CommonRepository<Car> { }
CarService:
@Service public class CarService extends AbstractService<Car, CarRepository> { private final CarRepository repository; @Autowired public CarService(CarRepository repository) { this.repository = repository; } @Override public CarRepository getRepository() { return repository; } }
CarController:
@Controller @RequestMapping("/car") public class CarController extends AbstractController<Car, CarRepository, CarService> { private final CarService service; @Autowired public CarController(CarService service) { this.service = service; } @Override public CarService getService() { return service; } }
Как мы видим, копирование одинаковой логики состоит в простом добавлении бина. Не надо заново писать логику в каждом бине с изменением параметров и сигнатур. Они написаны один раз и работают в каждом последующем случае.
Заключение.
Конечно, в примере описана этакая сферическая ситуация, в которой CRUD для каждой сущности имеет одинаковую логику. Так не бывает — какие-то методы Вам всё равно придётся переопределять в бине или добавлять новые. Но это будет происходить от конкретных потребностей обработки сущности. Хорошо, если процентов 60 от общего количества методов CRUD будет оставаться в абстракции. И это будет хорошим результатом, потому что чем больше мы генерим лишнего кода вручную, тем больше времени мы тратим на монотонную работу и тем выше риск ошибки или опечатки.
Надеюсь, статья была полезна, спасибо за внимание.
ссылка на оригинал статьи https://habr.com/post/423741/
Добавить комментарий