اگر مشتری پولتون رو نداد با بمب ساعتی منفجرش نکنید!

بمب ساعتی نرم‌افزاری چیه؟

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

چند روز پیش در یک مهمانی افطاری بودم. روبرویم مرد جوانی نشسته بود که گویا در حرفه‌ای از مشاغل ساختمانی مشغول به کار بود. داشت از یکی از خاطراتش درباره گرفتن بدهی مشتری صحبت می‌کرد. می‌گفت کار مشتری را انجام داده و مشتری یک میلیون و هفتصد هزار تومان پولش رو نداده، ایشان هم یک روز عصر با کمک برادرش رفته بود به محل ساختمان مشتری و از دیوار بالا رفته بود و زده بود بخشی از کارش را خراب کرده بود. نگهبان ساختمان به صاحبکار زنگ می‌زند. صاحبکار خودش را می‌رساند و تهدید به شکایت و پلیس ۱۱۰ می‌کند. پاسخ پیمانکار هم این بوده که «به ۱۲۰ هم زنگ بزنی فرقی نمی‌کنه، تا با پول نقد نیای همینه» صاحبکار در نهایت به دادن چک روز راضی می‌شود اما پیمانکار فقط پول نقد را مورد قبول می‌داند. نیم ساعت بعد صاحبکار با پول نقد آمده بود.
در نهایت هم آنطور که مرد جوان تعریف می‌کرد ۱۵۰ هزار تومان دیگر گرفته بود تا خرابکاری آن روزش را درست کند! نتیجه‌گیری که مرد جوان داشت هم برایم جالب بود. می‌گفت: «در این مملکت فقط زور جواب می‌ده!»

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

به زعم همکاران من در حوزه توسعه نرم‌افزار (چه اشخاص و چه شرکت‌ها) موثرترین روش استفاده از بمب ساعتی نرم‌افزاری هست. در این روش برنامه‌نویس یک لایسنس مدت‌دار را بدون اطلاع مشتری در نرم‌افزار قرار می‌دهد. در صورتی که همه چیز به خوبی پیش برود و پرداخت‌ها کامل شوند، کد فعالسازی محصول از طرف برنامه‌نویس اعمال می‌شود، در غیر اینصورت با رسیدن به زمان مقرر، محصول به صورت اتوماتیک از کار افتاده یا عملکرد آن دچار اختلال می‌شود. مکانیزم این روش را سال‌ها پیش اولین بار دوستی (که حالا خودش استاد دانشگاه شده) در برنامه‌ ویندوزی‌اش به من معرفی کرد.

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

در بعضی موارد هم استفاده از راهکارهای قانونی مثل شکایت و حکم جلب و ... دیده شده است.

راه حل چیست؟
قبل از اینکه درباره راه حل‌ها صحبت کنیم، می‌خواهم عبارتی در خصوص دلیل اصلی کسب و کارها را نقل قول کنم:

  هدف اصلی یک کسب و کار، سودآوری نیست، هدف اصلی یافتن و حفظ مشتریان است.

تا زمانی که درباره راه‌حل‌ها بنویسم، ضمن درخواست برای مشارکت در بحث، می‌خواهم خواهش کنم از ترکاندن مشتریان با بمب ساعتی یا هر روش دیگر جداً خودداری کنید!

به اشتراک گذاری این نوشته در شبکه‌های اجتماعی

۳۰ روز با TDD: روز نهم - مقدمات Refactoring

داستان چیه؟

سپتامبر سال گذشته آقای James Bender در وبلا‌گ‌های تلریک یک مجموعه نوشته منتشر کرد به نام 30 روز با TDD. من می‌خوام یک ترجمه آزاد از این نوشته‌ها براتون داشته باشم تا با هم درباره Test Driven Development بیشتر بدونیم. اگر نمی‌دونید داستان TDD چی هست نگران نباشید، این مجموعه نوشته‌ها برای همین هست که با این روش بیشتر آشنا بشیم ;)

#30RoozTDD 

سایر نوشته‌های مربوط به این سری رو در صفحه "30 روز با TDD" می‌تونید ببینید. در توییتر هم با هشتگ 30RoozTDD# می‌تونید مطالب مرتبط به این سری نوشته‌ها رو پیدا کنید. حرکت 30 روز با TDD برای ارتقاء دانش خودم و همه دوستان و همکاران برنامه‌نویسم هست و اگر کسی مایل هست از این حرکت حمایتی بکنه، می‌تونه با من در تماس باشه.

و اما نهمین روز: مقدمات Refactoring

نوشته زبان انگلیسی مربوط به روز نهم را از این آدرس می‌توانید مطالعه کنید. درباره مفهوم Refactoring در ادامه توضیح خواهم داد.

زمانی برای بازبینی کد (Code Review)

درنوشته قبلی از این سری نوشته‌ها درباره defect ها صحبت کردیم. آخرین کدی که نوشتیم این بود:

       
using System;
 
namespace ThirtyDaysOfTDD.UnitTests
{
    public class StringUtils
    {
        public int FindNumberOfOccurences(string sentenceToScan, string characterToScanFor)
        {
            try
            {
                var stringToCheckAsCharacterArray = sentenceToScan.ToCharArray();
                var characterToCheckFor = Char.Parse(characterToScanFor);
 
                var numberOfOccurenes = 0;
 
                for (var charIdx = 0; charIdx < stringToCheckAsCharacterArray.GetUpperBound(0); charIdx++)
                {
                    if (stringToCheckAsCharacterArray[charIdx] == characterToCheckFor)
                    {
                        numberOfOccurenes++;
                    }
                }
 
                return numberOfOccurenes;
            }
            catch
            {
                throw new ArgumentException();
            }
        }
    }
}

نقصی که برطرفش کردیم این بود که کاربر می‌توانست یک رشته دو حرفی را به عنوان پارامتر دوم به تابع FindNumberOfOccurences ارسال کند و exception ای از نوع FormatException بگیرد. رفتار مورد انتظار در این شرایط این بود که متد exception از نوع ArgumentException را throw (پرتاب) کند.

راه حل من برای رفع این نقص این بود که خیلی ساده کل بدنه تابع را داخل یک try/catch قرار بدهم و اگر خطایی رخ داد یک ArgumentException را throw کنم. این نیاز مربوط به رفع نقص کد را برآورده می‌کند اما اگر به دنبال کد خوب باشیم نیاز به کار بیشتری دارد و اینجاست که می‌توانیم کمی Refactor انجام بدهیم.

مارتین فولر Refactoring را این‌طور تعریف می‌کند: «... یک تکنیک کنترل‌شده برای بهبود طراحی کد موجود». اساساً Refactoring درباره یافتن راه‌هایی است که کدمان را بهتر کنیم. «بهتر» ممکن است معانی زیادی داشته باشد. بعضی مواقع به این معنی است که کد را تغییر بدهیم تا خوانایی بیشتری داشته باشد. بعضی اوقات به معنی حذف کردن کدها یا logic تکراری است. بعضی اوقات به معنی جدا کردن متدها یا کلاس‌های بزرگ برای مدیریت بهتر آن‌هاست. دلایل زیادی برای Refactor کردن کدها وجود دارد. در نوشته‌های بعدی درباره بعضی از مشکلات مشترک کدها و نحوه برخورد با آن‌ها صحبت خواهم کرد.

TDD به سادگی امکان Refactor را فراهم می‌کند. بدون آزمون‌های واحد من باید نگران تغییر کدهایی بودم که کار می‌کنند. چون از Unit Test ها استفاده می‌کنم می‌توانم تا هر جا که مایل باشم کدهایم را Refactor کنم و تا زمانی که تست‌ها pass می‌شوند، صرفنظر از تغییراتی که در کد دادم، مطمئن هستم که نیازمندی‌ها همچنان رعایت شده‌اند.

موقع Refactor کردن شما تغییرات کوچکی ایجاد کرده و بعد آزمون‌های واحد را اجرا می‌کنید تا مطمئن شوید که به برنامه آسیبی وارد نشده است. تغییرات کوچک باعث می‌شوند اگر به هر دلیل آن تغییرات کار نکردند (در واقع آزمون‌های واحد fail شدند) بتوانیم به سادگی به عقب برگردیم.

در پیاده‌سازی جاری متد FindNumberOfOccurences روشی که برای رفع نقص در نظر گرفتیم احتمالاً بهترین راه نبود. چیزی که در واقع احتمالاً به آن نیاز داریم این است که یک قانون اعتبارسنجی برای تعیین تعداد کاراکترهای پارامتر characterToScanFor داشته باشیم و اگر تعداد کاراکترها برابر یک نبود آنگاه یک exception را throw کنیم. من کد متد FindNumberOfOccurences را برای پیاده‌سازی این روش تغییر می‌دهم:

       
using System;
 
namespace ThirtyDaysOfTDD.UnitTests
{
    public class StringUtils
    {
        public int FindNumberOfOccurences(string sentenceToScan, string characterToScanFor)
        {
            if (characterToScanFor.Length != 1)
            {
                throw new ArgumentException();
            }
            var stringToCheckAsCharacterArray = sentenceToScan.ToCharArray();
            var characterToCheckFor = Char.Parse(characterToScanFor);
 
            var numberOfOccurenes = 0;
 
            for (var charIdx = 0; charIdx < stringToCheckAsCharacterArray.GetUpperBound(0); charIdx++)
            {
                if (stringToCheckAsCharacterArray[charIdx] == characterToCheckFor)
                {
                    numberOfOccurenes++;
                }
            }
 
            return numberOfOccurenes;
        }
    }
}

به جای اینکه کل بدنه متد را در یک try/catch قرار دهم، طول رشته characterToScanFor را چک می‌کنم. اگر طول رشته برابر یک نبود، آنگاه یک exception از نوع AgurmentException را throw می‌کنم. در مقایسه با روش قبل این روش بسیار بهتری است («کار کردن بر حسب تصادف» یکی از مشکلات کد است که در نوشته‌های بعدی درباره‌اش توضیح خواهم داد) این روش همچنین باعث می‌شود که نقایص احتمالی که به قانون اعتبارسنجی (همان قانون یک کاراکتری بودن پارامتر دوم) مرتبط نیستند فراموش نشوند. در هر حال این روش بهتری است اما آیا درست کار می‌کند. راه فهمیدن این موضوع اجرا کردن آزمون‌های واحد است.

چون unit test ها هنوز هم pass می‌شوند، متوجه می‌شویم که صرفنظر از تغییری که در کد دادیم، برنامه و منطق تجاری آن هنوز کار می‌کند. ممکن است defect ها یا نیازمندی‌های دیگری در آینده به وجود آیند اما در حال حاضر کد ما تمام نیازمندهای تجاری برنامه را پوشش می‌دهد.

Refactoring یک گام مهم در TDD است. در TDD شما معمولاً عبارت "Red,Green,Refactor" را می‌شنوید. Red اشاره به این ایده دارد که تستی را می‌نویسیم و fail شدنش را برای اولین بار می‌بینیم. به محض اینکه کد لازم برای pass کردن تست نوشته شد ما به بخش Green در چرخه کاری TDD می‌رسیم. Refactoring آخرین مرحله است: بهتر کردن کد به صورت مداوم برای اینکه خواناتر و بهینه‌تر و انعطاف‌پذیرتر باشد.

ادامه دارد...

به اشتراک گذاری این نوشته در شبکه‌های اجتماعی

۳۰ روز با TDD: روز هشتم: برخورد با defect ها

داستان چیه؟

سپتامبر سال گذشته آقای James Bender در وبلا‌گ‌های تلریک یک مجموعه نوشته منتشر کرد به نام 30 روز با TDD. من می‌خوام یک ترجمه آزاد از این نوشته‌ها براتون داشته باشم تا با هم درباره Test Driven Development بیشتر بدونیم. اگر نمی‌دونید داستان TDD چی هست نگران نباشید، این مجموعه نوشته‌ها برای همین هست که با این روش بیشتر آشنا بشیم ;)

#30RoozTDD 

سایر نوشته‌های مربوط به این سری رو در صفحه "30 روز با TDD" می‌تونید ببینید. در توییتر هم با هشتگ 30RoozTDD# می‌تونید مطالب مرتبط به این سری نوشته‌ها رو پیدا کنید. حرکت 30 روز با TDD برای ارتقاء دانش خودم و همه دوستان و همکاران برنامه‌نویسم هست و اگر کسی مایل هست از این حرکت حمایتی بکنه، می‌تونه با من در تماس باشه.

و اما هشتمین روز: چگونگی برخورد کردن با نقایص (defects)

نوشته به زبان انگلیسی روز هشتم را در این آدرس می‌توانید مطالعه کنید. قبل از شروع لازم است درباره موضوع امروز یعنی defect و تفاوت آن با bug نکته‌ای را عرض کنم. defect که شاید بتوان آن را نقص ترجمه کرد در واقع انحراف از نیازمندی‌های نرم‌افزار است و bug نتیجه خطا در کدنویسی، اینجا را ببینید.

defect بد در نرم‌افزار خوب

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

شاید دلیل defect ها، نیازمندی‌های تدوین شده برای نرم‌افزار باشد که دقیقاً مطابق نیاز تجاری نیست. شاید فهم نادرست یا ناقص از نیازمندی‌های نرم‌افزار باشد و یا شاید یک جنبه فنی از طراحی نرم‌افزار باشد که شما خیلی راحت آن را جا انداخته‌اید. صرفنظر از دلیل، اگر نرم‌افزار تولید می‌کنید، defect ها یک حقیقت در زندگی شما هستند. خبر خوب این است که اگر TDD را تمرین می‌کنید گردش کار برای مقابله با defect ها نه تنها ساده است، بلکه اگر درست اجرایی شود می‌تواند به اطمینان از اینکه defect های حل شده، حل شده باقی بمانند کمک می‌کند.

برای نشان دادن این موضوع، اجازه بدهید برگردیم و مثال روز سوم و چهارم را با هم مرور کنیم. برای یادآوری صورت مساله، قرار بود من یک متد بنویسیم که تعداد تکرار یک کاراکتر در یک رشته را بشمارد. برای این کار من تست‌های زیر را نوشتم

       
using System;
using System.Linq;
using NUnit.Framework;
 
namespace ThirtyDaysOfTDD.UnitTests
{
    [TestFixture]
    public class StringUtilsTest
    {
        [Test]
        public void ShouldBeAbleToCountNumberOfLettersInSimpleSentence()
        {
            var sentenceToScan = "TDD is awesome!";
            var characterToScanFor = "e";
            var expectedResult = 2;
            var stringUtils = new StringUtils();
 
            int result = stringUtils.FindNumberOfOccurences(sentenceToScan, characterToScanFor);
 
            Assert.AreEqual(expectedResult, result);
        }
 
        [Test]
        public void ShouldBeAbleToCountNumberOfLettersInAComplexSentence()
        {
            var sentenceToScan = "Once is unique, twice is a coincidence, three times is a pattern.";
            var characterToScanFor = "n";
            var expectedResult = 5;
            var stringUtils = new StringUtils();
 
            int result = stringUtils.FindNumberOfOccurences(sentenceToScan, characterToScanFor);
 
            Assert.AreEqual(expectedResult, result);
        }
    }
}

 الگوریتمی که برای pass کردن تست‌های بالا استفاده کردم را در زیر می‌توانید مشاهده کنید

       
using System;
 
namespace ThirtyDaysOfTDD.UnitTests
{
    public class StringUtils
    {
        public int FindNumberOfOccurences(string sentenceToScan, string characterToScanFor)
        {
            var stringToCheckAsCharacterArray = sentenceToScan.ToCharArray();
            var characterToCheckFor = Char.Parse(characterToScanFor);
 
            var numberOfOccurenes = 0;
            
            for (var charIdx = 0; charIdx < stringToCheckAsCharacterArray.GetUpperBound(0); charIdx++)
            {
                if (stringToCheckAsCharacterArray[charIdx] == characterToCheckFor)
                {
                    numberOfOccurenes++;
                }
            }
 
            return numberOfOccurenes;
        }
    }
}
بعد از اینکه مدتی از نوشتن این کدها گذشت، defect زیر گزارش شد:
کاربر قادر است که ۲ کاراکتر را به عنوان پارامتر دوم به تابع FindNumberOfOccurences پاس دهد و بعد از این کار یک exception از نوع FormatException ایجاد می‌شود. رفتار مورد انتظار این است که متد استثنا از نوع ArgumentException برگرداند.
 
وقتی به این موضوع فکر کنید، defect ها در واقع یک نوع دیگر نیازمندی نرم‌افزار هستند. نیازمندی‌های سنتی نرم‌افزار می‌گویند که یک نرم‌افزار چطور باید کار کند در حالی که defect‌ها می‌گویند نرم‌افزار چطور باید کار می‌کرده است. وقتی شما از TDD استفاده می‌کنید و نرم‌افزارتان را بر پایه تست‌ها می‌نویسید که خود آن تست‌ها بر پایه نیازمندی‌های نرم‌افزار تولید شده‌اند، شما باید همه آن نیازمندی‌ها را در کد خود ببینید. اگر این درست باشد، defect ها چیزی به جز یک نیازمندی جدید که شما نمی‌دانستید وجود دارد یا بهبود یک نیازمندی پیاده شده فعلی نیستند. این نیازمندی‌ها (در واقع defect ها) می‌توانند کاربردی باشند (مثل اینکه برنامه در IE 6 کار نمی‌کند) یا مانند مساله بالا، مربوط به حوزه نیازمندی‌های تجاری (business domain requirement) باشند.
 
صرفنظر از نوع نیازمندی‌ جدید، گردش کار مربوط به آن مشابه است. اول باید یک تست بنویسم. در مورد این مثال من تستی می‌نویسم که مشخص کند که رفتار اشاره شده در شرح defect در واقع یک خطا (error) است و بنابراین تست باید fail شود
       
[Test]
        [ExpectedException(typeof(ArgumentException))]
        public void ShouldGetAnArgumentExceptionWhenCharacterToScanForIsLargerThanOneCharacter()
        {
            var sentenceToScan = "This test should throw an exception";
            var characterToScanFor = "xx";
            var stringUtils = new StringUtils();
 
            stringUtils.FindNumberOfOccurences(sentenceToScan, characterToScanFor);
        }

این شبیه تست قبلی به نظر می‌رسد اما چند تفاوت وجود دارد. یکی از آن‌ تفاو‌ت‌ها این است که من نوع مقدار برگشتی از FindNumberOfOccurences را مشخص نکردم و حتی مقدار برگشتی را دریافت یا ذخیره نکردم و چون هیچ‌یک از این دو را ندارم هیچ فراخوانی Assert.AreEqual هم انجام نشده است. این مساله به این خاطر است که من انتظار دارم که تابع FindNumberOfOccurences یک استثنا (exception) از نوع  ArgumentException ایجاد کند.

انتظار دارم که در اولین اجرا، تست fail شود و نتیجه زیر مرا ناامید نمی‌کند:

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

       
using System;
 
namespace ThirtyDaysOfTDD.UnitTests
{
    public class StringUtils
    {
        public int FindNumberOfOccurences(string sentenceToScan, string characterToScanFor)
        {
            throw new ArgumentException();
            
            var stringToCheckAsCharacterArray = sentenceToScan.ToCharArray();
            var characterToCheckFor = Char.Parse(characterToScanFor);
 
            var numberOfOccurenes = 0;
            
            for (var charIdx = 0; charIdx < stringToCheckAsCharacterArray.GetUpperBound(0); charIdx++)
            {
                if (stringToCheckAsCharacterArray[charIdx] == characterToCheckFor)
                {
                    numberOfOccurenes++;
                }
            }
 
            return numberOfOccurenes;
        }
    }
}

همان‌طور که در خط ۹ می‌بینید من خیلی ساده یک وهله (instance) جدید از نوع ArgumentException ایجاد و throw کرده‌ام. اگر تستم را دوباره اجرا کنم خواهم دید که تست pass می‌شود.

تست جدید pass می‌شود. قبل از اینکه بگوییم کارمان تمام شده باید مطمئن شویم که چیزی خراب نشده باشد. این کار را با اجرا کردن همه تست‌ها می‌توانیم انجام دهیم.

در حالی که ما defect را fix کردیم چیز دیگری را دچار مشکل کردیم. این یک مثال خوب درباره اهمیت TDD و Unit Test به صورت کلی است. بدون وجود تست‌ها، هیچ راه ساده‌ای برای فهمیدن اینکه با تغییر تابع چه تاثیری در کد گذاشته شده است وجود نداشت. در این مورد در نوشته‌های بعدی که درباره refactoring هستند بیشتر صحبت می‌کنیم.

حالا باید کاری کنم که تمام تست‌ها pass‌ شوند. این کار با تغییری در تابع FindNumberOfOccurences انجام می‌شود

       
using System;
 
namespace ThirtyDaysOfTDD.UnitTests
{
    public class StringUtils
    {
        public int FindNumberOfOccurences(string sentenceToScan, string characterToScanFor)
        {
            try
            {
                var stringToCheckAsCharacterArray = sentenceToScan.ToCharArray();
                var characterToCheckFor = Char.Parse(characterToScanFor);
 
                var numberOfOccurenes = 0;
 
                for (var charIdx = 0; charIdx < stringToCheckAsCharacterArray.GetUpperBound(0); charIdx++)
                {
                    if (stringToCheckAsCharacterArray[charIdx] == characterToCheckFor)
                    {
                        numberOfOccurenes++;
                    }
                }
 
                return numberOfOccurenes;
            }
            catch
            {
                throw new ArgumentException();
            }
        }
    }
}

در این مثال، کل کد را در یک بلوک try/catch گذاشتم. اگر exception ای رخ بدهد آن را قورت می‌دهم و به جایش یک exception از نوع ArgumentException ایجاد و throw می‌کنم. می‌دانم این راه حل خوبی نیست اما نگران نباشید. فعلاً این کد پاسخگوی نیاز تست من است، در آینده وقتی درباره refactoring صحبت کنیم، برمی‌گردیم و این کد را دوباره با هم مرور خواهیم کرد.

با اجرای مجدد تست‌ها، می‌توانیم ببینیم که نه تنها defect مرتفع شده بلکه کلیه کاربردهای قبلی هم مثل قبل درست کار می‌کنند.

ادامه دارد...

پی‌نوشت: بابت تاخیر در انتشار ادامه نوشته‌های سری TDD عذرخواهی می‌کنم. به زودی شاهد تغییراتی در سایت آرایه خواهید بود.

به اشتراک گذاری این نوشته در شبکه‌های اجتماعی

۳۰ روز با TDD: روز هفتم - Software Factories و DI Frameworks

داستان چیه؟

سپتامبر سال گذشته آقای James Bender در وبلا‌گ‌های تلریک یک مجموعه نوشته منتشر کرد به نام 30 روز با TDD. من می‌خوام یک ترجمه آزاد از این نوشته‌ها براتون داشته باشم تا با هم درباره Test Driven Development بیشتر بدونیم. اگر نمی‌دونید داستان TDD چی هست نگران نباشید، این مجموعه نوشته‌ها برای همین هست که با این روش بیشتر آشنا بشیم ;)

#30RoozTDD 

سایر نوشته‌های مربوط به این سری رو در صفحه "30 روز با TDD" می‌تونید ببینید. در توییتر هم با هشتگ 30RoozTDD# می‌تونید مطالب مرتبط به این سری نوشته‌ها رو پیدا کنید. حرکت 30 روز با TDD برای ارتقاء دانش خودم و همه دوستان و همکاران برنامه‌نویسم هست و اگر کسی مایل هست از این حرکت حمایتی بکنه، می‌تونه با من در تماس باشه.

و اما هفتمین روز: Software Factories‌ و DI Frameworks

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


اگر مشکل را نبینم پس مشکلی نیست. درسته؟

تزریق وابستگی (DI) یک تکنیک برای تولید برنامه‌های با پیوند ضعیف به وابستگی‌ها (loosely bound dependencies) هست. با پاس دادن کلاس‌هایی که برنامه به آن‌ها وابسته است در constructor، به دو چیز دست پیدا می‌کنیم: اول اینکه می‌توانیم کلاس‌هایمان را به abstraction ها وابسته کنیم نه پیاده‌سازی اصلی و واقعی و دوم اینکه می‌توانیم در زمان اجرا تصمیم بگیریم که از کدام پیاده‌سازی استفاده کنیم.

این روش البته یک مشکل جدید هم برای ما ایجاد می‌کند. وقتی یک instance از یک کلاس ایجاد می‌کنیم، به دلیل اینکه در سازنده آن کلاس به کلاس‌های دیگری وابستگی ایجاد کردیم، کدی که کار ساختن instance را انجام می‌دهد باید component هایی که instance ما به آن‌ها وابسته است را ایجاد کند. این یعنی معرفی مجدد binding به روش tightly coupled. در واقع ما وابستگی‌ها را یک مرحله بالاتر برده‌ایم و در واقع به جای حل مشکل، آن را منتقل کردیم!

به علاوه انتقال مشکل قدیم، یک مشکل جدید هم ایجاد کرده‌ایم. ما نه تنها مسئولیت ساخت component های فرزند (child) را به کلاس parent منتقل کردیم، بلکه کلاس‌های parent را در موقعیتی قرار دادیم که باید حتماً بدانند کدام پیاده‌سازی را برای کلاس child باید استفاده کنند. با انجام این کار یک اصل دیگر SOLID (رجوع کنید به نوشته روز پنجم) یعنی اصل Single Responsibility یا SPR را هم نقض کرده‌ایم. کلاس parent فقط باید مسئول قابلیت اصلی خودش باشد و فقط باید زمانی تغییر کند که قابلیت اصلی‌اش تغییر کرده باشد، اما حالا با کاری که در تزریق وابستگی کردیم، بر اساس child ها و وابستگی آن‌ها تغییر می‌کند.


Software Factories

یک راه حل برای مشکلی که گفتم، استفاده از Software Factory است. software factory یک الگو برای abstract کردن اطلاعات و کد مورد نیاز برای ساختن یک instance از کلاس است. چند مدل از الگوهای software factory وجود دارد که هر کدام برای شرایط و نیازمندی‌های خاصی طراحی شده‌اند. برای آشنایی با مهم‌ترین و مشهورترین این الگوها، مطالعه کتاب Design Patterns را توصیه می‌کنم.

با وجود اینکه چندین الگوی مختلف software factory وجود دارد اما همه آن‌ها یک ایده مشابه دارند: یک component داریم که همه وابستگی‌ها و نحوه ایجادشان را یکی می‌کند. نمونه یک کلاس ساده factory به شکل زیر است:

       
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace StaticallyBoundLibrary
{
    public class BusinessServiceFactory
    {
        public static BusinessService GetNewBusinessService()
        {
            var dataStoreProvider = GetNewDataStoreProvider();
            var loggingProvider = GetNewLoggingProvider();
            var webServiceProvider = GetNewWebServiceProvider();
 
            return new BusinessService(dataStoreProvider, loggingProvider, webServiceProvider);
        }
  
        private static IWebServiceProvider GetNewWebServiceProvider()
        {
            return new WebServiceProvider();
        }
  
        private static ILoggingProvider GetNewLoggingProvider()
        {
            return new LoggingProvider();
        }
  
        private static IDataStoreProvider GetNewDataStoreProvider()
        {
            return new DataStoreProvider();
        }
    }
 
    public class BusinessService
    {
        private IDataStoreProvider _dataStoreProvider;
        private ILoggingProvider _loggingProvider;
        private IWebServiceProvider _webServiceProvider;
 
        public BusinessService(IDataStoreProvider dataStoreProvider, 
            ILoggingProvider loggingProvider, IWebServiceProvider webServiceProvider)
        {
            _dataStoreProvider = dataStoreProvider;
            _loggingProvider = loggingProvider;
            _webServiceProvider = webServiceProvider;
        }
    }
 
    public class DataStoreProvider : IDataStoreProvider
    {
    }
  
    public interface IDataStoreProvider
    {
    }
 
    public class LoggingProvider : ILoggingProvider
    {
    }
  
    public interface ILoggingProvider
    {
    }
 
    public class WebServiceProvider : IWebServiceProvider
    {
    }
  
    public interface IWebServiceProvider
    {
    }
}

الگوی software factory موثر و کاراست. همان‌طور که در کد بالا می‌توانید ببینید من کاری کردم که تمام قابلیت‌های مورد نیازم برای ساخت یک instance از کلاس BusinessService یکجا جمع شوند. این یعنی کلاس‌هایی که از BusinessService استفاده می‌کنند، می‌توانند به کار خودشان مشغول بوده و نگران وابستگی‌های BusinessService نباشند.

این یک مثال ساده software factory است که در آن هر interface یک رابطه یک به یک با پیاده‌سازی واقعی دارد. ولی الزامی به رعایت این نوع رابطه یک به یک نیست. چون software factory هم کد است، پس می‌توانم هر منطقی که برای مشخص شدن پیاده‌سازی برگشتی لازم دارم را استفاده کنم. مثلاً ممکن است بخواهم کاربر بتواند بین MS SQL Server و Oracle انتخاب داشته باشد، می‌توانم کاری کنم که factory من یک فایل تنظیمات را بخواند تا مشخص شود از کدام دیتابیس باید استفاده شود. 

با وجود موثر و کارا بودن software factory، آن‌ها لزوماً ایده‌آل نیستند. حتی با اینکه الگوهای خاص factory برای شرایط خاص طراحی شده‌اند، در برنامه‌های کوچک هم ممکن است نیازمند نگهداری و تغییرات در factory ها باشیم. خوشبختانه یک راه حتی بهتر از software factory برای مدیریت وابستگی‌ها در نرم‌افزار وجود دارد.


چرا از Software Factory استفاده کنیم وقتی می‌شود از Framework ها استفاده کرد؟

DI Framewrok ها مشابه software factory هستند اما این مزیت را دارند که بسیاری از وظایفی که باید به صورت دستی انجام دهیم را اتوماتیک می‌کنند. به عنوان مثال، به جای اینکه یک متد کامل برای bind کردن یک implement به یک interface را تعریف کنم، با استفاده از فریمورک‌های تزریق وابستگی، می‌توانم در یک خط کد یک binding rule تعریف کنم.

فریمورک‌های مختلفی برای تزریق وابستگی وجود دارد. من در این سری از نوشته‌ها از Ninject استفاده می‌کنم. در پایان این نوشته لینک چند DI Framework معروف و خوب دیگر را هم معرفی می‌کنم. اگر تا به حال از یک DI Framework استفاده نکردید، پیشنهاد می‌کنم که همه موارد معرفی شده در این نوشته یا سایر framework ها را امتحان کنید و فریمورکی که بیشتر مورد پسندتان است را انتخاب کنید.

با وجود اینکه DI Framework ها اصطلاحات مختلفی برای اجزای خودشان دارند، اما همه آن‌ها شامل ۴ بخش مهم زیر می‌شوند:

  • The Application: برنامه شما یا بخشی از آن که به یک instance از کلاس نیاز دارد.
  • The Kernel یا the Container: اینترفیسی در برنامه شما برای DI Framework که آن را به منظور ایجاد instance ها نیاز دارید.
  • The Provider: این شامل قوانین و راهنمایی‌ها برای ایجاد کلاس‌های مختلف و وابستگی‌های آن‌هاست.
  • The Created Class: این چیزی است که توسط Kernel ایجاد شده و به برنامه شما برگردانده می‌شود. instance ای از کلاس شما با همه وابستگی‌هایش است.
لطفاً توجه داشته باشید که تعاریف بالا بر اساس نام اصطلاحات Ninject است و در فریمورک شما ممکن است نام متفاوتی داشته باشند.
 
برای بازنویسی مثال بالا با Ninject باید ابتدا reference آن را به برنامه‌ام اضافه کنم. برای این کار می‌توان dll مربوط به Ninject را از سایت خودش دانلود و به صورت دستی به برنامه اضافه کرد یا به صورت اتوماتیک و با استفاده از Nuget آن را به برنامه افزود.
در Ninject اولین کاری که باید انجام دهم تعریف یک ماژول است. جایی که binding rule های مربوط به برنامه را در آن تعریف می‌کنم:
       
public class DomainModule : NinjectModule
{
   public override void Load()
   {
      Bind< IDataStoreProvider >().To< DataStoreProvider >();
      Bind< ILoggingProvider >().To< CLoggingProvider >();
      Bind< IWebServiceProvider >().To< CWebServiceProvider >();
    }
}
 برای ایجاد یک ماژول باید کلاسی که از NinjectModule ارث‌بری می‌کند ایجاد کنم. این کلاس در namespace (فضا نام) Ninject.Module قرار دارد. کلاس NinjectModule  یک متد abstract به نام Load‌ دارد که باید آن را override کرد. این جایی است که binding rule های مربوط به کلاس‌ها را در آن تعریف می‌کنیم.
برای تعریف binding rule ها من از متد Bind در Ninject استفاده می‌کنم. در مثال بالا کلاس DataStoreProvider را به اینترفیس IDataStoreProvider در خط ۵ کد bind کرده‌ام. این بدان معنی است که هر وقت از Ninject بخواهیم که instance از IDataStoreProvider به ما بدهد، instance ای از DataStoreProvider را ایجاد خواهد کرد.
binding rule های کد بالا ساده‌ترین نوع binding rule هستند. Ninject قابلیت ایجاد binding های پیچیده‌تر را نیز داراست. در نوشته‌های بعدی چند binding پیچیده را با استفاده از Ninject توضیح خواهم داد اما تا آن موقع نگاهی به این برگه تقلب داشته باشید ;)
 

می‌خواهید یک کلک جادویی ببینید؟
binding rule رو ایجاد کردیم، حالا از Ninject می‌خواهیم که یک instance از کلاسمان به ما بدهد. این کار هم همان‌طور که در کد زیر مشخص است خیلی ساده است:
       
public class BusinessServiceConsumer
{
    private BusinessService _businessService;
 
    public BusinessServiceConsumer()
    {
       var kernel = new StandardKernel(new DomainModule());
 
       _businessService = kernel.Get< BusinessService >();
    }
}
 
در کد بالا یک instance از کلاس StandardKernel در Ninject ایجاد می‌کنم. StandardKernel یک (یا حتی چند) instance از NinjectModule را به عنوان پارامتر سازنده (constructor) می‌گیرد. بعد از این کار  شی kernel در واقع container ماست که با فراخوانی متد Get به ما instance کلاس‌هایی که می‌خواهیم را ارائه می‌دهد. همان‌طور که در کد بالا مشخص است من در خط ۹ با استفاده از فراخوانی متد Get یک instance از کلاس BusinessService را درخواست کرده‌ام. اما اگر از کد قبلی به خاطر داشته‌ باشید، اینجا من به صورت دقیق نگفتم که چطور کلاس BusinessService را ایجاد کن، در واقع Ninject می‌تواند کلاس BusinessService را آنالیز کند و خودش وابستگی‌های این کلاس را از طریق سازنده (constructor) تشخیص داده و آن‌ها را تامین کند.
اما صبر کنید! هنوز تمام نشده. اجازه بدهید یک تغییر جزئی در DataStoreProvider ایجاد کنیم:
       
public class DataStoreProvider : IDataStoreProvider
{
    private ILoggingProvider _loggingProvider;
 
    public DataStoreProvider(ILoggingProvider loggingProvider)
    {
        this._loggingProvider = loggingProvider;
    }
}
من یک وابستگی به ILoggingProvider  در کلاس DataStoreProvider ایجاد کردم. حالا کلاس BusinessService من به کلاسی وابسته است که خودش به یک interface وابستگی دارد. من یک تغییر کلی در برنامه‌ام ایجاد کردم ولی بر خلاف software factory با Ninject نیاز به نوشتن کد بیشتر نیست. من همین حالا هم مشخص کردم که چطور LoggingProvider ایجاد شود بنابراین وقتی Ninject به کلاس DataStoreProvider می‌رسد خودش می‌داند که چطور این وابستگی به IDataStoreProvider را مدیریت کرده و instance لازم را ایجاد کند و من لازم نیست کار دیگری انجام دهم.


سایر DI Framework ها
در متن اشاره داشتم که DI Framework های مختلفی وجود دارد و در این سری نوشته‌ها من از Ninject استفاده می‌کنم، اما همان‌طور که گفتم بهتر است همه آن‌ها را بررسی و فریمورکی که می‌پسندید را انتخاب کنید. در این مسیر نگاهی به Structure Map و Microsoft Unity هم داشته باشید.
 
ادامه دارد ...
به اشتراک گذاری این نوشته در شبکه‌های اجتماعی

۳۰ روز با TDD: روز ششم - تزریق وابستگی (Dependency Injection) چیست؟

 داستان چیه؟

سپتامبر سال گذشته آقای James Bender در وبلا‌گ‌های تلریک یک مجموعه نوشته منتشر کرد به نام 30 روز با TDD. من می‌خوام یک ترجمه آزاد از این نوشته‌ها براتون داشته باشم تا با هم درباره Test Driven Development بیشتر بدونیم. اگر نمی‌دونید داستان TDD چی هست نگران نباشید، این مجموعه نوشته‌ها برای همین هست که با این روش بیشتر آشنا بشیم ;)

#30RoozTDD 

سایر نوشته‌های مربوط به این سری رو در صفحه "30 روز با TDD" می‌تونید ببینید. در توییتر هم با هشتگ 30RoozTDD# می‌تونید مطالب مرتبط به این سری نوشته‌ها رو پیدا کنید. حرکت 30 روز با TDD برای ارتقاء دانش خودم و همه دوستان و همکاران برنامه‌نویسم هست و اگر کسی مایل هست از این حرکت حمایتی بکنه، می‌تونه با من در تماس باشه.

و اما ششمین روز: تزریق وابستگی چیست؟

نوشته به زبان انگلیسی روز ششم را در این آدرس می‌توانید مشاهده کنید. در روز پنجم درباره اصل Dependency Inversion گفتم و اینکه با Dependency Injection یا تزریق وابستگی متفاوت است. امروز می‌خواهیم بیشتر درباره تزریق وابستگی صحبت کنیم.


Tinker Toy و Lego

همان‌طور که قبلاً هم در این سری نوشته‌ها گفتم، اتصال (Coupling) و انقیاد (binding) در نرم‌افزار یک حقیقت است. به عنوان برنامه‌نویس سعی می‌کنیم که اتصالات برنامه‌مان تا حد ممکن ضعیف باشد (به اصطلاح loosely coupled باشد) اما در نهایت برای اینکه اجزای مختلف برنامه‌ای که می‌سازیم قابل استفاده باشد باید آن‌ها را bind کنیم. مثال مشابه پیوندهای ضعیف نرم‌افزار، که در زندگی روزمره از آن استفاده کرده‌ایم بازی‌های لگو و تینکر توی (یک بازی مخصوص بچه‌ها که مشابه لگو با ترکیب اجزای مختلف می‌توان وسایل جدید ساخت) هستند.
ایده مشابه در نرم‌افزار هم مشابه لگو این است که اگر برنامه‌تان را از اجزا (component) مختلف که بر اساس یک اینترفیس استاندارد ساخته شده‌اند بسازید، باید بتوانید نرم‌افزارهای مختلف را با استفاده از ترکیب‌های مختلف این اجزا ایجاد کنید.

به نظر ایده جالبی است، اما چطور این کار را با استفاده از کد نرم‌افزاری انجام دهیم؟ برای پاسخ به این سوال به اصول SOLID‌ که در نوشته قبلی به آن‌ها اشاره کردیم نیاز داریم. گرچه هر ۵ اصل به تولید نرم‌افزاری که loosely coupled باشد کمک می‌کنند اما دو تا از آن‌ها برای به صورت خاص برای این منظور لازم هستند.

اصل Liskov Substitution یا LSP‌ می‌گفت که ما می‌توانیم یک کلاس مشتق شده را جایگزین کلاس پایه (base) کنیم بدون اینکه برنامه دچار مشکل شود. ما می‌توانیم این اصل را به Interface ها به این صورت تعمیم دهیم که کلاس‌هایی که یک اینترفیس مشترک را پیاده‌سازی می‌کنند را می‌توان جایگزین هم کرد. بر این اساس، پرنده و هواپیما و سوپرمن گرچه خیلی با هم متفاوتند اما هر سه می‌توانند پرواز کنند. بنابراین تا زمانی که آن‌ها اینترفیس IFly را پیاده‌سازی کنند و برای من فقط پرواز مهم باشد، می‌توانم از هر کدام از آن کلاس‌ها (پرنده و هواپیما و سوپرمن) استفاده کنم.

اصل Dependency Inversion یا DIP به ما می‌گوید که کد ما باید به abstraction ها وابسته باشد نه یک پیاده‌سازی واقعی. برنامه من ممکن است به یک انبار داده جهت ذخیره‌سازی اطلاعات نیاز داشته باشد، اگر من خودم را به یک دیتابیس رابطه‌ای مثل SQL Server محدود کنم،‌ راه خودم بر دیگر پیاده‌سازی‌ها برای انبار داده مثل file system و web service و object database یا هر چیز دیگری که برای ذخیره‌سازی اطلاعات می‌توان از آن استفاده کرد بسته‌ام. این روش اشتباه طراحی باعث ایجاد محدودیت در نرم‌افزار من از طریق اتکا به یک نوع خاص از انبار داده به جای استفاده از ایده «چیزی که داده ذخیره می‌کند» می‌شود.


اتصال (decouple) اجزا (components) از طریق تزریق وابستگی

با استفاده از LSP و DIP ما می‌توانیم به یک روش برای تولید نرم‌افزار با وابستگی‌ها ضعیف (loosely coupled dependencies) برسیم. این روش Dependency Injection یا تزریق وابستگی نام دارد و بر خلاف نامی که دارد بسیار ساده است. کد نمونه زیر را در نظر بگیرید

       
public class BusinessService
{
	private SqlDataStoreProvider _dataStoreProvider = new SqlDataStoreProvider();
	private DbLoggingProvider _loggingProvider = new DbLoggingProvider();
	private ProdWebServiceProvider _webServiceProvider = new ProdWebServiceProvider();
}

تعریف SqlDataStoreProvider و DbLoggingProvider و ProdWebServiceProvider خالی است ولی اگر بخواهید می‌توانید کد کامل را در این gist‌ ببینید.

کد بالا احتمالاً آشنا به نظر می رسد. بدون شک هر برنامه تجاری در NET. نیازمند انبار داده جهت ذخیره‌سازی اطلاعات است. خیلی از شرکت‌ها می‌خواهند که اتفاقاتی که در برنامه می‌افتد را ثبت کنند و این یعنی نیاز به کدی برای log کردن. با متداول شدن استفاده از سیستم‌های توزیع شده استفاده از وب سرویس هم به یک نیاز طبیعی تبدیل شده است.

برنامه‌های زیادی در NET. به خصوص قدیمی‌ترها از کد مشابه بالا برای ایجاد instance از کلاس‌هایی که به آن‌ها وابسته هستند استفاده می‌کنند. مشکل این روش این است که کلاس BusinessService  به شکل محکم (tight) یا استاتیک به پیاده‌سازی‌های خاصی bind شده‌ است. این کار، نرم‌افزار را شکننده می‌کند و تغییرات به ظاهر ساده در کلاس‌ها در آینده ممکن است عواقب غیرقابل پیش‌بینی داشته باشد. این همچنین امکان تصمیم‌گیری درباره object ای که می‌خواهیم به آن bind کنیم را از بین می‌برد. این یعنی من محدود به هر کدی که در زمان کامپایل استفاده شده است هستم.

کاری که تزریق وابستگی یا DI انجام می‌دهد این است که static binding به وابستگی‌ها را حذف می‌کند. برای این کار کلاس مصرف‌کننده (consumer) پیاده‌سازی‌های مختلفی که object می‌خواهد از آن‌ها استفاده کند را ارائه می‌کند. به کد زیر نگاه کنید:

       
public class BusinessService
{
	private DataStoreProvider _dataStoreProvider;
	private LoggingProvider _loggingProvider;
	private WebServiceProvider _webServiceProvider;

	public BusinessService(DataStoreProvider dataStoreProvider, 
		LoggingProvider loggingProvider, WebServiceProvider webServiceProvider)
	{
		_dataStoreProvider = dataStoreProvider;
		_loggingProvider = loggingProvider;
		_webServiceProvider = webServiceProvider;
	}
}

با تزریق وابستگی‌های کلاس از طریق سازنده (constructor) می‌توانم پیاده‌سازی اصلی که واقعاً می‌خواهم در زمان اجرا (runtime) از آن استفاده کنم را کنترل کنم. پیاده‌سازی اصلی فقط باید بر اساس abstraction ای که کلاس مصرف کننده (کلاس BusinessService) نیاز دارد باشد. 
به عنوان مثال من می‌توانم هر کلاسی که از DataStoreProvider مشتق شده باشد (مثلاً SqlDataStoreProvider) را به کلاس BusinessService  از طریق سازنده (constructor) پاس بدهم و کلاس BusinessService می‌تواند از آن استفاده کند. این روش همچنین کد را انعطاف‌پذیرتر در برابر تغییرات می‌کند. از آنجایی که کلاس فقط به یک abstraction از dependency وابسته است من می‌توانم در کلاس مشتق شده (که به کلاس BusinessService پاس داده می‌شود) تغییر ایجاد کنم و کلاس مصرف کننده یعنی BusinessService همچنان کار کند.

قرار دادن وابستگی‌ها در یک ساختار کلاس ارث‌بری شده یک روش درست و معتبر برای تزریق وابستگی (DI) است. با این حال استفاده از Interface به دلیل اینکه به رابطه سلسله‌مراتبی وابسته نیست (در واقع مثل کلاس‌های ارث‌بری شده، به صورت سلسله‌مراتبی نیستند) می‌تواند یک راه انعطاف‌پذیرتر برای تعریف abstraction مشترک فراهم کند.

اگر کد مربوط به کلاس BusinessService را برای اینکه به Interface متکی باشد refactor کنم، انعطاف‌پذیری بیشتر در binding بدست می‌آورم و خروجی شبیه کد زیر خواهد بود

       
public class BusinessService
{
	private IDataStoreProvider _dataStoreProvider;
	private ILoggingProvider _loggingProvider;
	private IWebServiceProvider _webServiceProvider;

	public BusinessService(IDataStoreProvider dataStoreProvider, 
		ILoggingProvider loggingProvider, IWebServiceProvider webServiceProvider)
	{
		_dataStoreProvider = dataStoreProvider;
		_loggingProvider = loggingProvider;
		_webServiceProvider = webServiceProvider;
	}
}

همان‌طور که می‌بینید تزریق وابستگی یک تکنیک ساده اما قدرتمند است. این تکنیک به شما این امکان را می‌دهد که با ایجاد تنوع در binding وابستگی‌ها در زمان اجرا، برنامه‌های loosely couple ایجاد کنید. در نوشته بعدی درباره software factories و فریمورک‌های DI صحبت خواهیم کرد.

ادامه دارد...

به اشتراک گذاری این نوشته در شبکه‌های اجتماعی

30 روز با TDD: روز پنجم - کد SOLID ایجاد کنید

داستان چیه؟

سپتامبر سال گذشته آقای James Bender در وبلا‌گ‌های تلریک یک مجموعه نوشته منتشر کرد به نام 30 روز با TDD. من می‌خوام یک ترجمه آزاد از این نوشته‌ها براتون داشته باشم تا با هم درباره Test Driven Development بیشتر بدونیم. اگر نمی‌دونید داستان TDD چی هست نگران نباشید، این مجموعه نوشته‌ها برای همین هست که با این روش بیشتر آشنا بشیم ;)

#30RoozTDD

سایر نوشته‌های مربوط به این سری رو در صفحه "30 روز با TDD" می‌تونید ببینید. در توییتر هم با هشتگ 30RoozTDD# می‌تونید مطالب مرتبط به این سری نوشته‌ها رو پیدا کنید. حرکت 30 روز با TDD برای ارتقاء دانش خودم و همه دوستان و همکاران برنامه‌نویسم هست و اگر کسی مایل هست از این حرکت حمایتی بکنه، می‌تونه با من در تماس باشه.

و اما پنجمین روز: کد SOLID ایجاد کنید

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

آیا کد شما SOLID است؟

اصول SOLID در توسعه نرم‌افزار اصولی هستند که توسط رابرت مارتین معرفی شده‌اند. در اوایل 2000 آقای مارتین 5 اصل را تشریح کرد که توسعه‌دهندگان نرم‌افزار می‌توانند از آن‌ها برای ایجاد نرم‌افزارهای با طراحی خوب، کیفیت بالا و سهولت نگهداری استفاده کنند. این 5 اصل ساده هستند، مروج شیوه‌های خوب در طراحی و تولید نرم‌افزارند و در TDD نیز به ما کمک می‌کنند. 
SOLID سرنام این اصول پنج‌گانه است: Single Responsibility Principle , Open/Close Principle, Liskov Substitution Principle, Interface Segregation Principle , Dependency Inversion Principle

در ادامه این نوشته به بررسی مفاهیم این اصول می‌پردازیم:

 

Single Responsibility Principle

ایده Single Responsibility Principle یا به اختصار SRP این است که هر متد یا کلاس در برنامه شما باید تنها یک دلیل برای تغییر داشته باشد. به صورت منطقی، می‌توان این ایده را این‌طور گسترش داد که هر کلاس یا متد در برنامه باید دقیقاً فقط یک وظیفه (یا کار) داشته باشد. به عبارت بهتر، هر کلاس یا متد باید در برابر یک و فقط یک وظیفه مسئول باشد.

به عنوان مثال اجازه بدهید کلاسی را با توابعی برای سبد خرید در یک سایت فروشگاه الکترونیک را فرض کنیم. به عنوان یک سبد خرید مجازی منطقی است که کلاس یک مجموعه (collection) از مواردی که کاربر آن‌ها را جهت خرید به سبد خود اضافه کرده و احتمالاً یک راه برای برقراری ارتباط با سرویسی جهت پرداخت داشته باشد. برای توسعه دادن این مثال فرض کنیم که این فروشگاه الکترونیک یک روال امتیاز وفاداری (loyalty reward) دارد که به مشتریان بر اساس خریدشان امتیاز جایزه می‌دهد.
هیچ‌کدام از قابلیت‌های برای جایزه دادن، رهگیری یا مدیریت امتیازات، مناسب اضافه شدن به کلاس سبد خرید نیستند. این قابلیت مربوط به امتیازات باید در سرویس جدایی ایجاد شود. سبد خرید نباید مسئول امتیازات باشد و در واقع حتی نباید از وجود برنامه امتیازات خبر داشته باشد. سبد خرید فقط یک کار دارد: ذخیره لیست مواردی که کاربر قصد خرید آن‌ها را دارد. بر این اساس کلاس سبد خرید فقط یک دلیل برای تغییر دارد: زمانی که روش ذخیره‌سازی آیتم‌های لیست خرید مشتری تغییر کند. تغییر در برنامه امتیازدهی به مشتریان، نباید هیچ تاثیری در سبد خرید داشته باشد بنابراین وقتی برنامه امتیازدهی تغییر می‌کند نیازی به تغییر کلاس سبد خرید نیست.

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

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


The Open/Close Principle

اصل Open/Close یا به اختصار OCP ارتباط نزدیکی با مباحث Encapsulation و Inheritance که روز دوم در موردشان صحبت کردیم دارد. در حقیقت می‌توان گفت OCP ایده‌ای است که این دو قانون OOP را با هم متحد می‌کند. OCP بیان می‌کند که در نرم‌افزار، خواه متد یا کلاس، باید راه برای توسعه (extension) باز و برای تغییر (modification) بسته باشد. برای اینکه بهتر متوجه دو بخش این عبارت شویم، هر کدام را به صورت جداگانه بررسی می‌کنیم.

وقتی برنامه‌نویسان یک نرم‌افزار می‌نویسند اغلب متکی به کتابخانه‌های نرم‌افزاری نوشته شده توسط سایر برنامه‌نویسان هستند. به منظور اینکه این اجزا (components) به صورت گسترده مورد استفاده قرار می‌گیرند، قابلیت‌های آن‌ها را در کلی‌ترین (general) حالات در نظر می‌گیرند. اغلب اوقات که از قابلیت‌های این component ها به همان صورتی که هست استفاده می‌کنیم، به معنی این است که نیاز ما در دایره حالات کلی تعریف شده در آن کتابخانه قرار دارد. اما بعضی اوقات ممکن است به نسخه خاص‌تری از این component ها نیاز داشته باشیم.
بر اساس OCP این component ها را باید بتوان گسترش داد (راه برای extension باز باشد). راه‌های مختلفی برای این کار وجود دارد: مشخص‌ترین راه ایجاد یک کلاس جدید مشتق شده از کلاس پایه (base) مربوط به component‌ است که یا متدهای موجود آن را override کند یا متدهای جدیدی را بر اساس نیاز به آن اضافه کند.
راه دیگری که نسبت به راه قبلی کمتر واضح است این است که از یک اصل دیگر SOLID به نام Dependency Inversion استفاده کنیم که در ادامه درباره‌اش توضیح خواهم داد.
این دو راه به من کمک می‌کند که قابلیت‌های یک کلاس را توسعه یا تغییر بدهم بدون اینکه داخل آن را دستکاری کنم چرا که "راه برای توسعه باز است"

دومین بخش OCP می‌گوید که راه برای تغییرات بسته است. این بخش با مفاهیم Encapsulation در ارتباط است و بیان می‌کند که با کارهای داخلی component ها باید به صورت خصوصی (private) برخورد شود. در این شرایط OCP‌ می‌گوید که اگر می‌خواهید یک قابلیت اضافی به component اضافه کنید یا روش کار قابلیت موجود را تغییر دهید، گزینه‌های کمی در اختیار دارید و تغییر داخل یک component به نحوی که Public API (یا قانون Encapsulation) را تحت تاثیر قرار دهد یکی از آن گزینه‌ها نیست!

بسته نگه‌داشتن base component ها تضمین می‌کند که دیگر component‌های وابسته به آن‌ها از تغییرات غیرمنتظره مربوط به قابلیت‌های جدید زجر نکشند. همچنین تضمین می‌کند که با آمدن آپدیت برای آن component‌ها شما می‌توانید آن به روزرسانی‌ها را با برنامه خود ترکیب (integrate) کنید.

وقتی در این نوشته و نوشته بعدی شروع به صحبت درباره Dependency Inversion کنیم OCP در TDD‌ بیشتر با معنی می‌شود. اما ذات OCP در mocking‌ (که در نوشته‌های بعدی به آن خواهیم پرداخت) به ما کمک می‌کند که مطمئن شویم راه mocking‌ در کلاس‌های ما با Dependency Inversion باز است.


The Liskov Substitution Principle

اصل Liskov Substitution یا به اختصار LSP بیان می‌کند که یک شی در برنامه شما باید قابلیت جایگزینی با شی‌ از کلاسی که از آن مشتق شده است را بدون ایجاد مشکل در برنامه داشته باشد. به عنوان مثال در بحث قبلی که در خصوص Polymorphism در روز دوم داشتیم درباره ایده super class‌ و Public API‌ در آن صحبت کردیم. برای یادآوری اگر یک کلاس از کلاس پایه (base) ارث بری کرده باشد آن وقت کلاس پایه super class و کلاس ارث بری شده کلاس مشتق (derived) نامیده می‌شود. به عنوان مثال Animal یک super class برای Dog است در حالی که Dog یک کلاس مشتق شده از  Animal است.

بر اساس LSP اگر برنامه من انتظار یک شی از نوع Animal داشته باشد، من باید بتوانم هر کلاسی که از Animal مشتق شده را به جای آن پاس بدهم (مثلاً Dog, Cat, Fish‌ و...) بدون اینکه مشکل و ایرادی در برنامه ایجاد شود. برنامه با این شی مشتق شده به عنوان یک Animal کلی (Generic) برخورد می‌کند (یعنی فقط متدهای Public API‌ مربوط به Animal‌ را می‌توان برای آن شی فراخوانی کرد) و لازم نیست بداند یا اهمیت بدهد که واقعاً چه نوع کلاسی پاس داده شده است.

مثل OCP قدرت اصلی LSP وقتی مشخص می‌شود که درباره Dependency Inversion صحبت کنیم. LSP به همراه OCP و Dependency Inversion امکان mocking را می‌دهد. به صورت خلاصه LSP به قابل آزمایش کردن کدهای ما از طریق ایجاد یک جایگزین برای کلاس‌های وابسته در کد کمک می‌کند که خودش باعث ایزوله شدن تست‌ از برنامه و وابستگی‌هایش می‌شود.


The Interface Segregation Principle

اصل Interface Segregation یا به اختصار ISP بیان می‌کند که مشتری‌ها نباید مجبور به استفاده از interface‌ هایی شوند که استفاده نمی‌کنند. در واقع شما باید interface های خوبی که به طور خاص برای نیاز و قابلیت مورد نظر مشتری هستند ایجاد کنید.

به عنوان مثال شما ممکن است سرویسی برای پذیرش تمام درخواست‌های وام یک بانک داشته باشید. اما مشتری شما ممکن است بین وام‌های امن (مثل وام مسکن و خودرو و ...) و وام‌های ناامن (کارت اعتباری و ...) تفاوت قائل شوند. به علاوه، API سرویس شما ممکن است متدهای مختلفی برای انواع مختلف وام داشته باشد. بر اساس ISP ایجاد یک intreface کلی که تمام این حالات را پوشش دهد راه اشتباهی است، به جای این کار شما باید چندین interface کوچکتر داشته باشید که نیاز تجاری خاصی را هدف گرفته‌اند.

مزیت این اصل، خیلی با اهمیت آن در TDD در ارتباط است. یک interface بزرگ که پر از متدها و property هایی است که مشتری به ندرت از آن‌ها استفاده می‌کند، interface را سنگین و پیچیده می‌کند. در TDD اینترفیس‌های کوچکتر را راحت‌تر می‌توان mock‌ کرد که باعث می‌شود نتایج تست‌ ما کوچکتر و با پیچیدگی کمتر و فهم آن‌ها راحت‌تر باشند.


The Dependency Inversion Principle

اتصال (Coupling) و انقیاد (binding) در نرم‌افزار یک حقیقت است. با همه تلاش‌هایی که می‌کنیم، در آخر کلاس ما برای اینکه قابل استفاده باشد باید بتواند چیزی را bind کند. بر این اساس، ما باید این اتصالات را تا حد ممکن شل کنیم. این جایی است که اصل Dependency Inversion  یا به اختصار DIP وارد می‌شود. 

DIP ایده‌ای است که می‌گوید کد باید به یک چیز انتزاعی (abstractions) وابسته باشد نه یک پیاده‌سازی (implementation) واقعی. به علاوه آن abstratcion ها نباید به جزئیات وابسته باشند و جزئیات نیز نباید به abstraction وابسته باشد.این یک راه پیچیده برای بیان یک ایده ساده است. 

به عنوان مثال شما یک نرم‌افزار منابع انسانی فروخته‌اید، برنامه توسط سرویس‌های دیتابیس‌ مختلفی قابل استفاده است. برنامه یک بخش مدیریت کارمندان دارد که در کنار کارهای دیگر، اطلاعات کارمندان را در دیتابیس سازمان به روز می‌کند. مدیریت کارمندان، احتمالاً بخشی دارد که دسترسی به دیتابیس را کنترل می‌کند. شما نبایست بخش مدیریت کارمندان را طوری بنویسید که به MS SQL Server یا Oracle وابسته باشد، به جای این کار، بایستی مدیریت کارمندان را طوری بنویسید که به یک سرویس داده کلی (generic) وابسته باشد که MS SQL Server یا Oracle را بتوان از آن سرویس generic مشتق کرد. در این حالت موقع نصب می‌توانم هر یک از این دیتابیس‌ها را که از سرویس داده پایه من مشتق شده باشند را استفاده کنم. در مثال نرم‌افزار منابع انسانی، ما وابستگی را invert کرده‌ایم، به جای اینکه به MS SQL Server یا Oracle و جزئیاتشان وابسته باشیم به یک abstraction که هر دو این دیتابیس‌ها به آن وابسته هستند،‌ وابستگی ایجاد می‌کنیم.

لطفاً توجه کنید که Dependency Inversion مشابه Dependency Injection نیست. Dependency Injection یک روش برای رسیدن به Dependency Inversion است، ولی این دو با هم یکی نیستند. Dependency Injection را در نوشته بعدی مورد بررسی قرار خواهیم داد. Dependency Injection یک بخش بسیار مهم در TDD است و در نوشته بعدی خواهید دید که چطور DIP و Dependency Injection به ما در استفاده از mocking در تست‌هایمان کمک می‌کند.

ادامه دارد...

به اشتراک گذاری این نوشته در شبکه‌های اجتماعی