الفصل 4: هندسة ثلاثية الطبقات احترافية
مقدمة
يلخص هذا الفصل كل ما تعلمته في الفصول السابقة لإنشاء هندسة احترافية، قوية وقابلة للصيانة.
نظرة عامة على الهندسة
مسؤوليات كل طبقة
| الطبقة | الدور | التقنيات | التعليقات التوضيحية |
|---|---|---|---|
| التحكم | إدارة HTTP، التحقق، التحويل | Spring MVC | @RestController, @GetMapping |
| الخدمة | المنطق التجاري، التنسيق، المعاملات | Spring Core | @Service, @Transactional |
| المستودع | الوصول إلى البيانات، استعلامات قاعدة البيانات | Spring Data JPA | @Repository, JpaRepository |
| معالج الاستثناءات | إدارة الأخطاء، تنسيق الردود | Spring MVC | @ControllerAdvice |
مشروع كامل: إدارة المنتجات
هيكل الحزم
src/main/java/com/example/demo/
├── DemoApplication.java
├── controller/
│ └── ProductController.java
├── service/
│ └── ProductService.java
├── repository/
│ └── ProductRepository.java
├── model/
│ └── Product.java
└── exception/
├── ProductNotFoundException.java
├── InvalidProductException.java
├── ErrorResponse.java
└── GlobalExceptionHandler.java
1. الكيان (النموذج)
package com.example.demo.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "الاسم لا يمكن أن يكون فارغًا")
@Size(min = 3, max = 50, message = "يجب أن يحتوي الاسم على 3 إلى 50 حرفًا")
private String name;
@NotNull(message = "السعر مطلوب")
@Min(value = 0, message = "السعر لا يمكن أن يكون سالبًا")
private Double price;
@NotNull(message = "الكمية مطلوبة")
@Min(value = 0, message = "الكمية لا يمكن أن تكون سالبة")
private Integer quantity;
// المنشئون
public Product() {}
public Product(String name, Double price, Integer quantity) {
this.name = name;
this.price = price;
this.quantity = quantity;
}
// الجالبون والضابطون
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }
public Integer getQuantity() { return quantity; }
public void setQuantity(Integer quantity) { this.quantity = quantity; }
}
2. المستودع
package com.example.demo.repository;
import com.example.demo.model.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
// طرق موروثة: save, findById, findAll, deleteById
// طرق مخصصة
List<Product> findByName(String name);
List<Product> findByPriceLessThan(Double price);
}
3. الخدمة
package com.example.demo.service;
import com.example.demo.exception.ProductNotFoundException;
import com.example.demo.model.Product;
import com.example.demo.repository.ProductRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
// الإنشاء
public Product createProduct(Product product) {
// تحقق إضافي إذا لزم الأمر
return productRepository.save(product);
}
// القراءة - الكل
public List<Product> getAllProducts() {
return productRepository.findAll();
}
// القراءة - حسب المعرف
public Product getProductById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
}
// التحديث
public Product updateProduct(Long id, Product productDetails) {
Product product = getProductById(id);
product.setName(productDetails.getName());
product.setPrice(productDetails.getPrice());
product.setQuantity(productDetails.getQuantity());
return productRepository.save(product);
}
// الحذف
public void deleteProduct(Long id) {
Product product = getProductById(id);
productRepository.delete(product);
}
}
4. التحكم
package com.example.demo.controller;
import com.example.demo.model.Product;
import com.example.demo.service.ProductService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
// الإنشاء
@PostMapping
public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
Product created = productService.createProduct(product);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
// القراءة - الكل
@GetMapping
public ResponseEntity<List<Product>> getAllProducts() {
List<Product> products = productService.getAllProducts();
return ResponseEntity.ok(products);
}
// القراءة - حسب المعرف
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
Product product = productService.getProductById(id);
return ResponseEntity.ok(product);
}
// التحديث
@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(@PathVariable Long id,
@Valid @RequestBody Product product) {
Product updated = productService.updateProduct(id, product);
return ResponseEntity.ok(updated);
}
// الحذف
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return ResponseEntity.noContent().build();
}
}
5. الاستثناءات
package com.example.demo.exception;
public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(Long id) {
super("Product not found with id: " + id);
}
}
package com.example.demo.exception;
import java.time.LocalDateTime;
public class ErrorResponse {
private LocalDateTime timestamp;
private int status;
private String error;
private String message;
private String path;
public ErrorResponse(int status, String error, String message, String path) {
this.timestamp = LocalDateTime.now();
this.status = status;
this.error = error;
this.message = message;
this.path = path;
}
// الجالبون والضابطون (مولدة)
public LocalDateTime getTimestamp() { return timestamp; }
public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; }
public int getStatus() { return status; }
public void setStatus(int status) { this.status = status; }
public String getError() { return error; }
public void setError(String error) { this.error = error; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public String getPath() { return path; }
public void setPath(String path) { this.path = path; }
}
6. مدير الاستثناءات
package com.example.demo.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity<ErrorResponse> handleProductNotFound(
ProductNotFoundException ex, WebRequest request) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
"Not Found",
ex.getMessage(),
request.getDescription(false).replace("uri=", "")
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGlobalException(
Exception ex, WebRequest request) {
ErrorResponse error = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"Internal Server Error",
ex.getMessage(),
request.getDescription(false).replace("uri=", "")
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
إعداد application.yml
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
h2:
console:
enabled: true
path: /h2-console
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
properties:
hibernate:
format_sql: true
server:
port: 8080
اختبارات باستخدام Postman
1. الإنشاء - POST /api/products
الطلب:
POST http://localhost:8080/api/products
Content-Type: application/json
{
"name": "Laptop",
"price": 999.99,
"quantity": 10
}
الاستجابة: 201 Created
{
"id": 1,
"name": "Laptop",
"price": 999.99,
"quantity": 10
}
2. الحصول على الكل - GET /api/products
الاستجابة: 200 OK
[
{
"id": 1,
"name": "Laptop",
"price": 999.99,
"quantity": 10
}
]
3. الحصول حسب المعرف - GET /api/products/1
الاستجابة: 200 OK
{
"id": 1,
"name": "Laptop",
"price": 999.99,
"quantity": 10
}
4. التحديث - PUT /api/products/1
الطلب:
{
"name": "Laptop Pro",
"price": 1299.99,
"quantity": 5
}
الاستجابة: 200 OK
{
"id": 1,
"name": "Laptop Pro",
"price": 1299.99,
"quantity": 5
}
5. الحذف - DELETE /api/products/1
الاستجابة: 204 No Content
اتفاقيات التسمية
الحزم
com.example.demo.controller
com.example.demo.service
com.example.demo.repository
com.example.demo.model
com.example.demo.exception
الفئات
- التحكم:
ProductController,UserController - الخدمة:
ProductService,UserService - المستودع:
ProductRepository,UserRepository - النموذج:
Product,User - الاستثناء:
ProductNotFoundException,InvalidProductException
الطرق
- الإنشاء:
create...()أوadd...() - القراءة:
get...(),find...(),getAll...() - التحديث:
update...() - الحذف:
delete...()
ملخص الفصل
هندسة احترافية
✅ التحكم: إدارة HTTP فقط
✅ الخدمة: المنطق التجاري والتنسيق
✅ المستودع: الوصول إلى البيانات
✅ معالج الاستثناءات: إدارة الأخطاء بشكل عام
النقاط الرئيسية
- فصل واضح للمسؤوليات
- حقن عبر المنشئ
- تحقق على مستويين (نحوي + تجاري)
- إدارة احترافية للأخطاء
- رموز HTTP مناسبة
- هيكل مشروع موحد