تنفيذ منطق الأعمال
طبقة الخدمة هي حيث يُثبت تطبيقك قيمته الحقيقية. المتحكّمات تُحلّل المدخلات وتُنسّق المخرجات؛ المستودعات تقرأ البيانات وتكتبها؛ لكن الخدمة هي حيث تُتّخذ القرارات الفعلية — ما الذي يُشكّل مهمة صالحة، وما انتقالات الحالة المشروعة، وما القواعد التي تحكم سلوك النظام. إتقان هذه الطبقة هو الفارق بين برمجيات سهلة التغيير وأخرى تُقاومك في كل مرة تلمسها.
في هذا الدرس ستُنفّذ TaskService وخدمة مخصّصة للتحقق ValidationService لمشروع إدارة المهام النهائي، مُطبّقًا أنماطًا احترافية لفرض القواعد وحارسات الشروط (guard clauses) ودلالات الاستثناءات الواضحة.
مسؤوليتان، صنفان
إغراء شائع هو كتابة خدمة واحدة ضخمة تفعل كل شيء. قاومه. قسّم منطق الأعمال على صنفين مُركّزين:
ValidationService — عديم الحالة، نقي. يُقيّم القيود على مستوى الحقول وقواعد الأعمال ويرمي استثناءً مُصنَّفًا عند انتهاكها. ليس له اعتمادية على المستودع.
TaskService — ذو حالة عبر مستودعه. يُنسّق حالات الاستخدام: يتحقّق، ويُحوّل الحالة، ويُثبّت البيانات. يُفوّض كل منطق القيود إلى ValidationService.
هذا الفصل يُبقي كل صنف صغيرًا، ويمنحك مكانًا واحدًا لتغيير قاعدة ما، ويجعل كلا الصنفين قابلَين للاختبار بمعزل بشكل تافه.
استثناء ValidationException
إخفاقات التحقق ليست أخطاء برمجية — بل نتائج متوقعة على مستوى النطاق. نمّذجها باستثناء مخصّص بدلًا من الإفراط في استخدام IllegalArgumentException:
// exception/ValidationException.java
package com.example.taskmanager.exception;
import java.util.List;
public class ValidationException extends RuntimeException {
private final List<String> errors;
public ValidationException(List<String> errors) {
super("Validation failed: " + String.join("; ", errors));
this.errors = List.copyOf(errors);
}
public ValidationException(String error) {
this(List.of(error));
}
public List<String> getErrors() {
return errors;
}
}
جمع جميع الأخطاء قبل الرمي (بدلًا من الفشل عند أول خطأ) يمنح المستدعين صورة كاملة ويُتيح تجربة مستخدم أفضل: يرى المستخدم جميع المشكلات دفعة واحدة.
تنفيذ ValidationService
كل طريقة تحقق تجمع الأخطاء في قائمة ثم ترمي إن كانت القائمة غير فارغة. هذا هو نمط الجمع والرمي (collect-and-throw).
// service/ValidationService.java
package com.example.taskmanager.service;
import com.example.taskmanager.exception.ValidationException;
import com.example.taskmanager.model.Priority;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
public class ValidationService {
private static final int TITLE_MAX_LENGTH = 120;
private static final int DESCRIPTION_MAX_LENGTH = 2000;
/** يتحقق من جميع الحقول المطلوبة لإنشاء مهمة جديدة. */
public void validateCreateTask(String title,
String description,
Priority priority,
LocalDate dueDate) {
List<String> errors = new ArrayList<>();
if (title == null || title.isBlank()) {
errors.add("Title must not be blank");
} else if (title.length() > TITLE_MAX_LENGTH) {
errors.add("Title must not exceed " + TITLE_MAX_LENGTH + " characters");
}
if (description != null && description.length() > DESCRIPTION_MAX_LENGTH) {
errors.add("Description must not exceed " + DESCRIPTION_MAX_LENGTH + " characters");
}
if (priority == null) {
errors.add("Priority is required");
}
if (dueDate != null && dueDate.isBefore(LocalDate.now())) {
errors.add("Due date must not be in the past");
}
if (!errors.isEmpty()) {
throw new ValidationException(errors);
}
}
/** قاعدة أعمال: لا يمكن إعادة فتح مهمة منتهية عبر التدفق الاعتيادي. */
public void validateStatusTransition(
com.example.taskmanager.model.Status current,
com.example.taskmanager.model.Status requested) {
if (current == com.example.taskmanager.model.Status.DONE
&& requested != com.example.taskmanager.model.Status.DONE) {
throw new ValidationException(
"A completed task cannot be reopened; create a new task instead");
}
}
}
أبقِ ValidationService عديم الحالة. يستقبل قيمًا خامة ثم إما يرمي أو يعود — لا شيء آخر. لا مستودع، ولا Clock إلا إن حقنته. الخدمات العديمة الحالة آمنة للمشاركة كـ singletons وتافهة الاختبار: استدع الطريقة وتحقق من رسالة الاستثناء.
تنفيذ TaskService
تستقبل TaskService كلا المتعاونين عبر منشئها. حارسات الشروط (guard clauses) تظهر في أعلى كل طريقة عامة؛ المسار السعيد يتبع في الأسفل. هذا الأسلوب — الإخفاق السريع، والنجاح المتأخر — يتجنب الشروط المتداخلة بعمق ويجعل التدفق الاعتيادي واضحًا.
// service/TaskService.java
package com.example.taskmanager.service;
import com.example.taskmanager.exception.TaskNotFoundException;
import com.example.taskmanager.model.*;
import com.example.taskmanager.repository.TaskRepository;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
public class TaskService {
private final TaskRepository repository;
private final ValidationService validator;
public TaskService(TaskRepository repository, ValidationService validator) {
this.repository = repository;
this.validator = validator;
}
// ------------------------------------------------------------------ //
// حالة استخدام: إنشاء مهمة جديدة //
// ------------------------------------------------------------------ //
public Task createTask(String title,
String description,
Priority priority,
LocalDate dueDate) {
validator.validateCreateTask(title, description, priority, dueDate);
Task task = new Task(
UUID.randomUUID(),
title.strip(), // تطبيع المسافات قبل الحفظ
description == null ? "" : description.strip(),
priority,
Status.OPEN,
dueDate
);
return repository.save(task);
}
// ------------------------------------------------------------------ //
// حالة استخدام: تحديد مهمة كمنجزة //
// ------------------------------------------------------------------ //
public Task completeTask(UUID id) {
Task existing = repository.findById(id)
.orElseThrow(() -> new TaskNotFoundException(id));
validator.validateStatusTransition(existing.status(), Status.DONE);
// السجلّات غير قابلة للتغيير — أنتج نسخة جديدة بالحالة المحدَّثة
Task completed = new Task(
existing.id(),
existing.title(),
existing.description(),
existing.priority(),
Status.DONE,
existing.dueDate()
);
return repository.save(completed);
}
// ------------------------------------------------------------------ //
// حالة استخدام: سرد المهام مع فلترة اختيارية //
// ------------------------------------------------------------------ //
public List<Task> listAllTasks() {
return repository.findAll();
}
public List<Task> listOverdueTasks() {
LocalDate today = LocalDate.now();
return repository.findAll().stream()
.filter(t -> t.status() != Status.DONE)
.filter(t -> t.dueDate() != null && t.dueDate().isBefore(today))
.toList();
}
// ------------------------------------------------------------------ //
// حالة استخدام: حذف مهمة //
// ------------------------------------------------------------------ //
public void deleteTask(UUID id) {
if (repository.findById(id).isEmpty()) {
throw new TaskNotFoundException(id);
}
repository.delete(id);
}
}
طبّع المدخلات عند حدود الخدمة. استدعاء title.strip() قبل إنشاء السجلّ يضمن ألّا يُخزّن قاعدة البيانات مسافات بيضاء غير مقصودة. افعل هذا في الخدمة، لا في النموذج، حتى يبقى النموذج حاوية بيانات نقية وتكون منطق التطبيع مرئيًا وقابلًا للاختبار في مكان واحد.
أين تعيش قواعد الأعمال — وأين لا تعيش
سؤال تصميمي متكرر: هل يجب أن يعيش المنطق في النموذج أم في الخدمة أم في المستودع؟ الجواب يتبع المسؤوليات:
- النموذج (record/entity) — الثوابت (invariants) التي تكون صحيحة دائمًا بغض النظر عن السياق. المنشئ المضغوط في
Task الذي يرفض العنوان الفارغ هو ثابت على مستوى النموذج.
- الخدمة — قواعد حالات الاستخدام التي تشمل كائنات متعددة أو انتقالات حالة أو قيودًا متقاطعة بين الحقول. "لا يمكن إنجاز مهمة منتهية بالفعل" هي قاعدة على مستوى الخدمة.
- المستودع — اهتمامات الإصرار فحسب. فلترة المهام المتأخرة ليست اهتمام مستودع؛ إنها قاعدة أعمال تستخدم مقارنة تاريخ. أبقها في الخدمة مع stream.
لا تُضمّن قواعد الأعمال في استعلامات SQL أو مواصفات JPA. يبدو فعّالًا دفع كل شيء إلى جملة WHERE، لكنه يُبعثر منطق أعمالك عبر لغتين، يجعله غير مرئي لاختبارات الوحدة (التي لا تصل إلى قاعدة البيانات)، ويُقرن قواعدك بتقنية إصرار محددة. استخدم المستودع لاسترجاع البيانات؛ استخدم الخدمة لتقييم القواعد.
حقن المنشئ — لا إطار مطلوب
لاحظ أن أيًا من صنفَي الخدمة لا يستخدم Spring أو CDI أو أي إطار حقن اعتماديات. الاعتماديات مُعلَنة كحقول final ومُسنَدة في المنشئ. يحدث الربط في Main.java (أو جذر تكوين مكتوب يدويًا) بجافا عادية:
// Main.java (جذر التكوين)
package com.example.taskmanager;
import com.example.taskmanager.repository.InMemoryTaskRepository;
import com.example.taskmanager.service.TaskService;
import com.example.taskmanager.service.ValidationService;
import com.example.taskmanager.ui.ConsoleApp;
public class Main {
public static void main(String[] args) {
var repository = new InMemoryTaskRepository();
var validator = new ValidationService();
var taskService = new TaskService(repository, validator);
var app = new ConsoleApp(taskService);
app.run(args);
}
}
هذا النمط — يُسمى جذر التكوين (composition root) — يُبقي ربط الرسم البياني الكامل للكائنات في مكان واحد واضح. استبدال InMemoryTaskRepository بـ JdbcTaskRepository هو تغيير سطر واحد هنا وصفر تغييرات في أي مكان آخر.
المقايضات: متى تُنشئ ValidationService مخصصًا
في التطبيقات الصغيرة جدًا، خدمة تحقق منفصلة مبالغة — حارسات الشروط المضمّنة داخل الخدمة كافية. أنشئ صنفًا مخصصًا حين:
- تنطبق نفس القواعد على حالات استخدام متعددة (الإنشاء والتحديث يشتركان في قيود العنوان ذاتها).
- تريد اختبار التحقق بمعزل تام، مفصولًا عن اهتمامات الإصرار.
- القواعد معقّدة بما يجعل طريقة خدمة واحدة صعبة القراءة.
الخلاصة
طبقة الخدمة هي قلب التطبيق: تتحقق من المدخلات عبر ValidationService مُركَّز، وتفرض قواعد انتقال الحالة، وتُطبّع البيانات عند الحدود، وتُنسّق الإصرار دون تسريب اهتمامات البنية التحتية صعودًا أو هبوطًا. إبقاء الخدمات رشيقة، وحقن الاعتماديات عبر المنشئ، واحترام مبدأ أن منطق الأعمال ينتمي إلى الخدمة — لا إلى المستودعات أو الاستعلامات — ينتج قاعدة كود يُسعدك توسيعها واختبارها. تاليًا ستبني طبقة الواجهة التي تستدعي هذه الخدمات.