أنماط التصميم في جافا

نمط البنّاء (Builder)

15 دقيقة الدرس 4 من 13

نمط البنّاء (Builder)

يفصل نمط البنّاء (Builder) عملية بناء الكائن المعقّد عن تمثيله، مما يسمح لنفس عملية البناء بإنتاج نتائج مختلفة. في Java الحديثة يُعدّ هذا النمط الحلّ القياسي لبناء كائنات قيمة ثابتة (Immutable) ذات حقول اختيارية كثيرة — تلك الحالات التي تتحوّل فيها المُنشئات المتسلسلة إلى شيفرة غير مقروءة ويمنع فيها استخدام setters تحقيق الثبات.

لماذا لا نكتفي بالمُنشئات أو الـ Setters؟

تخيّل كلاس HttpRequest يحتوي على حقل URL مطلوب وحقل method مطلوب وثمانية حقول اختيارية (headers، body، timeout، follow-redirects، auth، proxy، cache policy، retry count). هناك نهجان شائعان لكنّهما معيبان:

  • المُنشئات المتسلسلة (Telescoping constructors) — تكتب مُنشئًا لكل تركيبة ممكنة. مع ثمانية حقول اختيارية تنتهي بعشرات المُنشئات، وأماكن الاستدعاء مثل new HttpRequest(url, method, null, null, 30, true, null, null, 3) تصبح مستحيلة القراءة.
  • أسلوب JavaBean (باستخدام setters قابلة للتعديل) — يبدأ الكائن في حالة غير متسقة، ولا يمكن جعله final / ثابتًا، وسلامة الخيوط المتزامنة تستلزم تزامنًا خارجيًا.

يحلّ نمط Builder كلا المشكلتين: توفّر الواجهة البرمجية السلسة (Fluent API) أماكن استدعاء تشرح نفسها بنفسها، ويُجمَّع الكائن الهدف مرة واحدة فقط — عند استدعاء build() — فيمكن جعله ثابتًا بالكامل.

الشكل الكلاسيكي: Builder داخلي ساكن

الأسلوب القياسي في Java هو وضع public static final class Builder داخل الكلاس الهدف، ويكون للكلاس الهدف مُنشئ خاص لا يقبل سوى الـ Builder.

public final class HttpRequest { // الحقول المطلوبة private final String url; private final String method; // الحقول الاختيارية — قيم افتراضية منطقية private final int timeoutMs; private final int maxRetries; private final boolean followRedirects; private final String body; private final String authToken; // خاص: لا يستطيع استدعاءه إلا الـ Builder private HttpRequest(Builder b) { this.url = b.url; this.method = b.method; this.timeoutMs = b.timeoutMs; this.maxRetries = b.maxRetries; this.followRedirects = b.followRedirects; this.body = b.body; this.authToken = b.authToken; } // قراءة فقط — لا setters public String getUrl() { return url; } public String getMethod() { return method; } public int getTimeoutMs() { return timeoutMs; } public int getMaxRetries() { return maxRetries; } public boolean isFollowRedirects() { return followRedirects; } public String getBody() { return body; } public String getAuthToken() { return authToken; } @Override public String toString() { return method + " " + url + " [timeout=" + timeoutMs + "ms, retries=" + maxRetries + "]"; } // ---- Builder ---- public static final class Builder { // مطلوبة — تُمرَّر عبر المُنشئ private final String url; private final String method; // اختيارية — قيمها الافتراضية تُحدَّد هنا private int timeoutMs = 5_000; private int maxRetries = 0; private boolean followRedirects = true; private String body = null; private String authToken = null; public Builder(String url, String method) { if (url == null || url.isBlank()) throw new IllegalArgumentException("url required"); if (method == null || method.isBlank()) throw new IllegalArgumentException("method required"); this.url = url; this.method = method.toUpperCase(); } public Builder timeoutMs(int ms) { this.timeoutMs = ms; return this; } public Builder maxRetries(int n) { this.maxRetries = n; return this; } public Builder followRedirects(boolean f) { this.followRedirects = f; return this; } public Builder body(String body) { this.body = body; return this; } public Builder authToken(String token) { this.authToken = token; return this; } public HttpRequest build() { // التحقق من الحقول المتقاطعة يكون هنا if ("GET".equals(method) && body != null) { throw new IllegalStateException("GET requests must not have a body"); } return new HttpRequest(this); } } }

مكان الاستدعاء يُشبه جملة بشرية مفهومة:

HttpRequest req = new HttpRequest.Builder("https://api.example.com/users", "POST") .authToken("Bearer eyJhbG...") .body("{\"name\":\"Alice\"}") .timeoutMs(10_000) .maxRetries(3) .build();
الحقول المطلوبة مقابل الاختيارية: مرّر الحقول المطلوبة إلى مُنشئ الـ Builder ليفرضها المصرِّف (compiler). أما الحقول الاختيارية فتحصل على قيم افتراضية وـ setters سلسة. هذا أوضح من جعل كل الحقول اختيارية والتحقق من اكتمالها في build().

التحقق داخل build()

دالة build() هي المكان الصحيح للتحقق من القيود التي تشمل أكثر من حقل واحد. أما التحقق من حقل واحد (null checks، نطاق القيم) فيجب في كل setter سلس حتى يظهر الخطأ قريبًا من الاستدعاء المعيب. مثال:

public Builder maxRetries(int n) { if (n < 0) throw new IllegalArgumentException("maxRetries must be >= 0"); this.maxRetries = n; return this; }
أخفق مبكرًا، أخفق قريبًا: ارمِ IllegalArgumentException في الـ setter فور رؤية بيانات خاطئة. إن أجّلت كل التحقق إلى build() سيشير تتبّع المكدس (stack trace) إليها لا إلى الاستدعاء المعيب الفعلي، فيطول وقت التصحيح.

Copy Builders — تحديثات ثابتة آمنة

لأن الكائن الهدف ثابت لا يمكن تعديله بعد البناء. يتيح لك نمط copy builder (يُسمى أيضًا نمط withX) اشتقاق كائن جديد من كائن موجود مع تغيير حقول بعينها فقط — نفس فكرة تعبير with في سجلات Java (records):

// factory ساكت يملأ Builder مسبقًا من كائن موجود public static Builder from(HttpRequest source) { return new Builder(source.url, source.method) .timeoutMs(source.timeoutMs) .maxRetries(source.maxRetries) .followRedirects(source.followRedirects) .body(source.body) .authToken(source.authToken); }
// الاستخدام: اشتقاق طلب جديد بـ timeout مختلف HttpRequest retried = HttpRequest.from(original) .timeoutMs(30_000) .maxRetries(5) .build();

Builder مقابل Java Records

توفّر records في Java 16+ حاملات بيانات ثابتة مضغوطة للحالات البسيطة. استخدم Builder عندما:

  • يوجد عدد كبير من الحقول الاختيارية (تمتلك السجلات مُنشئًا قياسيًا واحدًا — جميع الحقول مطلوبة).
  • تحتاج إلى منطق تحقق يشمل أكثر من حقل في build().
  • الكائن جزء من واجهة برمجية سلسة (Fluent API) (بُناة الاستعلامات، عملاء HTTP، بيانات الاختبارات).
  • تريد دعم copy builders بصورة أنيقة.

أما حامل البيانات المكوّن من ثلاثة حقول بدون حقول اختيارية وبدون تحقق، فالـ record أبسط وأفضل.

Lombok @Builder — متى تلجأ إليه

في قواعد الشيفرة الإنتاجية تُولّد تعليمة Lombok @Builder الـ inner Builder وقت التصريف، مما يُزيل الشيفرة المتكررة. المقايضة: الـ builder المُولَّد لا يفرض الحقول المطلوبة ولا يُجري تحققًا مخصصًا إلا بإضافة @Builder.ObtainVia وتجاوز build() يدويًا. استخدمه مع كائنات DTO / القيم المبسوطة؛ واكتبه يدويًا حين تحتاج قيودًا صارمة.

سلامة الخيوط المتزامنة: الـ Builder بذاته ليس آمنًا للخيوط المتزامنة. لا تشارك Builder جزئيًا مبنيًا بين خيوط متعددة. أما الكائن المبني فآمن للمشاركة لأنه ثابت — لكن عملية البناء يجب أن تبقى في خيط واحد.

الاستخدام الفعلي في JDK والنظام البيئي

النمط موجود في كل مكان من Java الحديثة:

  • StringBuilder / StringJoiner — تجميع الأجزاء وإنتاج String.
  • HttpClient.newBuilder() / HttpRequest.newBuilder() — عميل HTTP في JDK 11+.
  • ProcessBuilder — تهيئة تشغيل عمليات نظام التشغيل.
  • MockMvcRequestBuilders في Spring، وCriteriaBuilder في Hibernate، وImmutableList.builder() في Guava.

الخلاصة

نمط Builder هو الإجابة المهنية لبناء الكائنات المعقدة في Java. بتمرير الحقول المطلوبة إلى مُنشئ الـ Builder، وتوفير setters سلسة للحقول الاختيارية، وتمركز التحقق في build()، والحفاظ على ثبات الكلاس الهدف بالكامل، تحصل على أماكن استدعاء مقروءة وأمان في وقت التصريف وكائنات يسهل مشاركتها بأمان عبر الخيوط المتزامنة. امتداد copy-builder يجعل التحديثات الثابتة مريحة. في الدرس التالي ننتقل إلى الأنماط السلوكية بدءًا بنمط Strategy.