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

داستان ۳۰ روز با TDD

سپتامبر سال ۲۰۱۳ آقای James Bender در وبلاگ‌های تلریک یک مجموعه نوشته منتشر کرد به نام ۳۰ روز با TDD. در ترجمه‌ای آزاد در وبلاگ آرایه، با هم در طول یک ماه با Test Driven Development آشنا می‌شویم. لینک سایر نوشته‌های این سری را در صفحه ۳۰ روز با TDD می‌توانید مشاهده کنید.

و اما هجدهمین روز: بازبینی Refactoring

توجه: قبل از این نوشته، آزمون‌های واحد (Unit testها) مربوط به تغییرات PlaceOrder نوشته قبلی را از اینجا می‌توانید دانلود کنید.

در چند نوشته گذشته، متد PlaceOrder را از OrderService بیرون بردیم. برای مرور، متد فعلی این شکلی است:


public Guid PlaceOrder(Guid customerId, ShoppingCart shoppingCart)
        {
            foreach (var item in shoppingCart.Items)
            {
                if (item.Quantity == 0)
                {
                    throw new InvalidOrderException();
                }
            }

            var customer = _customerService.GetCustomer(customerId);

            //Open Session
            var orderFulfillmentSessionId = _orderFulfillmentService.OpenSession(USERNAME, PASSWORD);
            
            var firstItemId = shoppingCart.Items[0].ItemId;
            var firstItemQuantity = shoppingCart.Items[0].Quantity;

            //Check Inventory Level
            var itemIsInInventory = _orderFulfillmentService.IsInInventory(orderFulfillmentSessionId, firstItemId, firstItemQuantity);

            //Place Order
            var orderForFulfillmentService = new Dictionary();
            orderForFulfillmentService.Add(firstItemId, firstItemQuantity);
            var orderPlaced = _orderFulfillmentService.PlaceOrder(orderFulfillmentSessionId, 
                orderForFulfillmentService, 
                customer.ShippingAddress.ToString());
                       
            //Close Session
            _orderFulfillmentService.CloseSession(orderFulfillmentSessionId);

            var order = new Order();
            return _orderDataService.Save(order);
        }

این متد کمی طولانی شده و همچنین داریم به محدوده نقض Single Responsibility Principel (برای مرور SRP روز پنجم را مطالعه کنید) وارد می‌شویم. در حال حاضر شش دلیل برای اینکه این متد باید تغییر کند شمردم:

۱. اعتبارسنجی هر آیتم سبد خرید
۲. روش باز شدن session ها با سرویس order fulfillment
۳. روش چک کردن انبار با سرویس order fulfillment
۴. روش ثبت سفارش با سرویس order fulfillment
۵. بسته شدن session ها با order fulfillment
۶. ایجاد و ذخیره یک سفارش

در لیست بالا «ایجاد و ذخیره یک سفارش» موردی است که کمترین نگرانی را درباره‌اش دارم. کد زیادی درباره ایجاد و ذخیره سفارش نیست پس فعلاً جای نگرای نیست. این موضوع بعداً در جریان توسعه نرم‌افزار قطعاً تغییر می‌کند اما فعلاً در رادار من نیست. مورد اول هم گرچه جای نگرانی دارد ولی در حال حاضر بدترین مشکل نقض SRP نیست، بنابراین می‌تواند منتظر بماند.

چیزی که الان می‌خواهم رویش تمرکز کنم موارد ۲ تا ۵ هستند. همه این موارد با سرویس order fulfillment در ارتباطند و بخش زیادی از کد را تشکیل می‌دهند.

توسعه‌دهنده‌هایی که درباره ایده SRP یا refactoring تازه‌وارد هستند احتمالاً به لیست بالا نگاه می‌کنند و فکر می‌کنند احتمالاً به ۴ متد جدید برای هر یک از مسائل ۲ تا ۵ نیاز داریم. این اشتباه نیست، اما بخشی از داستان است. اینکه خیلی ساده ۴ متد جدید اضافه کنیم، کد را خواناتر و قابل استفاده مجددتر می‌کند، اما همچنین شش دلیل برای تغییر PlaceOrder خواهیم داشت. SRP‌ و refactoring فقط این نیست که کدها را به متدهای خصوصی (private methods) ببریم تا متدهای اصلی را کوتاه‌تر کنیم، بلکه درباره ایجاد تجرد منطقی (logical abstraction) در کد است تا مرا در برابر تغییرات آینده محافظت کند.

آنچه می‌بینم، حجم زیادی پروسه کار با سرویس order fulfillment است که می‌خواهم از متد PlaceOrder جدایش کنم. وقتی این کار انجام شد، برای کل این پروسه طولانی کار با order fulfillment فقط یک متد باید فراخوانی کنم. این کار مرا در برابر تغییرات مرتبط در هر یک از مراحل کار با سرویس order fulfillment محافظت می‌کند. پس قدم اول استخراج همه کدهایی که با سرویس order fulfillment کار می‌کنند برای ایجاد یک متد خصوصی جداگانه است. چون از Just Code (محصول شرکت تلریک) استفاده می‌کنم این کار ساده است: می‌توانم یک بلوک کد را انتخاب و از منوی کمکی Just Code گزینه Extract Method را مشابه تصویر زیر انتخاب کنم یا از کلید میانبر (CTRL + R, CTRL + M) استفاده کنم و Just Code بخش انتخاب شده را برای یک متد خصوصی جدید استخراج می‌کند.

Just Code کد مورد نیاز را به همراه پارامترهای مورد نیاز استخراج می‌کند و از من درباره نام متد سوال می‌کند و من هم نام PlaceOrdeWithFulfillmentService را انتخاب می‌کنم. همچنین کد اصلی را با فراخوانی این متد تازه ساخته شده تغییر می‌دهد.

public Guid PlaceOrder(Guid customerId, ShoppingCart shoppingCart)
        {
            foreach (var item in shoppingCart.Items)
            {
                if (item.Quantity == 0)
                {
                    throw new InvalidOrderException();
                }
            }

            var customer = _customerService.GetCustomer(customerId);

            PlaceOrderWithFulfillmentService(shoppingCart, customer);

            var order = new Order();
            return _orderDataService.Save(order);
        }
  
        private void PlaceOrderWithFulfillmentService(ShoppingCart shoppingCart, Customer customer)
        {
            //Open Session
            var orderFulfillmentSessionId = _orderFulfillmentService.OpenSession(USERNAME, PASSWORD);
              
            var firstItemId = shoppingCart.Items[0].ItemId;
            var firstItemQuantity = shoppingCart.Items[0].Quantity;

            //Check Inventory Level
            var itemIsInInventory = _orderFulfillmentService.IsInInventory(orderFulfillmentSessionId, firstItemId, firstItemQuantity);

            //Place Orders
            var orderForFulfillmentService = new Dictionary();
            orderForFulfillmentService.Add(firstItemId, firstItemQuantity);
            var orderPlaced = _orderFulfillmentService.PlaceOrder(orderFulfillmentSessionId,
                orderForFulfillmentService,
                customer.ShippingAddress.ToString());
                         
            //Close Session
            _orderFulfillmentService.CloseSession(orderFulfillmentSessionId);
        }

تاثیر این کار بر روی متد PlaceOrder خیلی زیاد است و همین حالا هم بسیار خواناتر شده. متوجه شدم که JustCode متد تازه ساخته شده را با نوع بازگشتی void ایجاد کرده است. این به خاطر آن است که در هیچ کجای کد استخراج شده مقداری تولید نمی‌شود که در ادامه کد اصلی از آن استفاده شده باشد. در این مورد در نوشته بعدی بیشتر صحبت می‌کنیم. برای تایید این کارها و اینکه این تغییرات کد را دچار مشکل نکرده، باید unit test ها را اجرا کنم.

نتیجه اجرای تست‌ها به این معنی است که اولین refactor من کار کرده و نوع بازگشتی متد PlaceOrderWithFulfillmentService فعلاً می‌تواند void باشد. حالا اجازه بدهید به سراغ متد تازه ایجاد شده برویم و refactor کنیم.

متد PlaceOrderWithFulfillmentService باعث شده که کد ما کمتر شکننده باشد، اما خودش هم مشکلاتی دارد. ما به refactor بیشتری نیاز داریم و حالا می‌خواهم هر یک از مراحل را به متدهای خودشان منتقل کنم. به سراغ اولین مرحله می‌روم: OpenOrderFuilfillmentService


private void PlaceOrderWithFulfillmentService(ShoppingCart shoppingCart, Customer customer)
        {
            //Open Session
            var orderFulfillmentSessionId = OpenOrderFulfillmentSession();
              
            var firstItemId = shoppingCart.Items[0].ItemId;
            var firstItemQuantity = shoppingCart.Items[0].Quantity;

            //Check Inventory Level
            var itemIsInInventory = _orderFulfillmentService.IsInInventory(orderFulfillmentSessionId, firstItemId, firstItemQuantity);

            //Place Orders
            var orderForFulfillmentService = new Dictionary();
            orderForFulfillmentService.Add(firstItemId, firstItemQuantity);
            var orderPlaced = _orderFulfillmentService.PlaceOrder(orderFulfillmentSessionId,
                orderForFulfillmentService,
                customer.ShippingAddress.ToString());
                         
            //Close Session
            _orderFulfillmentService.CloseSession(orderFulfillmentSessionId);
        }
  
        private Guid OpenOrderFulfillmentSession()
        {
            var orderFulfillmentSessionId = _orderFulfillmentService.OpenSession(USERNAME, PASSWORD);
            return orderFulfillmentSessionId;
        }

اجرای تست‌ها نشان می‌دهد که این refactor هم موفق بوده است. حالا مرحله بستن session را هم به متد جدیدی به نام CloseOrderFulfillmentService می‌برم

private void PlaceOrderWithFulfillmentService(ShoppingCart shoppingCart, Customer customer)
        {
            //Open Session
            var orderFulfillmentSessionId = OpenOrderFulfillmentSession();
              
            var firstItemId = shoppingCart.Items[0].ItemId;
            var firstItemQuantity = shoppingCart.Items[0].Quantity;

            //Check Inventory Level
            var itemIsInInventory = _orderFulfillmentService.IsInInventory(orderFulfillmentSessionId, firstItemId, firstItemQuantity);

            //Place Orders
            var orderForFulfillmentService = new Dictionary();
            orderForFulfillmentService.Add(firstItemId, firstItemQuantity);
            var orderPlaced = _orderFulfillmentService.PlaceOrder(orderFulfillmentSessionId,
                orderForFulfillmentService,
                customer.ShippingAddress.ToString());
                         
            //Close Session
            CloseOrderFulfillmentService(orderFulfillmentSessionId);
        }
  
        private void CloseOrderFulfillmentService(Guid orderFulfillmentSessionId)
        {
            //Close Session
            _orderFulfillmentService.CloseSession(orderFulfillmentSessionId);
        }

اجرای تست‌ها نشان می‌دهد که همچنان کد بدون مشکل کار می‌کند.

دو مرحله باقیمانده کمی سخت‌تر هستند، بنابراین در نوشته بعدی درباره‌اش صحبت می‌کنیم.

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

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

نظرات شما درباره این نوشته

نظرات پس از بررسی و تایید منتشر خواهند شد.