توضیحات
در صنعت، اغلب از Legacy Code به عنوان اصطلاحی برای کدی که تغییرش سخت است و ما آن را نمیفهمیم استفاده میشود. اما من پس از سالها کار با تیمهای مختلف و کمک کردن به آنها در رفع کردن مشکلات جدی کد هایشان، به تعریف متفاوتی دست یافتم.
از نظر من Legacy Code، کدی است که فاقد تست است. چند دلیل نیز برای این تعریف خودم دارم. تستها چه ربطی به بد بودن کد دارند؟ از نظر من پاسخ بدیهی است و این نکته ای است که در این کتاب میخواهم آن را بیان کنم:
کد بدون تست، کد بدی است. اهمیتی ندارد که چقدر خوب نوشته شده باشد یا چقدر زیبا و شی گرا یا به خوبی کپسوله سازی شده باشد. با داشتن تستها است که ما میتوانیم رفتار کد را به سرعت و به طرز صحیحی تغییر دهیم. بدون آنها ما واقعا نمیدانیم که آیا کد بهتر شده یا بدتر شده است.
نظر
نظر
امتیاز: 00/10به دیگران توصیه میکنم:دوباره میخوانم:ایده برجسته:تاثیر در من:نکات مثبت:نکات منفی:
مشخصات
نویسنده: Michael C. Feathersانتشارات: Prentice Hallصفحه مشخصات: goodreads
بخشهایی از کتاب
بسیار خوب، اکنون محتوای ابتدایی کتاب را دریافت کردم. اجازه دهید اولین بخش را به صورت مفهومی و ساختاریافته برای شما تشریح کنم.
Part I: The Mechanics of Change (مکانیک تغییر)
فصل ۱: Changing Software (تغییر نرمافزار)
بخش اول: چهار دلیل برای تغییر نرمافزار
مایکل فِترز در ابتدای این فصل یک تمایز بنیادین را مطرح میکند: تغییر کد کار ماست، اما چگونگی این تغییر میتواند زندگی ما را سخت یا آسان کند. او چهار دلیل اصلی برای تغییر نرمافزار را مطرح میکند:
- اضافه کردن یک قابلیت (Adding a feature)
- رفع باگ (Fixing a bug)
- بهبود طراحی (Improving the design)
- بهینهسازی استفاده از منابع (Optimizing resource usage)
نکته کلیدی ۱: تمایز بین Add Feature و Fix Bug چالشبرانگیز است
فِترز مثال میزند: مدیری میگوید لوگوی صفحه را از چپ به راست ببرید و animated کنید. این یک bug است یا feature؟
- از نگاه مشتری: یک مشکل است که باید برطرف شود.
- از نگاه توسعهدهنده: این یک feature کاملاً جدید است.
اما واقعیت چیست؟
تمام این بحثها پردهای است که یک مفهوم فنی مهمتر را پنهان میکند: Behavioral Change (تغییر رفتار).
قاعده طلایی:
رفتار (Behavior) مهمترین چیز در نرمافزار است. کاربران به آن وابستهاند. اگر رفتاری که به آن اعتماد دارند را تغییر دهیم (یا باگ ایجاد کنیم)، اعتمادشان را از دست میدهیم.
نکته کلیدی ۲: تمایز بین “افزودن رفتار” و “تغییر رفتار”
فِترز یک تمایز فنی مهم ایجاد میکند:
- اگر باید کد را تغییر دهیم (modify existing code) → احتمالاً رفتار را تغییر میدهیم.
- اگر فقط کد جدید اضافه کنیم و آن را فراخوانی کنیم → معمولاً رفتار جدید اضافه میکنیم.
مثال Java:
public class CDPlayer {
public void addTrackListing(Track track) { ... }
}
اگر متد جدیدی اضافه کنیم:
public void replaceTrackListing(String name, Track track) { ... }
آیا رفتار تغییر کرد؟ خیر! چرا؟ چون متد فراخوانی نشده است.
اما اگر یک دکمه در UI اضافه کنیم که این متد را فراخوانی میکند → رفتار اضافه میشود و رفتار موجود (رندر UI) کمی تغییر میکند (یک دکمه بیشتر، زمان رندر کمی بالاتر).
بینش عمیق:
تقریباً غیرممکن است که بدون تغییر حداقلی رفتار، رفتار جدیدی اضافه کنیم.
بخش دوم: بهبود طراحی (Improving Design) و بهینهسازی (Optimization)
۳. بهبود طراحی (Design Improvement)
فِترز اینجا یک نکته بسیار اساسی را مطرح میکند: تغییر طراحی یک کار متفاوت است.
وقتی میخواهیم ساختار نرمافزار را تغییر دهیم تا قابلنگهداریتر شود، معمولاً میخواهیم رفتار نرمافزار بدون تغییر باقی بماند.
مشکل اساسی:
بسیاری از برنامهنویسان تلاش برای بهبود طراحی را انجام نمیدهند، چون بسیار آسان است که رفتار را تغییر دهند یا بدتر، باگ جدیدی ایجاد کنند.
تعریف Refactoring:
Refactoring (بازسازی) به معنای بهبود ساختار بدون تغییر رفتار است.
تعریف رسمی: "تغییری در ساختار داخلی نرمافزار که آن را راحتتر برای فهم
و ارزانتر برای تغییر میکند، بدون تغییر رفتار موجود"
نکته کلیدی: تفاوت بین refactoring و تمیزکاری عمومی:
- Refactoring: مجموعهای از تغییرات ریز و محدود (مثل extract method)، با تستهای حمایتی
- Cleanup عمومی: ممکن است شامل کارهای ریسکآمیز مانند بازنویسی بخشهای بزرگ باشد
اصل طلایی Refactoring:
هنگام refactoring، نباید تغییرات عملکردی رخ دهد (اگرچه عملکرد میتواند بهتر یا بدتر شود)
۴. بهینهسازی (Optimization)
Optimization شبیه refactoring است، اما هدف متفاوت دارد:
- Refactoring: ساختار را تغییر میدهیم تا قابلفهمتر شود
- Optimization: منابع (زمان، حافظه) را تغییر میدهیم
در هر دو حالت، عملکردی (functionality) را ثابت نگه میداریم.
۵. گردهمآوری (Putting It All Together)
فِترز اینجا یک جدول مهم ارائه میدهد که سه چیز متفاوتی را در هر نوع تغییر نشان میدهد:
| نوع تغییر | ساختار | عملکردی جدید | عملکردی موجود | استفاده از منابع |
|---|---|---|---|---|
| اضافه کردن قابلیت | تغییر | تغییر | - | - |
| رفع باگ | تغییر | - | تغییر | - |
| Refactoring | تغییر | - | - | - |
| Optimization | - | - | - | تغییر |
دو نکته مهم:
- در Refactoring و Optimization هر دو، عملکردی موجود ثابت میماند
- اضافه کردن قابلیت و Refactoring و Optimization همگی عملکردی موجود را حفظ میکنند (یا تغییرات بسیار کم)
۶. بخش پایانی: ریسک (Risky Change)
فِترز سؤالی بسیار مهم میپرسد:
“اگر هر تغییری ریسکآمیز است، چقدر تغییر میتوانیم تحمل کنیم؟”
او یک تصویر شماتیک ترسیم میکند که نشان میدهد ما همیشه مقدار زیادی رفتار موجود را باید حفظ کنیم:
- رفتار موجود = 95%
- رفتار جدید = 5%
این یعنی:
- باید تغییرات کوچک را درست انجام دهیم
- باید بیشتر رفتار را حفظ کنیم و نمیدانیم چه مقدار از آن در خطر است
سه سؤال برای کاهش ریسک:
- چه تغییراتی باید انجام دهیم؟
- چگونه میدانیم که درست انجام شده است؟
- چگونه میدانیم که چیزی را شکسته نیستیم؟
استراتژی نادرست:
بسیاری از تیمها تلاش میکنند با اجتناب از تغییرات ریسک را کم کنند:
- سیاست: “اگر خرابی نیست، دست نزنید”
- درخواست کم: کد را درون یک متد قرار دهید تا از نیاز به تغییرات بیشتر جلوگیری شود
مشکل این استراتژی: کد بزرگتر میشود، درک آن سختتر میشود، و کوچکتر کردن کلاسها سختتر میشود
فصل ۲: Working with Feedback (کار با بازخورد)
مقدمه: دو راه برای تغییر کد
فِترز دو روش بنیادین برای تغییر کد را معرفی میکند:
روش اول: Edit and Pray (ویرایش و دعا کردن)
این روش استاندارد صنعت است:
۱. برنامهریزی دقیق تغییرات
۲. اطمینان از فهم کد
۳. انجام تغییرات
۴. اجرای سیستم برای اینکه ببینید تغییر فعال شد
۵. "پوکر خوری" (poking around) برای اطمینان از عدم شکستن چیزی
به نظر حرفهای است؟
ظاهراً بله! اما یک مشکل عمیق دارد:
“سلامت (safety) فقط یک تابع از دقت نیست. هیچ کس جراحی را انتخاب نمیکند که با چاقو کاری کند، صرفنظر از اینکه چقدر مراقب است!”
فِترز اینجا یک تشبیه دقیق میکند: مثل جراحی، تغییر موثر نرمافزار نیاز به مهارتهای عمیقتر دارد.
روش دوم: Cover and Modify (پوشش و تغییر)
این روش متفاوت است:
“ایده اصلی: ممکن است هنگام تغییر نرمافزار با یک شبکه ایمنی کار کنیم.”
شبکه ایمنی چیست؟ تستها!
وقتی تستهای خوبی در اطراف کد داشته باشیم:
- تغییرات را انجام میدهیم
- فوری میفهمیم اثرات خوب است یا بد
- کنترل بیشتری بر کارمان داریم
مقایسه دو روش: یک سناریو واقعی
سناریوی Edit and Pray:
صبح: تغییر یک تابع پیچیده پس از تحلیل دقیق
شام: گزارش تستهای شب
✗ تستهای AE1021 و AE1029 شکستهاند!
نامشخص: آیا خطای ما است؟ یا تیم دیگری؟
→ ساعتها debugging برای پیدا کردن مشکل
سناریوی Cover and Modify:
۱. متد را ابتدا ۲۰ unit test مینویسیم
۲. تستها تمام pass میکنند → رفتار فعلی مشخص است
۳. میخواهیم کد را بهتر بفهمیم → refactor میکنیم
۴. بعد از هر تغییر ریز → تستها اجرا میشوند (سریع!)
۵. اگر logic غلط بود → تست ۱ دقیقه بعد ناموفق شد
۶. بازگشت عقب و دوباره سعی
۷. نتیجه: کد واضحتر + تستهای محافظ + اطمینان
تفاوت بازخورد (Feedback):
| معیار | Edit and Pray | Cover and Modify |
|---|---|---|
| زمان بازخورد | شب بعد (رگرسیون تست) | ۱ دقیقه (unit test) |
| دقت localization | ساعتها برای پیدا کردن مشکل | دقیقهها |
| تعداد تغییرات در هم | ۵-۶ تغییر = نامشخص مشکلها | ۱ تغییر = مشکل واضح |
| کاری که انجام شد | ؟ ✗ | ✓ ✓ ✓ |
تعریف Unit Testing (تست واحد)
Unit Test چیست؟
تست ایزولهشده (isolated) برای واحدهای اتمی رفتار:
- در کد imperative: تابعها
- در کد OOP: کلاسها
مثال:
public class EmployeeTest extends TestCase {
private Employee employee;
protected void setUp() {
// این کد قبل از هر تست اجرا میشود
employee = new Employee("Fred", 0, 10);
}
public void testNormalPay() {
assertEquals(400, employee.getPay());
}
public void testOvertime() {
TDate newCardDate = new TDate(11, 10, 2000);
employee.addTimeCard(new TimeCard(newCardDate, 50));
assertTrue(employee.hasOvertimeFor(newCardDate));
}
}
چرا ایزولاسیون مهم است؟
۳ مشکل با تستهای بزرگ:
- Error Localization (تشخیص خطا)
- تست بزرگ 100 کلاس را تمرین میکند
- اگر شکست بخورد: خطا کجاست؟ 😤
- تست کوچک ۱ کلاس را تمرین میکند
- اگر شکست بخورد: خطا نزدیک است! ✓
- Execution Time (سرعت اجرا)
- تست بزرغ: ۰.۱ ثانیه × ۳۰۰۰۰ تست = ۱ ساعت ❌
- تست کوچک: ۰.۰۰۱ ثانیه × ۳۰۰۰۰ تست = ۳۰ ثانیه ✓
- Coverage (پوشش)
- تست بزرگ: کدام بخش تمرین شد؟ نامشخص
- تست کوچک: دقیقاً کدام بخش
خصوصیات تست واحد خوب:
۱. ✓ سریع اجرا میشود
۲. ✓ مسائل را تیزتر localize میکند
اصل طلایی:
“یک unit test که ۰.۱ ثانیه طول کشد، یک unit test کند است!”
تست که unit test نیست:
یک تست unit test نیست اگر:
- ❌ با دیتابیس صحبت کند
- ❌ از شبکه استفاده کند
- ❌ فایلسیستم را لمس کند
- ❌ نیاز به تنظیمات محیط خاص داشته باشد
نکته: اینها تستهای خوبی هستند، اما “integration test” یا “functional test” هستند، نه unit test.
مشکل Legacy Code Dilemma:
“وقتی کد را تغییر میدهیم، باید تست در جا داشته باشیم.
برای گذاشتن تست در جا، اغلب باید کد را تغییر دهیم!”
این یک حلقه دریافت کننده است:
برای تست نوشتن
↓
باید کد را تغییر دهم (break dependencies)
↓
بدون تست این کار ریسکدار است!
↓
اما تست نوشتن نیاز به تغییر کد دارد...
حل: تغییرات بسیار محافظهکارانه و مرحلهای در ابتدا
The Legacy Code Change Algorithm (الگوریتم تغییر کد Legacy)
فِترز یک الگوریتم ۵ مرحلهای ارائه میدهد:
۱. شناسایی نقاط تغییر (Identify change points)
"کجا باید تغییر کنم؟"
۲. یافتن نقاط تست (Find test points)
"کجا باید تست بنویسم تا این تغییر را detect کنم؟"
۳. شکستن Dependencyها (Break dependencies)
"چگونه کد را از وابستگیهای سنگین جدا کنم؟"
۴. نوشتن تستها (Write tests)
"نوشتن تستهای محافظ در اطراف تغییرات"
۵. تغییر و Refactoring (Make changes and refactor)
"تغییر کد و بهتر کردن ساختار با تستهای حمایتی"
هدف نهایی:
هر روز نه فقط کد جدید، بلکه تستهای آن را هم بسازید.
بتدریج “جزایری” از کد تستشده ظاهر میشود.
در نهایت سرزمینهای بزرگ تستشده:
🏔️ 🏔️
🏔️ 🏔️ 🏔️ (continent of tested code)
🏔️ 🏔️ 🏔️ 🏔️
🏔️ 🏔️ 🏔️ 🏔️ 🏔️
خلاصه این فصل:
دو راه کار:
- Edit and Pray = بدون شبکه ایمنی، خطرناک
- Cover and Modify = با تستها، کنترلشده
واحد اساسی کار: Unit Test
- سریع
- ایزولهشده
- مسائل را تیز localize میکند
چالش Legacy: نیاز به تغییر کد برای تست، بدون تست!
حل: الگوریتم ۵ مرحلهای، گام به گام
بسیار خوب. اکنون بخش “Sensing and Separation” را با فارسی درست و روان دوباره برایتان مینویسم:
فصل ۳: Sensing and Separation (حس و جداسازی)
مقدمه: بین آرمان و واقعیت
فِترز درس را با یک تصویر ایدهآل شروع میکند:
“در دنیای بینقص، نباید چیزی را برای کار با یک کلاس تغییر دهیم. فقط میخواهیم کلاس را در test harness ایجاد کنیم، تست بنویسیم، و به کار بعدی برویم.”
اما واقعیت متفاوت است.
وابستگیها (Dependencies) مشکلآفرین هستند: برای ساخت یک شیء، به شیء دیگر نیاز داریم، و آن شیء دوباره به شیء سوم نیاز دارد… و این زنجیره ادامه مییابد. نتیجه: تقریباً کل سیستم باید در test harness بارگذاری شود!
در بعضی زبانها این مشکل کوچک است. اما در C++ مثلاً، فقط بخش linking میتواند بسیار وقتگیر باشد.
دو دلیل برای شکستن Dependencyها
فِترز دو دلیل جداگانه و فنی را معرفی میکند:
۱. Sensing (حس کردن)
مشکل: “نمیتوانم نتایجی را که این کلاس تولید میکند ببینم!”
مثال: کلاسی که مستقیم به شبکه (network) یا سختافزار صحبت میکند.
راهحل: یک fake object قرار دهید و نتایج را از آن بخوانید.
۲. Separation (جداسازی)
مشکل: “من نمیتوانم این کلاس را اصلاً instantiate کنم!”
مثال: برای ساخت کلاس، نیاز به دیتابیس یا منابع خارجی واقعی است.
راهحل: dependency را بشکنید تا بتوانید کلاس را بدون آن منابع بسازید.
مثال واقعی: NetworkBridge
public class NetworkBridge {
public NetworkBridge(EndPoint[] endpoints) { ... }
public void formRouting(String sourceID, String destID) { ... }
}
چالشها:
- ❌ EndPointها از طریق socket به شبکه واقعی متصل میشوند
- ❌ تست ممکن است freeze شود یا timeout کند
- ❌ نمیتوانید ببینید کی دادهای به شبکه ارسال میشود
- ❌ نیاز به هر دو: sensing و separation
حل: Fake Objects (اشیاء تقلبی)
مثال ساده: فروشگاه و صندوق
بدون Fake (مشکلدار):
public class Sale {
public void scan(String barcode) {
// ... کد پیچیده
realCashRegister.displayLine("Milk $3.99"); // نیاز به سختافزار واقعی!
}
}
با Fake (حلشده):
// مرحلۀ اول: یک interface برای Display
public interface Display {
void showLine(String line);
}
// مرحلۀ دوم: کلاس واقعی برای تولید
public class ArtR56Display implements Display {
public void showLine(String line) {
// کد درایور سختافزار حقیقی
}
}
// مرحلۀ سوم: Fake برای تستها
public class FakeDisplay implements Display {
private String lastLine = "";
public void showLine(String line) {
lastLine = line; // فقط ذخیره کن
}
public String getLastLine() {
return lastLine; // تست میتواند مقدار را بخواند
}
}
// مرحلۀ چهارم: Sale اصلاحشده
public class Sale {
private Display display;
public Sale(Display display) {
this.display = display;
}
public void scan(String barcode) {
// ... logic
String itemLine = item.name() + " " + item.price();
display.showLine(itemLine); // فقط از interface استفاده کن
}
}
// مرحلۀ پنجم: تست!
public class SaleTest extends TestCase {
public void testDisplayAnItem() {
FakeDisplay display = new FakeDisplay();
Sale sale = new Sale(display);
sale.scan("1");
assertEquals("Milk $3.99", display.getLastLine());
}
}
دو طرف یک Fake Object
FakeDisplay یک “دوگانهتن” دارد:
FakeDisplay display = new FakeDisplay();
Sale sale = new Sale(display); // Sale فقط Display را میبیند
// اما تست میتواند بیشتر ببیند:
display.getLastLine(); // این متد فقط برای تست است
| نگاه | کی | متدها |
|---|---|---|
| کلاس Sale | تولید | فقط showLine() |
| تست | testing | showLine() + getLastLine() |
سؤال انتقادی: آیا این “تست واقعی” است؟
اعتراض: “این تست سختافزار واقعی را تست نمیکند!”
جواب: “کاملاً درست است. اما یگانگی کار میکنیم:
- این تست: “آیا Sale به Display چه نوشت میفرستد؟”
- تست دیگر: “آیا ArtR56Display آن را درست نمایش میدهد؟”
اگر هر دو pass کنند → سیستم کامل کار میکند!”
Mock Objects (بهتر از Fake)
Fake را شما مینویسید. Mock خودکار است:
public void testDisplayAnItem() {
MockDisplay display = new MockDisplay();
// بگو که انتظار داری این متد با این داده فراخوانی شود
display.setExpectation("showLine", "Milk $3.99");
Sale sale = new Sale(display);
sale.scan("1");
display.verify(); // بررسی کن!
}
Mock میگوید:
- “من انتظار دارم
showLineبا “Milk $3.99” فراخوانی شود” - بعد از اجرا: ✓ (اگر درست بود) یا ✗ (اگر اشتباه بود)
خلاصه: Sensing بر Separation
| معیار | Sensing | Separation |
|---|---|---|
| مشکل | نمیتوانم نتایج را ببینم | نمیتوانم کلاس را instantiate کنم |
| راهحل | Fake Object | Break Dependency |
| مثال | FakeDisplay | Parameterize Constructor |
| سؤال | “چه نتیجهای صادر شد؟” | “چگونه شروع کنم؟” |
بسیار خوب. اکنون بخش “The Seam Model” را برایتان مینویسم:
فصل ۴: The Seam Model (مدل درزها)
مقدمه: نگاه جدید به کد
فِترز یک نکته مهم را مطرح میکند:
“وقتی سعی میکنی کدهای موجود را تحت تست درآوری، متوجه میشوی که کد برای تستنویسی ساختهنشده است. این فقط مربوط به یک زبان خاص نیست؛ این یک مشکل عمومی است.”
او یک راه فکر جدید پیشنهاد میدهد: مدل Seam (درز).
تعریف Seam (درز)
درز: نقطهای در برنامه است که میتوانی رفتار آن را بدون ویرایش در آن نقطه تغییر دهی.
مثال ساده:
تصور کنید این کد C++:
bool CAsyncSslRec::Init() {
if (m_bSslInitialized) {
return true;
}
// ... کد دیگر ...
if (!m_bFailureSent) {
m_bFailureSent = TRUE;
PostReceiveError(SOCKETCALLBACK, SSL_FAILURE); // ← این خط مشکلدار است
}
// ... کد دیگر ...
return true;
}
مشکل: PostReceiveError تابع global است که با سیستم دیگری ارتباط برقرار میکند. در تست، این مشکلآفرین است.
سؤال: چگونه میتوانیم این خط را بدون اینکه کد را ویرایش کنیم خاموش کنیم؟
جواب: Seamها!
سه نوع Seam (درز)
۱. Object Seam (درز شیءگرا)
بهترین گزینه برای زبانهای OOP:
// مرحلۀ اول: متد virtual در کلاس
class CAsyncSslRec {
// ...
virtual void PostReceiveError(UINT type, UINT errorcode);
};
// مرحلۀ دوم: پیادهسازی در فایل cpp
void CAsyncSslRec::PostReceiveError(UINT type, UINT errorcode) {
::PostReceiveError(type, errorcode); // delegate به تابع global
}
// مرحلۀ سوم: درهنگام تست، از subclass استفاده کن
class TestingAsyncSslRec : public CAsyncSslRec {
virtual void PostReceiveError(UINT type, UINT errorcode) {
// خالی بگذار یا log کن
}
};
// درهنگام تست:
TestingAsyncSslRec* obj = new TestingAsyncSslRec();
obj->Init(); // اکنون PostReceiveError فراخوانی نمیشود!
نقطۀ فعالسازی (Enabling Point): جایی که شیء را میسازی (constructor call)
۲. Preprocessing Seam (درز پیشپردازنده)
برای C/C++:
// فایل اصلی:
#include <DFHLItem.h>
#include "localdefs.h" // ← درز اینجا است
void account_update(int account_no, struct DFHLRecord *record, int activated) {
if (activated) {
db_update(account_no, record->item);
}
}
// فایل localdefs.h (فقط برای تست):
#ifdef TESTING
struct DFHLItem *last_item = NULL;
int last_account_no = -1;
#define db_update(account_no, item) \
{ last_item = (item); last_account_no = (account_no); }
#endif
نقطۀ فعالسازی: ثابت preprocessor TESTING
۳. Link Seam (درز linking)
برای سیستمهای compile و link:
// فایل اصلی (production)
void drawLine(int x1, int y1, int x2, int y2) {
// ... کد سختافزار واقعی ...
}
// فایل تست (test library)
// Makefile:
# production build:
gcc main.o graphics.o -o app
# test build:
gcc main.o test_graphics.o -o test_app
نقطۀ فعالسازی: تنظیم Makefile یا build script
Enabling Point (نقطۀ فعالسازی)
هر Seam یک "نقطۀ فعالسازی" دارد:
جایی که تصمیم میگیری کدام رفتار را استفاده کنی (تولید یا تست).
| Seam Type | Seam Location | Enabling Point |
|---|---|---|
| Object | متد virtual | جایی که شیء میسازی |
| Preprocessing | خط include | define preprocessor |
| Link | تابع global | Makefile/build script |
چرا Seam مهم است؟
فِترز توضیح میدهد:
“بزرگترین چالش برای تستنویسی در کد legacy شکستن dependencyها است. مدل Seam به ما کمک میکند **فرصتهایی را که در کد موجود است ببینیم.”**
نتیجه گیری: کدام Seam را انتخاب کنیم؟
ترجیح:
- ✅ Object Seams (بهترین) - روشن، تغییر design
- ⚠️ Link Seams - مفید، اما نیاز به تغییر build
- ⚠️ Preprocessing Seams - قدرتمند، اما پیچیده