مطالب
اصول طراحی شیء گرا: OO Design Principles - قسمت پنجم

دانای اطلاعات ( Information Expert )

بر طبق این اصل می­توان برای واگذاری هر مسئولیت، کلاسی را انتخاب کرد که بیشترین اطلاعات را در مورد انجام آن در اختیار دارد و لذا نیاز کمتری به ایجاد ارتباط با دیگر مولفه‌ها خواهد داشت.

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

public class User
    {
        public ShoppingCart ShoppingCart { get; set; }
        public void AddItem(string name) {
            // User class must know how to create OrderItem
            var item = new OrderItem() { Name = name };

            // User class must know how to add item to shopping cart
            ShoppingCart.Items.Add(item);
            
        }
        public void CheckOut() {
            // User class must know logic behind cost and discount calculations:
            // check for discount
            // check shipping method
            // check promotions
            // calculate total cost of items 
        }
    }
    public class OrderItem
    {
        public int Id { get; set; }
        public string Name { get; set; }

    }
    public class ShoppingCart
    {
        public int Id { get; set; }
        public List<OrderItem> Items { get; set; }
       
    }

بنابراین به جای این طراحی، مسئولیت‌ها را به ShoppingCart منتقل میکنیم:

 public class User
    {
        public ShoppingCart ShoppingCart { get; set; }
        
    }
    public class OrderItem
    {
        public int Id { get; set; }
        public string Name { get; set; }

    }
    public class ShoppingCart
    {
        public int Id { get; set; }
        public List<OrderItem> Items { get; set; }
        public void AddItem(string name)
        {
            // ShoppingCart class know how to create OrderItem
            var item = new OrderItem() { Name = name };

            // ShoppingCart class already know how to add item 
            Items.Add(item);

        }
        public void CheckOut()
        {
            // ShoppingCart class know logic behind cost and discount calculations:
            // check for discount
            // check shipping method
            // check promotions
            // calculate total cost of items 
        }
    }


اتصال ضعیف ( Low Coupling )

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

-  وابستگی بین کلاس‌ها کم است.

-  تغییرات در یک کلاس، اثر کمی بر دیگر کلاس‌ها دارد.

-  پتانسیل استفاده‌ی مجدد از مؤلفه‌ها بالا است.

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


چند ریختی ( Polymorphism )

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

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

public class ShapeWithoutPolymorphism
    {
        public double X { get; set; }
        public double Y { get; set; }
        public double Z { get; set; }
        public double Area(string shapeType)
        {
            switch (shapeType)
            {
                case "square":
                    return X * Y;
                case "rectangle":
                    return X * Y;
                case "trapze":
                    return (X + Z) * Y / 2;
                default:
                    return 0;
            }
        }
    }

با استفاده از چندریختی، طراحی به این صورت در خواهد آمد:

  public abstract class Shape
    {
        public double X { get; set; }
        public double Y { get; set; }
        public virtual double Area()
        {
            return X * Y;
        }
    }
    public class Rectangle : Shape
    {
        // No need to override
    }
    public class Square : Shape
    {
        // No need to override
    }
    public class Trapze : Shape
    {
        public double Z { get; set; }
        public override double Area()
        {
            return (X + Z) * Y / 2;

        }
    }


مصنوع خالص ( Pure Fabrication )

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

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

در مثال زیر این مساله مشهود است:

public class User
    {
        public int Id { get; set; }
        public string UserName { get; set; }
        public string Password { get; set; }
    }

    public class LibraryManagement
    {
        public User CurrentUser { get; set; }
        public void AddBookToLibrary(int bookId)
        {
            // check for CurrentUser authority:
            // not user's responsibility nor LibraryManagement
        }
        public void RearrangeBook(int bookId, int shelfId)
        {
            // check for CurrentUser authority
            // not user's responsibility nor LibraryManagement
        }
    }
    public class UserManagement
    {
        public User CurrentUser { get; set; }
        public void AddUser(string name)
        {
            // check for CurrentUser authority:
            // not user's responsibility nor UserManagement
        }
        public void ChangeUserRole(int userId, int roleId)
        {
            // check for CurrentUser authority
            // not user's responsibility nor UserManagement
        }
    }
    public class AuthorizationService
    {
        public bool IsAuthorized(int userId, int roleId)
        {
            // get user roles from data base
            // return true if user has the authority
        }
    }

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


حفاظت از تاثیر تغییرات ( Protected Variations )

این اصل میگوید که کلاس‌ها باید از تغییرات یکدیگر مصون بمانند. در واقع این اصل غایت یک طراحی خوب است. تمام اصولی را که تا به حال بررسی کرده‌ایم، به منظور دستیابی به چنین رفتاری از طراحی بوده‌است. بدین منظور باید از اصول Open/Closed برای واسط­ها، چند ریختی در توارث و ... استفاده کرد تا از تاثیرات زنجیره‌ای تغییرات در امان بمانیم.

بازخوردهای دوره
اصول طراحی یک سیستم افزونه پذیر به کمک StructureMap
«... اگر برنامه را اجرا کردید و پلاگینی بارگذاری نشد، به دنبال اسمبلی‌های تکراری بگردید. برای مثال PluginsBase نباید هم در پوشه‌ی اصلی اجرایی برنامه حضور داشته باشد و هم در پوشه‌ی پلاگین‌ها ... »
اشتراک‌ها
اعتبارسنجی در ASP.Net MVC با استفاده از کتابخانه jQuery

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

اعتبارسنجی در ASP.Net MVC با استفاده از کتابخانه jQuery
نظرات مطالب
توسعه سیستم مدیریت محتوای DNTCms - قسمت ششم
سلام .
ابتدا تشکر میکنم بابت این سری از مقالات ، واقعا مفید هستند ، کیفیت کار واقعا عالی هستش ، تشکر .

برای کپسوله کردن خصوصیت‌های تکراری ، دلیل دو کلاس  Entity , BaseEntity چیست ؟ می‌شود این دو را تبدیل به یک کلاس نمود  ؟
نظرات مطالب
انتقال دائمی آدرس ها (Permanent Ridirect) در ASP.NET Webform
ممنون به خاطر پاسختون . پس دقیقا به این صورت هستش که هر دو آدرس رو اگر درخواست کنیم صفحه مورد نظر نمایش داده میشه؟ ولی مسیر یاب گوگل این رو مسیر تکراری به حساب نمیاره؟ خطا رو نمایش نمیده پس؟
نظرات مطالب
ساخت DropDownList های مرتبط به کمک jQuery Ajax در MVC
با سلام
من از کد زیر استفاده کردم تا موارد تکراری حذف بشه اما dropdown بدون value هست در  هر آیتمش.
ViewBag.city = new SelectList(_db.regions.Select(r => r.region_main).Distinct(), "region_main");
لطفا راهنمایی بفرمایید
با تشکر
نظرات مطالب
ASP.NET MVC #22

سلام

ResourceType یکبار می‌بایست برای Display  ست شود و یکبار هم برای  ErrorMessageResourceType

که هر دوی این‌ها معمولا یکی است . همچنین در کلاس هر پراپرتی (Poco) ممکن است یک ErrorMessageResourceType داشته باشد که برای کل کلاس تکراری است.

میشه ResourceType  را برای کل یک کلاس یا یک NameSpace مشخص کرد؟

نظرات مطالب
EF Code First #6
واقعاً ممنون آقای نصیری.
حالا اگر بخوایم Unique Constraint رو همزمان روی دو فیلد اعمال کنیم به چه صورت خواهد بود؟ یعنی من توی جدول دیتابیسم CustomerId و ProductId دارم که میخوام هیچ دو رکوردی نباشه که این مقادیرش تکراری باشه.
بی نهایت ممنون.
نظرات مطالب
BloggerToCHM
سلام باز هم یک پیشنهاد جدید، البته باز از نظر من:
میدونید ممکنه بلاگ هایی رو بخواهید chm کنید ولی همه پست ها رو نخواهید که دانلود بشه پس اینجا یک گزینه برای دانلود پستهایی که دارای تگ خاصی که معمولا در بلاگها تکراری است رو بشه به هنگام دانلود پست ها اعمال کرد.
مطالب
تغییرات Logging در ASP.NET Core 6x
فرض کنید با استفاده از روش متداول زیر، کار ثبت یک واقعه را انجام داده‌اید:
public class TestController
{
    private readonly ILogger<TestController> _logger;
    public TestController(ILogger<TestController> logger)
    {
        _logger = logger;
    }

   [HttpGet("/")]
    public string Get()
    {
        _logger.LogInformation("hello world");
          return "Hello world!";
    }
}
در یک برنامه‌ی متداول ASP.NET Core، زیرساخت کار با ILogger از پیش تنظیم شده‌است. برای کار با آن فقط کافی است به نمونه‌های ILogger و یا <ILogger<T از طریق سیستم تزریق وابستگی‌ها دسترسی یافت و سپس متدهای الحاقی آن‌را مانند LogInformation فراخوانی کرد.

اگر یک چنین برنامه‌ای را به دات نت 6 ارتقاء دهید، با پیام اخطار زیر مواجه خواهید شد:
CA1848: For improved performance, use the LoggerMessage delegates instead of calling LogInformation
به صورت خلاصه، تمام متدهای پیشین LogInformation، LogDebug و امثال آن در دات نت 6 منسوخ شده درنظر گرفته می‌شوند! دلیل آن‌را در ادامه بررسی خواهیم کرد.


استفاده‌ی گسترده از source generators در دات نت 6

source generators، امکان مداخله در عملیات کامپایل برنامه را میسر کرده و امکان تولید کدهای پویایی را در زمان کامپایل، فراهم می‌کنند. هرچند این قابلیت به همراه دات نت 5 ارائه شدند، اما تا زمان دات نت 6 استفاده‌ی گسترده‌ای از آن در خود دات نت صورت نگرفت. موارد زیر، تغییراتی است که بر اساس source generators در دات نت 6 رخ داده‌اند:
- source generators مخصوص ILogger (موضوع این بحث؛ یعنی LoggerMessage source generator)
- source generators مخصوص System.Text.Json تا سربار تبدیل به JSON و یا برعکس کمتر شود.
- بازنویسی مجدد پروسه‌ی کامپایل Blazor/Razor بر اساس source generators، بجای روش دو مرحله‌ای قبلی که امکان Hot Reload را فراهم کرده‌است.

نوشتن یک source generator هرچند ساده نیست، اما چون نیاز به reflection را به حداقل می‌رساند، می‌تواند تغییرات کارآیی بسیار مثبتی را به همراه داشته باشد.


توصیه به استفاده از LoggerMessage.Define در دات نت 6

ILogger به همراه قابلیت‌هایی مانند structural logging نیز هست که امکان فرمت بهتر پیام‌های ثبت شده را میسر می‌کند تا توسط برنامه‌های جانبی که قرار است این لاگ‌ها را پردازش کنند، به سادگی قابل خواندن باشند. برای مثال رکورد زیر را در نظر بگیرید:
public record Person (int Id, string Name);
به همراه نمونه‌ای از آن:
var person = new Person(123, "Test");
خروجی لاگ زیر در این حالت:
_logger.LogInformation("hello to {Person}", person);
به صورت زیر خواهد بود:
info: TestController[0]
hello world to Person { Id = 123, Name = Test }
دقت کنید که رشته‌ی ارسالی به LogInformation به همراه $ نیست. یعنی از string interpolation استفاده نشده‌است و نام پارامتر تعریف شده (placeholder name) با حروف بزرگ شروع شده‌است.

اگر در اینجا مانند مثال زیر از string interpolation استفاده شود:
_logger.LogInformation($"hello world to {person}"); // Using interpolation instead of structured logging
هرچند کار با آن ساده‌تر است از string.Format، اما برای عملیات ثبت وقایع با کارآیی بالا توصیه نمی‌شود؛ به این دلایل:
- ویژگی «لاگ‌های ساختار یافته» را از دست می‌دهیم و دیگر توسط نرم افزارهای ثالث لاگ خوان، به سادگی پردازش نخواهند شد.
- ویژگی «قالب ثابت» پیام را نیز از دست خواهیم داد که باز هم یافتن پیام‌های مشابه را در بین انبوهی از لاگ‌های رسیده مشکل می‌کند.
-  کار serialization شیء ارسالی به آن، پیش از عملیات ثبت وقایع رخ می‌دهد. اما ممکن است سطح لاگ سیستم در این حد نباشد و اصلا این پیام لاگ نشود. در این حالت یک کار اضافی صورت گرفته و بر روی کارآیی برنامه تاثیر منفی خواهد گذاشت.

برای جلوگیری از serialization و همچنین تخصیص حافظه‌ی اضافی و مشکلات عدم ساختار یافته بودن لاگ‌ها، توصیه شده‌است که ابتدا سطح لاگ مدنظر بررسی شود و همچنین از string interpolation استفاده نشود:
if (_logger.IsEnabled(LogLevel.Information))
{
   _logger.LogInformation("hello world to {Person}", person);
}
البته مشکل این روش، تکرار این if/else‌ها در تمام برنامه‌است و همچنین باید دقت داشت که LogLevel انتخابی، با متد لاگ، هماهنگی دارد.
مشکل دیگر لاگ‌های ساختار یافته، امکان فراموش کردن یکی از پارامترها است که با یک خطای زمان اجرا گوشزد خواهد شد؛ مانند مثال زیر:
_logger.LogInformation("hello world to {Person} because {Reason}", person);
اکنون در دات نت 6 با پیام اخطار CA1848 که در ابتدای بحث مشاهده کردید، توصیه می‌کنند که اگر قالب نهایی خاصی را مدنظر دارید، آن‌را توسط متد LoggerMessage.Define دقیقا مشخص کنید:
private static readonly Action<ILogger, Person, Exception?> _logHelloWorld =
    LoggerMessage.Define<Person>(
        logLevel: LogLevel.Information,
        eventId: 0,
        formatString: "hello world to {Person}");
در این روش جدید باید یک Action را برای لاگ کردن پیام‌ها تهیه کرد که از همان ابتدا LogLevel آن مشخص است (و نیازی به بررسی مجزا ندارد؛ یعنی خودش logger.IsEnabled را فراخوانی می‌کند) و همچنین از روش لاگ ساختار یافته استفاده می‌کند. مزیت این روش کش شدن قالب لاگ، در بار اول فراخوانی آن است ( برخلاف متدهای الحاقی مانند LogInformation که هربار باید این قالب‌ها را پردازش کنند) و همچنین در اینجا دیگر خبری از boxing و تبدیل نوع پارامترها نیست.

اکنون روش فراخوانی این Action با کارآیی بالا به صورت زیر است:
[HttpGet("/")]
public string Get()
{
    var person = new Person(123, "Test");
    _logHelloWorld(_logger, person, null);
      return "Hello world!";
}
همانطور که مشاهده می‌کنید اینبار دیگر حتی امکان فراموش کردن پارامتری وجود ندارد (مشکلی که می‌تواند با LogInformation متداول رخ دهد).


معرفی [LoggerMessage] source generator در دات نت 6

هرچند LoggerMessage.Define، مزایای قابل توجهی مانند کش شدن قالب لاگ، عدم نیاز به بررسی ضرورت لاگ شدن پیام و ارسال تعداد پارامترهای صحیح را به همراه دارد، اما ... کار کردن با آن مشکل است و برای کار با آن باید کدهای زیادی را نوشت. به همین جهت با استفاده از قابلیت source generators، امکان تولید خودکار این نوع کدها در زمان کامپایل برنامه پیش‌بینی شده‌است:
public partial class TestController
{
   [LoggerMessage(0, LogLevel.Information, "hello world to {Person}")]
   partial void LogHelloWorld(Person person);
}
این قطعه کد، LoggerMessage.Define را به صورت خودکار برای ما تولید می‌کند. برای اینکار باید یک متد partial را تهیه کرد و سپس آن‌را به ویژگی جدید LoggerMessage مزین کرد. پس از آن source generator، مابقی کارها را در زمان کامپایل برنامه انجام می‌دهد.
ویژگی partial method، امکان تعریف یک متد را در یک فایل و سپس ارائه‌ی پیاده سازی آن‌را در فایلی دیگر میسر می‌کند که البته در اینجا آن فایل دیگر، توسط source generator تولید می‌شود.
باید دقت داشت که در اینجا TestController را نیز باید به صورت partial تعریف کرد تا آن نیز قابلیت بسط در چند فایل را پیدا کند. همچنین متد فوق را به صورت static partial void نیز می‌توان نوشت.

یکی از مزایای کار با source generator که خودش در اصل یک آنالایزر هم هست، بررسی تعداد پارامترهای ارسالی و تعریف شده‌است:
[LoggerMessage(0, LogLevel.Information, "hello world to {Person} with a {Reason}")]
partial void LogHelloWorld(Person person);
برای مثال در اینجا متد LogHelloWorld یک پارامتر دارد اما LoggerMessage آن به همراه دو پارامتر تعریف شده‌است که این مشکل در زمان کامپایل تشخیص داده شده و گوشزد می‌شود (برخلاف روش‌های پیشین که در زمان اجرا این نوع مشکلات نمایان می‌شدند).

در این روش، امکان ذکر پارامتر اختیاری LogLevel هم وجود دارد؛ اگر نیاز است مقدار آن به صورت پویا تغییر کند:
[LoggerMessage(Message = "hello world to {Person}")]
partial void LogHelloWorld(LogLevel logLevel, Person person);