Architecture & Design · معماری و طراحی متوسطIntermediate ~46 دقیقه مطالعه~38 min read
SOLID و الگوهای طراحی GoF در جاواSOLID & GoF Design Patterns in Java
در این درس، پنج اصل SOLID و پرتکرارترین الگوهای طراحی GoF را از صفر و با تشبیههای روزمره یاد میگیری، هر کدام را به کد واقعی جاوا و کاربردشان در JDK و Spring گره میزنی، و یاد میگیری کِی استفاده از آنها هوشمندی است و کِی افراط.In this lesson you learn the five SOLID principles and the highest-frequency GoF design patterns from scratch with everyday analogies, wire each one to real Java code and its use inside the JDK and Spring, and learn when reaching for them is smart and when it is over-engineering.
بیا از یک حقیقت ساده شروع کنیم: کدی که امروز مینویسی، فردا عوض میشود. نیازمندی جدید میآید، یک باگ کشف میشود، یک روش پرداخت تازه اضافه میشود. سؤال واقعیِ مهندسی نرمافزار این نیست که «چطور این را کار بیندازم؟» — بلکه این است که «وقتی این را عوض کنم، چقدر آسان میشکند؟». اصول SOLID و الگوهای GoF دقیقاً برای همین ساخته شدهاند: تا تغییر را ارزان و بیخطر کنند. اینها تزئین نیستند، ابزار بقا هستند.
در این درس هیچ اصطلاحی را بدون توضیح رها نمیکنیم. هر مفهوم را اول با یک تشبیه روزمره حس میکنی، بعد اسم فنیاش را میگذاریم، بعد به کد جاوا وصلش میکنیم، و آخر میبینیم چرا در مصاحبه و کد تولیدی به دردت میخورد.
اول دو ایدهی مادرِ همهچیز را جا میاندازیم: «به رابط برنامهنویسی کن، نه به پیادهسازی» و «ترکیب را بر وراثت ترجیح بده». بعد سراغ بخش ۱ — پنج اصل SOLID میرویم و برای هر کدام یک بازآراییِ بد-به-خوب میبینیم. سپس بخش ۲ — الگوهای پرتکرار GoF را در سه خانواده (ایجادی، ساختاری، رفتاری) با کد اصطلاحی و رد پایشان در JDK و Spring میآموزیم. بعد ضدالگوها و هشدارهای افراط را مرور میکنیم، و در پایان ۱۵ سؤال مصاحبه با پاسخ کامل. آخرِ کار هم یک جمعبندی فشرده داری.
بخش ۰ — چند کلمه که باید بلد باشی
قبل از هر چیز، چند واژه را که مدام تکرار میشوند از پایه بسازیم، تا بعداً هیچجا گیر نکنی.
تصور کن سرِ پیشخوان یک کافه میگویی «یک قهوه». تو با «مفهوم قهوه» حرف میزنی، نه با «دستان این باریستای بخصوص». اگر باریستا مرخصی برود، یکی دیگر همان سفارش را انجام میدهد و تو اصلاً متوجه نمیشوی. رابط (interface) همان منوی سفارش است — میگوید چه کاری میشود انجام داد. پیادهسازی (implementation) همان باریستای مشخص است — چطور انجامش میدهد. وقتی کدت به رابط وابسته باشد نه به یک کلاس مشخص، میتوانی پیادهسازی را بیدردسر عوض کنی.
- رابط (interface): قراردادی که فقط میگوید چه متدهایی هست، نه اینکه بدنهشان چطور کار میکند. در جاوا با کلمهی
interface. - پیادهسازی (implementation): کلاس واقعیای که آن قرارداد را با کد پُر میکند (
implements). - جفتشدگی (coupling): میزان وابستگیِ دو تکه کد به هم. جفتشدگیِ زیاد یعنی با دستزدن به یکی، دیگری میشکند — بد است.
- همبستگی (cohesion): میزان بههممربوطبودنِ چیزهای داخل یک کلاس. همبستگیِ بالا یعنی همهچیزِ داخل کلاس به یک هدف مشترک خدمت میکند — خوب است.
- چندریختی (polymorphism): توانایی صدازدنِ یک متد روی یک رابط، بدون اینکه بدانی کدام کلاسِ مشخص پشتش نشسته و پاسخ میدهد. مثل زدن دکمهی «پخش» روی هر پخشکنندهای؛ خودت را با نوعِ دستگاه درگیر نمیکنی.
- وراثت (inheritance) در برابر ترکیب (composition): وراثت یعنی «کلاس B یک نوع از A است» (
B extends A). ترکیب یعنی «کلاس B دارای یک A است» (فیلدی از جنس A نگه میدارد و کارش را به آن واگذار میکند).
وراثت مثل تراشیدن یک مجسمه از یک تکه سنگ است: قدرتمند، اما اگر بعداً بخواهی یک دست را جای دیگری بگذاری، باید کل سنگ را دوباره بتراشی. ترکیب مثل لِگو است: قطعات مستقل را کنار هم میچینی و هر وقت خواستی یکی را با دیگری عوض میکنی. بههمیندلیل قانون طلاییِ طراحیِ مدرن این است: اول بهسراغ ترکیب برو.
مدل ذهنی: الگوها ابزار فشردهسازیِ تغییرند
اصول و الگوهای طراحی را با یک نگاه بفهم: آنها ابزار فشردهسازی تغییر (compression tools for change) هستند — یعنی دامنهی اثر یک تغییر آینده را محدود (localize) میکنند. هنگام ارزیابی هر اصل یا الگو، دقیقاً یک سؤال بپرس:
وقتی نیازمندی X تغییر کند، چند فایل را باید دست بزنم و چقدر احتمال دارد چیزِ نامرتبطی را بشکنم؟
اصول SOLID پنج راهنمای عملی (heuristic) برای ساختاردهیِ وابستگیها میدهند. الگوهای GoF بیستوسه شکلِ تکرارشونده و نامگذاریشده هستند که همین راهنماها معمولاً تولیدشان میکنند. حواست باشد: یک مهندس ارشد «الگو اعمال نمیکند»؛ او تشخیص میدهد که مسئله از پیش دارد به چه شکلی درمیآید، و نامِ مرسومش را رویش میگذارد تا تیم بتواند دربارهاش راحت حرف بزند.
۱) به رابط برنامهنویسی کن، نه به پیادهسازی. ۲) ترکیب را بر وراثت ترجیح بده. تقریباً هر الگویی که در ادامه میبینی، یکی از همین دو ایده است که فقط لباسِ خاصی پوشیده. اگر این دو را عمیق بفهمی، بقیهی درس تقریباً بدیهی میشود.
بخش ۱ — SOLID، با یک بازآراییِ مشخص برای هر اصل
S — اصل مسئولیت واحد (Single Responsibility Principle)
کارمندی را تصور کن که همزمان به مدیر مالی و به مدیر بازاریابی گزارش میدهد. مدیر مالی میگوید «فرمول محاسبه را عوض کن»، مدیر بازاریابی میگوید «شکل گزارش را عوض کن». هر تغییری که برای یکی انجام دهد، ممکن است کارِ دیگری را خراب کند و بین دو خواسته گیر میافتد. کلاسی که دو ارباب دارد هم دقیقاً همین سرنوشت را دارد.
قاعده این است: یک کلاس باید فقط یک دلیل برای تغییر داشته باشد. اما «دلیل تغییر» یعنی چه؟ یعنی یک بازیگر / یک محورِ تغییر (one actor / one axis of change) — نه «فقط یک کار انجام میدهد». بازیگر یعنی یک منبعِ نیازمندی: تیم مالی، تیم گزارشگیری، تیم زیرساخت. اگر دو بازیگرِ مختلف بتوانند مستقلاً از یک کلاس تغییر بخواهند، آن کلاس SRP را نقض کرده.
بیا مشکل را در کد ببینیم. این کلاس همزمان مالیات را حساب میکند، هم HTML میسازد، هم در دیتابیس ذخیره میکند — سه بازیگر در یک بسته:
// بد: سه دلیل تغییر در یک کلاس بستهبندی شدهاند
class InvoiceService {
BigDecimal calculateTotal(Invoice inv) { /* قواعد مالیات */ return null; }
String toHtml(Invoice inv) { /* ارائه/نمایش */ return null; }
void save(Invoice inv) { /* JDBC */ }
}
راهحل، تقسیمِ کلاس بر اساس بازیگر است — هر همکار (collaborator) فقط برای یک بازیگر تغییر میکند:
// خوب: هر همکار برای یک بازیگر تغییر میکند
class InvoiceCalculator { BigDecimal total(Invoice inv) { /* قواعد مالیات */ return null; } }
class InvoiceHtmlRenderer { String render(Invoice inv) { /* نمایش */ return null; } }
interface InvoiceRepository { void save(Invoice inv); }
SRP را میشود آنقدر افراطی گرفت که به فاجعه برسی: سیستمهای کمخون (anemic) و هزارکلاسی که رفتار چنان روی فایلهای زیاد پخش شده که هیچکس پیدایش نمیکند. دانهبندیِ درست این است: «آنچه با هم تغییر میکند، با هم زندگی کند» — همان همبستگی (cohesion). معیار «تعداد متد» نیست؛ معیار «تعداد بازیگر» است.
O — اصل باز/بسته (Open/Closed Principle)
وقتی یک وسیلهی برقیِ جدید میخری، دیوار را نمیشکافی تا سیم تازه بکشی؛ فقط دوشاخهاش را به چندراهیِ برق میزنی. دیوار «بسته برای تغییر» است اما سیستم «باز برای توسعه». کدِ خوب هم همین است: برای افزودن رفتار جدید، کد قدیمیِ تستشده را دست نمیزنی؛ فقط یک قطعهی تازه وصل میکنی.
قاعده: باز برای توسعه، بسته برای تغییر. باید بتوانی رفتار تازه بیفزایی بدون ویرایشِ کدِ موجود و تستشده. مکانیزمش همان چندریختی (polymorphism) است: یک switch روبهرشد را با مجموعهای از پیادهسازیها جایگزین میکنی.
این کدِ بد، هر بار که روشِ ارسالِ تازهای اضافه شود باید دستکاری شود — یعنی هر بار خطرِ شکستنِ چیزی که کار میکرد:
// بد: هر روش ارسال جدید این متد را ویرایش میکند
double cost(Order o) {
switch (o.method()) {
case STANDARD: return 5;
case EXPRESS: return 15;
default: throw new IllegalArgumentException();
}
}
نسخهی خوب: برای روشِ جدید فقط یک کلاس اضافه میکنی و به هیچ کدِ موجودی دست نمیزنی. (خبرِ خوب: این دقیقاً همان الگوی Strategy است که بعداً میبینی.)
// خوب: یک کلاس اضافه کن، به کد موجود دست نزن (این همان Strategy است)
interface ShippingCost { BigDecimal cost(Order o); }
class StandardShipping implements ShippingCost { public BigDecimal cost(Order o){ return new BigDecimal("5"); } }
class ExpressShipping implements ShippingCost { public BigDecimal cost(Order o){ return new BigDecimal("15"); } }
نمیتوانی همهچیز را بسته کنی؛ اگر تلاش کنی، فریمورکی میسازی که هیچکس لازمش ندارد. فقط محورهایی را ببند که واقعاً در دامنهی تو تغییر میکنند، و بقیه را باز به تغییر رها کن. هنر کار، درست حدسزدنِ «چه چیزی تغییر خواهد کرد» است.
L — اصل جایگزینیِ لیسکوف (Liskov Substitution Principle)
اگر روی جعبهای نوشته «مستطیل»، هر دستورالعملی که برای مستطیل معتبر است باید روی محتوای این جعبه هم کار کند — وگرنه برچسب دروغ گفته. زیرنوع هم مثل محتوای جعبه است: اگر خودش را «مستطیل» جا میزند، باید در هر جایی که کد انتظارِ مستطیل دارد بیسروصدا و بدون شگفتی کار کند.
قاعده: زیرنوعها باید بدون شگفتی از طریق نوعِ پایهشان قابلاستفاده باشند. مشخصتر: یک زیرنوع میتواند پیششرطها را ضعیفتر و پسشرطها را قویتر کند (weaken preconditions, strengthen postconditions)، هرگز برعکس، و نباید استثنای بررسیشدهی (checked exception) جدید پرتاب کند یا رفتار را تنگتر کند.
بگذار این دو اصطلاح را باز کنم: پیششرط (precondition) چیزی است که کد قبل از فراخوانیِ یک متد باید فراهم کند (مثلاً «ورودی نباید null باشد»). پسشرط (postcondition) چیزی است که متد بعد از اجرا تضمین میکند (مثلاً «خروجی همیشه مرتب است»). زیرنوع مجاز است سختگیریِ ورودی را کم کند و تضمینِ خروجی را بیشتر کند — نه برعکس؛ چون کلاینت روی رفتارِ نوعِ پایه حساب باز کرده.
نقضِ کلاسیک، Square extends Rectangle است. مربع ادعا میکند یک مستطیل است، اما اضلاعش مستقل نیستند:
// نقض LSP: جایگزینی Square قرارداد Rectangle را میشکند
class Rectangle { int w, h; void setW(int w){this.w=w;} void setH(int h){this.h=h;} int area(){return w*h;} }
class Square extends Rectangle {
void setW(int w){ this.w=w; this.h=w; } // ارتفاع را بیصدا تغییر میدهد
void setH(int h){ this.w=h; this.h=h; }
}
// تستی که برای Rectangle پاس میشود برای Square شکست میخورد:
// r.setW(5); r.setH(4); assert r.area()==20; // برای Square میشود ۱۶
هر کلاینتی که فرض میکند اضلاع مستطیل مستقلاند، با یک مربع بیسروصدا میشکند. راهحل، وراثتِ زیرکانه نیست — بلکه دستبرداشتن از این تظاهر است که مربع یک مستطیلِ تغییرپذیر است. آنها را دو شکلِ تغییرناپذیرِ جدا مدل کن.
LSP همان اصلی است که OCP را ایمن میکند. OCP میگوید «رفتار را با افزودنِ زیرنوعها گسترش بده»؛ اما این فقط زمانی امن است که آن زیرنوعها قرارداد را محترم بشمارند. جایگزینی بدونِ احترام به قرارداد، یعنی OCP روی شن ساخته شده.
I — اصل تفکیک رابط (Interface Segregation Principle)
تصور کن رستورانی مجبورت کند قبل از سفارشِ سالاد، کل دستورِ پختِ استیک را هم بخوانی و امضا کنی. بیمعنی است — تو فقط به بخشِ کوچکی نیاز داری. رابطِ چاق (fat interface) هم همین بلا را سرِ کلاینتها میآورد: مجبورشان میکند به متدهایی وابسته شوند که اصلاً استفاده نمیکنند.
قاعده: هیچ کلاینتی نباید مجبور به وابستگی به متدهایی شود که استفاده نمیکند. رابطهای چاق، کلاینتهای نامرتبط را به هم جفت (couple) میکنند و پیادهسازیهای بیمعنا تحمیل میکنند — همان بویِ بدِ throw new UnsupportedOperationException().
// بد: یک کلاینت فقطخواندنی مجبور است متدهای تغییردهنده را ببیند
interface Repository<T> { T find(long id); void save(T t); void delete(long id); }
// خوب: بر اساس نیاز کلاینت تفکیک کن
interface Reader<T> { T find(long id); }
interface Writer<T> { void save(T t); void delete(long id); }
جالب اینجاست که خودِ JDK این ضدالگو را به نمایش گذاشته: java.util.List متدهای add/remove را به لیستهای تغییرناپذیر هم تحمیل میکند، که آنها سپس در زمان اجرا UnsupportedOperationException پرتاب میکنند. این یک مصالحهی مستند و عمدی است، اما دقیقاً همان چیزی است که ISP میگوید در کدِ خودت از آن پرهیز کن.
D — اصل وارونگیِ وابستگی (Dependency Inversion Principle)
کارخانهی برق نمیداند یخچالِ تو چه مدلی است، و یخچالِ تو نمیداند برق از کدام نیروگاه میآید. هر دو فقط روی یک استاندارد توافق کردهاند: شکلِ پریز. این استانداردِ مشترک همان «انتزاع» است، و نکتهی ظریف این است که استاندارد را نه سازندهی یخچال تعریف میکند و نه نیروگاه — بلکه سطحِ بالاتری (مقرراتِ ملی) مالکِ آن است.
قاعده: سیاستِ سطحبالا نباید به جزئیاتِ سطحپایین وابسته باشد؛ هر دو به انتزاعها وابستهاند. و نکتهی حیاتی: انتزاع را ماژولِ سطحبالا مالک است (owns)، نه جزئیات. یعنی رابط را کنارِ منطقِ کسبوکار تعریف میکنی، و لایهی دیتابیس مجبور است خودش را با آن وفق دهد — نه برعکس.
// خوب: سیاست به رابطی که مالکش است وابسته است؛ جزئیات پایگاهداده آن را پیاده میکند
class OrderService { // سیاست سطحبالا
private final OrderRepository repo; // انتزاع (مالکش اینجاست)
OrderService(OrderRepository repo) { this.repo = repo; }
}
interface OrderRepository { void save(Order o); } // انتزاع
class JpaOrderRepository implements OrderRepository { /* جزئیات */ } // سطحپایین
این دقیقاً همان کاری است که یک ظرفِ تزریق وابستگی (DI container) مثل Spring خودکار میکند: جزئیاتِ مشخص را میسازد و «تزریق (inject)» میکند، تا سیاست هرگز نامِ یک کلاسِ مشخص را نبرد. بههمیندلیل منطقِ کسبوکارِ تو با یک مخزنِ ساختگی (mock — یعنی یک پیادهسازیِ قلابی مخصوصِ تست) قابلِ تستِ واحد است و هرگز به دیتابیسِ واقعی دست نمیزند.
میتوانی وابستگیها را با دست هم تزریق کنی (تزریق از طریق سازنده / constructor injection). برعکس، اگر @Autowired بزنی ولی سرویسات درونِ یک متد یک JpaRepository مشخص را new کند، این DIP نیست. وارونگی دربارهی اینکه چه کسی مالکِ انتزاع است است، نه دربارهی انوتیشنها.
بخش ۲ — الگوهای پرتکرار GoF
«GoF» یعنی Gang of Four، لقبِ چهار نویسندهی کتابِ مرجعِ الگوهای طراحی. آنها بیستوسه الگو را در سه خانواده دستهبندی کردند:
- ایجادی (Creational): دربارهی ساختِ شیء — چطور اشیاء را میسازیم بدون اینکه کد به کلاسهای مشخص گره بخورد.
- ساختاری (Structural): دربارهی ترکیبِ اشیاء — چطور اشیاء را کنارِ هم میچینیم تا ساختارهای بزرگتر بسازیم.
- رفتاری (Behavioral): دربارهی تعاملِ اشیاء — چطور مسئولیت و ارتباط را بینشان تقسیم میکنیم.
در ادامه فقط آنهایی را میآموزیم که واقعاً در مصاحبه و کدِ تولیدی ظاهر میشوند، هر کدام با هدف، جاوای اصطلاحی، و جای زندگیشان در JDK/Spring.
الگوی راهبرد (Strategy) — رفتاری
یک دریل داری و یک جعبه پر از سرمتهی مختلف. بدنهی دریل ثابت است؛ بسته به کار، سرمته را عوض میکنی. Strategy همین است: یک خانواده الگوریتمِ قابلتعویض داری و در زمانِ اجرا یکی را انتخاب میکنی.
هدف: خانوادهای از الگوریتمهای قابلتعویض تعریف کن و یکی را در زمانِ اجرا انتخاب کن. این عملاً همان OCP در قالبِ شیء است.
@FunctionalInterface interface DiscountPolicy { BigDecimal apply(BigDecimal price); }
class Checkout {
BigDecimal total(BigDecimal price, DiscountPolicy policy) { return policy.apply(price); }
}
// با لامبدا، Strategy تقریباً رایگان است:
new Checkout().total(new BigDecimal("100"), p -> p.multiply(new BigDecimal("0.9")));
در دنیای واقعی: java.util.Comparator نمونهی درسیِ Strategy است — list.sort(comparator) بخشِ مقایسهی الگوریتمِ مرتبسازی را مستقل تغییر میدهد. PlatformTransactionManager اسپرینگ، مبدلهای پیامِ RestTemplate و هر @FunctionalInterfaceای که پاس میدهی، Strategy هستند. از جاوا ۸ به بعد، یک Strategy با یک متد صرفاً یک لامبدا است — بههمینسادگی.
الگوی متد کارخانه و کارخانهی انتزاعی (Factory Method & Abstract Factory) — ایجادی
از کاتالوگ سفارش میدهی «یک صندلی» — و کارخانه تصمیم میگیرد کدام مدلِ مشخص را بسازد و تحویل دهد. تو با جزئیاتِ ساخت درگیر نمیشوی. Factory هم مسئولیتِ «ساختنِ شیء» را از تو میگیرد تا کدت به کلاسِ مشخص گره نخورد.
Factory Method: نمونهسازی را از طریقِ متدی قابلبازنویسی به زیرکلاس واگذار کن. Abstract Factory: یک کارخانه از کارخانههای مرتبط، که خانوادههایی از محصولات را تولید میکند که باید با هم استفاده شوند (مثلاً همهی ویجتها به سبکِ مک، یا همه به سبکِ ویندوز).
// Factory Method
abstract class Dialog {
abstract Button createButton(); // متد کارخانه
void render() { createButton().render(); }
}
class WindowsDialog extends Dialog { Button createButton(){ return new WindowsButton(); } }
// Abstract Factory: خانوادهای از ویجتهای رابط کاربری که باید هماهنگ باشند
interface GuiFactory { Button button(); Checkbox checkbox(); }
class MacFactory implements GuiFactory { public Button button(){return new MacButton();} public Checkbox checkbox(){return new MacCheckbox();} }
در دنیای واقعی: Calendar.getInstance()، DateFormat.getInstance()، ThreadFactory در ThreadPoolExecutor، و کلِ BeanFactory/ApplicationContext اسپرینگ کارخانهاند. Collection.iterator() یک Factory Method است که Iterator تولید میکند.
الگوی سازنده (Builder) — ایجادی
سرِ پیشخوانِ ساندویچی، مرحلهبهمرحله میسازی: نان را انتخاب میکنی، بعد پروتئین، بعد سبزیجات، بعد سس — و آخر میگویی «تمام». Builder همین است: یک شیءِ پیچیده را گامبهگام میسازی و در پایان build() را صدا میزنی.
هدف: ساختِ گامبهگامِ یک شیءِ پیچیده، بهویژه وقتی پارامترهای اختیاریِ زیادی هست. این مسئلهی «سازندهی تلسکوپی (telescoping constructor)» را حل میکند — یعنی آن حالتی که مجبوری چند سازندهی تودرتو با تعدادِ آرگومانِ متفاوت بنویسی و کدت شبیه تلسکوپِ بازشونده میشود. Builder همچنین تغییرناپذیری (immutability) را ممکن میکند.
public final class HttpRequest {
private final String url; // اجباری
private final String method; // اختیاری
private final Duration timeout; // اختیاری
private HttpRequest(Builder b){ this.url=b.url; this.method=b.method; this.timeout=b.timeout; }
public static Builder builder(String url){ return new Builder(url); }
public static final class Builder {
private final String url;
private String method = "GET";
private Duration timeout = Duration.ofSeconds(30);
Builder(String url){ this.url = url; }
public Builder method(String m){ this.method = m; return this; } // روان (fluent)
public Builder timeout(Duration t){ this.timeout = t; return this; }
public HttpRequest build(){ return new HttpRequest(this); }
}
}
توجه کن هر متدِ Builder خودِ Builder را برمیگرداند (return this) — به این میگویند سبکِ روان (fluent)، که اجازه میدهد فراخوانیها را زنجیره کنی: builder(url).method("POST").timeout(...).build().
در دنیای واقعی: StringBuilder، Stream.Builder، java.net.http.HttpRequest.newBuilder() (از JDK 11)، Locale.Builder و @Builder لومبوک.
با رکوردها (records) در جاوا ۱۶+، برای چند فیلدِ اجباری معمولاً Builder غیرلازم است — رکورد خودش سازنده و فیلدهای تغییرناپذیر را میدهد. Builder را وقتی نگه دار که فیلدهای اختیاریِ زیاد داری یا در build() به اعتبارسنجی نیاز داری.
الگوی تکنمونه (Singleton) — و تکنمونهی enum — ایجادی
یک شرکت فقط یک مُهرِ رسمی دارد؛ اگر دهتا مُهر باشد، هر کس چیزی را رسمی میکند و هرجومرج میشود. Singleton یعنی «دقیقاً یک نمونه در کلِ برنامه، در دسترسِ همه».
هدف: دقیقاً یک نمونه که سراسری در دسترس است. پیادهسازیِ درست و امندربرابرِنخ (thread-safe) در جاوا، تکنمونهی enum است، که از دو باگِ کلاسیک آزاد است: قفلگذاریِ دوبارهبررسیشدهی (double-checked locking) خرابِ تنبل، و حملاتِ بازتاب/سریالسازی (reflection/serialization).
public enum ConfigCache {
INSTANCE;
private final Map<String,String> cache = new ConcurrentHashMap<>();
public String get(String k){ return cache.get(k); }
}
// استفاده: ConfigCache.INSTANCE.get("region");
اگر به مقداردهیِ تنبل (lazy — یعنی «فقط وقتی واقعاً لازم شد بساز») بدونِ enum نیاز داری، از اصطلاحِ نگهدارندهی مقداردهیدرصورتنیاز (initialization-on-demand holder) استفاده کن. این ترفند برای امنیتِ نخ به تضمینهای بارگذاریِ کلاسِ JVM متکی است — یعنی JVM خودش تضمین میکند یک کلاس فقط یکبار و بهصورتِ اتمیک بارگذاری میشود — و صفر همگامسازیِ (synchronization) دستی لازم دارد:
class Lazy {
private Lazy(){}
private static class Holder { static final Lazy INSTANCE = new Lazy(); }
public static Lazy get(){ return Holder.INSTANCE; } // Holder در اولین فراخوانی، اتمیک بارگذاری میشود
}
کلاسِ داخلیِ Holder تا وقتی get() صدا زده نشود اصلاً بارگذاری نمیشود؛ و لحظهای که بارگذاری شد، JVM تضمین میکند فقط یکبار و امن اتفاق میافتد. تنبلی رایگان، بدونِ یک خط قفل.
در دنیای واقعی: Runtime.getRuntime()، Collections.emptyList(). بینهای اسپرینگ بر اساسِ دامنه (scope) تکنمونهاند (یکی بهازای هر ظرف)، که گونهای مدیریتشده توسطِ ظرف است — نه تکنمونهی ایستایِ GoF، و بسیار قابلتستتر.
قفلگذاریِ دوبارهبررسیشده (double-checked locking) بدونِ volatile روی فیلد خراب است. چرا؟ چون JVM اجازه دارد بهدلیلِ بازچینشِ دستورات (instruction reordering)، یک شیءِ نیمهساخته را منتشر کند — یعنی نخِ دیگری مرجعِ شیء را میبیند در حالی که سازندهاش هنوز تمام نشده. کلیدواژهی volatile (مدلِ حافظهی جاوا ۵+) این بازچینش را ممنوع میکند و رفعش میکند. اما enum و holder سادهتر و ترجیحدادهشدهاند — چرا خودت را درگیرِ volatile کنی وقتی enum مجانی همهچیز را حل کرده؟
الگوی مبدل (Adapter) — ساختاری
دوشاخهی لپتاپت با پریزِ کشورِ مقصد نمیخواند. لپتاپ را عوض نمیکنی، پریز را هم عوض نمیکنی — یک مبدلِ کوچک وسط میگذاری که یکی را به دیگری ترجمه میکند. Adapter همین مبدل است.
هدف: تبدیلِ یک رابط به رابطِ دیگری که کلاینت انتظارش را دارد. یک نوعِ ناسازگار را میپیچد (wrap میکند) تا با کدِ تو جور شود.
interface PaymentGateway { void charge(long cents); } // چیزی که کد ما میخواهد
class StripeApi { void makePayment(BigDecimal dollars){ /* شخص ثالث */ } } // چیزی که داریم
class StripeAdapter implements PaymentGateway {
private final StripeApi stripe;
StripeAdapter(StripeApi s){ this.stripe = s; }
public void charge(long cents){ stripe.makePayment(BigDecimal.valueOf(cents, 2)); } // تطبیق
}
در دنیای واقعی: Arrays.asList()، java.io.InputStreamReader (یک InputStream بایتی را به Reader کاراکتری تطبیق میدهد)، و HandlerAdapter در Spring MVC که به DispatcherServlet اجازه میدهد انواعِ کاملاً متفاوتِ کنترلر را یکنواخت فراخوانی کند.
الگوی تزئینگر (Decorator) — ساختاری
یک قهوهی ساده سفارش میدهی، بعد رویش شیر میریزی، بعد شکر، بعد خامه. هر افزودنی، قهوه را «میپیچد» و چیزی به آن اضافه میکند، ولی هنوز یک نوشیدنی است که میشود نوشید. اگر میخواستی برای هر ترکیبِ ممکن یک منویِ جدا داشته باشی، به تعدادِ نجومی گزینه میرسیدی.
هدف: افزودنِ پویا (dynamic — یعنی در زمانِ اجرا، نه زمانِ کامپایل) مسئولیت به یک شیء، با پیچیدنش، ضمنِ حفظِ همان رابط. این پاسخِ «ترکیب بر وراثت» به مسئلهی «۸ افزونهی اختیاری میخواهم» است — بهجای ساختنِ ۲⁸ (یعنی ۲۵۶) زیرکلاس برای هر ترکیبِ ممکن، افزونهها را روی هم میپیچی.
interface DataSource { void write(String s); }
class FileSource implements DataSource { public void write(String s){ /* خام */ } }
abstract class SourceDecorator implements DataSource {
protected final DataSource wrapped;
SourceDecorator(DataSource d){ this.wrapped = d; }
}
class Encrypting extends SourceDecorator {
Encrypting(DataSource d){ super(d); }
public void write(String s){ wrapped.write(encrypt(s)); } // رفتار افزوده، سپس واگذاری
private String encrypt(String s){ return s; }
}
// new Encrypting(new Compressing(new FileSource()));
نکتهی کلیدی در آن خطِ آخر است: هر تزئینگر یکی دیگر را در بر میگیرد و رفتارش را اضافه میکند، بعد به لایهی درونی واگذار (delegate) میکند.
در دنیای واقعی: کلِ پشتهی java.io — عبارتِ new BufferedReader(new InputStreamReader(new FileInputStream(f))) سه تزئینگر است که روی هم پیچیدهاند. همچنین Collections.unmodifiableList()/synchronizedList() و HttpServletRequestWrapper اسپرینگ.
الگوی نماینده (Proxy) — ساختاری
برای رسیدن به مدیرعامل، اول باید از منشی رد شوی. منشی همان چهره را دارد (تو با «دفترِ مدیرعامل» حرف میزنی)، اما دسترسی را کنترل میکند: شاید تماس را فیلتر کند، شاید تا لحظهی لازم مدیر را بیدار نکند، شاید لاگ بگیرد. Proxy همان منشی است — جانشینی با همان رابط که کنترلِ دسترسی را دستش میگیرد.
هدف: جانشینی با همان رابط که دسترسی را کنترل میکند — برای تنبلی (نمایندهی مجازی)، امنیت، دسترسی از راه دور، یا دغدغههای عرضی (cross-cutting — یعنی دغدغههایی مثل لاگ و تراکنش که در کلِ برنامه پخشاند و به منطقِ اصلی ربطی ندارند).
interface Image { void display(); }
class RealImage implements Image {
RealImage(String f){ load(f); } // پرهزینه
public void display(){ /* ... */ }
private void load(String f){ /* ورودی/خروجی دیسک */ }
}
class LazyImage implements Image { // نمایندهی مجازی
private final String file; private RealImage real;
LazyImage(String f){ this.file = f; }
public void display(){ if (real==null) real = new RealImage(file); real.display(); }
}
در دنیای واقعی: نمایندههای بارگذاریِ تنبلِ JPA/Hibernate، java.lang.reflect.Proxy (نمایندههای پویا)، و Spring AOP — @Transactional، @Cacheable و @Async همگی با پیچیدنِ بینِ تو در یک نماینده پیاده میشوند.
چون اسپرینگ @Transactional را از طریقِ یک نماینده پیاده میکند، فراخوانیِ یک متدِ @Transactional از درونِ همان کلاس (خودفراخوانی / self-invocation) بیصدا تراکنش را نادیده میگیرد — چون فراخوانیِ داخلی مستقیم روی this انجام میشود و نماینده را دور میزند. این یکی از رایجترین باگهای تولیدیِ جاواست و در سؤالِ مصاحبهی ۵ دوباره سراغش میرویم.
الگوی ناظر (Observer) — رفتاری
روزنامه را مشترک میشوی؛ هر بار شمارهی جدید چاپ شود، برایت میآید. روزنامه نمیداند تو کی هستی و چند مشترک دارد — فقط منتشر میکند و هر که مشترک است دریافت میکند. Observer همین رابطهی «یکبهچند» است.
هدف: اطلاعرسانیِ یکبهچند — سوژهها (subjects) تغییرِ حالت را به هر تعداد مشترک (subscriber) منتشر میکنند بدونِ آنکه آنها را بشناسند.
interface Listener { void onEvent(String e); }
class EventBus {
private final List<Listener> listeners = new CopyOnWriteArrayList<>(); // پیمایش امن
void subscribe(Listener l){ listeners.add(l); }
void publish(String e){ listeners.forEach(l -> l.onEvent(e)); }
}
چرا CopyOnWriteArrayList و نه یک ArrayList ساده؟ چون ممکن است در حینِ انتشار (publish)، یک لیسنر بخواهد مشترک یا لغوِ اشتراک شود و لیست را وسطِ پیمایش تغییر دهد. این ساختارِ داده هر بار که تغییر میکند یک کپیِ تازه میسازد، پس پیمایش همیشه امن است. (به این تله در سؤالِ مصاحبهی ۱۱ برمیگردیم.)
در دنیای واقعی: ApplicationEventPublisher / @EventListener اسپرینگ، java.util.Observer منسوخشده (بهعنوانِ یک درسِ طراحی حذف شد — بینوع و بدقواره بود)، لیسنرهای Swing/JavaFX، و Flow.Publisher/Subscriber واکنشی (JDK 9، جریانهای واکنشی). توجه: ناظرِ همگامِ خام میتواند باعث شود استثنای یک لیسنر، ناشر را بشکند — باسهای تولیدی هندلرها را ایزوله میکنند.
الگوی متدِ قالبی (Template Method) — رفتاری
یک دستورِ پختِ کیک داری: «فر را روشن کن، خمیر را آماده کن، بپز، بگذار خنک شود». مرحلهی «خمیر را آماده کن» بسته به نوعِ کیک فرق میکند، اما ترتیبِ کلی همیشه ثابت است. Template Method همین است: اسکلتِ ثابت با یک (یا چند) جای خالی که زیرکلاس پُر میکند.
هدف: اسکلتِ یک الگوریتم را در یک متدِ پایه تعریف کن و به زیرکلاسها اجازه بده گامهای مشخص را پُر کنند بدونِ تغییرِ ساختارِ کلی. این مبتنی بر وراثت است؛ Strategy پسرعمویِ مبتنی بر ترکیبِ آن است.
abstract class DataImporter {
public final void run() { // متد قالبی — final تا اسکلت تغییر نکند
open(); parse(); close();
}
protected abstract void parse(); // گام متغیر («قلاب» / hook)
protected void open(){} // قلابهای اختیاری با پیشفرض
protected void close(){}
}
دقت کن run() با کلمهی final علامت خورده — یعنی هیچ زیرکلاسی نمیتواند ترتیبِ اسکلت را عوض کند؛ فقط قلابها (hooks) را پُر میکند. قلاب (hook) همان جای خالیِ قابلبازنویسی است.
در دنیای واقعی: AbstractList، AbstractMap، و HttpServlet.service() که به doGet/doPost توزیع میکند، متدهای قالبیاند. JdbcTemplate، JmsTemplate و RestTemplate اسپرینگ نمونههای سازمانیِ کلاسیکاند: قالب مالکِ مدیریتِ اتصال/استثنا/منبع است و تو کالبکِ متغیر را میدهی.
JdbcTemplate عملاً یک Strategy است که به یک Template Method تحویل داده شده: قالب کارِ خستهکننده (بازوبستهکردنِ اتصال، مدیریتِ استثنا) را میکند، و منطقِ متغیرِ SQL را تو بهصورتِ یک کالبک (که همان Strategy است) میدهی. این دو الگو مدام با هم ترکیب میشوند — الگوها لِگو هستند، نه جزیرههای مجزا.
الگوی فرمان (Command) — رفتاری
گارسون سفارشت را روی یک برگه مینویسد. آن برگه یک شیء است: میشود در صف گذاشتش، بایگانیاش کرد، یا اگر نظرت عوض شد لغوش کرد. خودِ «درخواست» تبدیل به چیزی ملموس شده. Command همین است: یک درخواست را در قالبِ یک شیء کپسوله میکنی.
هدف: یک درخواست را بهعنوانِ یک شیء کپسوله کن، تا بتوانی صف کنی، لاگ بگیری، بازگردانی (undo) یا پارامتریاش کنی.
interface Command { void execute(); }
class LightOn implements Command {
private final Light light; LightOn(Light l){ this.light = l; }
public void execute(){ light.on(); }
}
class Remote { private Command slot; void set(Command c){slot=c;} void press(){slot.execute();} }
در دنیای واقعی: Runnable یک Command است (یک Runnable که به ExecutorService سپرده میشود، فرمانی صفشده است)، Callable، Action در Swing، و هر صفِ کار/تسک. پشتههای undo و outboxهای تراکنشی، Command با یک undo() افزودهاند.
الگوی زنجیرهی مسئولیت (Chain of Responsibility) — رفتاری
تماست اول به پشتیبانیِ سطحِ یک میرسد؛ اگر نتوانند حل کنند، به سطحِ دو ارجاع میدهند، بعد سطحِ سه. هر لایه یا خودش حل میکند یا به بعدی پاس میدهد. فرستنده نمیداند کدام لایه در نهایت جوابش را میدهد. Chain of Responsibility همین زنجیرهی ارجاع است.
هدف: درخواست را در طولِ زنجیرهای از هندلرها عبور بده تا یکی آن را مدیریت کند — این فرستنده را از گیرنده جدا (decouple) میکند.
abstract class Handler {
protected Handler next;
Handler linkTo(Handler n){ this.next = n; return n; }
abstract void handle(Request r);
protected void forward(Request r){ if (next != null) next.handle(r); }
}
در دنیای واقعی: زنجیرهی javax.servlet.Filter و زنجیرهی فیلترِ Spring Security نمونهی درسیِ CoR هستند، و همینطور HandlerInterceptor و هر خطلولهی میانافزار (middleware). سطوحِ اپندرِ Log4j/SLF4J و هندلرهای استثنا هم زنجیرهاند.
الگوی وضعیت (State) — رفتاری
چراغ راهنمایی بسته به وضعیتِ فعلیاش رفتارِ متفاوتی دارد: در حالتِ سبز، رویدادِ «زمان تمام شد» آن را به زرد میبرد؛ در زرد به قرمز؛ در قرمز به سبز. انگار با هر تغییرِ حالت، «کلاسِ» چراغ عوض میشود. State همین است.
هدف: به یک شیء اجازه بده هنگامِ تغییرِ حالتِ داخلی، رفتارش را تغییر دهد — انگار کلاسش عوض میشود. این if/switchهای پراکندهی وضعیت را با اشیای وضعیتِ چندریخت جایگزین میکند.
interface OrderState { OrderState pay(); OrderState ship(); }
class New implements OrderState { public OrderState pay(){ return new Paid(); } public OrderState ship(){ throw new IllegalStateException("اول پرداخت کن"); } }
class Paid implements OrderState { public OrderState pay(){ throw new IllegalStateException("قبلاً پرداخت شده"); } public OrderState ship(){ return new Shipped(); } }
class Shipped implements OrderState { public OrderState pay(){ throw new IllegalStateException(); } public OrderState ship(){ throw new IllegalStateException(); } }
در دنیای واقعی: Thread.State، ماشینهای حالتِ اتصالِ TCP، موتورهای گردشکار/سفارش، و Spring StateMachine. State و Strategy از نظرِ ساختاری یکساناند (هر دو به یک رابط واگذار میکنند)؛ هدف فرق دارد — Strategy از بیرون انتخاب میشود و بیحالت است، State خودش را از درون منتقل میکند.
ضدالگوها و هشدارهای استفادهی افراطی
الگوها ابزارند، نه هدف. استفادهی نابهجا از آنها بدتر از نداشتنشان است. اینها رایجترین دامهایند:
- فضانوردِ الگو / مهندسیِ افراطی (over-engineering). یک
SingletonFactoryStrategyProxyبرای مسئلهای که یک متدِ ساده حلش میکند، ضررِ خالص است. الگوها لایهی غیرمستقیم (indirection) اضافه میکنند، و غیرمستقیمی هزینه دارد. الگو را وقتی معرفی کن که واقعاً دومین تنوع برسد («قاعدهی سه» / Rule of Three)، نه گمانهزنانه. - تکنمونه بهعنوانِ حالتِ سراسریِ تغییرپذیر. Singleton پرسوءاستفادهترین الگوست — وابستگیها را پنهان میکند، تستپذیری را نابود و جفتشدگیِ پنهان ایجاد میکند. یک نمونهی دامنهدار (scoped) مدیریتشده توسطِ DI را بر
getInstance()ایستا ترجیح بده. - دامنهی کمخون + انفجارِ سرویسها از بدفهمیِ SRP: دهها «سرویسِ» تکمتدی بدونِ همبستگی. SRP دربارهی دلایلِ تغییر است، نه تعدادِ متد.
- OCP گمانهزنانه. ساختنِ نقاطِ توسعه برای محورهایی که هرگز تغییر نمیکنند، فریمورکی با یک پیادهسازی تولید میکند. آنچه امروز تغییر میکند را ببند.
- نشتِ Decorator/Proxy. پشتههای پیچیدنِ عمیق، دیباگ و استکتریس را دردناک میکنند و میتوانند
equals/instanceofرا بشکنند. تلهی خودفراخوانیِ نمایندهی اسپرینگ رایجترین باگِ تولیدی از این دست است. - سفتیِ Template Method. وراثت تو را در یک سلسلهمراتب قفل میکند؛ وقتی گامها باید مستقل تغییر کنند، Strategy/ترکیب را ترجیح بده (اسپرینگ ترکیب را انتخاب کرد —
JdbcTemplateکالبک میگیرد نه اینکه زیرکلاسسازی را تحمیل کند). - اشتباهگرفتنِ وراثت با بازاستفاده. بیشترِ الگوهای ساختاریِ GoF دقیقاً وجود دارند چون وراثت مکانیزمِ بازاستفادهی ضعیفی است. اول بهسراغِ ترکیب برو.
سؤالات مصاحبه
هر سؤال را با پاسخِ کامل بخوان؛ اینها همان جاهایی هستند که مصاحبهگر عمقِ فهمت را میسنجد.
دلیلِ تغییر یعنی یک بازیگر (actor) — یک منبعِ نیازمندی. کلاس وقتی SRP را نقض میکند که دو ذینفعِ مختلف (مثلاً مالی در برابر گزارشگیری) هرکدام بتوانند مستقلاً تغییری در آن بخواهند. «یک کار» ذهنی و مبهم است و به تکهتکهشدنِ افراطی میانجامد؛ آزمونِ بازیگر مشخص و عینی است.
Square extends Rectangle. هر دو کامپایل میشوند؛ تستهای خودِ Square پاس میشوند. اما کلاینتی که r.setWidth(5); r.setHeight(4); assert r.area()==20; میکند — که برای هر مستطیل معتبر است — برای مربع شکست میخورد (مساحت ۱۶)، چون setWidth بهطورِ نامشروع ارتفاع را هم تغییر میدهد. زیرنوع یک پیششرط را قویتر یا یک پسشرط را دگرگون کرده. راهحل: آن را با وراثت مدل نکن.
نه. DIP دربارهی اینکه چه کسی مالکِ انتزاع است و وابستگی به رابطهاست، نه دربارهی انوتیشنها. میتوانی با اسپرینگ DIP را نقض کنی (مثلاً newکردنِ یک مخزنِ مشخص درونِ یک متد) و میتوانی با تزریقِ ساده از طریقِ سازنده و بدونِ هیچ فریمورکی DIP را برآورده کنی.
تکنمونههای enum ذاتاً امندربرابرِنخاند (مقداردهیِ کلاس اتمیک است)، امندربرابرِسریالسازیاند (JVM یک نمونهی واحد را در بازسریالسازی تضمین میکند) و امندربرابرِبازتاباند (نمیتوان یک enum را با بازتاب نمونهسازی کرد). DCL به فیلدِ volatile نیاز دارد و بهآسانی اشتباه میشود؛ بدونِ volatile ممکن است بهدلیلِ بازچینش، شیءِ نیمهساخته منتشر شود.
@Service
class OrderService {
@Transactional public void outer(){ inner(); } // خودفراخوانی
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void inner(){ throw new RuntimeException("boom"); }
}
REQUIRES_NEW در inner() نادیده گرفته میشود. چون اسپرینگ @Transactional را از طریقِ یک نماینده (Proxy) پیاده میکند، فراخوانیِ مستقیمِ inner() روی this نماینده را دور میزند، پس هیچ تراکنشِ جدیدی شروع نمیشود — استثنا تراکنشِ بیرونی را بازمیگرداند (rollback). راهحل: از طریقِ نماینده فراخوانی کن (تزریقِ خود، AopContext.currentProxy()، یا انتقالِ inner به بینِ دیگر).
از نظرِ ساختاری یکساناند (هر دو به یک پیادهسازیِ رابط واگذار میکنند). تفاوت در هدف و چرخهی حیات است: Strategy را کلاینت انتخاب میکند، معمولاً بیحالت است و خودش را تغییر نمیدهد؛ اشیای State خودشان بر اساسِ رویدادهای داخلی منتقل میشوند و یک ماشینِ حالتِ متناهی را نمایندگی میکنند.
هر دو اجازه میدهند یک گام تغییر کند. Template Method از وراثت استفاده میکند (زیرکلاس یک قلابِ protected را بازنویسی میکند؛ اسکلت یک متدِ final است)، پس تنوع در زمانِ کامپایل و یکی بهازای هر زیرکلاس تثبیت میشود. Strategy از ترکیب استفاده میکند (تزریقِ یک شیء)، پس در زمانِ اجرا تغییر میکند و قابلتعویض است. JdbcTemplate اسپرینگ هر دو را ترکیب میکند: متدِ قالبی برای مدیریتِ منبع، کالبکِ Strategy برای منطقِ SQL متغیر.
Decorator (پیچیدنِ جریانها با BufferedReader/InputStreamReader برای افزودنِ بافر/تبدیلِ کدگذاری ضمنِ حفظِ همان رابط)، Adapter (InputStreamReader یک جریانِ بایتی را به خوانندهی کاراکتری تطبیق میدهد)، و احتمالاً Factory Method برای ساختِ جریان. سبکِ سازندهی تودرتوی new BufferedReader(new InputStreamReader(in)) همان Decorator است.
برای مجموعهی کوچک و ثابتِ فیلدهای اجباری، رکورد (record) جاوا ۱۶+ جای بیشترِ کاربردهای Builder را میگیرد. Builder وقتی ضروری است که فیلدهای اختیاریِ زیادی هست (پرهیز از سازندههای تلسکوپی)، وقتی ساخت به اعتبارسنجی/نرمالسازی در build() نیاز دارد، یا وقتی برای ۵+ پارامتر که آرگومانهای موقعیتی خطاخیزند، محلِ فراخوانیِ روان و خوانا میخواهی.
بینوع بود (از Object خام استفاده میکرد)، وراثت از یک کلاس را تحمیل میکرد (مسدودکردنِ سلسلهمراتبِ خودت)، هیچ تضمینِ ترتیب/نخ نمیداد و پروتکلِ setChanged() بدقوارهای داشت. در جاوا ۹ منسوخ شد؛ از نظرِ مفهومی با java.util.concurrent.Flow (جریانهای واکنشی)، PropertyChangeListener، یا باسهای رویدادِ فریمورک مانند ApplicationEvent اسپرینگ جایگزین شد.
class Bus {
private final List<Listener> ls = new ArrayList<>();
void subscribe(Listener l){ ls.add(l); }
void publish(String e){ for (Listener l : ls) l.onEvent(e); }
}
اگر یک لیسنر در حینِ publish مشترک/لغوِاشتراک شود (یا نخِ دیگری ls را تغییر دهد)، ConcurrentModificationException یا بهروزرسانیهای گمشده میگیری — تلهی کلاسیکِ بازورودی (re-entrancy) در Observer. راهحل: از CopyOnWriteArrayList استفاده کن، یا روی یک کپی از تصویرِ لحظهای پیمایش کن.
ISP رابطها را کوچک نگه میدارد تا کلاینتها به متدهای بیمعنا مجبور نشوند؛ LSP تضمین میکند پیادهسازیها رابطی را که ادعا میکنند محترم بشمارند. JDK با List ISP را نقض میکند: لیستهای تغییرناپذیر باید add/remove را پیاده کنند و UnsupportedOperationException پرتاب کنند — که همچنین در مرزِ نقضِ LSP است (کلاینتِ List نمیتواند امن add را صدا بزند). این یک مصالحهی عملگرایانهی مستند و نمونهای هشداردهنده برای APIهای خودت است.
OCP را نقض میکند؛ به Strategy یا رویکردِ State/چندریختی بازآرایی کن (یا رفتار را روی خودِ ثابتِ enum بگذار). مصالحه: یک switch خوانای واحد به N کلاس/فایل تبدیل میشود — غیرمستقیمیِ بیشتر، دیدنِ همهی حالتها با هم سختتر. وقتی بازآرایی کن که switch در چند جا ظاهر شود (جراحیِ ساچمهای / shotgun surgery) یا بیحد رشد کند؛ وقتی محلی و پایدار است switch را نگه دار. enumهای با متدِ انتزاعی اغلب نقطهی مطلوب در جاوا هستند.
متدهای @Bean همان Factory Method هستند (و ظرف یک Abstract Factory از بینهاست). تکنمونهی اسپرینگ یک نمونه بهازای دامنهی ظرف است که از طریقِ DIP تزریق میشود — قابلتست، قابلماک و قابلتعویض. تکنمونهی GoF یک getInstance() ایستای سراسری با حالتِ سراسریِ پنهان است که در برابرِ تست مقاومت میکند. اولی را ترجیح بده.
وقتی هندلرها وابستگیِ ترتیبیِ ضمنی دارند، وقتی درخواست بیصدا از انتهای زنجیره بدونِ رسیدگی میافتد (بدونِ پیشفرض)، یا وقتی «چه کسی رسیدگیاش کرد؟» غیرقابلردیابی میشود. برای فیلترها/میانافزارها که استقلال واقعی است قدرتمند است (فیلترهای سرولت، Spring Security)، اما اگر هندلرها باید هماهنگ شوند یا زمینهی تغییرپذیرِ مشترکی را تنگاتنگ بهاشتراک بگذارند، یک خطلولهی صریح یا یک میانجی (mediator) روشنتر است.
اصول SOLID و الگوهای GoF همه یک هدف دارند: ارزان و بیخطر کردنِ تغییر. زیربنای همهشان دو ایده است — «به رابط برنامهنویسی کن، نه پیادهسازی» و «ترکیب را بر وراثت ترجیح بده». SOLID پنج راهنما میدهد: S یک بازیگر برای هر کلاس، O با افزودن گسترش بده نه با ویرایش، L زیرنوع باید بیشگفتی جای پایه بنشیند، I رابطها را کوچک و مخصوصِ کلاینت نگه دار، D انتزاع را سطحبالا مالک باشد و هر دو به آن وابسته باشند. الگوهای GoF در سه خانوادهاند: ایجادی (Factory، Builder، Singleton)، ساختاری (Adapter، Decorator، Proxy)، رفتاری (Strategy، Observer، Template Method، Command، Chain of Responsibility، State). هر کدام در JDK و Spring زندهاند — Comparator، java.io، @Transactional، JdbcTemplate. و مهمترین بلوغ این است که بدانی کِی نباید الگو بزنی: افراط، لایهی غیرمستقیمِ بیفایده و هزینهدار میسازد. الگو را وقتی معرفی کن که تنوعِ واقعی رسیده باشد، نه از روی گمانهزنی.
منابع
Let's start with a plain truth: the code you write today will change tomorrow. A new requirement arrives, a bug is found, a new payment method is added. The real question in software engineering isn't "how do I make this work?" — it's "when I change this, how easily does it break?" SOLID principles and GoF patterns exist for exactly that: to make change cheap and safe. They are not decoration; they are survival tools.
In this lesson we never drop a term without explaining it. For each idea you first feel it through an everyday analogy, then we name the technical term, then we wire it to real Java, and finally we see why it matters in interviews and production code.
First we plant the two mother-ideas everything grows from: "program to an interface, not an implementation" and "prefer composition over inheritance." Then Part 1 — the five SOLID principles, each with a concrete bad-to-good refactor. Then Part 2 — the high-frequency GoF patterns in three families (creational, structural, behavioral), each with idiomatic Java and its footprint in the JDK and Spring. Then anti-patterns and over-use warnings, and finally 15 interview questions with full answers. A compact summary closes it out.
Part 0 — words you must know
Before anything else, let's build a few words that keep recurring, so you never get stuck later.
Picture yourself at a café counter saying "one coffee." You're talking to the concept of coffee, not to "this particular barista's hands." If the barista goes on break, someone else fulfills the same order and you never notice. An interface is the order menu — it says what can be done. An implementation is the specific barista — how it gets done. When your code depends on an interface instead of a concrete class, you can swap the implementation painlessly.
- Interface: a contract that says only which methods exist, not how their bodies work. In Java, the
interfacekeyword. - Implementation: the actual class that fills that contract with code (
implements). - Coupling: how much two pieces of code depend on each other. High coupling means touching one breaks the other — bad.
- Cohesion: how related the things inside one class are. High cohesion means everything inside serves one shared purpose — good.
- Polymorphism: the ability to call one method on an interface without knowing which concrete class is sitting behind it answering. Like pressing "play" on any player — you don't concern yourself with the device's brand.
- Inheritance vs. composition: inheritance means "class B is a kind of A" (
B extends A). Composition means "class B has an A" (it holds a field of type A and delegates work to it).
Inheritance is like carving a statue from a single block of stone: powerful, but if you later want to move an arm, you must re-carve the whole block. Composition is like Lego: independent pieces you snap together and swap whenever you like. That's why the golden rule of modern design is: reach for composition first.
Mental model: patterns are compression tools for change
See principles and patterns through one lens: they are compression tools for change — they localize the blast radius of a future modification. When you evaluate any principle or pattern, ask exactly one question:
When requirement X changes, how many files do I touch, and how likely am I to break something unrelated?
SOLID gives you five heuristics for structuring dependencies. The GoF patterns are 23 named, recurring shapes those heuristics tend to produce. Note this well: a senior engineer does not "apply patterns"; they recognize the shape a problem is already trying to take and give it its conventional name so the team can reason about it easily.
- Program to an interface, not an implementation. 2) Prefer composition over inheritance. Almost every pattern you're about to meet is one of these two ideas wearing a specific costume. Understand these deeply and the rest of the lesson becomes nearly obvious.
Part 1 — SOLID, with a concrete refactor per principle
S — Single Responsibility Principle (SRP)
Imagine an employee who reports to both the finance manager and the marketing manager. Finance says "change the formula," marketing says "change the report's look." Any change for one may break the other's work, and the employee is trapped between two demands. A class with two masters suffers the exact same fate.
The rule: a class should have one reason to change. But what is a "reason to change"? It means one actor / one axis of change — not "does one thing." An actor is a source of requirements: the finance team, the reporting team, the infra team. If two different actors can independently demand a change to a class, that class violates SRP.
Let's see the problem in code. This class computes tax, renders HTML, and saves to the database all at once — three actors in one bundle:
// BAD: three reasons to change bundled together
class InvoiceService {
BigDecimal calculateTotal(Invoice inv) { /* tax rules */ return null; }
String toHtml(Invoice inv) { /* presentation */ return null; }
void save(Invoice inv) { /* JDBC */ }
}
The fix is to split the class by actor — each collaborator changes for exactly one actor:
// GOOD: each collaborator changes for one actor
class InvoiceCalculator { BigDecimal total(Invoice inv) { /* tax rules */ return null; } }
class InvoiceHtmlRenderer { String render(Invoice inv) { /* presentation */ return null; } }
interface InvoiceRepository { void save(Invoice inv); }
You can take SRP so far it becomes its own disaster: anemic, thousand-class systems where behavior is smeared across so many files nobody can find it. The right granularity is "changes together, lives together" — that's cohesion. The metric is not "method count"; it's "actor count."
O — Open/Closed Principle (OCP)
When you buy a new appliance, you don't tear open the wall to run new wiring; you just plug it into a power strip. The wall is "closed for modification" but the system is "open for extension." Good code is the same: to add new behavior you don't touch the old, tested code — you plug in a fresh piece.
The rule: open for extension, closed for modification. You should be able to add new behavior without editing existing, tested code. The mechanism is polymorphism: replace a growing switch with a set of implementations.
This bad code must be edited every time a new shipping method appears — meaning every time you risk breaking something that worked:
// BAD: every new shipping method edits this method
double cost(Order o) {
switch (o.method()) {
case STANDARD: return 5;
case EXPRESS: return 15;
default: throw new IllegalArgumentException();
}
}
The good version: for a new method you add one class and touch no existing code. (Good news: this is exactly the Strategy pattern you'll meet later.)
// GOOD: add a new class, touch nothing existing (this is Strategy)
interface ShippingCost { BigDecimal cost(Order o); }
class StandardShipping implements ShippingCost { public BigDecimal cost(Order o){ return new BigDecimal("5"); } }
class ExpressShipping implements ShippingCost { public BigDecimal cost(Order o){ return new BigDecimal("15"); } }
You cannot make everything closed; if you try, you build a framework nobody needs. Close only the axes that actually vary in your domain, and leave the rest open to editing. The craft is guessing correctly what will change.
L — Liskov Substitution Principle (LSP)
If a box is labeled "rectangle," any instruction valid for a rectangle must also work on what's inside — otherwise the label lied. A subtype is like the contents of that box: if it labels itself "rectangle," it must work, silently and without surprises, everywhere the code expects a rectangle.
The rule: subtypes must be usable through their base type without surprises. More precisely: a subtype may weaken preconditions and strengthen postconditions, never the reverse, and must not throw new checked exceptions or narrow behavior.
Let me unpack those two terms. A precondition is what the caller must guarantee before calling a method (e.g., "input must not be null"). A postcondition is what the method guarantees after running (e.g., "the output is always sorted"). A subtype may relax the input demand and tighten the output guarantee — never the reverse — because the client is counting on the base type's behavior.
The canonical violation is Square extends Rectangle. A square claims to be a rectangle, but its sides aren't independent:
// LSP violation: substituting Square breaks Rectangle's contract
class Rectangle { int w, h; void setW(int w){this.w=w;} void setH(int h){this.h=h;} int area(){return w*h;} }
class Square extends Rectangle {
void setW(int w){ this.w=w; this.h=w; } // silently mutates height
void setH(int h){ this.w=h; this.h=h; }
}
// A test that passes for Rectangle fails for Square:
// r.setW(5); r.setH(4); assert r.area()==20; // 16 for Square
Any client assuming a rectangle's sides are independent silently breaks with a square. The fix isn't clever inheritance — it's to stop pretending a square is-a mutable rectangle. Model them as two separate immutable shapes.
LSP is the principle that makes OCP safe. OCP says "extend behavior by adding subtypes"; but that's only safe if those subtypes honor the contract. Substitution without honoring the contract means OCP was built on sand.
I — Interface Segregation Principle (ISP)
Imagine a restaurant that made you read and sign the entire steak-cooking procedure before you could order a salad. Absurd — you only need a small part. A fat interface does the same to clients: it forces them to depend on methods they never use.
The rule: no client should be forced to depend on methods it does not use. Fat interfaces couple unrelated clients and force meaningless implementations — the throw new UnsupportedOperationException() smell.
// BAD: a read-only client is forced to see mutation methods
interface Repository<T> { T find(long id); void save(T t); void delete(long id); }
// GOOD: segregate by client need
interface Reader<T> { T find(long id); }
interface Writer<T> { void save(T t); void delete(long id); }
Interestingly, the JDK itself displays this anti-pattern: java.util.List forces add/remove onto immutable lists too, which then throw UnsupportedOperationException at runtime. It's a documented, deliberate trade-off, but it's exactly what ISP tells you to avoid in your own code.
D — Dependency Inversion Principle (DIP)
The power plant doesn't know your fridge's model, and your fridge doesn't know which plant the electricity comes from. Both simply agreed on a standard: the socket shape. That shared standard is the "abstraction," and here's the subtle point — the standard is defined neither by the fridge maker nor the plant, but owned by a higher level (national regulations).
The rule: high-level policy should not depend on low-level detail; both depend on abstractions. And crucially: the abstraction is owned by the high-level module, not the detail. You define the interface next to your business logic, and the database layer must conform to it — not the other way around.
// GOOD: policy depends on an interface it owns; the DB detail implements it
class OrderService { // high-level policy
private final OrderRepository repo; // abstraction (owned here)
OrderService(OrderRepository repo) { this.repo = repo; }
}
interface OrderRepository { void save(Order o); } // abstraction
class JpaOrderRepository implements OrderRepository { /* detail */ } // low-level
This is exactly what a DI container (Spring) automates: it constructs the concrete detail and "injects" it, so policy never names a concrete class. That's why your business logic can be unit-tested with a mock repository (a fake test-only implementation) and never touches a real database.
You can inject dependencies by hand (constructor injection). Conversely, using @Autowired while your service news up a concrete JpaRepository inside a method is not DIP. The inversion is about who owns the abstraction, not about annotations.
Part 2 — The high-frequency GoF patterns
"GoF" means Gang of Four, the nickname for the four authors of the seminal design-patterns book. They sorted 23 patterns into three families:
- Creational: about object construction — how we build objects without tying code to concrete classes.
- Structural: about composing objects — how we assemble objects into larger structures.
- Behavioral: about object interaction — how we split responsibility and communication among them.
Below are only the ones that actually appear in interviews and production code, each with intent, idiomatic Java, and where it lives in the JDK/Spring.
Strategy (behavioral)
You have one drill and a box of interchangeable bits. The drill body stays fixed; depending on the job you swap the bit. Strategy is exactly this: a family of interchangeable algorithms, and you pick one at runtime.
Intent: define a family of interchangeable algorithms and select one at runtime. It is effectively OCP in object form.
@FunctionalInterface interface DiscountPolicy { BigDecimal apply(BigDecimal price); }
class Checkout {
BigDecimal total(BigDecimal price, DiscountPolicy policy) { return policy.apply(price); }
}
// With lambdas, Strategy is almost free:
new Checkout().total(new BigDecimal("100"), p -> p.multiply(new BigDecimal("0.9")));
In the wild: java.util.Comparator is the textbook Strategy — list.sort(comparator) varies the sort algorithm's comparison independently. Spring's PlatformTransactionManager, RestTemplate's message converters, and any @FunctionalInterface you pass around are Strategy. Since Java 8, a Strategy with one method is just a lambda — that simple.
Factory Method & Abstract Factory (creational)
You order "a chair" from a catalog — and the factory decides which specific model to build and ship. You don't wrestle with construction details. A Factory likewise takes the "make the object" job off your hands, so your code isn't tied to a concrete class.
Factory Method: defer instantiation to a subclass via an overridable method. Abstract Factory: a factory of related factories, producing families of products that must be used together (e.g., all Mac-style widgets, or all Windows-style).
// Factory Method
abstract class Dialog {
abstract Button createButton(); // the factory method
void render() { createButton().render(); }
}
class WindowsDialog extends Dialog { Button createButton(){ return new WindowsButton(); } }
// Abstract Factory: a family of UI widgets that must match
interface GuiFactory { Button button(); Checkbox checkbox(); }
class MacFactory implements GuiFactory { public Button button(){return new MacButton();} public Checkbox checkbox(){return new MacCheckbox();} }
In the wild: Calendar.getInstance(), DateFormat.getInstance(), ThreadPoolExecutor's ThreadFactory, and Spring's entire BeanFactory/ApplicationContext are factories. Collection.iterator() is a Factory Method producing an Iterator.
Builder (creational)
At the sandwich counter you build step by step: pick the bread, then the protein, then the veggies, then the sauce — and finally you say "done." Builder is exactly this: you construct a complex object step by step and call build() at the end.
Intent: construct a complex object step by step, especially when there are many optional parameters. It solves the "telescoping constructor" problem — that situation where you end up writing several nested constructors with different argument counts and your code looks like an extending telescope. Builder also enables immutability.
public final class HttpRequest {
private final String url; // required
private final String method; // optional
private final Duration timeout; // optional
private HttpRequest(Builder b){ this.url=b.url; this.method=b.method; this.timeout=b.timeout; }
public static Builder builder(String url){ return new Builder(url); }
public static final class Builder {
private final String url;
private String method = "GET";
private Duration timeout = Duration.ofSeconds(30);
Builder(String url){ this.url = url; }
public Builder method(String m){ this.method = m; return this; } // fluent
public Builder timeout(Duration t){ this.timeout = t; return this; }
public HttpRequest build(){ return new HttpRequest(this); }
}
}
Notice each Builder method returns the Builder itself (return this) — this is the fluent style, which lets you chain the calls: builder(url).method("POST").timeout(...).build().
In the wild: StringBuilder, Stream.Builder, java.net.http.HttpRequest.newBuilder() (JDK 11+), Locale.Builder, and Lombok's @Builder.
With records in Java 16+, a Builder is often unnecessary for a handful of required fields — a record hands you the constructor and immutable fields for free. Keep the Builder when you have many optional fields or need validation in build().
Singleton — and the enum singleton (creational)
A company has exactly one official seal; if there were ten, anyone could stamp anything official and chaos ensues. Singleton means "exactly one instance across the whole program, reachable by everyone."
Intent: exactly one instance, globally reachable. The correct thread-safe implementation in Java is the enum singleton, which is free of two classic bugs: broken lazy double-checked locking, and reflection/serialization attacks.
public enum ConfigCache {
INSTANCE;
private final Map<String,String> cache = new ConcurrentHashMap<>();
public String get(String k){ return cache.get(k); }
}
// Usage: ConfigCache.INSTANCE.get("region");
If you need lazy initialization (lazy = "build it only when it's actually needed") without an enum, use the initialization-on-demand holder idiom. It leans on the JVM's class-loading guarantees for thread safety — the JVM itself guarantees a class loads exactly once, atomically — with zero manual synchronization:
class Lazy {
private Lazy(){}
private static class Holder { static final Lazy INSTANCE = new Lazy(); }
public static Lazy get(){ return Holder.INSTANCE; } // Holder loads on first call, atomically
}
The inner Holder class isn't loaded at all until get() is called; and the moment it loads, the JVM guarantees it happens exactly once and safely. Lazy loading for free, without a single lock line.
In the wild: Runtime.getRuntime(), Collections.emptyList(). Spring beans are singletons by scope (one per container), a container-managed variant — not the GoF static singleton, and far more testable.
Double-checked locking without volatile on the field is broken. Why? Because the JVM is allowed to publish a partially-constructed object due to instruction reordering — another thread may see the object's reference while its constructor hasn't finished. The volatile keyword (Java 5+ memory model) forbids that reordering and fixes it. But enum and holder are simpler and preferred — why fuss with volatile when an enum solves everything for free?
Adapter (structural)
Your laptop's plug doesn't fit the destination country's socket. You don't replace the laptop, and you don't replace the socket — you put a little adapter between them that translates one to the other. Adapter is that adapter.
Intent: convert one interface into another that a client expects. It wraps an incompatible type so it fits your code.
interface PaymentGateway { void charge(long cents); } // what our code wants
class StripeApi { void makePayment(BigDecimal dollars){ /* 3rd party */ } } // what we have
class StripeAdapter implements PaymentGateway {
private final StripeApi stripe;
StripeAdapter(StripeApi s){ this.stripe = s; }
public void charge(long cents){ stripe.makePayment(BigDecimal.valueOf(cents, 2)); } // adapt
}
In the wild: Arrays.asList(), java.io.InputStreamReader (adapts a byte InputStream to a char Reader), and Spring MVC's HandlerAdapter, which lets DispatcherServlet invoke wildly different controller types uniformly.
Decorator (structural)
You order a plain coffee, then pour in milk, then sugar, then cream. Each add-on "wraps" the coffee and adds something, yet it's still a drink you can sip. If you'd wanted a separate menu item for every possible combination, you'd hit an astronomical number of options.
Intent: add responsibilities to an object dynamically (dynamically = at runtime, not compile time) by wrapping it, preserving the same interface. It's the composition-over-inheritance answer to "I need 8 optional add-ons" — instead of building 2⁸ (256) subclasses for every combination, you stack wrappers.
interface DataSource { void write(String s); }
class FileSource implements DataSource { public void write(String s){ /* raw */ } }
abstract class SourceDecorator implements DataSource {
protected final DataSource wrapped;
SourceDecorator(DataSource d){ this.wrapped = d; }
}
class Encrypting extends SourceDecorator {
Encrypting(DataSource d){ super(d); }
public void write(String s){ wrapped.write(encrypt(s)); } // add behavior, then delegate
private String encrypt(String s){ return s; }
}
// new Encrypting(new Compressing(new FileSource()));
The key is that last line: each decorator wraps another, adds its behavior, then delegates to the inner layer.
In the wild: the entire java.io stack — new BufferedReader(new InputStreamReader(new FileInputStream(f))) is three decorators stacked. Also Collections.unmodifiableList()/synchronizedList(), and Spring's HttpServletRequestWrapper.
Proxy (structural)
To reach the CEO, you first go through the receptionist. The receptionist wears the same face (you're talking to "the CEO's office") but controls access: maybe screens the call, maybe doesn't wake the CEO until truly needed, maybe logs it. Proxy is that receptionist — a stand-in with the same interface that takes control of access.
Intent: a stand-in with the same interface that controls access — for laziness (virtual proxy), security, remoting, or cross-cutting concerns (cross-cutting = concerns like logging and transactions that are spread across the whole app and unrelated to the core logic).
interface Image { void display(); }
class RealImage implements Image {
RealImage(String f){ load(f); } // expensive
public void display(){ /* ... */ }
private void load(String f){ /* disk I/O */ }
}
class LazyImage implements Image { // virtual proxy
private final String file; private RealImage real;
LazyImage(String f){ this.file = f; }
public void display(){ if (real==null) real = new RealImage(file); real.display(); }
}
In the wild: JPA/Hibernate lazy-loading proxies, java.lang.reflect.Proxy (dynamic proxies), and Spring AOP — @Transactional, @Cacheable, and @Async are all implemented by wrapping your bean in a proxy.
Because Spring implements @Transactional via a proxy, calling a @Transactional method from within the same class (self-invocation) silently skips the transaction — the internal call goes straight through this and bypasses the proxy. This is one of Java's most common production bugs, and we revisit it in interview question 5.
Observer (behavioral)
You subscribe to a newspaper; every time a new issue is printed, it arrives at your door. The newspaper doesn't know who you are or how many subscribers it has — it just publishes, and whoever subscribed receives. Observer is that one-to-many relationship.
Intent: one-to-many notification — subjects publish state changes to any number of subscribers without knowing them.
interface Listener { void onEvent(String e); }
class EventBus {
private final List<Listener> listeners = new CopyOnWriteArrayList<>(); // safe iteration
void subscribe(Listener l){ listeners.add(l); }
void publish(String e){ listeners.forEach(l -> l.onEvent(e)); }
}
Why CopyOnWriteArrayList and not a plain ArrayList? Because during publish, a listener might want to subscribe or unsubscribe and mutate the list mid-iteration. This data structure makes a fresh copy on every change, so iteration is always safe. (We return to this trap in interview question 11.)
In the wild: Spring's ApplicationEventPublisher / @EventListener, the deprecated java.util.Observer (removed as a design lesson — it was untyped and clunky), Swing/JavaFX listeners, and the reactive Flow.Publisher/Subscriber (JDK 9, reactive streams). Note: the raw synchronous Observer can let one listener's exception break the publisher — production buses isolate handlers.
Template Method (behavioral)
You have a cake recipe: "preheat the oven, prepare the batter, bake, let it cool." The "prepare the batter" step differs by cake type, but the overall order is always fixed. Template Method is exactly this: a fixed skeleton with one (or a few) blanks the subclass fills in.
Intent: define an algorithm's skeleton in a base method, letting subclasses fill in specific steps without changing the overall structure. This is inheritance-based; Strategy is its composition-based cousin.
abstract class DataImporter {
public final void run() { // template method — final so the skeleton can't change
open(); parse(); close();
}
protected abstract void parse(); // the varying step ("hook")
protected void open(){} // optional hooks with defaults
protected void close(){}
}
Notice run() is marked final — meaning no subclass can alter the skeleton's order; it only fills in the hooks. A hook is that overridable blank.
In the wild: AbstractList, AbstractMap, and HttpServlet.service() dispatching to doGet/doPost are template methods. Spring's JdbcTemplate, JmsTemplate, and RestTemplate are the canonical enterprise examples: the template owns connection/exception/resource management, and you supply the varying callback.
JdbcTemplate is effectively a Strategy handed to a Template Method: the template does the tedious work (opening/closing the connection, handling exceptions), and you supply the varying SQL logic as a callback (which is Strategy). These two patterns combine constantly — patterns are Lego, not isolated islands.
Command (behavioral)
The waiter writes your order on a ticket. That ticket is an object: it can be queued, filed away, or cancelled if you change your mind. The "request" itself has become something tangible. Command is exactly this: you encapsulate a request as an object.
Intent: encapsulate a request as an object, so you can queue, log, undo, or parameterize it.
interface Command { void execute(); }
class LightOn implements Command {
private final Light light; LightOn(Light l){ this.light = l; }
public void execute(){ light.on(); }
}
class Remote { private Command slot; void set(Command c){slot=c;} void press(){slot.execute();} }
In the wild: Runnable is Command (a Runnable submitted to an ExecutorService is a queued command), Callable, Swing Action, and any job/task queue. Undo stacks and transactional outboxes are Command with an added undo().
Chain of Responsibility (behavioral)
Your call first reaches tier-one support; if they can't solve it, they escalate to tier two, then tier three. Each layer either handles it or passes it on. The sender doesn't know which tier ultimately answers. Chain of Responsibility is that escalation chain.
Intent: pass a request along a chain of handlers until one handles it — this decouples sender from receiver.
abstract class Handler {
protected Handler next;
Handler linkTo(Handler n){ this.next = n; return n; }
abstract void handle(Request r);
protected void forward(Request r){ if (next != null) next.handle(r); }
}
In the wild: the javax.servlet.Filter chain and Spring Security's filter chain are textbook CoR, as is HandlerInterceptor and any middleware pipeline. Log4j/SLF4J appender levels and exception handlers are chains too.
State (behavioral)
A traffic light behaves differently depending on its current state: in green, the "timer expired" event moves it to yellow; in yellow, to red; in red, to green. It's as if the light's "class" changes with each state transition. State is exactly this.
Intent: let an object change its behavior when its internal state changes — it appears to change class. It replaces sprawling state if/switch with polymorphic state objects.
interface OrderState { OrderState pay(); OrderState ship(); }
class New implements OrderState { public OrderState pay(){ return new Paid(); } public OrderState ship(){ throw new IllegalStateException("pay first"); } }
class Paid implements OrderState { public OrderState pay(){ throw new IllegalStateException("already paid"); } public OrderState ship(){ return new Shipped(); } }
class Shipped implements OrderState { public OrderState pay(){ throw new IllegalStateException(); } public OrderState ship(){ throw new IllegalStateException(); } }
In the wild: Thread.State, TCP connection state machines, workflow/order engines, and Spring StateMachine. State and Strategy are structurally identical (both delegate to an interface); the intent differs — Strategy is chosen externally and is stateless, State transitions itself internally.
Anti-patterns & overuse warnings
Patterns are tools, not goals. Misapplying them is worse than not having them. These are the most common traps:
- Pattern astronaut / over-engineering. A
SingletonFactoryStrategyProxyfor a problem a plain method solves is a net loss. Patterns add indirection, and indirection has a cost. Introduce a pattern when a second variation actually arrives (the "Rule of Three"), not speculatively. - Singleton as global mutable state. Singletons are the most abused pattern — they hide dependencies, defeat testability, and create hidden coupling. Prefer a single scoped instance managed by DI over a static
getInstance(). - Anemic domain + service explosion from misreading SRP: dozens of one-method "services" with no cohesion. SRP is about reasons to change, not method count.
- Speculative OCP. Building extension points for axes that never vary produces a framework with one implementation. Close what varies today.
- Decorator/Proxy leakage. Deep wrapping stacks make debugging and stack traces painful and can break
equals/instanceof. Spring's self-invocation proxy trap is the most common production bug from this. - Template Method rigidity. Inheritance locks you into a single hierarchy; when steps need to vary independently, prefer Strategy/composition (Spring chose composition —
JdbcTemplatetakes callbacks rather than forcing subclassing). - Confusing inheritance with reuse. Most GoF structural patterns exist precisely because inheritance is a poor reuse mechanism. Reach for composition first.
Interview Questions
Read each with its full answer; these are exactly where an interviewer probes the depth of your understanding.
A reason to change is an actor — a source of requirements. A class violates SRP when two different stakeholders (e.g., finance vs. reporting) can each independently demand a change to it. "Does one thing" is subjective and vague and leads to over-fragmentation; the actor test is concrete.
Square extends Rectangle. Both compile; Square's own tests pass. But a client that does r.setWidth(5); r.setHeight(4); assert r.area()==20; — valid for any Rectangle — fails for a Square (area 16), because setWidth illegally mutates height too. The subtype strengthened a precondition or altered a postcondition. Fix: don't model it as inheritance.
No. DIP is about who owns the abstraction and depending on interfaces, not about annotations. You can violate DIP with Spring (e.g., new-ing a concrete repository inside a method) and satisfy DIP with plain constructor injection and no framework at all.
Enum singletons are inherently thread-safe (class init is atomic), serialization-safe (the JVM guarantees a single instance across deserialization), and reflection-safe (you cannot reflectively instantiate an enum). DCL requires a volatile field and is easy to get wrong; without volatile it can publish a partially-constructed object due to reordering.
@Service
class OrderService {
@Transactional public void outer(){ inner(); } // self-invocation
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void inner(){ throw new RuntimeException("boom"); }
}
inner()'s REQUIRES_NEW is ignored. Because Spring implements @Transactional via a proxy (Proxy pattern), calling inner() directly on this bypasses the proxy, so no new transaction is started — the exception rolls back the outer transaction. Fix: call through the proxy (self-inject, AopContext.currentProxy(), or move inner to another bean).
Structurally identical (both delegate to an interface implementation). The difference is intent and lifecycle: a Strategy is selected by the client, is usually stateless, and doesn't change itself; State objects transition themselves based on internal events and represent a finite-state machine.
Both let a step vary. Template Method uses inheritance (a subclass overrides a protected hook; the skeleton is a final method), so variation is fixed at compile time and one per subclass. Strategy uses composition (inject an object), so it varies at runtime and can be swapped. Spring's JdbcTemplate combines both: template method for resource management, Strategy callback for the varying SQL logic.
Decorator (BufferedReader/InputStreamReader wrapping streams to add buffering/charset conversion while keeping the same interface), Adapter (InputStreamReader adapts a byte stream to a char reader), and arguably Factory Method for stream creation. The nested-constructor style new BufferedReader(new InputStreamReader(in)) is Decorator.
For a small fixed set of required fields, a Java 16+ record replaces most Builder use. A Builder is still essential when there are many optional fields (avoiding telescoping constructors), when construction needs validation/normalization in build(), or when you want a fluent, readable call site for 5+ parameters where positional args are error-prone.
It was untyped (used raw Object), forced inheritance from a class (blocking your own hierarchy), gave no ordering/threading guarantees, and had a clumsy setChanged() protocol. Deprecated in Java 9; replaced conceptually by java.util.concurrent.Flow (reactive streams), PropertyChangeListener, or framework event buses like Spring's ApplicationEvent.
class Bus {
private final List<Listener> ls = new ArrayList<>();
void subscribe(Listener l){ ls.add(l); }
void publish(String e){ for (Listener l : ls) l.onEvent(e); }
}
If a listener subscribes/unsubscribes during publish (or another thread mutates ls), you get ConcurrentModificationException or lost updates — the classic Observer re-entrancy trap. Fix: use CopyOnWriteArrayList, or iterate over a snapshot copy.
ISP keeps interfaces small so clients aren't forced into meaningless methods; LSP ensures implementations honor the interface they claim. The JDK violates ISP with List: immutable lists must implement add/remove and throw UnsupportedOperationException — which also borders on an LSP violation (a List client can't safely call add). It's a documented pragmatic trade-off, and a cautionary example for your own APIs.
It violates OCP; refactor to Strategy or the State/polymorphism approach (or put behavior on the enum constant itself). Trade-off: a single readable switch becomes N classes/files — more indirection, harder to see all cases at once. Refactor when the switch appears in multiple places (shotgun surgery) or grows unbounded; keep the switch when it's local and stable. Enums with abstract methods are often the sweet spot in Java.
@Bean methods are Factory Methods (and the container is an Abstract Factory of beans). A Spring singleton is one instance per container scope, injected via DIP — testable, mockable, and replaceable. A GoF singleton is a global static getInstance() with hidden global state that resists testing. Prefer the former.
When handlers have implicit ordering dependencies, when a request silently falls off the end unhandled (no default), or when "who handled it?" becomes impossible to trace. It's powerful for filters/middleware where independence is real (Servlet filters, Spring Security), but if handlers must coordinate or share mutable context tightly, an explicit pipeline or a mediator is clearer.
SOLID and the GoF patterns share one goal: making change cheap and safe. Underneath them all lie two ideas — "program to an interface, not an implementation" and "prefer composition over inheritance." SOLID gives five heuristics: S one actor per class, O extend by adding rather than editing, L a subtype must sit in the base's place without surprises, I keep interfaces small and client-specific, D the high level owns the abstraction and both depend on it. GoF patterns come in three families: creational (Factory, Builder, Singleton), structural (Adapter, Decorator, Proxy), and behavioral (Strategy, Observer, Template Method, Command, Chain of Responsibility, State). Each lives in the JDK and Spring — Comparator, java.io, @Transactional, JdbcTemplate. And the most important maturity is knowing when not to reach for a pattern: overuse builds useless, costly indirection. Introduce a pattern when real variation has arrived, not on speculation.