حالت تاریک
فصل ۱۰ — انسجام
انسجام (cohesion) در علوم کامپیوتر بهعنوان «میزانی که عناصر داخل یک ماژول با هم همخانوادهاند» تعریف میشود.[^1]
مدولاریته و انسجام: مبانی طراحی
راه موردعلاقهٔ من برای توصیف طراحی خوب نرمافزار بر این نقلقول Kent Beck استوار است:
چیزهایی که به هم ربطی ندارند را از هم دورتر کنید، و چیزهایی که به هم مربوطاند را به هم نزدیکتر.
این عبارت ساده و کمی شوخطبعانه حقیقت واقعی دارد. طراحی خوب در نرمافزار واقعاً دربارهٔ نحوهٔ سازماندهی کدی است که در سیستمهایی که میسازیم ایجاد میکنیم. همهٔ اصول توصیهشدهٔ من برای مدیریت پیچیدگی در واقع دربارهٔ بخشبندی سیستمهایمان هستند. باید بتوانیم سیستمهایمان را از قطعات کوچکتر، قابلفهمتر، قابلآزمونتر و مجزا بسازیم. برای دستیابی به این هدف، قطعاً به تکنیکهایی نیاز داریم که به ما اجازه دهند «چیزهای نامرتبط را از هم دورتر کنیم»، اما همچنین باید نیاز به «چیزهای مرتبط را به هم نزدیکتر کنیم» را جدی بگیریم. اینجاست که انسجام وارد میشود.
انسجام یکی از مفاهیم لغزانتر در این میان است. میتوانم کار سادهلوحانهای انجام دهم، مثل پشتیبانی از ایدهٔ ماژول در زبان برنامهنویسیام، و ادعا کنم که در نتیجه کدم مدولار است. این اشتباه است؛ صرفاً ریختن مجموعهای از چیزهای نامرتبط در یک فایل، کد را بهجز در معنای بسیار سطحی مدولار نمیکند.
وقتی از مدولاریته صحبت میکنم، واقعاً منظورم اجزای سیستمی هستند که واقعاً اطلاعات را از سایر اجزا (ماژولها) پنهان میکنند. اگر کد داخل ماژول منسجم نباشد، این کار نمیکند.
مشکل این است که این موضوع در برابر تفسیرهای بیشازحد سادهلوحانه آسیبپذیر است. احتمالاً این نقطهای است که هنر، مهارت و تجربهٔ عملکننده واقعاً وارد عمل میشوند. این نقطهٔ تعادل بین سیستمهای واقعاً مدولار و انسجام اغلب مردم را گیج میکند.
کاهش پایهای انسجام
چند بار دیدهاید قطعهای از کد دادهای را بازیابی کند، آن را parse کند و سپس جای دیگری ذخیره کند؟ مطمئناً گام «ذخیره» به گام «تغییر» مربوط است، نه؟ آیا این انسجام خوبی نیست؟ آیا اینها همهٔ گامهایی نیستند که با هم نیاز داریم؟
خب، نه واقعاً — بیایید مثالی ببینیم. اول هشدارهایم: جدا کردن چند ایده در اینجا سخت خواهد بود. این کد ناگزیر کمی از هر یک از ایدههای این بخش را نشان میدهد، پس به شما وابستهام که روی جایی که به انسجام مربوط است تمرکز کنید و وقتی به تفکیک دغدغهها، مدولاریته و غیره هم اشاره میکنم، با آگاهی لبخند بزنید.
Listing 10.1 کد ناخوشایندی را بهعنوان نمایش نشان میدهد. با این حال برای هدف من که چیز ملموسی برای کاوش داشته باشیم مناسب است. این کد فایل کوچکی حاوی فهرستی از کلمات را میخواند، آنها را بهترتیب الفبایی مرتب میکند و سپس فایل جدیدی با فهرست مرتبشده مینویسد — بارگذاری، پردازش و ذخیره!
این الگویی نسبتاً رایج برای بسیاری از مسائل مختلف است: داده بخوانید، پردازش کنید و سپس نتایج را جای دیگری ذخیره کنید.
java
public class ReallyBadCohesion
{
public boolean loadProcessAndStore() throws IOException
{
String[] words;
List<String> sorted;
try (FileReader reader =
new FileReader("./resources/words.txt"))
{
char[] chars = new char[1024];
reader.read(chars);
words = new String(chars).split(" |\0");
}
sorted = Arrays.asList(words);
sorted.sort(null);
try (FileWriter writer =
new FileWriter("./resources/test/sorted.txt"))
{
for (String word : sorted)
{
writer.write(word);
writer.write("\n");
}
return true;
}
}
}این کد برای من فوقالعاده ناخوشایند است و مجبور شدم آن را اینطور بنویسم. این کد فریاد میزند «تفکیک دغدغهٔ ضعیف»، «مدولاریتهٔ ضعیف»، «coupling تنگ» و تقریباً «صفر انتزاع»، اما انسجام چطور؟
اینجا همهٔ کاری که انجام میدهد در یک تابع است. کد production زیادی میبینم که شبیه این است، فقط معمولاً خیلی طولانیتر و پیچیدهتر، پس حتی بدتر!
دیدگاه سادهلوحانه از انسجام این است که همهچیز با هم است و پس آسان دیده میشود. پس اگر برای لحظهای تکنیکهای دیگر مدیریت پیچیدگی را نادیده بگیریم، آیا این قابلفهمتر است؟ چقدر طول میکشد بفهمید این کد چه میکند؟ اگر با نام توصیفی متد کمک نکرده بودم چقدر؟
حالا به Listing 10.2 نگاه کنید که کمی بهتر است.
java
public class BadCohesion
{
public boolean loadProcessAndStore() throws IOException
{
String[] words = readWords();
List<String> sorted = sortWords(words);
return storeWords(sorted);
}
private String[] readWords() throws IOException
{
try (FileReader reader =
new FileReader("./resources/words.txt"))
{
char[] chars = new char[1024];
reader.read(chars);
return new String(chars).split(" |\0");
}
}
private List<String> sortWords(String[] words)
{
List<String> sorted = Arrays.asList(words);
sorted.sort(null);
return sorted;
}
private boolean storeWords(List<String> sorted) throws IOException
{
try (FileWriter writer =
new FileWriter("./resources/test/sorted.txt"))
{
for (String word : sorted)
{
writer.write(word);
writer.write("\n");
}
return true;
}
}
}Listing 10.2 هنوز خوب نیست، اما منسجمتر است؛ بخشهای کدی که بههم نزدیکتر مرتبطاند، واضحتر تفکیک شده و از نظر فیزیکی به هم نزدیکترند. بهطور ساده، همهٔ آنچه دربارهٔ readWords باید بدانید نامگذاری و در یک متد واحد گنجانده شده. جریان کلی متد loadProcessAndStore اکنون روشن است، حتی اگر نام کمتری توصیفی انتخاب کرده بودم. اطلاعات در این نسخه نسبت به Listing 10.1 منسجمتر است. اکنون بهطور قابلتوجهی روشنتر است کدام بخشهای کد به یکدیگر نزدیکتر مرتبطاند، حتی اگر کد از نظر عملکردی یکسان باشد. همهٔ اینها این نسخه را بهطور قابلتوجهی خواناتر و در نتیجه بسیار آسانتر برای تغییر میکند.
توجه کنید در Listing 10.2 خطوط کد بیشتری وجود دارد. این مثال به Java نوشته شده که زبان نسبتاً پرحجمی است و هزینههای boilerplate نسبتاً بالاست، اما حتی بدون آن هم سربار کوچکی برای بهبود خوانایی وجود دارد. این لزوماً بد نیست!
تمایل رایج میان برنامهنویسان کاهش مقدار تایپی است که انجام میدهند. اختصار روشن ارزشمند است. اگر بتوانیم ایدهها را ساده بیان کنیم، ارزش قابلتوجهی دارد، اما سادگی را با کمترین کاراکتر تایپشده نمیسنجید. ICanWriteASentenceOmittingSpaces کوتاهتر است، اما خواندنش هم بسیار ناخوشایندتر است!
بهینهسازی کد برای کاهش تایپ اشتباه است. برای چیزهای اشتباه بهینه میکنیم. کد ابزار ارتباطی است؛ باید از آن برای ارتباط استفاده کنیم. البته باید برای ماشین هم قابلخواندن و اجرا باشد، اما این واقعاً هدف اصلی آن نیست. اگر بود، هنوز با چرخاندن سوئیچهای جلوی کامپیوترمان برنامهنویسی میکردیم یا machine code مینوشتیم.
هدف اصلی کد ارتباط ایدهها به انسانهاست. کد مینویسیم تا ایدهها را تا حد امکان روشن و ساده بیان کنیم — حداقل باید اینطور کار کند. هرگز نباید اختصار را به قیمت ابهام انتخاب کنیم. خوانا کردن کد، به نظر من، هم وظیفهٔ حرفهای ماست و هم یکی از مهمترین اصول راهنما در مدیریت پیچیدگی. پس ترجیح میدهم برای کاهش فکر کردن بهینه کنم نه کاهش تایپ.
برگردیم به کد: این مثال دوم بهوضوح خواناتر است. قصدش آسانتر دیده میشود، هنوز نسبتاً وحشتناک است، مدولار نیست، تفکیک دغدغهٔ زیادی ندارد، با رشتههای hard-coded برای نام فایلها انعطافناپذیر است و جز با اجرای کل آن و سر و کله زدن با file system قابلآزمون نیست. اما انسجام را بهبود دادهایم. هر بخش کد اکنون روی یک بخش از کار متمرکز است. هر بخش فقط به آنچه برای انجام آن کار نیاز دارد دسترسی دارد. در فصلهای بعدی به این مثال برمیگردیم تا ببینیم چطور میتوانیم آن را بیشتر بهبود دهیم.
زمینه مهم است
از دوستی که کدش را تحسین میکنم پرسیدم آیا توصیهای برای نشان دادن اهمیت انسجام دارد و او ویدیوی YouTube خیابان سسام «One of these things is not like another»[^2] را پیشنهاد داد.
پس کمی شوخی است، اما نکتهٔ کلیدی را هم مطرح میکند. انسجام، بیش از سایر ابزارهای مدیریت پیچیدگی، زمینهای (contextual) است. بسته به زمینه، «همهٔ این چیزها ممکن است شبیه هم نباشند.» باید انتخاب کنیم و این انتخابها بهشدت با سایر ابزارها درهم تنیدهاند. نمیتوانم انسجام را از مدولاریته یا تفکیک دغدغهها بهوضوح جدا کنم چون آن تکنیکها به تعریف معنای انسجام در زمینهٔ طراحی من کمک میکنند.
یک ابزار مؤثر برای هدایت این نوع تصمیمگیری، domain-driven design (DDD) است.[^3] اجازه دادن به تفکر و طراحیهایمان که توسط حوزهٔ مسئله هدایت شوند به ما کمک میکند مسیرهایی را شناسایی کنیم که در بلندمدت احتمالاً سودآورترند.
Domain-Driven Design
Domain-driven design رویکردی به طراحی است که در آن هدف داریم رفتارهای اصلی کدمان را در اصل بهعنوان شبیهسازیهای حوزهٔ مسئله ثبت کنیم. طراحی سیستم ما هدف دارد مسئله را بهدرستی مدل کند.
این رویکرد شامل چند ایدهٔ مهم و ارزشمند است.
به ما اجازه میدهد احتمال سوءتفاهم را کاهش دهیم. هدف ایجاد «زبان فراگیر» (ubiquitous language) برای بیان ایدهها در حوزهٔ مسئله است. این روشی توافقشده و دقیق برای توصیف ایدهها در حوزهٔ مسئله است، با استفادهٔ یکنواخت از واژهها و با معانی توافقشده. سپس این زبان را به نحوهٔ صحبت دربارهٔ طراحی سیستمهایمان هم اعمال میکنیم.
پس اگر دربارهٔ نرمافزارم بگویم این «Limit-order matched»، در زمینهٔ کد معنا دارد، جایی که مفاهیم «limit orders» و «matching» بهوضوح نمایندگی شده و نامگذاری شدهاند LimitOrder و Match. اینها دقیقاً همان واژههاییاند که هنگام توصیف سناریو به اصطلاحات کسبوکار با افراد غیرفنی استفاده میکنیم.
این زبان فراگیر عملاً از طریق ثبت نیازمندیها و نوعی test caseهای سطح بالا که میتوانند بهعنوان «مشخصات اجرایی برای رفتار سیستم» عمل کنند و فرایند توسعه را هدایت کنند، توسعه و پالایش میشوند.
DDD همچنین مفهوم «bounded context» را معرفی کرد. این بخشی از سیستم است که مفاهیم مشترک را به اشتراک میگذارد. مثلاً سیستم مدیریت سفارش احتمالاً مفهوم متفاوتی از «سفارش» نسبت به سیستم صورتحساب دارد، پس اینها دو bounded context مجزا هستند.
این مفهوم بسیار مفیدی برای کمک به شناسایی ماژولها یا زیرسیستمهای منطقی هنگام طراحی سیستمهایمان است. مزیت بزرگ استفاده از bounded contextها به این شکل این است که در حوزهٔ مسئلهٔ واقعی بهطور طبیعی loosely coupledترند، پس احتمالاً ما را به ساخت سیستمهای loosely coupledتر هدایت میکنند.
میتوانیم از ایدههایی مثل زبان فراگیر و bounded context برای هدایت طراحی سیستمهایمان استفاده کنیم. اگر از راهنمایی آنها پیروی کنیم، تمایل داریم سیستمهای بهتری بسازیم و به ما کمک میکنند پیچیدگی اصلی و ضروری سیستم را روشنتر ببینیم و آن را از پیچیدگی تصادفی (accidental complexity) که اغلب میتواند آنچه کدمان واقعاً میخواهد انجام دهد را پنهان کند تمایز دهیم.
اگر سیستم را طوری طراحی کنیم که تا جایی که میفهمیم شبیهسازی حوزهٔ مسئله باشد، ایدهای که از منظر حوزهٔ مسئله تغییر کوچکی به نظر میرسد در کد هم گام کوچکی خواهد بود. این ویژگی خوبی است.
Domain-driven design ابزار قدرتمندی در ایجاد طراحیهای بهتر است و مجموعهای از اصول سازماندهی را فراهم میکند که میتواند تلاشهای طراحی ما را هدایت کند و ما را به بهبود مدولاریته، انسجام و تفکیک دغدغهها در کدمان تشویق کند. در عین حال، ما را به سمت سازماندهی درشتبافت کدی که بهطور طبیعی loosely coupledتر است هدایت میکند.
ابزار مهم دیگری که به ساخت سیستمهای بهتر کمک میکند تفکیک دغدغههاست که در فصل بعد بهطور قابلتوجهی بیشتر دربارهٔ آن صحبت میکنیم، اما فعلاً شاید نزدیکترین چیزی است که بهعنوان قاعده برای هدایت برنامهنویسی خودم دارم: «یک class، یک چیز؛ یک method/function، یک چیز.»
هر دو مثال کدی که تاکنون در این فصل ارائه شده را بهشدت دوست ندارم و کمی خجالتزدهام که آنها را نشان میدهم، چون غریزههای طراحیام فریاد میزنند تفکیک دغدغه در هر دو مورد وحشتناک است. Listing 10.2 بهتر است؛ حداقل هر متد اکنون یک کار انجام میدهد، اما class هنوز وحشتناک است. اگر هنوز نمیبینید، در فصل بعد میبینیم چرا مهم است.
سرانجام، در جعبهٔ ابزارم قابلیت آزمون (testability) است. این مثالهای بد کد را همانطور که همیشه کد مینویسم شروع کردم: با نوشتن آزمون. اما تقریباً فوراً مجبور شدم متوقف شوم، چون راهی نبود TDD را تمرین کنم و کدی به این بدی بنویسم! مجبور شدم آزمون را دور بیندازم و دوباره شروع کنم و اعتراف میکنم احساس کردم به عقب برگشتهام. برای مثالهایم آزمون نوشتم تا ببینم آنچه انتظار داشتم انجام میدهند یا نه، اما این کد بهدرستی قابلآزمون نیست.
قابلیت آزمون بهشدت مدولاریته، تفکیک دغدغهها و سایر ویژگیهایی را که در کد باکیفیت ارزش مینهیم تشویق میکند. این به نوبهٔ خود به ما کمک میکند تقریب اولیهای از زمینهها و انتزاعهایی که در طراحی دوست داریم و جایی که کدمان را منسجمتر کنیم بسازیم.
توجه کنید، اینجا تضمینی نیست و این نکتهٔ نهایی این کتاب است. پاسخهای ساده و قالبی وجود ندارد. این کتاب ابزارهای ذهنی میدهد که به ساختاربندی تفکرمان کمک کند وقتی پاسخ را نداریم.
تکنیکهای این کتاب قرار نیست پاسخها را به شما تحویل دهند؛ این هنوز با شماست. بلکه مجموعهای از ایدهها و تکنیکها را میدهند که به شما اجازه دهند حتی وقتی هنوز پاسخ را نمیدانید، با اطمینان پیشرفت کنید. وقتی سیستمی با پیچیدگی واقعی میسازید، همیشه اینطور است؛ هرگز پاسخها را تا وقتی تمام نشده نمیدانیم!
میتوانید این را رویکردی نسبتاً دفاعی بدانید و هست، اما هدف باز نگهداشتن آزادی انتخاب ماست. این یکی از مزایای قابلتوجه کار برای مدیریت پیچیدگی است. با یادگیری بیشتر، میتوانیم کدمان را بهطور مداوم تغییر دهیم تا آن یادگیری را منعکس کند. فکر میکنم صفت بهتری از «دفاعی»، «افزایشی» (incremental) است.
از طریق مجموعهای از آزمایشها بهصورت افزایشی پیشرفت میکنیم و از تکنیکهای مدیریت پیچیدگی برای محافظت در برابر اشتباهاتی که خیلی آسیبزا هستند استفاده میکنیم.
اینطور علم و مهندسی کار میکنند. متغیرها را کنترل میکنیم، گام کوچکی برمیداریم و ارزیابی میکنیم کجا هستیم. اگر ارزیابی نشان دهد اشتباه برداشتیم، یک گام به عقب برمیگردیم و تصمیم میگیریم بعد چه امتحان کنیم. اگر خوب به نظر رسید، متغیرها را کنترل میکنیم، گام کوچک دیگری برمیداریم و همینطور ادامه میدهیم.
راه دیگر فکر کردن به این موضوع این است که توسعهٔ نرمافزار نوعی فرایند تکاملی است. وظیفهٔ ما بهعنوان برنامهنویس هدایت یادگیری و طراحیهایمان از طریق فرایند تکاملی هدایتشده به سمت نتایج مطلوب است.
نرمافزار پرکارایی
یکی از بهانههای رایج برای کد ناخوشایند مثل Listing 10.1 این است که اگر کارایی بالا میخواهید باید کد پیچیدهتری بنویسید. بخش پایانی حرفهام را روی سیستمهای در لبهٔ کارایی بالا کار کردم و میتوانم اطمینان دهم اینطور نیست. سیستمهای پرکارایی به کد ساده و خوشطراحشده نیاز دارند.
لحظهای فکر کنید کارایی بالا در اصطلاحات نرمافزاری چه معنایی دارد. برای دستیابی به «کارایی بالا» باید حداکثر کار را با کمترین تعداد دستورالعمل انجام دهیم.
هرچه کد پیچیدهتر باشد، احتمال بیشتری دارد مسیرهای عبور از کدمان بهینه نباشند، چون «سادهترین مسیر ممکن» توسط پیچیدگی خود کد پنهان میشود. این ایدهٔ غافلگیرکنندهای برای بسیاری از برنامهنویسان است، اما راه به کد سریع نوشتن کد ساده و قابلفهم است.
این با دید گستردهتر سیستم حتی بیشتر درست است.
بیایید دوباره به مثال پیشپاافتادهمان برگردیم. شنیدهام برنامهنویسان استدلال کنند کد Listing 10.1 سریعتر از Listing 10.2 خواهد بود بهخاطر «سربار» فراخوانی متدهایی که Listing 10.2 اضافه میکند. متأسفم که برای اکثر زبانهای مدرن این حرف بیمعناست. اکثر کامپایلرهای مدرن به کد Listing 10.2 نگاه میکنند و متدها را inline میکنند. اکثر کامپایلرهای بهینهساز بیش از آن هم کار میکنند. کامپایلرهای مدرن کار فوقالعادهای در بهینهسازی کد برای اجرای کارآمد روی سختافزار مدرن انجام میدهند. وقتی کد ساده و قابلپیشبینی است عالی عمل میکنند، پس هرچه کد پیچیدهتر باشد، کمتر از optimizer کامپایلرتان سود میبرید. اکثر optimizerهای کامپایلر وقتی cyclomatic complexity[^4] یک بلوک کد از آستانهای فراتر رود، تلاش را رها میکنند.
سری benchmark علیه هر دو نسخه این کد اجرا کردم. خیلی خوب نبودند، چون این کد بد است. متغیرها را بهاندازهٔ کافی کنترل نکردهایم تا واقعاً ببینیم چه میگذرد، اما واضح بود تفاوت قابلسنجش واقعی در این سطح آزمون وجود نداشت. تفاوتها خیلی کوچک بودند که از بقیهٔ اتفاقات قابلتمایز نبودند. در یک اجرا نسخهٔ BadCohesion بهتر بود؛ در دیگری ReallyBadCohesion. در مجموعهای از اجراهای benchmark، برای هر کدام ۵۰٬۰۰۰ تکرار متد loadProcessStore، تفاوت کلی بیش از ۳۰۰ میلیثانیه نبود، پس بهطور میانگین تقریباً تفاوت ۶ نانوثانیه در هر فراخوانی بود و در واقع کمی بیشتر به نفع نسخهای با فراخوانی متد اضافی بود.
این آزمون ضعیفی است، چون چیزی که به آن علاقه داریم — هزینهٔ فراخوانی متد — توسط هزینهٔ I/O تحتالشعاع قرار گرفته. قابلیت آزمون — در این مورد قابلیت آزمون performance — باز هم میتواند ما را به نتیجهٔ بهتر هدایت کند. در فصل بعد بیشتر دربارهٔ این صحبت میکنیم.
زیر پوست اتفاقات زیادی میافتد که حتی برای خبرهها پیشبینی نتیجه سخت است. پاسخ چیست؟ اگر واقعاً به performance کدتان اهمیت میدهید، حدس نزنید چه سریع و چه کند خواهد بود؛ اندازه بگیرید!
ارتباط با coupling
اگر بخواهیم آزادی کاوش و گاهی اشتباه کردن را حفظ کنیم، باید به هزینههای coupling نگران باشیم.
Coupling: اگر دو خط کد A و B داشته باشیم، وقتی coupled هستند که B فقط بهخاطر تغییر A مجبور به تغییر رفتار شود.
Cohesion: وقتی منسجماند که تغییر در A به B اجازه دهد طوری تغییر کند که هر دو ارزش جدید اضافه کنند.[^5]
coupling واقعاً اصطلاحی بیشازحد عمومی است. انواع مختلف coupling وجود دارد که باید در نظر گرفته شوند (ایدهای که در فصل ۱۳ بیشتر کاوش میکنیم).
تصور سیستمی بدون هیچ coupling مضحک است. اگر دو بخش سیستم بخواهند ارتباط برقرار کنند، تا حدی باید coupled باشند. پس مثل انسجام، coupling مسئلهٔ درجه است نه معیار مطلق. با این حال هزینهٔ سطوح نامناسب coupling بسیار بالاست، پس مهم است تأثیرش را در طراحیهایمان در نظر بگیریم.
coupling بهنوعی هزینهٔ انسجام است. در بخشهای سیستم که منسجماند، احتمالاً tightly coupledتر هم هستند.
هدایت انسجام بالا با TDD
باز هم استفاده از آزمونهای خودکار، و بهطور خاص TDD، برای هدایت طراحی مزایای زیادی دارد. تلاش برای دستیابی به طراحی قابلآزمون و آزمونهای خوشانتزاع و متمرکز بر رفتار برای سیستم، فشاری روی طراحی اعمال میکند تا کدمان منسجمتر شود.
قبل از نوشتن کدی که رفتاری را که میخواهیم observe کنیم توصیف میکند، test case میسازیم. این به ما اجازه میدهد روی طراحی API/Interface خارجی کدمان — هرچه باشد — تمرکز کنیم. حالا برای برآورده کردن مشخصات کوچک و اجرایی که ساختهایم پیادهسازی مینویسیم. اگر بیش از آنچه برای برآورده کردن مشخصات لازم است کد بنویسیم، به فرایند توسعه تقلب کردهایم و انسجام پیادهسازی را کاهش میدهیم. اگر کمتر بنویسیم، قصد رفتاری برآورده نمیشود. انضباط TDD ما را تشویق میکند به نقطهٔ شیرین انسجام برسیم.
مثل همیشه، تضمینی نیست. این فرایند مکانیکی نیست و هنوز به تجربه و مهارت برنامهنویس وابسته است، اما رویکرد فشاری به سمت نتیجهٔ بهتر اعمال میکند که قبلاً نبود و آن مهارتها و تجربه را تقویت میکند.
چگونه نرمافزار منسجم بسازیم
معیار کلیدی انسجام، گستره یا هزینهٔ تغییر است. اگر مجبور شوید در codebase بگردید و در جاهای زیاد آن را تغییر دهید تا یک تغییر انجام شود، سیستم خیلی منسجم نیست.
انسجام معیاری از مرتبط بودن عملکردی است. اندازهگیری مرتبط بودن هدف است. این چیز لغزانی است!
بیایید مثال سادهای ببینیم.
اگر classی با دو متد بسازم، هر کدام مرتبط با یک متغیر عضو (Listing 10.3)، انسجام ضعیفی دارد، چون متغیرها واقعاً نامرتبطاند. مختص متدهای مختلفاند اما در سطح class با هم ذخیره شدهاند در حالی که نامرتبطاند.
python
class PoorCohesion:
def __init__(self):
self.a = 0
self.b = 0
def process_a(x):
a = a + x
def process_b(x):
b = b * xListing 10.4 راهحل بسیار بهتر و منسجمتری نشان میدهد. توجه کنید علاوه بر منسجمتر بودن، این نسخه هم مدولارتر است و تفکیک دغدغهٔ بهتری دارد. نمیتوانیم از ارتباط این ایدهها فرار کنیم.
python
class BetterCohesionA:
def __init__(self):
self.a = 0
def process_a(x):
a = a + x
class BetterCohesionB:
def __init__(self):
self.b = 0
def process_b(x):
b = b * xدر ترکیب با بقیهٔ اصول مدیریت پیچیدگی، تمایل به طراحی قابلآزمون به بهبود انسجم راهحلهایمان کمک میکند. نمونهٔ خوبی از این تأثیر، جدی گرفتن تفکیک دغدغههاست، بهویژه هنگام تفکیک پیچیدگی تصادفی[^6] از پیچیدگی ضروری.[^7]
Listing 10.5 سه مثال ساده از بهبود انسجام کد با تمرکز آگاهانه بر تفکیک پیچیدگی «ضروری» و «تصادفی» نشان میدهد. در هر مثال، آیتمی به سبد خرید اضافه میکنیم، در پایگاه داده ذخیره میکنیم و ارزش سبد را محاسبه میکنیم.
python
def add_to_cart1(self, item):
self.cart.add(item)
conn = sqlite3.connect('my_db.sqlite')
cur = conn.cursor()
cur.execute('INSERT INTO cart (name, price)
values (item.name, item.price)')
conn.commit()
conn.close()
return self.calculate_cart_total();
def add_to_cart2(self, item):
self.cart.add(item)
self.store.store_item(item)
return self.calculate_cart_total();
def add_to_cart3(self, item, listener):
self.cart.add(item)
listener.on_item_added(self, item)تابع اول بهوضوح کد منسجم نیست. مفاهیم و متغیرهای زیادی درهم آمیختهاند و مخلوط کاملی از پیچیدگی ضروری و تصادفی وجود دارد. میگویم حتی در این مقیاس بسیار ساده کد بسیار ضعیفی است. از نوشتن چنین کدی پرهیز میکنم چون فکر کردن به آنچه میگذرد را سختتر میکند، حتی در این مقیاس بسیار ساده.
مثال دوم کمی بهتر است. منسجمتر است. مفاهیم این تابع مرتبطاند و سطح انتزاعی سازگارتری را نشان میدهند چون عمدتاً به پیچیدگی ضروری مسئله مربوطاند. دستور «store» شاید قابل بحث باشد، اما حداقل جزئیات پیچیدگی تصادفی را در این نقطه پنهان کردهایم.
آخرین مثال جالب است. استدلال میکنم قطعاً منسجم است. برای کار مفید باید هم آیتم را به سبد اضافه کنیم و هم طرفهای علاقهمند احتمالی را از افزودن مطلع کنیم. نگرانیهای ذخیرهسازی و محاسبهٔ مجموع را کاملاً جدا کردهایم. اینها ممکن است در پاسخ به اطلاع از افزودن اتفاق بیفتند، یا اگر آن بخشهای کد به رویداد «item added» علاقه نداشتند شاید نیفتند.
این کد یا منسجمتر است — جایی که پیچیدگی ضروری مسئله همگی اینجاست و رفتارهای دیگر side effect هستند — یا کمتر منسجم اگر «store» و «total» را بخشی از این مسئله بدانید. در نهایت این زمینهای و انتخاب طراحی بر اساس زمینهٔ مسائلی است که حل میکنید.
هزینههای انسجام ضعیف
انسجام شاید کممستقیمترین جنبهٔ «ابزارهای مدیریت پیچیدگی» من برای کمیسازی باشد، اما مهم است. مشکل این است که وقتی انسجام ضعیف است، کد و سیستمهایمان کمانعطافتر، سختتر برای آزمون و سختتر برای کار هستند.
در مثال ساده Listing 10.5 تأثیر کد منسجم روشن است. اگر کد مسئولیتهای مختلف را قاطی کند، وضوح و خوانایی را از دست میدهد همانطور که add_to_cart1 نشان میدهد. اگر مسئولیتها پراکندهتر باشند، دیدن آنچه میگذرد سختتر میشود، مثل add_to_cart3. با نزدیک نگهداشتن ایدههای مرتبط، خوانایی را مثل add_to_cart2 بیشینه میکنیم.
در واقعیت، مزایایی برای سبک طراحی که در add_to_cart3 اشاره شده وجود دارد و این کد قطعاً جای کار بهتری از نسخه ۱ است.
نکتهٔ من این است که نقطهٔ شیرینی برای انسجام وجود دارد. اگر مفاهیم زیادی را درهم بریزید، انسجام را در سطح جزئی از دست میدهید. در مثال ۱ میتوانید استدلال کنید همهٔ کار داخل یک متد انجام میشود، اما این فقط بهصورت سادهلوحانه منسجم است.
در واقعیت، مفاهیم مرتبط با افزودن آیتم به سبد خرید — کسبوکار تابع — با وظایف دیگری که تصویر را مبهم میکنند مخلوط شدهاند. حتی در این مثال ساده، تا وقتی عمیقتر نگاه نکنیم کمتر روشن است این کد چه میکند. باید چیزهای بیشتری بدانیم تا این کد را درست بفهمیم.
گزینهٔ دیگر، add_to_cart3، در حالی که بهعنوان طراحی انعطافپذیرتر است، هنوز وضوح کم دارد. در این extreme آسان است مسئولیتها آنقدر پراکنده و گسترده شوند که بدون خواندن و فهمیدن کد زیاد تصویر را نفهمید. این میتواند خوب باشد، اما نکتهٔ من این است که coupling به این شکل loose هزینهای در وضوح دارد، علاوه بر برخی مزایا.
هر دو این کاستیها در سیستمهای production بسیار رایجاند. در واقع آنقدر رایجاند که شاید برای سیستمهای بزرگ پیچیده عادی باشند.
این شکست طراحی است و هزینهٔ قابلتوجهی دارد. اگر روی «legacy code»[^8] کار کردهاید، با این هزینه آشنا هستید.
راه ساده و ذهنی برای تشخیص انسجام ضعیف: اگر قطعهای از کد خواندهاید و فکر کردهاید «نمیدانم این کد چه میکند»، احتمالاً بهخاطر انسجام ضعیف است.
انسجام در سیستمهای انسانی
مثل بسیاری از ایدههای دیگر این کتاب، مشکلات انسجام فقط به کدی که مینویسیم و سیستمهایی که میسازیم محدود نیست. انسجام ایدهای است که در سطح اطلاعات کار میکند، پس در ساختاردهی منطقی سازمانهایی که در آن کار میکنیم هم به همان اندازه مهم است.
واضحترین مثال این در سازماندهی تیم است. یافتههای گزارش «State of DevOps» میگوید یکی از پیشبینیکنندههای اصلی عملکرد بالا، اندازهگیریشده از نظر throughput و پایداری، توانایی تیمها برای تصمیمگیری مستقل بدون نیاز به اجازه از کسی خارج از تیم است. راه دیگر فکر کردن به آن این است که اطلاعات و مهارتهای تیم منسجماند، به این معنا که تیم همهٔ آنچه برای تصمیمگیری و پیشرفت در مرزهای خود نیاز دارد را دارد.
خلاصه
انسجام احتمالاً لغزانترین ایده در فهرست ایدههای مدیریت پیچیدگی است. توسعهدهندگان نرمافزار گاهی استدلال میکنند که داشتن همهٔ کد در یک جا، یک فایل و حتی یک تابع، حداقل منسجم است، اما این بیشازحد سادهانگارانه است.
کدی که ایدهها را بهصورت تصادفی اینطور ترکیب میکند منسجم نیست؛ فقط ساختارنیافته است. بد است. مانع میشود بهوضوح ببینیم کد چه میکند و چطور آن را با اطمینان تغییر دهیم.
انسجام دربارهٔ کنار هم گذاشتن مفاهیم مرتبط، مفاهیمی که با هم تغییر میکنند، در کد است. اگر فقط «با هم» باشند بهخاطر تصادف چون همهچیز «با هم» است، واقعاً پیشرفت چندانی نکردهایم.
انسجام متضاد مدولاریته است و عمدتاً وقتی معنا دارد که در ترکیب با مدولاریته در نظر گرفته شود. یکی از مؤثرترین ابزارها برای برخوردن به تعادل خوب بین انسجام و مدولاریته، تفکیک دغدغههاست.
[^1]: منبع: Wikipedia، https://en.wikipedia.org/wiki/Cohesion_(computer_science)
[^2]: آهنگ خیابان سسام به نام «One of these things is not like the other»: https://youtu.be/rsRjQDrDnY8
[^3]: Domain Driven Design عنوان کتابی نوشتهٔ Eric Evans و رویکردی به طراحی سیستمهای نرمافزاری است. ببینید https://amzn.to/3cQpNaL.
[^4]: معیار نرمافزاری برای نشان دادن پیچیدگی یک برنامه.
[^5]: coupling و cohesion در ویکی معروف C2 توصیف شدهاند، https://wiki.c2.com/?CouplingAndCohesion.
[^6]: پیچیدگی تصادفی سیستم پیچیدگیای است که بهخاطر اجرا روی کامپیوتر به سیستم تحمیل میشود. چیزهایی که side effect حل مسئلهٔ واقعی موردعلاقهمان هستند، مثلاً مسائل persistence اطلاعات، concurrency یا APIهای پیچیده و غیره.
[^7]: پیچیدگی ضروری سیستم پیچیدگی ذاتی حل مسئله است، مثلاً محاسبهٔ نرخ بهره یا افزودن آیتم به سبد خرید.
[^8]: legacy code یا legacy systems سیستمهاییاند که مدتی وجود داشتهاند. احتمالاً هنوز ارزش مهمی برای سازمانهایی که آنها را اداره میکنند دارند، اما اغلب به تودههای درهمتنیدهٔ بدطراح تبدیل شدهاند. Michael Feathers legacy system را «سیستمی بدون آزمون» تعریف میکند.