في الآونة الأخيرة ، تومض
مقالة زميل على حبري ، الذي وصف نهجًا مثيرًا للاهتمام إلى حد ما للجمع بين Generics وقدرات الربيع. ذكّرتني بأحد الأساليب التي أستخدمها لكتابة الخدمات الدقيقة ، وهذا ما قررت مشاركته مع القراء.

عند المخرج ، نحصل على نظام نقل ، لنضيف إلى أي كيان جديد سنحتاج إلى أن نقتصر على تهيئة حبة واحدة في كل عنصر من عناصر حزمة المستودع - خدمة - تحكم.
الموارد على الفور.
الفرع ، وأنا لا:
standart_version .
النهج الموصوف في المقالة موجود في فرع
abstract_version .
لقد قمت بتجميع مشروع من خلال
Spring Initializr ، بإضافة إطارات JPA و Web و H2. Gradle ، حذاء الربيع 2.0.5. هذا سيكون كافيا

للبدء ، ضع في اعتبارك الإصدار الكلاسيكي للنقل من وحدة التحكم إلى المستودع والعكس بالعكس ، خاليًا من أي منطق إضافي. إذا كنت تريد الانتقال إلى جوهر النهج ، فانتقل لأسفل إلى النسخة المجردة. ولكن ، مع ذلك ، أوصي بقراءة المقالة الكاملة.
الإصدار الكلاسيكي.
توفر
موارد المثال العديد من الكيانات والأساليب لها ، ولكن في المقالة دعونا نمتلك كيان مستخدم واحدًا وطريقة حفظ واحدة فقط ، والتي سنقوم بسحبها من المستودع من خلال الخدمة إلى وحدة التحكم. في الموارد ، هناك 7 منها ، ولكن بشكل عام ، يتيح لك Spring CRUD / JPA Repository استخدام حوالي اثني عشر طريقة للحفظ / الاستلام / الحذف ، بالإضافة إلى أنه يمكنك استخدام ، على سبيل المثال ،
بعض الأساليب
العالمية . أيضًا ، لن نتشتت بأشياء ضرورية مثل التحقق من الصحة ورسم الخرائط dto وما إلى ذلك. يمكنك إضافته بنفسك أو الدراسة
في مقالات أخرى من هبر .
المجال:
@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; }
المستودع:
@Repository public interface UserRepository extends CrudRepository<User, Long> { }
الخدمة:
public interface UserService { Optional<User> save(User user); }
الخدمة (التنفيذ):
@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)); } }
تحكم:
@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()) )); } }
حصلنا على مجموعة معينة من الفئات التابعة التي ستساعدنا في العمل على كيان المستخدم على مستوى CRUD. في مثالنا ، هذه طريقة ، هناك المزيد في الموارد. لا يتم تقديم هذه النسخة المجردة على الإطلاق من طبقات الكتابة في فرع
standart_version .
لنفترض أننا بحاجة إلى إضافة كيان آخر ، على سبيل المثال ، سيارة. لن نجني المال على مستوى الكيانات لبعضنا البعض (إذا كنت تريد ، يمكنك تعيينه).
أولاً ، قم بإنشاء كيان.
@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; }
ثم قم بإنشاء مستودع.
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(); }
ثم تنفيذ الخدمة ....... تحكم ...........
نعم ، يمكنك فقط نسخ نفس الأساليب (فهي عالمية هنا) من فئة المستخدم ، ثم تغيير المستخدم إلى السيارة ، ثم القيام بنفس الشيء مع التنفيذ ، باستخدام وحدة التحكم ، ثم يكون الكيان التالي في السطر ، وهناك يبحثون بالفعل أكثر وأكثر ... عادة ما تتعب من الثاني ، يؤدي إنشاء بنية خدمة لعشرات الكيانات (لصق النسخ ، استبدال اسم الكيان ، ارتكاب خطأ في مكان ما ، مختوم في مكان ما ...) إلى العذاب الذي يسببه أي عمل رتيب. حاول أن تصف عشرين كيانًا بطريقة ما في وقت فراغك وسوف تفهم ما أعنيه.
وهكذا ، في مرحلة ما ، عندما كنت مهتمًا فقط بالأدوية والمعلمات النموذجية ، بدا لي أنه يمكن جعل العملية أقل روتينية.
لذا ، فإن التجريد يعتمد على المعلمات النموذجية.
معنى هذا النهج هو أخذ كل المنطق في التجريد ، وربط التجريد بالمعلمات النموذجية للواجهة ، وحقن الصناديق الأخرى في الصناديق. هذا كل ما في الأمر. لا منطق في الفاصوليا. فقط حقن حبوب أخرى. يتضمن هذا النهج كتابة العمارة والمنطق مرة واحدة وعدم تكرارها عند إضافة كيانات جديدة.
لنبدأ بحجر التجريد لدينا - كيان تجريدي. ومنها ستبدأ سلسلة التبعيات المجردة ، والتي ستكون بمثابة إطار للخدمة.
تحتوي جميع الكيانات على حقل مشترك واحد على الأقل (عادة أكثر). هذا هو المعرف. نأخذ هذا الحقل إلى كيان تجريدي منفصل ونرث المستخدم والسيارة منه.
الخلاصة
@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 - يجب أن يعرف السبات أيضًا أنه تجريد.
المستخدم:
@Entity public class User extends AbstractEntity { private String name; private String phone;
مع السيارة ، وفقا لذلك ، نفس الشيء.
في كل طبقة ، بالإضافة إلى الصناديق ، سيكون لدينا واجهة واحدة مع معلمات نموذجية وفئة مجردة مع المنطق. بالإضافة إلى المستودع - بفضل تفاصيل Spring Data JPA ، سيكون كل شيء أكثر بساطة هنا.
أول شيء نحتاجه في المستودع هو مستودع مشترك.
مستودع مشترك:
@NoRepositoryBean public interface CommonRepository<E extends AbstractEntity> extends CrudRepository<E, Long> { }
في هذا المستودع ، وضعنا قواعد عامة للسلسلة بأكملها: جميع الكيانات المشاركة فيه سوف ترث من الملخص. بعد ذلك ، بالنسبة لكل كيان ، يجب أن نكتب واجهة المستودع الخاصة بنا ، والتي نشير فيها إلى الكيان الذي ستعمل معه سلسلة وحدة تحكم خدمة المستودع.
مستودع المستخدم:
@Repository public interface UserRepository extends CommonRepository<User> { }
بفضل ميزات Spring Data JPA ، ينتهي إعداد المستودع هنا - كل شيء سيعمل على هذا النحو. بعد ذلك تأتي الخدمة. نحن بحاجة إلى إنشاء واجهة مشتركة وتجريد وحبوب.
الخدمة العامة:
public interface CommonService<E extends AbstractEntity> { { Optional<E> save(E entity);
الخدمة:
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; }
هنا نعيد تعريف جميع الطرق ، وأيضًا ، ننشئ مُنشئًا ذي معلمات للمستودع المستقبلي ، والذي نعيد تعريفه في الفول. وبالتالي ، فإننا نستخدم بالفعل مستودعًا لم نقم بتحديده بعد. لا نعرف حتى الآن أي كيان سيتم معالجته في هذا التجريد وأي مستودع سنحتاجه.
خدمة المستخدم:
@Service public class UserService extends AbstractService<User, UserRepository> { public UserService(UserRepository repository) { super(repository); } }
في الحاوية ، نقوم بالشيء الأخير - نحدد بوضوح المستودع الذي نحتاجه ، والذي يتم استدعاؤه بعد ذلك في مُنشئ التجريد. هذا كل ما في الأمر.
باستخدام الواجهة والتجريد ، أنشأنا طريقًا سريعًا سنقود من خلاله جميع الكيانات. في الحاوية ، نعرض الخاتمة على الطريق السريع ، والتي من خلالها سنعرض الكيان الذي نحتاجه على الطريق السريع.
وحدة التحكم مبنية على نفس المبدأ: الواجهة ، التجريد ، بن.
CommonController:
public interface CommonController<E extends AbstractEntity> { @PostMapping ResponseEntity<E> save(@RequestBody E entity);
كونترولر:
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); } }
هذا هو الهيكل كله. مكتوب مرة واحدة.
ما هي الخطوة التالية؟
والآن دعونا نتخيل أن لدينا كيانًا جديدًا ورثناه بالفعل من AbstractEntity ، ونحتاج إلى كتابة نفس السلسلة له. سوف يستغرق منا دقيقة. ولا نسخ ولصق وتصحيحات.
خذ بالفعل موروثة من AbstractEntity Car.
مستودع السيارة:
@Repository public interface CarRepository extends CommonRepository<Car> { }
خدمة السيارات:
@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); } }
كما نرى ، فإن نسخ نفس المنطق يتكون ببساطة من إضافة حبة. لا حاجة لإعادة كتابة المنطق في كل صندوق مع تغيير المعلمات والتوقيعات. يتم كتابتها مرة واحدة والعمل في كل حالة لاحقة.
الخلاصة
بالطبع ، يصف المثال نوعًا من المواقف الكروية التي يكون فيها CRUD لكل كيان له نفس المنطق. هذا لا يحدث - لا يزال عليك إعادة تعريف بعض الطرق في سلة المهملات أو إضافة طرق جديدة. ولكن هذا سيأتي من الاحتياجات الخاصة لمعالجة الكيان. حسنًا ، إذا بقي 60 في المائة من العدد الإجمالي لطرق CRUD في التجريد. وستكون هذه نتيجة جيدة ، لأنه كلما قمنا بإنشاء رمز زائدة يدويًا ، زاد الوقت الذي نقضيه في العمل الرتيب وكلما زاد خطر الأخطاء أو الأخطاء المطبعية.
آمل أن تكون المقالة مفيدة ، شكرا لاهتمامكم.
UPD
بفضل الاقتراح ، تمكن
aleksandy من الحصول على تهيئة الفاصوليا في المنشئ وبالتالي تحسين النهج بشكل ملحوظ. إذا رأيت كيف يمكنك تحسين المثال ، اكتب في التعليقات ، وربما سيتم إرسال اقتراحاتك.