كيفية بناء معمارية نظيفة حديثة للتطبيقات البرمجية
مقدمة: ما المقصود بالمعمارية النظيفة الحديثة؟
يشير مفهوم Clean Architecture إلى أسلوب تصميم برمجي يهدف إلى فصل منطق الأعمال عن تفاصيل الأطر البرمجية، وواجهات المستخدم، وقواعد البيانات، والخدمات الخارجية. صاغ هذا المصطلح Robert C. Martin، وتكمن فكرته الأساسية في أن الكيانات وحالات الاستخدام يجب أن تبقى مستقلة عن أي تقنية قابلة للتغيير.
هذا الفصل ينعكس مباشرة على جودة المشروع على المدى الطويل، لأنه يمنح الفريق قدرة أفضل على الاختبار، والتطوير، واستبدال الأدوات دون المساس بجوهر النظام.

لماذا تُعد Clean Architecture خياراً عملياً؟
الاعتماد على معمارية نظيفة لا يقتصر على الجانب الأكاديمي، بل يقدم فوائد عملية واضحة في المشاريع الصغيرة والمتوسطة والكبيرة.
- إمكانية اختبار منطق المجال وحالات الاستخدام دون الحاجة إلى إطار عمل أو قاعدة بيانات.
- تقليل أثر تغيير التقنيات على منطق الأعمال.
- جعل التطبيق أكثر مرونة عند الانتقال إلى إطار جديد.
- تبسيط الصيانة المستقبلية وتوزيع المسؤوليات بين الطبقات.
الهدف من النهج الحديث هنا هو تقليل منحنى التعلّم وتسهيل تطبيق هذا النمط في مشروع حقيقي، من الواجهة الأمامية المبنية بـHTML/JavaScript إلى الواجهة الخلفية باستخدام Spring Boot.
نظرة عامة على التطبيق التجريبي: قائمة المهام
سنستخدم تطبيق TODO بسيطاً بوصفه مثالاً عملياً. ورغم بساطته، فإنه مناسب جداً لفهم المفاهيم الأساسية للمعمارية النظيفة.
قائمة المهام هي مجموعة من العناصر، وكل عنصر يمثل مهمة تملك اسماً، وتكون إما مكتملة أو غير مكتملة. ويستطيع المستخدم تنفيذ العمليات التالية:
- إنشاء قائمة مهام وحفظها.
- إضافة مهمة جديدة.
- تمييز المهمة كمكتملة أو إلغاء هذا التمييز.
- حذف مهمة.
- عرض جميع المهام.
- تصفية المهام المكتملة وغير المكتملة.

سنبدأ من قلب التطبيق، أي طبقة المجال، ثم نتحرك تدريجياً إلى الخارج حتى نصل إلى الواجهة الأمامية.
كيانات المجال: الأساس الحقيقي للتطبيق
الكيانات المركزية في هذا المثال هي TodoList وTask. تمثل TodoList الجذر الرئيسي الذي يحتوي على:
- معرّف فريد.
- قائمة من المهام.
- دوال مجال لإضافة المهام وإكمالها وحذفها.
من المهم هنا أن الكيان TodoList لا يحتوي على دوال ضبط عامة من نوع setters، لأن ذلك يضعف مبدأ التغليف Encapsulation. بدلاً من ذلك، تُدار التغييرات من خلال سلوك واضح داخل الكيان نفسه.
public class TodoList implements AggregateRoot < TodoList , TodoListId > {
private final TodoListId id;
private final List<Task> tasks;
@Value(staticConstructor = "of")
public static class TodoListId implements Identifier {
@NonNull UUID uuid;
}
@Override
public TodoListId getId () {
return id;
}
...
public TaskId addTask (String taskName) {
if (taskName == null || isWhitespaceName(taskName)) {
throw new IllegalTaskName(
"Please specify a non-null, non-whitespace task name!"
);
}
TaskId taskId = add(TaskId.of(UUID.randomUUID()), taskName, false);
return taskId;
}
...
public void deleteTask (TaskId task) {
Optional<Task> foundTask = findTask(task);
foundTask.ifPresent(tasks::remove);
}
...
}
ما فائدة الواجهة AggregateRoot؟
مصطلح Aggregate Root يأتي من منهجية Domain-Driven Design أو DDD. والمقصود به وجود مجموعة من الكائنات المرتبطة التي تُعامل كوحدة واحدة عند تعديل البيانات. داخل هذه الوحدة يوجد جذر واحد فقط هو المسؤول عن ضبط التغييرات وفرض القيود.
في مثالنا، هذا يعني أن أي تعديل على المهام يجب أن يمر عبر TodoList. فلا يجوز إنشاء مهمة أو حذفها أو تغيير حالتها من خارج هذا الجذر بشكل عشوائي. بهذه الطريقة يمكن للكيان فرض قواعد العمل، مثل منع إضافة مهمة باسم فارغ.
الواجهة AggregateRoot جزء من مكتبة jMolecules التي تساعد على جعل مفاهيم DDD صريحة داخل كود المجال. وخلال عملية البناء، يتولى مكوّن ByteBuddy تحويل هذه التعريفات إلى تكامل مناسب مع Spring Data، ما يسمح باستخدام نموذج واحد يمثل المجال والتخزين معاً، دون تلويث منطق المجال بتفاصيل إطار العمل.
الكيان Task ولماذا صُمم بهذه الطريقة؟
فئة Task مشابهة في بنيتها، لكنها تطبق الواجهة Entity بدلاً من AggregateRoot.
public class Task implements Entity < TodoList , TaskId > {
private final TaskId id;
private final String name;
private final boolean completed;
@Value(staticConstructor = "of")
public static class TaskId implements Identifier {
@NonNull UUID uuid;
}
Task(@NonNull TaskId id, @NonNull String name, boolean completed) {
this.id = id;
this.name = name;
this.completed = completed;
}
}
لاحظ أن المُنشئ constructor هنا له نطاق package-private، وهذا يمنع إنشاء كائن Task من خارج حزمة المجال. كما أن الكيان غير قابل للتغيير immutable، ما يضمن عدم تعديل حالته من خارج حدود التجميع aggregate.
المستودع: حفظ الكيان بلغة المجال
لحفظ TodoList واسترجاعها نحتاج إلى مستودع repository. ومن الجيد في هذا النمط أن يُسمّى بلغة المجال نفسها، لذلك استُخدم الاسم TodoLists.
public interface TodoLists extends Repository < TodoList , TodoListId > {
TodoList save (TodoList entity);
Optional<TodoList> findById (TodoListId id);
Iterable<TodoList> findAll ();
}
مرة أخرى، تُستخدم هنا واجهة Repository من مكتبة jMolecules. ويجري تحويلها أثناء البناء إلى مستودع متوافق مع Spring Data، ما يتيح الحفاظ على استقلالية طبقة المجال عن أي إطار عمل محدد.
سلوك التطبيق وحالات الاستخدام
بعد تعريف كيانات المجال، ننتقل إلى السلوك الذي يراه المستخدم. في هذا الأسلوب، أي تفاعل مع النظام يمر غالباً عبر المراحل التالية:
- ترسل واجهة المستخدم طلباً.
- تستقبل الواجهة الخلفية هذا الطلب.
- ينفذ معالج الطلب كل ما يلزم: الوصول إلى قاعدة البيانات، استدعاء الخدمات، أو تشغيل منطق المجال.
- قد يعيد المعالج استجابة إذا كانت العملية تتطلب ذلك.
تُنفذ معالجات الطلبات باستخدام واجهات دالية من Java 8 مثل Function وConsumer.
مثال: معالج إضافة مهمة AddTask
هذا المعالج يستقبل كائناً من النوع AddTaskRequest، ويستخرج منه معرّف القائمة واسم المهمة، ثم يبحث عن القائمة، ويضيف المهمة، ويحفظ النتيجة، ثم يعيد استجابة من النوع AddTaskResponse.
@AllArgsConstructor
class AddTask implements Function < AddTaskRequest , AddTaskResponse > {
@NonNull private final TodoLists repository;
@Override
public AddTaskResponse apply(@NonNull AddTaskRequest request) {
final UUID todoListUuid = request.getTodoListUuid();
final String taskName = request.getTaskName();
final TodoList todoList = repository.findById(TodoListId.of(todoListUuid))
.orElseThrow(() -> new TodoListNotFound(
"Repository doesn't contain a TodoList of id " + todoListUuid));
TaskId taskId = todoList.addTask(taskName);
repository.save(todoList);
return new AddTaskResponse(taskId.getUuid());
}
}
يعتمد هذا المعالج على المستودع TodoLists عبر الحقن في المُنشئ، ما يجعل الاعتماد على البنية الخارجية واضحاً وقابلاً للاستبدال والاختبار.
الطلبات والاستجابات ككائنات غير قابلة للتغيير
يفضل في هذا الأسلوب أن تكون كائنات الطلب والاستجابة غير قابلة للتعديل، لأن ذلك يسهل تتبع البيانات ويحسن موثوقية السلوك.
@Value
public class AddTaskRequest {
@NonNull UUID todoListUuid;
@NonNull String taskName;
}
تتولى مكتبات Modern Clean Architecture تحويل هذه الكائنات من وإلى JSON دون الحاجة إلى تعريفات مرهقة.
مثال: معالج حذف مهمة DeleteTask
إذا كان المعالج لا يعيد استجابة، فيمكنه تطبيق الواجهة Consumer.
@AllArgsConstructor
class DeleteTask implements Consumer < DeleteTaskRequest > {
@NonNull private final TodoLists repository;
@Override
public void accept(@NonNull DeleteTaskRequest request) {
final UUID todoListUuid = request.getTodoListUuid();
final UUID taskUuid = request.getTaskUuid();
final TodoList todoList = repository.findById(TodoListId.of(todoListUuid))
.orElseThrow(() -> new TodoListNotFound(
"Repository doesn't contain a TodoList of id " + todoListUuid));
todoList.deleteTask(TaskId.of(taskUuid));
repository.save(todoList);
}
}
نموذج السلوك BehaviorModel: مركز ربط الطلبات بالمعالجات
يبقى سؤال مهم: من الذي ينشئ هذه المعالجات ويربط كل طلب بالمنفذ المناسب؟ الجواب هو فئة تطبق الواجهة BehaviorModel.
@AllArgsConstructor
public class TodoListBehaviorModel implements BehaviorModel {
@NonNull private final TodoLists todoLists;
...
@Override
public Model model() {
return Model.builder()
.user(FindOrCreateListRequest.class).systemPublish(findOrCreateList())
.user(AddTaskRequest.class).systemPublish(addTask())
.user(ToggleTaskCompletionRequest.class).system(toggleTaskCompletion())
...
.build();
}
private Function<FindOrCreateListRequest, FindOrCreateListResponse> findOrCreateList() {
return new FindOrCreateList(todoLists);
}
private Function<AddTaskRequest, AddTaskResponse> addTask() {
return new AddTask(todoLists);
}
private Consumer<ToggleTaskCompletionRequest> toggleTaskCompletion() {
return new ToggleTaskCompletion(todoLists);
}
...
}
في هذا النموذج:
user(...)يحدد نوع الطلب.systemPublish(...)يستخدم عندما يكون هناك رد.system(...)يستخدم عندما لا توجد استجابة مرجعة.
الميزة الأهم هنا أن جميع الاعتمادات الخارجية تُحقن في مكان مركزي واحد. وهذا يسهّل تبديل التقنيات أو توسيع السلوك دون تعديل منطق المجال نفسه.
طبقة الويب: محولات رشيقة وخفيفة
في المعمارية النظيفة الحديثة، ليست طبقة الويب بالضرورة معقدة. في أبسط صورة، قد تتكون من فئتين فقط:
- فئة لضبط الاعتمادات.
- فئة لمعالجة الاستثناءات.
فئة الإعداد TodoListConfiguration
@Configuration
class TodoListConfiguration {
@Bean
TodoListBehaviorModel behaviorModel(TodoLists repository) {
return new TodoListBehaviorModel(repository);
}
}
يقوم Spring بحقن تنفيذ المستودع داخل هذه الفئة، ثم تُنشأ منها نسخة من behavior model. وإذا كان التطبيق يعتمد على خدمات خارجية، فهذا هو المكان المناسب لإنشائها وحقنها.
أين وحدات التحكم Controllers؟
في هذا النهج، لا تحتاج غالباً إلى إنشاء controllers يدوياً للتعامل مع طلبات POST. مكتبة spring-behavior-web تتكفل بهذا الأمر تلقائياً عند تعريف نقطة نهاية واحدة داخل ملف application.properties:
behavior.endpoint = /todolist
عند وجود هذا الإعداد، تنشئ المكتبة متحكماً داخلياً في الخلفية يتولى استقبال طلبات POST وتمريرها إلى السلوك المناسب.
كيف تمر الطلبات داخل النظام؟
- تستقبل نقطة النهاية طلب
POST. - تقوم مكتبة
spring-behavior-webبفكJSONإلى كائن طلب مناسب. - يُمرر الطلب إلى السلوك المربوط به في
BehaviorModel. - ينفذ المعالج منطق المجال ويعيد استجابة إن وجدت.
- تُحوّل الاستجابة إلى
JSONوتُعاد إلى العميل.
وبشكل افتراضي، تُغلّف هذه العملية داخل معاملة transaction لكل استدعاء.
كيفية إرسال طلبات POST إلى التطبيق
بعد تشغيل تطبيق Spring Boot، يمكن إرسال الطلبات إلى نقطة النهاية المحددة. يجب تضمين الخاصية @type داخل محتوى JSON حتى تتمكن المكتبة من معرفة نوع الطلب أثناء فك التحويل.
مثال باستخدام curl:
curl -H "Content-Type: application/json" -X POST -d '{"@type": "FindOrCreateListRequest"}' http://localhost:8080/todolist
ومثال مكافئ في Windows PowerShell:
iwr http://localhost:8080/todolist -Method 'POST' -Headers @{'Content-Type' = 'application/json'} -Body '{"@type": "FindOrCreateListRequest"}'
معالجة الاستثناءات في طبقة الويب
من ناحية معالجة الأخطاء، لا يختلف هذا النهج عن أسلوب Spring التقليدي. يمكن استخدام فئة مشروحة بالوسم @ControllerAdvice مع دوال تحمل الوسم @ExceptionHandler.
@ControllerAdvice
class TodoListExceptionHandling {
@ExceptionHandler({ Exception.class })
public ResponseEntity<ExceptionResponse> handle(Exception e) {
return responseOf(e, BAD_REQUEST);
}
...
}
وفي التطبيقات الحقيقية، من الأفضل تخصيص معالجة مختلفة لكل نوع استثناء بدلاً من الاكتفاء بمعالجة عامة.
الواجهة الأمامية للتطبيق
تتكون الواجهة الأمامية في هذا المثال من:
- صفحة
HTML. - ملف تنسيق
CSS. - ملف
main.jsالذي يدير التفاعل مع الواجهة الخلفية.
الجزء الأكثر أهمية هنا هو main.js، لأنه يرسل الطلبات ويحدث الصفحة بناءً على الاستجابات.
// URL for posting all requests,
// Must be the same as the one in application.properties
const BEHAVIOR_ENDPOINT = "/todolist";
// variables
var todoListUuid;
...
// functions
function restoreList() {
const request = { "@type": "FindOrCreateListRequest" };
post(request, function(response) {
todoListUuid = response.todoListUuid;
restoreTasksOf(todoListUuid);
});
}
function restoreTasksOf(todoListUuid) {
const request = { "@type": "ListTasksRequest", "todoListUuid": todoListUuid };
post(request, function(response) {
showTasks(response.tasks);
});
}
...
function post(jsonObject, responseHandler) {
const xhr = new XMLHttpRequest();
xhr.open("POST", BEHAVIOR_ENDPOINT);
xhr.setRequestHeader("Accept", "application/json");
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
response = xhr.responseText.length > 0 ? JSON.parse(xhr.response) : "";
if (response.error) {
alert('Status ' + response.status + ' "' + response.message + '"');
} else {
responseHandler(response);
}
}
};
const jsonString = JSON.stringify(jsonObject);
xhr.send(jsonString);
}
مثال على كائن طلب من النوع ListTasksRequest:
const request = {"@type":"ListTasksRequest", "todoListUuid":todoListUuid};
تقوم الدالة post(...) بإرسال الطلب إلى الواجهة الخلفية، ثم تسلّم الاستجابة إلى دالة المعالجة التي تم تمريرها. وهذا الأسلوب يبقي الواجهة الأمامية بسيطة وواضحة.
لماذا يُعد هذا النهج مناسباً للمشاريع الحديثة؟
الجانب المميز في هذا التصميم هو أنه يمنحك مرونة كبيرة دون التضحية بالوضوح. فبدلاً من بعثرة منطق الأعمال بين وحدات التحكم والمستودعات والخدمات، يتم تنظيمه في صورة طلبات ومعالجات وكيانات مجال مترابطة بمنطق واضح.
ومن أبرز مزاياه العملية:
- إمكانية استخدام نفس كائنات الطلب والاستجابة بين طبقة الويب وحالات الاستخدام.
- تقليل الحاجة إلى كائنات نقل البيانات
DTOsفي كثير من الحالات. - إضافة سلوك جديد دون كتابة كود خاص بالإطار في كل مرة.
- الحفاظ على استقلالية منطق الأعمال عن
Springأو غيره من الأطر.
أفضل ممارسات عند تطبيق Modern Clean Architecture
1) اجعل طبقة المجال نقية قدر الإمكان
تجنب إدخال تفاصيل البنية التحتية إلى كيانات المجال. كلما بقيت الكيانات مستقلة، أصبح اختبارها وإعادة استخدامها أسهل.
2) استخدم أسماء تعبر عن المجال
تسمية المستودعات والطلبات والكيانات بلغة العمل الفعلية تجعل الكود أكثر وضوحاً للمطورين وأصحاب المصلحة.
3) افصل السلوك عن التقنية
حاول أن تكون الاعتمادات الخارجية قابلة للحقن والاستبدال، حتى لا ترتبط حالات الاستخدام بأداة واحدة بشكل دائم.
4) لا تبالغ في التعقيد مبكراً
المعمارية النظيفة مفيدة، لكن يجب تطبيقها بقدر يتناسب مع حجم المشروع. ليست كل التطبيقات بحاجة إلى أعلى درجات التجريد من اليوم الأول.
الخلاصة التقنية
المعمارية النظيفة الحديثة ليست مجرد ترتيب جميل للملفات، بل هي طريقة عملية لحماية منطق الأعمال من تقلبات التقنية. عندما تكون كيانات المجال مستقلة، ومعالجات الطلبات واضحة، وطبقة الويب رشيقة، يصبح النظام أسهل في الاختبار، وأسرع في التطوير، وأكثر استعداداً للتغيير. هذا النهج مناسب جداً للتطبيقات التي يُتوقع لها النمو، لأنه يقلل الديون التقنية منذ البداية ويمنح الفريق أساساً مستقراً للتوسع.