جلسه شماره 113رایگان

Static Factory vs Public Constructor | استاتیک فکتوری یا کانستراکتور

00:52:16

توضیحات جلسه

🎥 این ویدیو دومین قسمت از سری بررسی کتاب افکتیو جاوا است. 📚 در این جلسه به سراغ اولین آیتم از فصل دوم کتاب می‌رویم که یکی از بنیادی‌ترین تغییرات در طراحی کلاس‌ها را معرفی می‌کند: استفاده از Static Factory Methods به جای Public Constructors. در این ویدیو بررسی می‌کنیم که چرا همیشه استفاده از سازنده عمومی (Constructor) انتخاب ایده‌آلی نیست و چگونه متدهای استاتیک کارخانه‌ای می‌توانند کد شما را خواناتر، منعطف‌تر و بهینه‌تر کنند. این مبحث برای درک بهتر معماری نرم‌افزار و نوشتن کدهای تمیز (Clean Code) بسیار حیاتی است. در این جلسه یاد می‌گیریم: 💡 تفاوت متد استاتیک فکتوری با سازنده‌های معمولی 🏷️ اهمیت نام‌گذاری معنادار و خوانایی بالاتر کد ♻️ قابلیت کنترل نمونه‌ها، کش کردن و جلوگیری از ایجاد آبجکت‌های غیرضروری 🏗️ انعطاف در برگرداندن انواع مختلف زیرکلاس‌ها و پیاده‌سازی اینترفیس‌ها ⚠️ محدودیت‌های این روش در زیرکلاس‌سازی و قابلیت پیدا شدن در مستندات اگر به دنبال نوشتن کدهای اصولی و حرفه‌ای در جاوا هستید، درک این آیتم دیدگاه شما را نسبت به نحوه ایجاد آبجکت‌ها تغییر می‌دهد. 🔗 لینک دانلود کتاب: https://github.com/farzadafi/Teaching/blob/master/Book/EffectiveJava/2018-Effective%20Java.pdf 📌 یوتیوب: youtube.com/@farzadafi 📌 آپارات: aparat.com/farzadafi 📌 وب سایت: https://farzadafi.ir 💬 گروه پرسش و پاسخ تلگرام: @programmingByFarzadAfi 💬 گروه پرسش و پاسخ بله: @programming_by_farzadafi هر سوالی داشته باشید می‌توانید داخل گروه بپرسید تا من یا مربی‌هایی که در گروه حضور دارند کمکتان کنند :) 🎯 بوت‌کمپ رایگان جاوا – آموزش جاوا پروژه محور برای یادگیری عمیق و ورود به بازار کار.

جزوه و مستندات

مفاهیم کلیدی

  • Object Creation and Destruction

مدیریت صحیح ایجاد و نابودی objectها، بخش مهمی از طراحی API در Java است. باید مشخص شود یک object چه زمانی ساخته شود، چه زمانی از ساخت غیرضروری آن جلوگیری شود و پیش از حذف آن چه عملیات پاک‌سازی لازم است.

  • Public Constructor

روش متداول ساخت object در Java است. constructor با new فراخوانی می‌شود و معمولاً در هر فراخوانی یک نمونه‌ی جدید می‌سازد.

  • Static Factory Method

یک static method عمومی است که به‌جای constructor، نمونه‌ای از همان نوع یا یکی از زیرنوع‌های آن را برمی‌گرداند. این روش با الگوی Factory Method در کتاب Design Patterns یکسان نیست.

  • نام‌گذاری در Static Factory Method

برخلاف constructor، یک static factory method نام دارد. نام مناسب، هدف ساخت object و معنای پارامترها را روشن می‌کند؛ مانند BigInteger.probablePrime(...).

  • ابهام در Constructorهای مشابه

یک class نمی‌تواند دو constructor با امضای کاملاً یکسان داشته باشد. تعریف constructorهایی که فقط ترتیب نوع پارامترهایشان تفاوت دارد، طراحی ضعیفی است؛ زیرا استفاده‌کننده ممکن است به‌اشتباه constructor نادرست را فراخوانی کند.

  • Object Caching

در static factory method لازم نیست در هر فراخوانی یک object جدید ساخته شود. می‌توان نمونه‌های قبلی را نگه داشت و در درخواست‌های مشابه همان نمونه را بازگرداند.

  • Immutable Class

کلاسی است که وضعیت آن پس از ساخت تغییر نمی‌کند. چنین کلاس‌هایی می‌توانند از نمونه‌های از پیش ساخته‌شده یا cache استفاده کنند تا از ایجاد objectهای تکراری جلوگیری شود.

  • Boolean.valueOf(boolean)

نمونه‌ای از static factory method است که به‌جای ساخت Boolean جدید، یکی از نمونه‌های آماده‌ی Boolean.TRUE یا Boolean.FALSE را برمی‌گرداند.

  • Flyweight Pattern

الگویی برای کاهش مصرف حافظه و هزینه‌ی ساخت objectها از طریق اشتراک‌گذاری نمونه‌های معادل. بازگرداندن نمونه‌های cached در static factory method رفتاری مشابه این الگو دارد.

  • Instance-Controlled Class

کلاسی که کنترل می‌کند چه نمونه‌هایی از آن در سیستم وجود داشته باشند. این کنترل می‌تواند تضمین کند که فقط یک نمونه وجود دارد یا برای هر مقدار منطقی فقط یک نمونه ساخته می‌شود.

  • Singleton

کلاسی که فقط یک instance از آن در کل برنامه وجود دارد. کنترل نمونه‌ها با static factory method یکی از راه‌های پیاده‌سازی این محدودیت است.

  • Noninstantiable Class

کلاسی که نباید از آن object ساخته شود. این نوع کلاس معمولاً شامل ابزارهای کمکی static است.

  • Identity Equality

در برخی immutable value classها می‌توان تضمین کرد که اگر دو object از نظر مقدار برابر باشند، همان مرجع حافظه را نیز داشته باشند؛ یعنی نتیجه‌ی a.equals(b) با نتیجه‌ی a == b یکسان باشد.

  • Return Type Flexibility

یک static factory method می‌تواند objectی از هر زیرنوعِ نوع بازگشتی اعلام‌شده را برگرداند. در نتیجه، مصرف‌کننده به interface یا نوع انتزاعی وابسته می‌ماند، نه به کلاس پیاده‌سازی.

  • Interface-based Programming

در این رویکرد، متغیر و نوع بازگشتی بر اساس interface تعریف می‌شود، نه کلاس پیاده‌سازی. این کار وابستگی به جزئیات داخلی را کاهش می‌دهد و تغییر پیاده‌سازی را ساده می‌کند.

  • Implementation Hiding

با بازگرداندن interface از static factory method، کلاس‌های پیاده‌سازی می‌توانند nonpublic باقی بمانند. بنابراین جزئیات داخلی API پنهان می‌شود و سطح API کوچک‌تر و ساده‌تر خواهد بود.

  • java.util.Collections

نمونه‌ای از استفاده‌ی گسترده از static factory method است. این کلاس پیاده‌سازی‌های مختلفی مانند مجموعه‌های unmodifiable و synchronized را بدون عمومی‌کردن کلاس‌های پیاده‌سازی در اختیار برنامه‌نویس قرار می‌دهد.

  • Static Method in Interface

از Java 8 به بعد، interfaceها می‌توانند static method داشته باشند. بنابراین در بسیاری از موارد، دیگر لازم نیست برای نگهداری static factory methodها یک کلاس کمکی جداگانه ایجاد شود.

  • Private Static Method in Interface

از Java 9 به بعد، interface می‌تواند private static method داشته باشد؛ اما static field و static member class همچنان باید عمومی باشند.

  • Variable Implementation

کلاس واقعیِ بازگشتی از یک static factory method می‌تواند بر اساس پارامتر ورودی یا نسخه‌ی کتابخانه تغییر کند، بدون اینکه کد مصرف‌کننده تغییر کند.

  • EnumSet

کلاسی بدون public constructor است که از static factory method استفاده می‌کند. بسته به تعداد اعضای enum، ممکن است پیاده‌سازی سبک‌تر یا پیاده‌سازی مناسب برای مجموعه‌های بزرگ‌تر را برگرداند.

  • Service Provider Framework

چارچوبی برای جداکردن مصرف‌کننده‌ی سرویس از پیاده‌سازی آن. ارائه‌دهندگان سرویس، پیاده‌سازی را ثبت می‌کنند و مصرف‌کننده از طریق یک API به سرویس دسترسی می‌یابد.

  • Service Interface

قراردادی که قابلیت اصلی سرویس را تعریف می‌کند و پیاده‌سازی‌ها باید از آن تبعیت کنند.

  • Provider Registration API

بخشی از API که ارائه‌دهندگان با آن پیاده‌سازی سرویس خود را ثبت می‌کنند.

  • Service Access API

بخشی از API که مصرف‌کننده از طریق آن یک نمونه از سرویس را دریافت می‌کند. این بخش معمولاً نقش یک static factory method انعطاف‌پذیر را دارد.

  • Service Provider Interface

یک interface اختیاری برای ساخت نمونه‌های سرویس توسط ارائه‌دهندگان. در نبود آن، ممکن است ساخت پیاده‌سازی با reflection انجام شود.

  • JDBC Service Provider Framework

در JDBC، نوع Connection نقش service interface را دارد، متد DriverManager.registerDriver(...) نقش ثبت ارائه‌دهنده و متد DriverManager.getConnection(...) نقش دریافت سرویس را ایفا می‌کند.

  • ServiceLoader

ابزار استاندارد Java از نسخه‌ی Java 6 برای پیاده‌سازی عمومی service provider framework است. معمولاً بهتر است به‌جای ساخت چارچوب اختصاصی، از آن استفاده شود.

  • Composition over Inheritance

وقتی یک کلاس public یا protected constructor ندارد، نمی‌توان از آن ارث‌بری کرد. این محدودیت در بسیاری موارد مفید است؛ زیرا برنامه‌نویس را به استفاده از composition به‌جای inheritance تشویق می‌کند.

  • Static Factory Method Naming Conventions

برای قابل‌کشف‌بودن static factory methodها باید از نام‌گذاری‌های متعارف مانند from، of، valueOf، getInstance و newInstance استفاده شود.

  • from

برای تبدیل یک مقدار از نوع دیگر به نمونه‌ای از نوع مقصد استفاده می‌شود؛ مانند Date.from(instant).

  • of

برای ساخت یک object از چند مقدار ورودی یا ترکیب چند عنصر استفاده می‌شود؛ مانند EnumSet.of(JACK, QUEEN, KING).

  • valueOf

نسخه‌ای توضیحی‌تر از from یا of است؛ مانند BigInteger.valueOf(...).

  • getInstance و instance

نمونه‌ای را بر اساس پارامترها برمی‌گردانند، اما لزوماً تضمین نمی‌کنند که مقدار نمونه با پارامترها برابر باشد یا هر بار نمونه‌ی جدید ساخته شود.

  • newInstance و create

مشابه getInstance هستند، با این تفاوت که تضمین می‌کنند در هر فراخوانی یک instance جدید برگردد.

  • getType و newType

وقتی static factory method در کلاسی غیر از نوع بازگشتی قرار دارد، از این نام‌گذاری استفاده می‌شود؛ مانند Files.getFileStore(...) و Files.newBufferedReader(...).


موارد مصاحبه ای

  • تفاوت constructor و static factory method چیست؟

constructor با new فراخوانی می‌شود و نام ندارد، اما static factory method نام‌دار است، می‌تواند نمونه‌ی قبلی را برگرداند و قادر است زیرنوعی از نوع بازگشتی را ایجاد کند.

  • چرا static factory method خواناتر از constructor است؟

چون نام متد می‌تواند هدف ساخت object و معنای پارامترها را نشان دهد. در مقابل، نام کلاس به‌تنهایی معمولاً برای تشخیص تفاوت بین چند constructor کافی نیست.

  • چرا تعریف constructorهایی با تفاوت فقط در ترتیب پارامترها خطرناک است؟

زیرا احتمال فراخوانی اشتباه بالا می‌رود و خواننده‌ی کد نیز بدون مراجعه به مستندات، هدف constructor را متوجه نمی‌شود.

  • آیا static factory method همیشه یک object جدید می‌سازد؟

خیر. می‌تواند یک نمونه‌ی cached، نمونه‌ی از پیش ساخته‌شده یا نمونه‌ای مشترک را برگرداند.

  • مزیت caching در static factory method چیست؟

با جلوگیری از ساخت objectهای تکراری، مصرف حافظه و هزینه‌ی پردازش کاهش می‌یابد؛ به‌ویژه وقتی ساخت object پرهزینه یا درخواست برای مقادیر مشابه پرتکرار باشد.

  • instance-controlled class چیست؟

کلاسی است که تعداد و نوع نمونه‌های موجود از خود را کنترل می‌کند؛ مانند singleton یا کلاس‌هایی که برای هر مقدار معادل، فقط یک نمونه دارند.

  • چرا بازگرداندن interface از static factory method مفید است؟

چون مصرف‌کننده به قرارداد وابسته می‌شود، نه پیاده‌سازی. در نتیجه، پیاده‌سازی داخلی می‌تواند بدون تغییر کد کلاینت عوض شود.

  • چرا پنهان‌سازی کلاس‌های پیاده‌سازی در طراحی API اهمیت دارد؟

باعث کوچک‌ترشدن سطح عمومی API، کاهش پیچیدگی مفهومی و جلوگیری از وابستگی مستقیم کاربران به جزئیات داخلی می‌شود.

  • چگونه EnumSet از انعطاف‌پذیری static factory method استفاده می‌کند؟

بر اساس تعداد اعضای enum، یک زیرکلاس مناسب را انتخاب می‌کند؛ اما مصرف‌کننده فقط با نوع EnumSet کار می‌کند و از پیاده‌سازی داخلی مستقل می‌ماند.

  • محدودیت اصلی استفاده‌ی انحصاری از static factory method چیست؟

اگر کلاس public یا protected constructor نداشته باشد، نمی‌توان از آن ارث‌بری کرد.

  • چرا این محدودیت گاهی مزیت محسوب می‌شود؟

زیرا استفاده از composition را تشویق می‌کند و برای طراحی immutable classها ضروری است.

  • چرا کشف static factory methodها دشوارتر از constructorها است؟

چون در مستندات API مانند constructorها به‌صورت برجسته دیده نمی‌شوند. استفاده از نام‌گذاری متعارف و مستندسازی مناسب این مشکل را کاهش می‌دهد.

  • اجزای اصلی service provider framework چیست؟

شامل service interface، provider registration API و service access API است. service provider interface نیز می‌تواند به‌صورت اختیاری برای ساخت نمونه‌ها وجود داشته باشد.

  • نقش DriverManager.getConnection(...) در JDBC چیست؟

این متد نقش service access API را دارد و از طریق یک static factory method، اتصال مناسب را برای کلاینت فراهم می‌کند.

  • چه زمانی static factory method نسبت به constructor ترجیح دارد؟

وقتی نام‌گذاری شفاف اهمیت دارد، ساخت object باید کنترل شود، caching لازم است، نوع بازگشتی باید انعطاف‌پذیر باشد یا می‌خواهیم پیاده‌سازی را از مصرف‌کننده پنهان کنیم.


سناریو کاربردی

  • ساخت Boolean بدون ایجاد نمونه‌ی تکراری

برای تبدیل مقدار boolean به Boolean، به‌جای ساخت مستقیم object جدید، از متد زیر استفاده می‌شود:

public static Boolean valueOf(boolean value) {
    return value ? Boolean.TRUE : Boolean.FALSE;
}

این متد برای مقدارهای true و false، نمونه‌های از پیش موجود را برمی‌گرداند. در نتیجه، برای هر فراخوانی یک Boolean جدید در حافظه ایجاد نمی‌شود.

  • طراحی API برای عدد اول احتمالی

فرض کنید یک constructor با پارامترهای متعدد، یک BigInteger اول احتمالی می‌سازد. نام constructor قابل تغییر نیست و هدف آن نیز ممکن است نامشخص باشد. طراحی خواناتر این است که از متدی با نام گویا استفاده شود:

BigInteger prime = BigInteger.probablePrime(bitLength, random);

نام probablePrime بلافاصله مشخص می‌کند که خروجی چه ماهیتی دارد.

  • انتخاب پیاده‌سازی داخلی در EnumSet

کلاینت با EnumSet کار می‌کند و از static factory method برای دریافت آن استفاده می‌کند. اگر enum حداکثر ۶۴ عضو داشته باشد، یک پیاده‌سازی سبک بر پایه‌ی long انتخاب می‌شود؛ برای تعداد بیشتر، پیاده‌سازی مبتنی بر آرایه انتخاب خواهد شد.

کد کلاینت به هیچ‌کدام از این کلاس‌های داخلی وابسته نیست؛ بنابراین کتابخانه می‌تواند در نسخه‌های بعدی پیاده‌سازی را تغییر دهد، بدون اینکه کد مصرف‌کننده نیاز به تغییر داشته باشد.

  • دریافت سرویس در JDBC

کلاینت برای گرفتن اتصال به پایگاه داده، مستقیماً یک پیاده‌سازی مشخص از Connection را نمی‌سازد. در عوض از DriverManager.getConnection(...) استفاده می‌کند. در این طراحی، انتخاب driver و پیاده‌سازی اتصال از مصرف‌کننده پنهان می‌ماند.


بیشتر بدانید

  • نکته‌ی طراحی API

پیش از تعریف public constructor، بررسی کنید که آیا static factory method خواناتر، منعطف‌تر یا بهینه‌تر است یا خیر.

  • نکته‌ی استفاده از interface

در متغیرها، پارامترها و نوع بازگشتی، در صورت امکان از interface استفاده کنید تا کد به کلاس پیاده‌سازی وابسته نشود.

  • نکته‌ی نام‌گذاری

برای static factory method از نام‌های استاندارد استفاده کنید تا توسعه‌دهندگان سریع‌تر هدف متد را تشخیص دهند:

from(...)
of(...)
valueOf(...)
getInstance(...)
newInstance(...)
  • نکته‌ی کارایی

از ایجاد بی‌دلیل objectهای معادل، به‌ویژه در مسیرهای پرتکرار برنامه، پرهیز کنید. caching و استفاده‌ی مجدد از immutable objectها می‌تواند کارایی را بهبود دهد.

  • نکته‌ی مستندسازی

اگر کلاس فقط static factory method دارد، این موضوع و روش ساخت نمونه را به‌وضوح در مستندات کلاس یا interface بیان کنید.

  • نکته‌ی Java 8 و نسخه‌های جدیدتر

از Java 8، امکان تعریف static method در interface فراهم شده است. از Java 9 نیز می‌توان از private static method برای پنهان‌کردن منطق کمکی در interface استفاده کرد.

  • نکته‌ی چارچوب‌های تزریق وابستگی

چارچوب‌های Dependency Injection را می‌توان شکل قدرتمندتری از service provider framework دانست؛ زیرا پیاده‌سازی مناسب را انتخاب، ایجاد و به مصرف‌کننده تزریق می‌کنند.