Static Factory vs Public Constructor | استاتیک فکتوری یا کانستراکتور
توضیحات جلسه
جزوه و مستندات
مفاهیم کلیدی
- 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 دانست؛ زیرا پیادهسازی مناسب را انتخاب، ایجاد و به مصرفکننده تزریق میکنند.
