Əsas məzmuna keç
DevScallix

Müraciətdən əvvəl sınayın

İlk code review-unuz: sifariş yaradan REST endpoint

Beginner səviyyəli Spring Boot kodunu oxuyun, real review şərhlərini seçin və təkmilləşdirilmiş versiya ilə müqayisə edin.

25 dəqiqəDərs Azərbaycan dilindədir.

Bu dərsdə

  • Code review-nun niyə mövcud olduğunu və yaxşı şərhin nəyi fərqləndirdiyini izah etmək
  • Controller, service və repository qatlarının məsuliyyətini ayırmaq
  • Bean Validation ilə girişi sərhəddə yoxlamaq və düzgün HTTP status qaytarmaq (201 + Location, 400 + Problem Details)
  • Entity-ni birbaşa qaytarmağın və exception-u udmağın niyə təhlükəli olduğunu göstərmək

1 · Öyrən

Code review niyə var?

Code review kodu bəyənmək və ya ələ salmaq üçün deyil. Məqsəd üçdür: səhvi istifadəçiyə çatmadan tutmaq, komandanın kodu eyni cür başa düşməsini təmin etmək və bilik paylaşmaq. Yaxşı review şərhi konkretdir (hansı sətir), səbəbi izah edir (nə risk var) və çıxış yolu təklif edir. "Bu pisdir" şərhi yox, "bu sətir 0 miqdarını qəbul edir, @Positive əlavə edək" şərhi faydalıdır. Şərhin blocking (merge-dən əvvəl düzəlməlidir) yoxsa nit (zövq məsələsi) olduğunu da açıq yazmaq lazımdır. Daha ətraflı: code review mədəniyyəti.

Qatlar: controller, service, repository

  • Controller HTTP dilini tərcümə edir: request-i oxuyur, servisi çağırır, düzgün status və body qaytarır. Biznes qaydası burada yaşamır.
  • Service biznes qaydalarını və transaction sərhədlərini saxlayır (qiymət, endirim, stok yoxlaması).
  • Repository yalnız məlumatın saxlanması və oxunmasıdır.

Qaydanı Controller-də yazsanız, onu başqa yerdən (job, message consumer) təkrar istifadə edə bilməzsiniz və test üçün HTTP qatını qaldırmalı olursunuz. Ətraflı: Spring Boot layering.

Girişi sərhəddə yoxlayın

Client-dən gələn hər şey etibarsızdır. Typed request DTO yazın və Bean Validation constraint-ləri əlavə edin; controller parametrində @Valid olsun. Yoxlama uğursuz olarsa Spring MethodArgumentNotValidException atır və siz onu @RestControllerAdvice ilə 400 Bad Request cavabına çevirirsiniz. Cavabın formatı üçün RFC 9457 (Problem Details) standartı var: Spring 6 bunun üçün ProblemDetail sinfini verir, client isə həmişə eyni strukturda xəta alır. Ətraflı: validation və xətalar.

Düzgün HTTP status

Yeni resurs yaradılanda 201 Created qaytarın və Location header-də yeni resursun ünvanını göstərin. 200 OK "nəsə oldu" deməkdir, 201 isə "yeni resurs yarandı və budur ünvanı" deməkdir. Xəta olanda isə heç vaxt 200 və boş body qaytarmayın.

Entity-ni xaricə çıxarmayın

JPA entity verilənlər bazası modelidir. Onu API cavabı etsəniz, daxili sahələr (məsələn, daxili statuslar) sızır, cədvəl dəyişəndə API sınır, lazy əlaqələr isə gözlənilməz sorğular və ya serializasiya xətaları yaradır. Ayrıca response DTO yazın: API müqaviləsi bazadan asılı olmasın.

Adlandırma

q, p, o yazan müəllif bu gün başa düşür, 3 aydan sonra isə heç kim. quantity, product, order və OrderStatus.NEW kodu şərhsiz oxunaqlı edir. 10 və 0.9 kimi "magic number"-lar adlandırılmış sabit olmalıdır ki, qaydanın nə olduğu aydın görünsün.

Qısa nümunə

@PostMappingpublic ResponseEntity<OrderResponse> create(@Valid @RequestBody CreateOrderRequest request) {    OrderResponse created = orderService.create(request);    URI location = URI.create("/orders/" + created.id());    return ResponseEntity.created(location).body(created);}

Controller nazik qalır: validasiya annotasiyada, qayda servisdədir, status və header isə burada təyin olunur.

2 · Review et

Koda diqqətlə baxın. Aşağıdakı şərhlərdən yaxşı reviewer-in yazacaqlarını seçin (bəziləri yanlış və ya əhəmiyyətsizdir), sonra seçiminizi izahlarla və təkmilləşdirilmiş versiya ilə müqayisə edin.

@RestController@RequestMapping("/orders")public class OrderController {    @Autowired OrderRepository repo;    @Autowired ProductRepository prodRepo;    @PostMapping    public Order create(@RequestBody Map<String, Object> body) {        try {            Product p = prodRepo.findById(Long.valueOf(body.get("pid").toString())).get();            int q = (int) body.get("q");            Order o = new Order();            o.setProduct(p);            o.setTotal(p.getPrice().multiply(BigDecimal.valueOf(q)));            if (q > 10) o.setTotal(o.getTotal().multiply(new BigDecimal("0.9")));            o.setStatus("N");            return repo.save(o);        } catch (Exception e) {            return null;        }    }}

Yaxşı reviewer-in yazacağı şərhləri seçin, sonra “Yoxla” düyməsini basın.

3 · Təkmilləşdirilmiş versiya

@RestController@RequestMapping("/orders")public class OrderController {    private final OrderService orderService; // constructor injection    @PostMapping    public ResponseEntity<OrderResponse> create(@Valid @RequestBody CreateOrderRequest request) {        OrderResponse created = orderService.create(request);        URI location = URI.create("/orders/" + created.id());        return ResponseEntity.created(location).body(created);    }}public record CreateOrderRequest(@NotNull Long productId, @Positive @Max(1000) int quantity) {}public record OrderResponse(Long id, BigDecimal total, OrderStatus status) {}@Servicepublic class OrderService {    private static final int BULK_THRESHOLD = 10;    private static final BigDecimal BULK_FACTOR = new BigDecimal("0.90");    // productRepository and orderRepository: constructor injection    @Transactional    public OrderResponse create(CreateOrderRequest request) {        Product product = productRepository.findById(request.productId())                .orElseThrow(() -> new ProductNotFoundException(request.productId()));        BigDecimal total = product.getPrice().multiply(BigDecimal.valueOf(request.quantity()));        if (request.quantity() > BULK_THRESHOLD) {            total = total.multiply(BULK_FACTOR);        }        Order saved = orderRepository.save(new Order(product, request.quantity(), total, OrderStatus.NEW));        return new OrderResponse(saved.getId(), saved.getTotal(), saved.getStatus());    }}

Nə dəyişdi və niyə

  • Giriş yoxlaması: CreateOrderRequest record-u @NotNull, @Positive və @Max ilə kontraktı özündə daşıyır. @Valid səbəbindən səhv giriş servisə çatmadan 400 ilə rədd olunur.
  • Nazik controller: yalnız servisi çağırır, 201 Created və Location qaytarır. Constructor injection field injection-dan daha təhlükəsizdir: asılılıqlar görünür və test zamanı əllə verilə bilər.
  • Qayda servisdə: endirim məntiqi OrderService-dədir, rəqəmlər adlı sabitlərdir, əməliyyat @Transactional-dır.
  • Response DTO: OrderResponse yalnız client-in görməli olduqlarını açır, entity xaricə çıxmır.
  • Udulan exception yoxdur: məhsul tapılmasa ProductNotFoundException atılır. Onu ayrıca @RestControllerAdvice sinfi ProblemDetail formatında 404-ə, validasiya xətalarını isə 400-ə çevirir. Beləliklə bütün endpoint-lər eyni xəta formatında cavab verir.

Kompromis

Daha çox sinif deməkdir: request DTO, response DTO, service, exception handler. Çox kiçik bir CRUD üçün bu, ilk baxışda artıq görünə bilər, və DTO ↔ entity çevirmələri əlavə kod tələb edir. Müqabilində API sabit qalır, qayda test oluna bilir və xətalar görünür. Layihə böyüdükcə bu ayırma özünü doğruldur; çox kiçik prototip üçün isə sadə saxlamaq məntiqli seçim ola bilər, əsas odur ki, bu seçim şüurlu olsun.

4 · Özünüzü yoxlayın

  1. 1. Yeni sifariş uğurla yaradıldı. Hansı cavab ən düzgündür?
  2. 2. Miqdarın 0 və ya mənfi olmasını ən düzgün harada rədd etmək lazımdır?
  3. 3. JPA entity-ni birbaşa API cavabı kimi qaytarmaq niyə tövsiyə olunmur?
  4. 4. catch (Exception e) { return null; } nə üçün problemdir?

Düşünmək üçün

  • Öz son kodunuzda hansı sətir 3 ay sonra sizi çaşdıra bilər? Onu necə adlandırardınız?
  • Comment yazarkən nit ilə blocking arasında fərqi necə göstərərdiniz və bunu niyə vacib hesab edirsiniz?
  • Bu dərsdəki kompromisi (daha çox sinif) hansı layihədə qəbul edər, hansında etməzdiniz?

Proqramda hər tapşırıq belə keçir.

Kod Pull Request kimi təhvil verilir, review olunur, düzəldilir və müdafiə edilir. Bu dərs bəyəndinizsə, Java Backend Career Track-ə baxın.