الفصل 4: تصميم REST والأنماط التصميمية
تغطي هذه التمارين تصميم واجهات برمجة التطبيقات REST وتنفيذ الأنماط التصميمية - وهما فئتان شائعتان من الأسئلة في مقابلات الخلفية Java/Spring Boot.
التمرين 1 — تصميم نقطة نهاية REST كاملة
"صمم ونفذ نقطة نهاية لإنشاء واسترجاع المستخدمين."
الحل
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
@PostMapping
public ResponseEntity<UserDTO> create(@Valid @RequestBody CreateUserRequest req) {
UserDTO user = userService.create(req);
return ResponseEntity.status(HttpStatus.CREATED).body(user);
}
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getById(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping
public ResponseEntity<Page<UserDTO>> list(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return ResponseEntity.ok(userService.findAll(PageRequest.of(page, size)));
}
}
كائنات نقل البيانات (DTOs)
public record CreateUserRequest(
@NotBlank String name,
@Email String email
) {}
public record UserDTO(Long id, String name, String email) {}
النقاط الرئيسية التي يجب ذكرها في المقابلة
| ممارسة جيدة | لماذا |
|---|---|
| DTOs بدلاً من الكيانات | لا تعرض الكيانات JPA في الـ API |
| @Valid | تحقق من صحة المدخلات على جانب الخادم |
| 201 Created لـ POST | رموز HTTP الصحيحة |
| 404 Not Found إذا لم يكن موجودًا | إرجاع الحالة الصحيحة |
| التقسيم في القوائم | لا ترجع كل البيانات دفعة واحدة |
| سجلات Java لـ DTOs | غير قابلة للتغيير، مختصرة، تولد تلقائيًا equals/hashCode/toString |
"I use DTOs to avoid exposing entities, @Valid for input validation, proper HTTP status codes (201 Created, 404 Not Found), and pagination for list endpoints. I use Java Records for immutable DTOs."
التمرين 2 — تصميم واجهة برمجة تطبيقات للتجارة الإلكترونية
"صمم نقاط النهاية REST لنظام التجارة الإلكترونية."
الحل
Products:
GET /api/products → قائمة المنتجات مع تقسيم
GET /api/products/{id} → تفاصيل المنتج
POST /api/products → إنشاء منتج (ADMIN)
PUT /api/products/{id} → تعديل منتج (ADMIN)
DELETE /api/products/{id} → حذف منتج (ADMIN)
Orders:
POST /api/orders → تقديم طلب
GET /api/orders → طلباتي (المستخدم المتصل)
GET /api/orders/{id} → تفاصيل الطلب
PATCH /api/orders/{id}/status → تعديل الحالة (ADMIN)
Auth:
POST /api/auth/register → تسجيل
POST /api/auth/login → تسجيل الدخول (يعيد JWT)
POST /api/auth/refresh → تحديث التوكن
Cart:
GET /api/cart → سلتي
POST /api/cart/items → إضافة عنصر إلى السلة
PUT /api/cart/items/{id} → تعديل الكمية
DELETE /api/cart/items/{id} → إزالة من السلة
الممارسات الجيدة لـ REST المذكورة
| القاعدة | المثال |
|---|---|
| الأسماء بصيغة الجمع | /products ليس /product |
| أفعال HTTP = الإجراءات | GET = قراءة، POST = إنشاء، PUT = استبدال، PATCH = تعديل جزئي، DELETE = حذف |
| لا أفعال في URL | /api/orders ليس /api/getOrders |
| الموارد المتداخلة | /api/cart/items (العناصر تنتمي إلى السلة) |
| الأمان حسب الدور | ADMIN للعمليات الحساسة |
التمرين 3 — تنفيذ نمط Builder
"أنشئ فئة HttpRequest غير قابلة للتغيير باستخدام نمط Builder."
الحل
public final class HttpRequest {
private final String url;
private final String method;
private final Map<String, String> headers;
private final String body;
private HttpRequest(Builder builder) {
this.url = Objects.requireNonNull(builder.url, "URL required");
this.method = builder.method;
this.headers = Map.copyOf(builder.headers); // نسخة دفاعية
this.body = builder.body;
}
public String getUrl() { return url; }
public String getMethod() { return method; }
public Map<String, String> getHeaders() { return headers; }
public String getBody() { return body; }
public static class Builder {
private String url;
private String method = "GET";
private final Map<String, String> headers = new HashMap<>();
private String body;
public Builder url(String url) { this.url = url; return this; }
public Builder method(String m) { this.method = m; return this; }
public Builder header(String k, String v) { headers.put(k, v); return this; }
public Builder body(String b) { this.body = b; return this; }
public HttpRequest build() { return new HttpRequest(this); }
}
}
الاستخدام
HttpRequest req = new HttpRequest.Builder()
.url("https://api.sonar.com/issues")
.method("POST")
.header("Authorization", "Bearer token123")
.header("Content-Type", "application/json")
.body("{\"severity\": \"CRITICAL\"}")
.build();
النقاط الرئيسية
| التقنية | لماذا |
|---|---|
فئة final | يمنع الوراثة |
حقول final | عدم القابلية للتغيير |
منشئ private | فرض استخدام الـ Builder |
Map.copyOf() | نسخة دفاعية - لا يمكن تعديل الكائن بعد الإنشاء |
Objects.requireNonNull() | التحقق من الحقول المطلوبة |
واجهة برمجة سلسة (return this) | تسلسل الاستدعاءات |
"Immutable object, defensive copies for collections, required fields validated in constructor. The fluent API makes the call site self-documenting."
التمرين 4 — تنفيذ نمط Singleton (آمن للخيوط)
"نفذ Singleton آمن للخيوط في Java."
الحل الحديث (Enum Singleton)
public enum DatabaseConnection {
INSTANCE;
private final Connection connection;
DatabaseConnection() {
this.connection = createConnection();
}
public Connection getConnection() {
return connection;
}
private Connection createConnection() {
// منطق الاتصال
return DriverManager.getConnection("jdbc:postgresql://localhost/mydb");
}
}
// الاستخدام
Connection conn = DatabaseConnection.INSTANCE.getConnection();
الحل الكلاسيكي (Double-Checked Locking)
public class DatabaseConnection {
private static volatile DatabaseConnection instance;
private final Connection connection;
private DatabaseConnection() {
this.connection = createConnection();
}
public static DatabaseConnection getInstance() {
if (instance == null) {
synchronized (DatabaseConnection.class) {
if (instance == null) {
instance = new DatabaseConnection();
}
}
}
return instance;
}
}
"I prefer the enum approach — it's the simplest, thread-safe by default, and handles serialization. Joshua Bloch recommends it in Effective Java."
في Spring Boot، نادرًا ما يتم تنفيذ نمط Singleton يدويًا. الحبوب Spring هي singleton افتراضيًا في حاوية IoC. إذا طرح المحاور السؤال، اذكر أنك تستخدم @Service أو @Component التي تكون singleton افتراضيًا.
التمرين 5 — نمط Observer مع أحداث Spring
"نفذ نظام إشعارات غير مرتبط."
الحل مع أحداث Spring
// الحدث
public class OrderCreatedEvent {
private final Order order;
public OrderCreatedEvent(Order order) { this.order = order; }
public Order getOrder() { return order; }
}
// الناشر (الخدمة التي تنشئ الطلب)
@Service
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
@Transactional
public Order createOrder(OrderRequest req) {
Order order = orderRepository.save(new Order(req));
eventPublisher.publishEvent(new OrderCreatedEvent(order));
return order;
}
}
// المستمعون (يتفاعلون مع الحدث)
@Component
class EmailNotificationListener {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
emailService.sendConfirmation(event.getOrder());
}
}
@Component
class InventoryListener {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
inventoryService.decrementStock(event.getOrder().getItems());
}
}
@Component
class AnalyticsListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderCreated(OrderCreatedEvent event) {
analyticsService.trackOrder(event.getOrder());
}
}
المزايا
| بدون Observer | مع Observer |
|---|---|
| OrderService يعرف EmailService، InventoryService، AnalyticsService... | OrderService ينشر حدثًا، هذا كل شيء |
| إضافة مستمع = تعديل OrderService | إضافة مستمع = إنشاء فئة، صفر تعديل |
| ترابط قوي | عدم ترابط كامل |
"I use Spring Events for the Observer Pattern. The OrderService just publishes an event — it doesn't know who listens. Adding a new reaction is just creating a new @EventListener class."
ملخص الأنماط التصميمية
| النمط | المشكلة المحلولة | مثال Spring |
|---|---|---|
| Strategy | تنويع خوارزمية | FeeCalculator مع Map |
| Builder | بناء كائنات معقدة | HttpRequest.Builder |
| Singleton | مثيل فريد | Beans Spring (افتراضيًا) |
| Observer | إشعارات غير مرتبطة | @EventListener |
| Factory | إنشاء كائنات دون معرفة النوع | BeanFactory |
| Template Method | هيكل خوارزمية | JdbcTemplate, RestTemplate |
| Proxy | اعتراض شفاف | @Transactional, @Cacheable (proxies AOP) |
⬅️ الفصل 3: الخوارزميات | الفصل 5: تصحيح الأخطاء ومراجعة الكود →