مطالب
الگوی استراتژی - Strategy Pattern
الگوی استراتژی (Strategy)  اجازه می­دهد که یک الگوریتم در یک کلاس بسته بندی شود و در زمان اجرا برای تغییر رفتار یک شیئ تعویض شود.
برای مثال فرض کنید که ما در حال طراحی یک برنامه مسیریابی برای یک شبکه هستیم. همانطوریکه می‌دانیم برای مسیر یابی الگوریتم‌های مختلفی وجود دارد که هر کدام دارای مزایا و معایبی هستند. و با توجه به وضعیت موجود شبکه یا عملی که قرار است انجام پذیرد باید الگوریتمی را که دارای بالاترین کارائی است انتخاب کنیم. همچنین این برنامه باید امکانی را به کاربر بدهد که کارائی الگوریتم‌های مختلف را در یک شبکه فرضی بررسی کنید. حالا طراحی پیشنهادی شما برای این مسئله چست؟

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

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

الگوی استراتژی گزینه مناسبی برای مسائلی است که می‌توانند از چندین الگوریتم مختلف به مقصود خود برسند.

نمودار UML الگوی استراتژی به صورت زیر است : 


اجازه بدهید، شیوه کار این الگو را با مثال مربوط به مرتب سازی بررسی کنیم. فرض کنید که ما تصمیم گرفتیم که از سه الگویتم زیر برای مرتب سازی استفاده کنیم.

1 - الگوریتم مرتب سازی Shell Sort
2 - الگوریتم مرتب سازی Quick Sort
3 - الگوریتم مرتب سازی Merge Sort

ما برای مرتب سازی در این برنامه دارای سه استراتژی هستیم. که هر کدام را به عنوان یک کلاس جداگانه در نظر می‌گیریم (همان کلاس‌های ConcreteStrategy ). برای اینکه کلاس Client بتواند به سادگی یک از استراتژی‌ها را انتخاب کنید بهتر است که تمام کلاس‌های استراتزی دارای اینترفیس مشترک باشند. برای این کار می‌توانیم یک کلاس abstract تعریف کنیم و ویژگیهای مشترک کلاس‌های استراتژی را در آن قرار دهیم و کلاس‌های استراتژی آنها را به ارث ببرند(همان کلاس Strategy ) و پیاده سازی کنند.


در زیل کلاس Abstract که کل کلاس‌های استراتژی از آن ارث می‌برند را مشاهده می‌کنید :

abstract class SortStrategy
{
        public abstract void Sort(ArrayList list);
}
کلاس مربوط به QuickSort
class QuickSort : SortStrategy
{
        public override void Sort(ArrayList list)
        {
          // الگوریتم مربوطه   
        }
}

کلاس مربوط به ShellSort

class ShellSort : SortStrategy
{
        public override void Sort(ArrayList list)
        {
          // الگوریتم مربوطه
        }
}
کلاس مربوط به MergeSort
 class MergeSort : SortStrategy
{
        public override void Sort(ArrayList list)
        {
          // الگوریتم مربوطه
        }
}
و در آخر کلاس Context که یکی از استراتژی‌ها را برای مرتب کردن به کار می‌برد :
 class SortedList
{
        private ArrayList list = new ArrayList();
        private SortStrategy sortstrategy;

        public void SetSortStrategy(SortStrategy sortstrategy)
        {
            this.sortstrategy = sortstrategy;
        }
        public void Add(string name)
        {
            list.Add(name);
        }
        public void Sort()
        {
            sortstrategy.Sort(list);
        }
}
مطالب
مقدمه ای بر CQRS و Event Sourcing
به صورت عام، functionality اکثر پروژه‌های نرم افزاری تجاری خلاصه میشود به مخفف معروفی به نام CRUD، که object‌ها را میسازیم، آن‌ها را میخوانیم و تغییر میدهیم.
اپلیکیشن‌های طراحی شده بدین صورت، قابلیت خوانایی بالایی خواهند داشت و دیاگرام طراحی آنها چیزی شبیه به تصویر زیر میباشد

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

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

ذات CQRS بر آن است که شما مدل‌های مختلفی را برای خواندن و نوشتن دیتا داشته باشید. الگوی آن چیزی شبیه به تصویر زیر است

چیزی که در این روش مشهود است این میباشد که برنامه نویسان باید قسمت‌های Command و Query را به صورت جداگانه طراحی نمایند.
CQRS این قابلیت را به شما میدهد که interface و Datastore و حتی بطور کامل Technology مجزایی در قسمت‌های CQ داشته باشید.

Event Sourcing
قسمت دوم و متمایز، معماری Event Sourcing یا ES میباشد که بصورت کوتاه، ES یک روش متفاوت برای Data storage میباشد.
اکثرا ما از Datastore‌هایی که مدلی از دیتا را انعکاس می‌دهند استفاده میکنیم. به مثال ساده‌ی زیر توجه کنید

ES از برنامه نویسان میخواهد که مدل سنتی CRUD را فراموش کرده و بجای آن تغییراتی را که روی دیتا صورت گرفته، نیز درج نمایند. اینکار به وسیله‌ی یک دیتابیس Append-only انجام میشود که به نام Event Store شناخته میشود.

در این معماری ما همه‌ی تغییرات روی دیتا را به صورت Serialize Event ذخیره میکنیم که میتواند دوباره در هر زمانی اجرا شده و current state هر objectی را در اختیار بگذارد.

این روش به ما کمک بزرگی میکند تا وضعیت یک object را در گذشته به راحتی پیدا کنیم و از آن میتوان به غیر از فوایدی که دارد، به عنوان یک Logger نیز استفاده نمود. به دلیل اینکه جزء به جزء تغییرات بر روی state سیستم، در آن ثبت شده است. از آنجاییکه دیتا بصورت serialize ذخیره میشود، بارگزاری آن نیز با سرعت بالایی انجام خواهد شد.

پس بصورت خلاصه در معماری Command Query Responsibility Segregation ابتدا باید به این موضوع توجه داشت که قسمت‌های Read و Write نرم افزار به صورت مجزایی طراحی میشوند و Event Sourcing شامل تغییراتی که روی data انجام شده است، میباشد و به‌صورت Serialize شده ذخیره میشود. ما تنها به یک دیتابیس و یک جدول برای نمایش event store نیازمندیم (بستگی به نیازتان میتوان تعداد آن را نیز بیشتر نمود و همچنین حتما لزومی ندارد که از دیتابیس‌های رابطه‌ای استفاده شود؛ بصورت مثال پیاده سازی این قسمت را میتوان با استفاده از Redis که دیتابیسی غیر رابطه‌ای و باسرعت میباشد استفاده نمود).
برای شروع کار (نه پیاده سازی کامل) باید با قسمت‌های مختلف طراحی در این معماری آشنا شویم:
Domain Object
نکته: SimpleCqrs فریم ورکی برای پیاده سازی معماری CQRS , ES میباشد که برای ساده‌تر شدن کار، از آن استفاده شده است (شما حتی میتوانید پیاده سازی خود را داشته باشید)
مدل Movie از کلاسی به نام AggregateRoot ارث بری کرده‌است که توسط SimpleCQRS پیاده سازی شده‌است و یک guid key در آن تعبیه شده است (Aggregate root از مباحث Domain Driven برگرفته شده است و آشنایی با آن کمک شایانی به درک عمیق‌تر روی این مباحث مینماید).
public class Movie : AggregateRoot  
{
    public string Title { get; set; }
    public DateTime ReleaseDate { get; set; }
    public int RunningTimeMinutes { get; set; }

    public Movie() { }

    public Movie(Guid movieId, string title, DateTime releaseDate, int runningTimeMinutes)
    {
        //پیاده سازی خواهد شد
    }
}
توجه: SimpleCQRS فقط پیاده سازی guid برای کلید مربوط به هر مدل را پیاده سازی نموده است؛ بنابراین کلید مدل نمیتواند integer باشد.

Commands
command دستوراتی است که توسط end user فراخوانی میشود که باعث تغییرات خواهد شد. وقتی اپلیکیشن یک command را دریافت مینماید، command handler به پردازش آن برای فهمیدن خواسته کاربر میپردازد و پس از آن event مربوطه را برای اجرای آن وظیفه‌ی خاص صدا میزند.
همه‌ی command‌ها تغییراتی بر روی state جاری خواهند داشت. در نتیجه دیتا‌های ذخیره شده درون دیتابیس تغییرات خواهند کرد. هر commandی که تغییری بر روی State سیستم نداشته باشد، یک دستور غلط محسوب شده و باید در سمت query‌ها آن را پیاده سازی نمود.
در نتیجه Commnad‌ها دستوراتی هستند که از طرف کاربر برای تغییرات بر روی دیتا‌های ذخیره شده، ارسال میشوند.
فرض کنید Domain Objectی برای Movie تعریف کرده‌ایم و میخواهیم دستور اضافه کردن فیلم را پیاده سازی نماییم
public class CreateMovieCommand : ICommand  
{
    public string Title { get; set; }
    public DateTime ReleaseDate { get; set; }
    public int RunningTimeMinutes { get; set; }
    public CreateMovieCommand(string title, DateTime releaseDate, int runningTime)
    {
        Title = title;
        ReleaseDate = releaseDate;
        RunningTimeMinutes = runningTime;
    }
}
توجه: ICommand از طریق SimpleCQRS اضافه شده‌است.

Command Handler
بعد از اینکه Command مورد نیاز نوشته شد، حال احتیاج به پیاده سازی CommandHandler مربوطه که دستور متناظر را پردازش میکند، داریم.
public class CreateMovieCommandHandler : CommandHandler<CreateMovieCommand>  
{
    protected IDomainRepository _repository;

    public CreateMovieCommandHandler(IDomainRepository repository)
    {
        _repository = repository;
    }

    public override void Handle(CreateMovieCommand command)  
    {
        var movie = new Domain.Movie(Guid.NewGuid(), command.Title, 
    command.ReleaseDate, command.RunningTimeMinutes);

        _repository.Save(movie);
    }
}
Command Handler باید از کلاس جنریک <CommandHandler<T ارث بری نماید و T باید از نوع Command در نظر گرفته شود و همچنین IDomainRepository اینترفیسی است که توسط SimpleCQRS تعریف شده‌است و ما احتیاجی به پیاده سازی آن نداریم (در قسمت‌های بعدی پیکربندی آن را انجام میدهیم).
برای رسیدگی کردن به دستور مربوطه احتیاج به override کردن متد Handle میباشد.
کار اساسی توسط متد Save انجام میشود که همه‌ی event‌های pending شده توسط Domain Object را گرفته و آنها را به Event Store میفرستد.

Events
event‌ها تغییراتی هستند بر روی State جاری سیستم که توسط کاربر به وسیله‌ی Commandها فراخوانی میشوند.
رویداد‌ها serialize میشوند و درون Event Store ذخیره میشوند؛ بنابراین میتوان فراخوانی آنها را در هر لحظه انجام داد.
هر تعداد Event میتواند توسط یک دستور raise شود.
ساخت یک Event:
قبلا دستوری را برای ساخت یک movie نوشتیم و حال احتیاج به event مربوطه را داریم:
public class MovieCreatedEvent : DomainEvent  
{
    public Guid MovieId
    {
        get { return AggregateRootId; }
        set { AggregateRootId = value;}
    }

    public string Title { get; set; }
    public DateTime ReleaseDate { get; set; }
    public int RunningTimeMinutes { get; set; }

    public MovieCreatedEvent(Guid movieId, string title, DateTime releaseDate, int runningTime)
    {
        MovieId = movieId;
        Title = title;
        ReleaseDate = releaseDate;
        RunningTimeMinutes = runningTime;
    }
}
فراموش نکنید که این کلاس آبجکتی خواهد بود که Serialize شده و در دیتابیس ذخیره خواهد شد. باید همه‌ی پراپرتی‌های لازم که با استفاده از این Event ممکن است تغییر کنند را شامل شود (بدیهی است که این پراپرتی‌ها از Domain Object گرفته میشود).
public class Movie : AggregateRoot  
{
    public string Title { get; set; }
    public DateTime ReleaseDate { get; set; }
    public int RunningTimeMinutes { get; set; }

    public Movie(Guid movieId, string title, DateTime releaseDate, int runningTimeMinutes)
    {
        Apply(new MovieCreatedEvent(Guid.NewGuid(), title, releaseDate, runningTimeMinutes));
    }
}
به Aggregate فوق که در اوایل بحث صحبت شده‌است دقت کنید. حال متد Apply باعث میشود که event مربوطه درون بخش لوکال aggregate root ذخیره شود. بنابراین بعدا میتواند به صورت فیزیکی درون Event Store ذخیره شود.

Event Handler
هر Event Handler  میتواند تعداد زیادی از IHandleDomainEvents ‌ها را پیاده سازی نماید. حال متد Handle این اینترفیس را پیاده سازی نمودیم. 
public class MovieEventHandler : IHandleDomainEvents<MovieCreatedEvent>  
{
    public void Handle(MovieCreatedEvent createdEvent)
    {
        using (MoviesContext entities = new MoviesContext())
        {
            entities.Movies.Add(new Movie()
            {
                Id = createdEvent.AggregateRootId,
                Title = createdEvent.Title,
                ReleaseDate = createdEvent.ReleaseDate,
                RunningTimeMinutes = createdEvent.RunningTimeMinutes
            });

            entities.SaveChanges();
        }
    }
}
مثلا در این قسمت با استفاده از ORM، شیء مورد نظر به صورت فیزیکی درون دیتابیس ذخیره میشود.
در قسمت آخر نیازمندیم که تغییرات زیر را به Movie اضافه نماییم.
درون Doamin Objectی که قبلا تعریف کرده بودیم متدی را به صورت زیر پیاده سازی مینماییم
protected void OnMovieCreated(MovieCreatedEvent domainEvent)
        {
            Id = domainEvent.AggregateRootId;
            Title = domainEvent.Title;
            ReleaseDate = domainEvent.ReleaseDate;
            RunningTimeMinutes = domainEvent.RunningTimeMinutes;
        }
باعث میشود پس از فراخوانی شدن Event، تغییرات صورت گرفته‌ی بر state سیستم، بر روی Domain Object اعمال شود و آن را بروزرسانی نماید. این متد دقیقا بصورت اتوماتیک وقتی که event مربوطه raise میشود، فراخوانی میشود.
پس از ترکیب CQRS و ES معماری اولیه‌ی سیستم چیزی شبیه به دیاگرام زیر خواهد بود (بسته به سناریوهای خاص میتواند سفارشی سازی شود)

خلاصه:
کاربر دستوری را از طریق برنامه به سیستم ارسال مینماید.
command مربوطه دریافت میشود و به روی Command Bus قرار داده میشود.
Command Handler وظیفه‌ی تفسیر کردن Command مربوطه را به عهده میگیرد و به وسیله‌ی Domain object آن event مورد نظر فراخوانی خواهد شد و باعث میشود domain object بروزرسانی گردد.
Event همان objectی است که باید به صورت serialize شده درون append only database ذخیره شود.
Event handler رویداد مربوطه را گرفته و بصورت فیزیکی مقادیر مورد نظر را در دیتابیس ذخیره مینماید.

Query
از آنجاییکه قسمت Read، در سیستم به صورت CQRS طراحی میشود، به راحتی میتوان query‌ها را optimize کرده و به صورت مثال به جای استفاده از ORM‌های معمول بطور مستقیم Stored Procedure فراخوانی کرده، تا جای ممکن کیفیت query‌ها بهترین حالت ممکن باشند. در حالیکه در مدل CRUD بهینه کردن بخش read بسیار پیچیده و بعضا غیر ممکن میباشد.

مزایای استفاده از این مدل
Distributed Systems Capabilities
یکی از مهمترین مزیت‌های این مدل تسهیم گسترش پذیری سیستم بر روی ماشین‌های فیزیکی مختلف از طریق messaging pattern میباشد.
High Availability
از آنجایی که سیستم توزیع پذیر طراحی شده‌است، هر قسمت از آن میتواند بدون توجه به fail شدن قسمت‌های دیگر به کار خود ادامه دهد.
Reduce Complexity
در domain‌های پیچیده طراحی و پیاده سازی objectهایی که مسئول دو قسمت read و write هستند، میتواند کار را بیش از حد پیچیده کرده و در این صورت چون business logic و read logic در هم ترکیب میشوند، مدیریت کردن موارد multiple user, shared data, performance, transactions, consistency سخت و سخت‌تر میشود.
Facilitates Building Task-based UI
وقتی شما به پیاده سازی الگوی CQRS میپردازید، اصولا هر عملی که توسط End user از طریق ui ارسال میشود، معادل command مربوط به آن وجود دارد. به همین جهت میتوان عملیات لازم برای اجرای یک پروسه را بصورت واضحی درک کرد.
 Maintenance And Flexibility
هر چند پیاده سازی این مدل سخت خواهد بود، اما در ابعاد وسیع‌تر به دلیل اینکه هر قسمت به صورت مجزایی طراحی شده و اینکه دستورات و رویداد‌ها به صورت تفکیک شده پیاده سازی شده‌اند، همچنین وجود ES، قابلیت زیادی به debug سیستم می‌دهد.
نکته: ES مدل مورد قبولی برای اکثر معماری‌های نوین سیستم‌های نرم افزاری امروزی میباشد و فقط مختص به CQRS نمیباشد. بطور مثال در معماری Microservices به وفور از Event Sourcing استفاده میشود.

مشکلات استفاده از این مدل
  • ذاتا پیاده سازی این مدل سخت و دشوار است و از آنجاییکه سادگی در پیاده سازی سیستم‌های نرم افزاری، یک اصل مهم محسوب میشود، بنابراین استفاده از این مدل محدود میشود به سیستم‌های نرم افزاری که مزیت‌های گفته شده در قسمت فوق برایشان حیاتی محسوب شود.
  • برای پیاده سازی سیستمی با این مدل احتیاج به تیم توسعه‌ای است که با مفاهیم آن کاملا آشنا باشد.
  • هر چند امروزه فضای فیزیکی برای ذخیره سازی دیتا ارزان محسوب میشود، اما به هر حال استفاده از این مدل به همراه ES، حجم زیادی از Disk space را خواهد گرفت.
  • همانطور که دیدید برای پیاده سازی یک Insert ساده، حجم زیادی کد نوشته شده‌است. بنابراین تولید اینگونه نرم افزار‌ها به زمان بیشتری نیاز دارد.
بنابراین باید در انتخاب معماری سیستم بسیار دقت شود؛ هر چند که این مدل برای سیستم‌های بزرگ و پیچیده خیلی کارآمد محسوب میشود و باعث یک Domain object غنی ، History Tracking، شفافیت در مشکلات Concurrency و همچنین Scalability و غیره خواهد شد، اما پیدا کردن برنامه نویسانی با داشتن درک عمیق روی این مباحث کمی سخت به نظر میرسد.
در قسمت بعدی بصورت کامل به پیاده سازی این الگو در یک اپلیکشن دات نتی خواهیم پرداخت.
مطالب
الگوهای طراحی API - مکانیزم جلوگیری از پردازش تکراری درخواست ها - Request Deduplication

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

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

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

Idempotent and Safe HTTP Methods

طبق HTTP RFC، متدهایی که پاسخ یکسانی را برای درخواست‌های همسان مهیا می‌کنند، به اصطلاح Idempotent هستند. همچنین متدهایی که باعث نشوند تغییری در وضعیت سیستم در سمت سرور ایجاد شود، به اصطلاح Safe در نظر گرفته خواهند شد. برای هر دو خصوصیت عنوان شده، سناریوهای استثناء و قابل بحثی وجود دارند؛ به‌عنوان مثال در مورد خصوصیت Safe بودن، درخواست GET ای را تصور کنید که یکسری لاگ آماری هم ثبت می‌کند یا عملیات بازنشانی کش را نیز انجام می‌دهد که در خیلی از موارد به عنوان یک قابلیت شناسایی خواهد شد. در این سناریوها و طبق RFC، باتوجه به اینکه هدف مصرف کننده، ایجاد Side-effect نبوده‌است، هیچ مسئولیتی در قبال این تغییرات نخواهد داشت. لیست زیر شامل متدهای مختلف HTTP به همراه دو خصوصیت ذکر شده می باشد:

HTTP MethodSafeIdempotent
GETYesYes
HEADYesYes
OPTIONSYesYes
TRACEYesYes
PUTNoYes
DELETENoYes
POSTNoNo
PATCHNoNo

Request Identifier as a Solution

راهکاری که عموما مورد استفاده قرار می‌گیرد، استفاده از یک شناسه‌ی یکتا برای درخواست ارسالی و ارسال آن به سرور از طریق هدر HTTP می باشد. تصویر زیر از کتاب API Design Patterns، روش استفاده و مراحل جلوگیری از پردازش درخواست تکراری با شناسه‌ای همسان را نشان می‌دهد:

در اینجا ابتدا مصرف کننده درخواستی با شناسه «۱» را برای پردازش به سرور ارسال می‌کند. سپس سرور که لیستی از شناسه‌های پردازش شده‌ی قبلی را نگهداری کرده‌است، تشخیص می‌دهد که این درخواست قبلا دریافت شده‌است یا خیر. پس از آن، عملیات درخواستی انجام شده و شناسه‌ی درخواست، به همراه پاسخ ارسالی به کلاینت، در فضایی ذخیره سازی می‌شود. در ادامه اگر همان درخواست مجددا به سمت سرور ارسال شود، بدون پردازش مجدد، پاسخ پردازش شده‌ی قبلی، به کلاینت تحویل داده می شود.

Implementation in .NET

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

public class IdempotentId(string id, DateTime time)
{
    public string Id { get; private init; } = id;
    public DateTime Time { get; private init; } = time;
}

هدف از این موجودیت ثبت و نگهداری شناسه‌های درخواست‌های دریافتی می‌باشد. در ادامه واسط IIdempotencyStorage را برای مدیریت نحوه ذخیره سازی و پاکسازی شناسه‌های دریافتی خواهیم داشت:

public interface IIdempotencyStorage
{
    Task<bool> TryPersist(string idempotentId, CancellationToken cancellationToken);
    Task CleanupOutdated(CancellationToken cancellationToken);
    bool IsKnownException(Exception ex);
}

در اینجا متد TryPersist سعی می‌کند با شناسه دریافتی یک رکورد را ثبت کند و اگر تکراری باشد، خروجی false خواهد داشت. متد CleanupOutdated برای پاکسازی شناسه‌هایی که زمان مشخصی (مثلا ۱۲ ساعت) از دریافت آنها گذشته است، استفاده خواهد شد که توسط یک وظیفه‌ی زمان‌بندی شده می تواند اجرا شود؛ به این صورت، امکان استفاده‌ی مجدد از آن شناسه‌ها برای کلاینت‌ها مهیا خواهد شد. پیاده سازی واسط تعریف شده، به شکل زیر خواهد بود:

/// <summary>
/// To prevent from race-condition, this default implementation relies on primary key constraints.
/// </summary>
file sealed class IdempotencyStorage(
    AppDbContext dbContext,
    TimeProvider dateTime,
    ILogger<IdempotencyStorage> logger) : IIdempotencyStorage
{
    private const string ConstraintName = "PK_IdempotentId";

    public Task CleanupOutdated(CancellationToken cancellationToken)
    {
        throw new NotImplementedException(); //TODO: cleanup the outdated ids based on configurable duration
    }

    public bool IsKnownException(Exception ex)
    {
        return ex is UniqueConstraintException e && e.ConstraintName.Contains(ConstraintName);
    }

    // To tackle race-condition issue, the implementation relies on storage capabilities, such as primary constraint for given IdempotentId.
    public async Task<bool> TryPersist(string idempotentId, CancellationToken cancellationToken)
    {
        try
        {
            dbContext.Add(new IdempotentId(idempotentId, dateTime.GetUtcNow().UtcDateTime));
            await dbContext.SaveChangesAsync(cancellationToken);

            return true;
        }
        catch (UniqueConstraintException e) when (e.ConstraintName.Contains(ConstraintName))
        {
            logger.LogInformation(e, "The given idempotentId [{IdempotentId}] already exists in the storage.", idempotentId);
            return false;
        }
    }
}

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

در این پیاده سازی از کتابخانه MediatR استفاده می کنیم؛ در همین راستا برای مدیریت تراکنش ها به صورت زیر می توان TransactionBehavior را پیاده سازی کرد:

internal sealed class TransactionBehavior<TRequest, TResponse>(
    AppDbContext dbContext,
    ILogger<TransactionBehavior<TRequest, TResponse>> logger) :
    IPipelineBehavior<TRequest, TResponse>
    where TRequest : IBaseCommand
    where TResponse : IErrorOr
{
    public async Task<TResponse> Handle(
        TRequest command,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        string commandName = typeof(TRequest).Name;
        await using var transaction = await dbContext.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted, cancellationToken);

        TResponse? result;
        try
        {
            logger.LogInformation("Begin transaction {TransactionId} for handling {CommandName} ({@Command})", transaction.TransactionId, commandName, command);

            result = await next();
            if (result.IsError)
            {
                await transaction.RollbackAsync(cancellationToken);

                logger.LogInformation("Rollback transaction {TransactionId} for handling {CommandName} ({@Command}) due to failure result.", transaction.TransactionId, commandName, command);

                return result;
            }

            await transaction.CommitAsync(cancellationToken);

            logger.LogInformation("Commit transaction {TransactionId} for handling {CommandName} ({@Command})", transaction.TransactionId, commandName, command);
        }
        catch (Exception ex)
        {
            await transaction.RollbackAsync(cancellationToken);

            logger.LogError(ex, "An exception occured within transaction {TransactionId} for handling {CommandName} ({@Command})", transaction.TransactionId, commandName, command);

            throw;
        }

        return result;
    }
}

در اینجا مستقیما AppDbContext تزریق شده و با استفاده از خصوصیت Database آن، کار مدیریت تراکنش انجام شده‌است. همچنین باتوجه به اینکه برای مدیریت خطاها از کتابخانه‌ی ErrorOr استفاده می کنیم و خروجی همه‌ی Command های سیستم، حتما یک وهله از کلاس ErrorOr است که واسط IErrorOr را پیاده سازی کرده‌است، یک محدودیت روی تایپ جنریک اعمال کردیم که این رفتار، فقط برروی IBaseCommand ها اجرا شود. تعریف واسط IBaseCommand به شکل زیر می‌باشد:

 
/// <summary>
/// This is marker interface which is used as a constraint of behaviors.
/// </summary>
public interface IBaseCommand
{
}

public interface ICommand : IBaseCommand, IRequest<ErrorOr<Unit>>
{
}

public interface ICommand<T> : IBaseCommand, IRequest<ErrorOr<T>>
{
}

public interface ICommandHandler<in TCommand> : IRequestHandler<TCommand, ErrorOr<Unit>>
    where TCommand : ICommand
{
    Task<ErrorOr<Unit>> IRequestHandler<TCommand, ErrorOr<Unit>>.Handle(TCommand request, CancellationToken cancellationToken)
    {
        return Handle(request, cancellationToken);
    }

    new Task<ErrorOr<Unit>> Handle(TCommand command, CancellationToken cancellationToken);
}

public interface ICommandHandler<in TCommand, T> : IRequestHandler<TCommand, ErrorOr<T>>
    where TCommand : ICommand<T>
{
    Task<ErrorOr<T>> IRequestHandler<TCommand, ErrorOr<T>>.Handle(TCommand request, CancellationToken cancellationToken)
    {
        return Handle(request, cancellationToken);
    }

    new Task<ErrorOr<T>> Handle(TCommand command, CancellationToken cancellationToken);
}

در ادامه برای پیاده‌سازی IdempotencyBehavior و محدود کردن آن، واسط IIdempotentCommand را به شکل زیر خواهیم داشت:

/// <summary>
/// This is marker interface which is used as a constraint of behaviors.
/// </summary>
public interface IIdempotentCommand
{
    string IdempotentId { get; }
}

public abstract class IdempotentCommand : ICommand, IIdempotentCommand
{
    public string IdempotentId { get; init; } = string.Empty;
}

public abstract class IdempotentCommand<T> : ICommand<T>, IIdempotentCommand
{
    public string IdempotentId { get; init; } = string.Empty;
}

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

[HttpPost]
public async Task<ActionResult<long>> Register(
     [FromBody] RegisterUserCommand command,
     [FromIdempotencyToken] string idempotentId,
     CancellationToken cancellationToken)
{
     command.IdempotentId = idempotentId;
     var result = await sender.Send(command, cancellationToken);

     return result.ToActionResult();
}

در اینجا از همان Command به عنوان DTO ورودی استفاده شده‌است که وابسته به سطح Backward compatibility مورد نیاز، می توان از DTO مجزایی هم استفاده کرد. سپس از طریق FromIdempotencyToken سفارشی، شناسه‌ی درخواست، دریافت شده و بر روی command مورد نظر، تنظیم شده‌است.

رفتار سفارشی IdempotencyBehavior از ۲ بخش تشکیل شده‌است؛ در قسمت اول سعی می شود، قبل از اجرای هندلر مربوط به command مورد نظر، شناسه‌ی دریافتی را در storage تعبیه شده ثبت کند:

internal sealed class IdempotencyBehavior<TRequest, TResponse>(
    IIdempotencyStorage storage,
    ILogger<IdempotencyBehavior<TRequest, TResponse>> logger) :
    IPipelineBehavior<TRequest, TResponse>
    where TRequest : IIdempotentCommand
    where TResponse : IErrorOr
{
    public async Task<TResponse> Handle(
        TRequest command,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        string commandName = typeof(TRequest).Name;

        if (string.IsNullOrWhiteSpace(command.IdempotentId))
        {
            logger.LogWarning(
                "The given command [{CommandName}] ({@Command}) marked as idempotent but has empty IdempotentId",
                commandName, command);
            return await next();
        }

        if (await storage.TryPersist(command.IdempotentId, cancellationToken) == false)
        {
            return (dynamic)Error.Conflict(
                $"The given command [{commandName}] with idempotent-id [{command.IdempotentId}] has already been received and processed.");
        }

        return await next();
    }
}

در اینجا IIdempotencyStorage تزریق شده و در صورتی که امکان ذخیره سازی وجود نداشته باشد، خطای Confilict که به‌خطای 409 ترجمه خواهد شد، برگشت داده می‌شود. در غیر این صورت ادامه‌ی عملیات اصلی باید اجرا شود. پس از آن اگر به هر دلیلی در زمان پردازش عملیات اصلی،‌ درخواست همزمانی با همان شناسه، توسط سرور دریافت شده و پردازش شود، عملیات جاری با خطای UniqueConstaint برروی PK_IdempotentId در زمان نهایی سازی تراکنش جاری، مواجه خواهد شد. برای این منظور بخش دوم این رفتار به شکل زیر خواهد بود:

internal sealed class IdempotencyExceptionBehavior<TRequest, TResponse>(IIdempotencyStorage storage) :
    IPipelineBehavior<TRequest, TResponse>
    where TRequest : IIdempotentCommand
    where TResponse : IErrorOr
{
    public async Task<TResponse> Handle(
        TRequest command,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(command.IdempotentId)) return await next();

        string commandName = typeof(TRequest).Name;
        try
        {
            return await next();
        }
        catch (Exception ex) when (storage.IsKnownException(ex))
        {
            return (dynamic)Error.Conflict(
                $"The given command [{commandName}] with idempotent-id [{command.IdempotentId}] has already been received and processed.");
        }
    }
}

در اینجا عملیات اصلی در بدنه try اجرا شده و در صورت بروز خطایی مرتبط با Idempotency، خروجی Confilict برگشت داده خواهد شد. باید توجه داشت که نحوه ثبت رفتارهای تعریف شده تا اینجا باید به ترتیب زیر انجام شود:

services.AddMediatR(config =>
{
   config.RegisterServicesFromAssemblyContaining(typeof(DependencyInjection));

   // maintaining the order of below behaviors is crucial.
   config.AddOpenBehavior(typeof(LoggingBehavior<,>));
   config.AddOpenBehavior(typeof(IdempotencyExceptionBehavior<,>));
   config.AddOpenBehavior(typeof(TransactionBehavior<,>));
   config.AddOpenBehavior(typeof(IdempotencyBehavior<,>));
});

به این ترتیب بدنه اصلی هندلرهای موجود در سیستم هیچ تغییری نخواهند داشت و به صورت ضمنی و انتخابی، امکان تعیین command هایی که نیاز است به صورت Idempotent اجرا شوند را خواهیم داشت.

References

https://www.mscharhag.com/p/rest-api-design

https://www.manning.com/books/api-design-patterns

https://codeopinion.com/idempotent-commands/

مطالب
طراحی شیء گرا: OO Design Heuristics - قسمت سوم
پیشنهاد می‌کنم قسمت‌های قبل را مطالعه کنید تا با اصطلاحات استفاده شده در ادامه مقالات آشنا باشید. در مقالات آتی، مباحث کمی قابل بحث‌تر خواهند بود.

Class Coupling and Cohesion

تعدادی از قواعد شهودی هم، با Coupling و Cohesion به ترتیب مابین و درون کلاس‌ها، سروکار دارند. تلاش ما در راستای افزایش Cohesion درون کلاس‌ها و سست کردن و کاهش Coupling مابین کلاس‌ها می‌باشد. این قواعد شهودی همین اهداف را در پارادایم action-oriented، در ارتباط با توابع دارند. هدف از Tight Cohesion (انسجام و چسبندگی قوی) در توابع، انسجام بالا و ارتباط نزدیک مابین کدهای موجود در تابع، می‌باشد. هدفی که Loose Coupling (اتصال سست و ضعیف، وابستگی ضعیف) در بین توابع دنبال می‌کند، اشاره دارد به اینکه اگر تابعی قصد استفاده از تابع دیگری را داشته باشد، باید وارد شدن و خروج از آن، از یک نقطه صورت گیرد. این مباحث منجربه مطرح شدن قواعد شهودی از جمله: «یک تابع باید طوری سازماندهی شود که تنها یک دستور return  داشته باشد»، در پارادایم action-oriented می‌شود.
ما در پارادایم شیء گرا، اهداف خود از Loose Coupling و Tight Cohesion را در سطح کلاس مطرح می‌کنیم. 5 شکل اصلی Coupling مابین کلاس‌ها به شرح زیر می‌باشد:

  •  Ni Coupling
  •  Export Coupling
  •  Overt Coupling
  •  Covert Coupling
  •  Surreptitious Coupling
Nil Coupling

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

Export Coupling

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

Overt Coupling

این نوع اتصال زمانی رخ می‌دهد که یک کلاس از جزئیات پیاده سازی کلاس دیگر با داشتن اجازه دسترسی از جانب آن، استفاده کند. به عنوان مثال، مکانیزم کلاس‌های friend در زبان سی پلاس پلاس، که امکان این را می‌دهد کلاس X اجازه دوستی به کلاس Y را اعطا کند و در این صورت کلاس Y می‌تواند به جزئیات پیاده سازی خصوصی کلاس X دسترسی داشته باشد.

Cover Coupling

این نوع اتصال هم به مانند Overt می‌باشد؛ با این تفاوت که هیچ اجازه دسترسی به کلاس Y داده نشده است. اگر زبانی داشته باشیم که به کلاس Y اجازه دهد خود را به عنوان دوست کلاس X معرفی کند، در این صورت نوع اتصال بین دو کلاس X و Y از نوع Covert می‌باشد. به عنوان مثال واقعی، می‌توان به استفاده از Reflection در دات نت اشاره کرد.

Surreptitious Coupling (اتصال پنهان)

آخرین نوع اتصال که بدترین حالت هم محسوب می‌شود، مربوط است به زمانیکه کلاس X به هر طریقی که شده از جزئیات داخلی کلاس Y آگاه باشد و از اعضای عمومی داده‌ای (public data member) آن کلاس استفاده کند. منظور این است که با تغییر این داده‌های کلاس متوجه میشود که بر روی عملیات b از کلاس چه تأثیری می‌گذارد و با اتکاء به این دستاورد، جزئیات داخلی خود را پیاده سازی می‌کند و یک اتصال پنهان را با کلاس Y ایجاد کرده است. در این حالت یک وابستگی قوی به صورت پنهان مابین رفتاری از کلاس Y و پیاده سازی کلاس X ایجاد شده است.

قاعده شهودی 2.7
اتصال و پیوستگی مابین کلاس‌ها باید از نوع Nil یا Export باشد؛ به این معنی که یک کلاس فقط از واسط عمومی کلاس دیگر استفاده کند یا کاری با آن نداشته باشد. (Classes should only exhibit nil or export coupling with other classes, that is, a class should only use operations in the public interface of another class or have nothing to do with that class.)
بجز این دو نوع اتصال، بقیه شکل‌های اتصال به طریقی اجازه دسترسی به جزئیات پیاده سازی کلاس‌ها را اعطا می‌کنند. در نتیجه باعث ایجاد وابستگی مابین پیاده سازی دو کلاس می‌شوند. این وابستگی ما بین پیاده سازی‌ها به محض نیاز به تغییر پیاده سازی یکی از کلاس‌ها ، باعث به وجود آمدن مشکلات نگهداری خواهند شد.
Cohesion درون کلاس‌ها سعی بر این دارد که مطمئن شود تمام اجزای یک کلاس به شدت باهم مرتبط هستند. تعدادی از قواعد شهودی نیز در ادامه بر این خصوصیت دلالت دارند.

قاعده شهودی 2.8
یک کلاس باید یک و تنها یک Key Abstraction را تسخیر نماید. (A class should capture one and only one key abstraction)
یک Key Abstraction به عنوان یک Entity در Domain Model تعریف می‌شود و اغلب در غالب اسم در اسناد و مشخصات نیازمندی‌ها ظاهر می‌شوند. هر کدام از آنها باید فقط به یک کلاس نگاشت پیدا کنند. اگر این نگاشت به بیش از یک کلاس انجام گیرد، در نتیجه احتمالا طراح هر تابع را به عنوان یک کلاس تسخیر کرده است. اگر بیش از یک Key Abstraction به یک کلاس نگاشت پیدا کرده باشد، پس احتمالا طراح یک سیستم متمرکز را طراحی کرده است. این کلاس‌ها Vague Classes نامیده می‌شوند و باید آنها در دو کلاس یا بیشتر، تسخیر شوند.

قاعده شهودی 2.9
داده و رفتار مرتبط را در یک جا (کلاس) نگه دارید. (Keep related data and behavior in one place)
در واقع هدفی که این قاعده به دنبال آن می‌باشد این است که هر دو جزء تشکیل دهنده یک Key Abstraction ، یعنی همان داده و رفتار، باید توسط فقط یک کلاس تسخیر شوند. با نقض این قاعده، توسعه دهنده باید با قرار داد (Convention) خاصی برنامه نویسی کند. 
راه شناسایی
طراح باید کلاس‌هایی را که مرتبا داده‌های مورد نیاز خود را با متدهای get از سایر کلاس‌ها دریافت می‌کنند، شناسایی کند. زیرا این نوع کلاس‌ها این قاعده شهودی را نقض کرده‌اند.

مثال واقعی
استفاده از الگوی Domain Model ارائه شده توسط اقای Martin Fowler که دقیقا اشاره به این قاعده دارد.
یا برعکس آن ضد الگوی Anemic Domain Model که ناقض این قاعده می‌باشد. 
در قسمت اول اشاره کردیم این قواعد را به راحتی می‌توان در صورت نیاز نقض کرد. بعضی از مواقع نیاز به طراحی فیزیکی است که باعث تغییر در طراحی منطقی شده و چه بسا می‌تواند باعث نقض هر کدام از این قواعد شهودی نیز شود. اگر به بخش پروژه‌های سایت رجوع کنید این نقض کاملا مشهود (DomainClasses و ServiceLayer موجود در طراحی فیزیکی آنها) می‌باشد (بیشتر از Anemic Domain Model استفاده شده است)؛ ولی نمی‌توان گفت که این کار اشتباه است.

قاعده شهودی 2.10
اطلاعات نامرتبط به هم را در کلاس‌های جدا از هم قرار دهید. ((Spin off nonrelated information into another class (i.e., noncommunicating behavior) 

هدف از این قاعده این است که اگر کلاسی داریم که یکسری از متدهایش با بخشی از داده و یکسری دیگر با بخش دیگر داده‌ها کار میکنند، در واقع شما دو Key Abstraction را به یک کلاس نگاشت کرده اید (Vague Class) و باید آنها را به کلاس‌های جدا نگاشت کنید.

شکل 2.6 A class with noncommunicating behavior

مثال واقعی

به کلاس Dictionary در تصویر زیر توجه کنید.
برای تعداد کمی داده، بهترین پیاده سازی با استفاده از List و در مقابل برای تعداد داده زیاد بهترین پیاده سازی، استفاده از HashTable می‌باشد. هر یک از این پیاده سازی‌ها، به متدهایی برای add و find کلمات نیاز دارند. طراحی سمت چپ تصویر نشان از نقض این قاعده شهودی دارد.

شکل 2.7 (Noncommunicating behavior (real-world example

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

اشتراک‌ها
مشکلات استفاده از bool در طراحی API

تا حد ممکن از پارامترهای bool ایی در طراحی API استفاده نکنید. برای مثال مفهوم (widget.repaint(false بدون مراجعه به مستندات آن ممکن نیست. bool را بهتر است تبدیل به enum کنید.

مشکلات استفاده از bool در طراحی API
مطالب
طراحی شیء گرا: OO Design Heuristics - قسمت پنجم

(The God Class Problem (Behavioral Form 

یکی از مخاطراتی که ممکن است موجب عدم بروز مزایای شیء گرایی در طرح شما شود، بحث God Class می‌باشد. شکل رفتاری آن (Behavioral Form) بیشتر در اثر یک خطای مشترک بین توسعه دهندگان پارادایم action-oriented و در جریان مهاجرت به سمت پارادایم شیء گرا، رخ می‌دهد.

این توسعه دهندگان بیشتر سعی در تسخیر و دستیابی به یک مکانیزم کنترل مرکزی شبیه به آنچه در پارادایم action-oriented داشته‌اند، در طراحی شیء گرای خود دارند. حاصل این کار تشکیل کلاسی خواهد بود که همه کارها را انجام می‌دهد، درحالیکه جزئیات ناچیزی هم به عهده مجموعه‌ای از کلاس‌ها سپرده شده است.

قاعده شهودی 3.1

تا حد ممکن هوشمندی سیستم را به صورت افقی و به طور یکنواخت توزیع کنید. به این معنی که کلاس‌های سطح بالای موجود در طراحی، باید کار را به طور یکسان مابین خود به اشتراک بگذارند. (Distribute system intelligence horizontally as uniformly as possible, that is, the top-level classes in a design should share the work uniformly)
منظور اینکه Businessای را که سیستم قرار است پیاده سازی کند، بین کلاس‌های سطح بالا تقسیم کنید. در حالت vertical یا عمودی می‌توان در نظر گرفت که کلاسی این Business را توسط یکسری متد در دل خود پیاده سازی کند و این متدها یکدیگر را فراخوانی خواهند کرد. 
قاعده شهودی 3.2
در سیستم خود God Class ایجاد نکنید. به کلاس هایی که نام آنها شامل Driver، Manager و یا Subsystem می‌باشد، مشکوک باشید. (Do not create god classes/objects in your system. Be very suspicious of a class whose name contains Driver, Manager, System, or Subsystem)

مانند: BlahBlahSystem یا BlahManager

قاعده شهودی 3.3
مراقب کلاس هایی باشید که در واسط عمومی آنها تعداد زیادی Accessor Method تعریف شده است؛ وجود آنها نشان از این دارد که داده و رفتار، در یکجا نگه داشته نشده اند. (Beware of classes that have many accessor methods defined in their public interface. Having many implies that related data and behavior are not being kept in one place)
ازدیاد عملیات get و set در واسط عمومی کلاس‌ها که Accessor Method نامیده می‌شوند، نشان دهنده ایجاد شکل رفتاری God Class می‌باشند. منظور این است که یک کلاس، رفتارهایی برای کار کردن با داده‌های خود در نظر نگرفته است و این داده‌ها را از طریق accessor method‌ها در اختیار کلاس دیگری قرار می‌دهد تا عملیات روی داده‌ها را انجام دهد. در اینجا هم مقصود God Class شدن کلاسی است که از این accessor method‌ها استفاده می‌کند و نشان از این دارد که تعداد رفتارهای آن زیاد خواهد شد. 
قاعده شهودی 3.4
مراقب کلاس هایی باشید که تعداد خیلی زیادی رفتار نامرتبط دارند؛ یعنی رفتارهایی که فقط برروی زیر مجموعه ای از داده‌های کلاس کار می‌کنند. God Class‌ها اغلب دارای اینگونه رفتارهای نامرتبط به هم هستند. (Beware of classes that have too much noncommunicating behavior, that is, methods that operate on a proper subset of the data members of a class. God classes often exhibit much noncommunicating behavior)
منظور اینکه کلاس مورد نظر را می‌توان شکست و تبدیل به دو کلاس مختلف کرد. به عنوان اولین مثال، دامنه مربوط به سیستم برنامه ریزی دوره‌های آموزشی را در نظر بگیرید. در این دامنه، ما با وهله هایی از «Student» ،«Course» و «CourseOffering» سروکار خواهیم داشت. 
قصد داریم با فراخوانی متد ()add_student مربوط به CourseOffering، یک دانشجو را به لیست شرکت کنندگان یک دوره اضافه کنیم. همچنین در این زمان لازم است مطمئن شویم که دانشجوی مورد نظر تمام پیش نیاز‌های دوره انتخاب شده را گذرانده باشد. به نظر شما کدام کلاس می‌بایست عملیات چک کردن پیش نیازها را انجام دهد؟
کلاس دانشجو از دوره‌هایی که گذرانده است آگاه است و کلاس دوره هم از پیش نیاز‌های خود. در بهترین حالت شاید یکی از طراحی‌های زیر را ارائه دهید. به شکلی که یا کلاس دوره با استفاده از متد get_courses مربوط به کلاس دانشجو، داده مورد نیاز را بدست آورده و عملیات چک کردن را در دل خود بگنجاند یا برعکس.

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

راه حل‌های پیشنهادی برای مشکل مطرح شده به شکل زیر می‌باشند:

با توجه به طراحی شکل بالا، یا خود کلاس CourseOffering با استفاده از لیست دوره‌های گذرانده شده توسط دانشجو و لیست دوره‌های پیش نیاز دوره انتخابی، چک کردن را انجام دهد و یا کلاسی با عنوان PrereqChecker که یک Controller Class (کلاسی که فقط رفتار دارد و داده مورد نظر خود را توسط سایر کلاس‌ها و از طریق accessor methodهای آنها تأمین می‌کند) می‌باشد، وظیفه چک کردن را برعهده بگیرد.


علاوه بر اینکهaccessor method ها، داده را برای کلاس‌های کنترلر مهیا می‌کنند (مانند مثال بالا)، وظیفه‌ی مهیا کردن داده برای UI (رابط کاربری) را نیاز بر عهده خواهند داشت. به این صورت که رابط کاربری، با استفاده از این متدها، مشخصات درونی مدل را نمایش دهد و یا این امکان را به کاربر می‌دهد که این مشخصات درونی مدل را ویرایش کرده و به سمت مدل ارسال نماید.

قاعده شهودی 3.5

در برنامه‌هایی که شامل یک مدل شی گرایی می‌باشند که با رابط کاربری تعامل دارند، مدل نباید به رابط کاربری وابسته باشد. رابط کاربری می‌بایست وابسته به مدل باشد. (In applications that consist of an object-oriented model interacting with a user interface, the model should never be dependent on the interface. The interface should be dependent on the model)

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

مطالب
اصول طراحی شیء گرا: OO Design Principles

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

این مطالب نیز چکیده ای از آموزش‌های Lynda, Pluralsight , SkillFeed  و کتاب های Gang of four, Headfirst Design patterns  و ... میباشد


اصل اول: Encapsulate what varies

"آنچه را که تغییر می‌کند مشخص و جدا کن یا به عبارتی آنرا کپسوله کن"

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


اصل دوم: Program to an interface not implementation

" برنامه نویسی را متمرکز بر واسط کن، نه نحوه‌ی پیاده سازی "

برای این اصل تعابیر مختلفی ارائه شده است:

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

برای رعایت این اصل باید:

  • سعی بر تعریف واسط برای اکثر کلاس‌ها کنیم
  • اشیا را از نوع واسط تعریف کنیم، نه کلاس‌های پیاده ساز آن

در کد زیر نحوه‌ی تعریف واسط را برای کلاس و تعریف اشیاء از نوع واسط را میبینیم: 

    public interface IMyInterface
    {
        void DoWork();
    }

    public class MyImplementation1 : IMyInterface
    {
        public void DoWork()
        {
            var implementationName = this.GetType().ToString();
            Console.WriteLine("DoWork from " + implementationName);
        }
    }
    public class MyImplementation2 : IMyInterface
    {
        public void DoWork()
        {
            var implementationName = this.GetType().ToString();
            Console.WriteLine("DoWork from " + implementationName);
        }
    }

    public class Context
    {
        IMyInterface myInterface;

        public void Print() 
        {
            myInterface = new MyImplementation1();
            myInterface.DoWork();

            myInterface = new MyImplementation2();
            myInterface.DoWork();
        }
    }


اصل سوم: Favor composition over inheritance

"ترکیب را بر توارث ترجیح بده"

رابطه "دارد" (has a ) انعطاف بیشتری نسبت به ارث بری یا "از نوع ... هست" ( is a ) دارد. برای مثال فرض کنید کلاس Enemy رفتار Movable را دارد و این رفتار در طول بازی بر حسب موقعیتی تغییر میکند. اگر این رفتار را با توارث مدل کنیم، یعنی Enemy از نوع Movable باشد، آنگاه برای هر نوع رفتار Movable (هر نوع پیاده سازی) باید یک نوع Enemy داشته باشیم و تصور کنید بعضی از این پیاده سازی‌ها در کلاس Player یکسان باشد. آنگاه کد باید دوباره تکرار شود. حال تصور کنید این موقعیت را با ترکیب مدل کنیم. آنگاه کلاس Enemy یک شیء از جنس Movable خواهد داشت و در زمان نیاز میتواند نوع این رفتار را با نمونه گیری از کلاس‌های پیاده ساز آن، تغییر دهد. در واقع با اینکار اصل اول را رعایت کرده ایم و بخش ثابت را از بخش متغیر جدا نموده ایم.

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

public interface Movable
    {
        void Move();
    }
    public class EnemyBase : Movable
    {
        // Enemy properties goes here
        protected int x, y;
        public virtual void Move()
        {
            x += 2;
            y += 2;
        }
    }
    public class EnemyWithMoveType2 : EnemyBase
    {
        // override the move method
        public override void Move()
        {
            x += 4;
            y += 4;
        }
    }
    public class EnemyWithMoveType3 : EnemyBase
    {
        // override the move method
        public override void Move()
        {
            x += 6;
            y += 6;
        }
    }
    public class PlayerBase : Movable
    {
        // Player properties goes here
        protected int x, y;
        public  virtual void Move()
        {
            // same code as EnemyBase move method
            x += 2;
            y += 2;
        }
    }
    public class PlayerWithMoveType2 : PlayerBase
    {
        // override the move method
        public override void Move()
        {
            // same code as EnemyWithMoveType2 move method
            x += 4;
            y += 4;
        }
    }
    public class PlayerWithMoveType3 : PlayerBase
    {
        // override the move method
        public override void Move()
        {
            // same code as EnemyWithMoveType3 move method
            x += 6;
            y += 6;
        }
    }  

در ادامه نیز مدلسازی با ترکیب را میبینیم: 

     public interface IMovable
    {
        void Move(ref int x, ref int y);
    }
    public class EnemyBase
    {
        // Enemy properties goes here
        protected int x, y;
        IMovable moveBehavior;
        public void SetMoveBehavior(IMovable _moveBehavior) { moveBehavior = _moveBehavior; }
        public void Move()
        {
            moveBehavior.Move(ref x,ref y);
        }
    }
    public class PlayerBase
    {
        // Player properties goes here
        protected int x, y;
        IMovable moveBehavior;
        public void SetMoveBehavior(IMovable _moveBehavior) { moveBehavior = _moveBehavior; }
        public void Move()
        {
            moveBehavior.Move(ref x, ref y);
        }
    }
    public class MoveBehavior1
    {
        public void Move(ref int x, ref int y)
        {
            x += 2;
            y += 2;
        }
    }
    public class MoveBehavior2 : IMovable
    {
        public void Move(ref int x, ref int y)
        {
            x += 4;
            y += 4;
        }
    }
    public class MoveBehavior3 : IMovable
    {
        public void Move(ref int x, ref int y)
        { 
            x += 6;
            y += 6;
        }
    }  

همانطور که میبینید، با فراخوانی تابع SetMoveBehavior میتوان رفتار را در زمان اجرا تغییر داد.

در مقاله‌ی بعدی به ادامه‌ی اصول خواهم پرداخت. از شنیدن نظرات و سوالات شما خرسند خواهم شد.

مطالب
طبقه بندی Bad Code Smell ها
نقل قول‌های زیادی، در مورد کیفیت کد وجود دارند. دستور العمل‌های فراوانی نیز در این راستا وجود دارند. یکی از ابزارهایی که برای نوشتن کدهایی با کیفیت مطلوب وجود دارد، مجموعه الگوهای بد کد نویسی است که به Code smell یا بوی بد کد مشهور هستند.  
بوی بد کد، نشانه‌هایی در کد هستند که حکایت از مشکلات عمیق‌تری دارند. بوی بد کد مساوی با باگ نیست. ولی خطر افزایش باگ‌ها و یا مشکلاتی را در آینده، به دنبال خواهند داشت. بوی بد کد معمولا حاصل رعایت نکردن یک سری اصول اولیه برنامه نویسی و یا طراحی شیء گرا هستند. 
برای بهبود کیفیت نرم افزار در دراز مدت نیاز است موارد بوی بد کد به دقت بررسی و رفع شوند. رفع شدن آنها ریسک انباشته شدن بوی بد کد را در پروژه کم خواهد کرد. یکی از فواید جلوگیری از انباشته شدن چنین الگوهای بدی در پروژه، بهبود فرآیند نگهداشت آن می‌باشد که موضوعی بسیار مهم برای چابکی یک تیم نرم افزاری است. 
هنگام مشاهده‌ی بوی بد، در بخشی از کدها، معمولا اولین اقدام، رفع آن است (Refactoring). در فرآیند رفع آن ممکن است الگوهای بد دیگری در کد یافت شوند که با آنها نیز به همین صورت برخورد خواهد شد. 
انوع بوهای بد کد به دسته‌های زیر طبقه بندی می‌شوند. 

کدهای متورم (Bloaters) 


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

1 - متدهای بلند (Long method): در این الگوی بد، متدها تعداد خطهای زیادی از کد را شامل می‌شوند. به طور معمول متدهایی با تعداد خطوط بیشتر از 10 خط، متدهای بلند محسوب می‌شوند. نکته قابل توجه این است که هیچ کس متدی را با تعداد خطوط زیاد طراحی نمی‌کند! معمولا به مرور زمان تعداد خط‌های یک متد افزایش می‌یابند. 
2 - کلاس‌های بزرگ (Large class): کلاسی که تعداد فیلدها، متدها و خطوط کد زیادی دارد. 
3 - وسواس استفاده از متغیرهای داده‌ای اولیه (Primitive obsession): این بوی بد معمولا به سه شکل بروز می‌کند. 
  • استفاده از متغیرهای اولیه بجای ساختارهای کوچک برای کارهای اولیه مانند Currency, DateTime, PhoneNumber 
  • استفاده از constant‌ها برای کد کردن اطلاعات مانند USER_ADMIN_ROLE = 1 
  • استفاده از constant‌های رشته‌ای به عنوان نام فیلدها در آرایه‌های داده 
4 - تعداد پارامترهای زیاد متد (Long parameter list): تعداد پارامترهای بیشتر از سه یا چهار عدد در یک متد. 
5 - توده داده (Data clumps): در بعضی موارد ممکن است از متغیرها به صورت دسته‌ای در مکان‌های مختلف کد استفاده شود. مانند استفاده از دسته‌ای از متغیرها برای نگه داشتن اطلاعات مربوط به اتصال پایگاه داده. این دسته‌ها باید به کلاس‌های حمل کننده داده خود تغییر کنند. 
 

بد استفاده کنندگان از شیء گرایی (Object orientation abusers)  


تکه کدهای این بخش در واقع بد استفاده کنندگاه یا ناقص استفاده کنندگان از اصول شیء گرایی هستند. در این دسته بندی موارد زیر وجود دارند:  

1 - گذاره‌های switch: وجود یک گذاره switch پیچیده یا دنباله‌ای از گذاره‌های if  
2 - درخواست رد شده (Refused request): در این حالت یک کلاس مجموعه محدودی از اعضای کلاس پدر خود را پیاده سازی می‌کند و باقی اعضای کلاس پدر یا بدون استفاده می‌مانند یا با استفاده از پرتاب کردن استثناء (Exception throwing) از کار انداخته می‌شوند. 
3 - فیلد موقتی (Temporary field): در این حالت متغیرها مقدار خود را در شرایط خاصی می‌گیرند و در بقیه شرایط خالی هستند. 
4 - کلاس هایی دقیقا مشابه در کارایی ولی متفاوت در مشخصات (Alternative Classes with Different Interfaces): دو کلاس دقیقا یک کار را انجام می‌دهند ولی نام اعضای آنها (متد و ...) متفاوت است. 

جلوگیری کنندگان از تغییر(Change preventers) 


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

1 - تغییر واگرا (Divergent change): این حالت زمانی اتفاق می‌افتد که برای اعمال یک تغییر به کلاس نیاز است متدهای زیادی را تغییر دهید. به طور مثال به ازای هر نوع محصولی که به محصولات شما اضافه می‌شود باید متدهای ذخیره، بازیابی، جستجو را تغییر دهید. 
2 - Shotgun Surgery: این حالت شباهت زیادی به تغییر واگرا دارد. تنها تفاوت آن این است که در این حالت شما به ازای هر تغییر نیاز است کلاس‌های زیادی را تغییر دهید. تغییر واگرا در بدنه یک کلاس اتفاق می‌افتد. 
3 - سلسله مراتب موازی ارث بری (Parallel inheritance hierarchy): این مورد یکی کمتر درک شده‌ترین موارد است. در این حالت زمانی که یک زیر کلاس برای یک کلاس ایجاد می‌کنید به ازای آن ناخودآگاه مجبور می‌شوید یک زیر کلاس برای کلاس دیگری ایجاد کنید. 

کدهای غیر ضروری (Dispensables) 


این دسته از کدها معمولا کدهایی هستند بی دلیل و بی استفاده. کدهایی که نبودنشان بهتر از بودنشان است! حذف کردن این کدها به خوانایی و قابلیت نگهداری کد خواهد افزود. بوهای بدی که در این دسته بندی قرار دارند به صورت زیر می‌باشند: 

1 - کامنت: یک متد، با مقادیر فراوانی از کامنت‌های توضیحی پر شده است. 
2 - کد تکراری: در این بوی بد، دو قطعه کد دقیقا مانند یکدیگر هستند. 
3 - کلاس داده (ِData class): کلاس‌هایی که تنها فیلدهای اطلاعاتی در آنها وجود دارند و متدهای خامی که جهت دریافت یا ذخیره اطلاعات در آنها استفاده می‌شوند. این کلاس‌های معمولا هیچ روال منطقی ای در خود ندارند. یکی از قدرت‌های شیء گرایی افزودن رفتار به کلاس‌ها در کنار اقلام اطلاعاتی موجود در آن است.  
4 - کلاس تنبل (Lazy class):  اگر کلاس کار چندانی که درخور نگهداری و توسعه باشد، انجام نمی‌دهد بهتر است از بین برود. 
5 - کد مرده (Dead code): متغیر، پارامتر، متد یا کلاسی که دیگر هیچ استفاده‌ای از آن متصور نیست و هیچ استفاده‌ای در حال حاضر از آن وجود ندارد. 
6 - کلی نگری بیش از اندازه (Speculative Generality): این الگو نیز کدهایی را شامل می‌شود که بلااستفاده هستند. ولی دلیل بلااستفاده بودن آن کلی نگری و دور اندیشی بدون دلیل است. معمولا کدهای تولیدی برای شرایط فعلی و پیش‌بینی آینده تولید می‌شوند. اگر این پیش‌بینی آینده به درستی و بر مبنای واقعیات انجام نشود، معمولا نتیجه کار، طراحی و پیاده سازی ای بی فایده و بلااستفاده خواهد بود. 

کدهایی بیش از اندازه وابسته به هم (Couplers) 


کدهایی که در این دسته قرار می‌گیرند معمولا یا خود درگیر یک وابستگی شدید هستند یا به ایجاد وابستگی بین کلاس‌ها کمک می‌کنند. بوی‌های بدی که در این دسته بندی قرار می‌گیرند به صورت زیر هستند: 

1- متد حسود (Feature envy): متدی که از اعضای یک شیء دیگر بیشتر از اعضای کلاس خود استفاده می‌کند! این اتفاق معمولا زمانی می‌افتد که فیلدهایی به یک "کلاس داده" منتقل می‌شوند. وقتی این اتفاق می‌افتد یکی از راه حل‌ها، انتقال روالهای استفاده کننده از فیلدها به "کلاس داده" مربوطه است.
2 - کلاس‌های بیش از اندازه صمیمی (Inappropriate Intimacy): کلاس‌ها از اعضای internal یکدیگر بیش از اندازه استفاده می‌کنند. کلاس‌های خوب کلاس‌هایی هستند که کمترین اطلاعی را از وضعیت داخلی یکدیگر دارند. 
3 - کتابخانه‌های ناقص (Incomplete Library Class): زمانیکه کتابخانه‌ای آماده می‌شود، بالاخره روزی می‌رسد که این کتابخانه نیازهای پروژه را رفع نمی‌کند و نیاز به توسعه خواهد داشت. ولی از آنجایی که کتابخانه‌ها به صورت فقط خواندنی در اختیار پروژه‌ها قرار می‌گیرند، در صورتیکه توسعه دهنده اصلی آن از توسعه کتابخانه سر باز بزند، مشکلاتی بوجود خواهد آمد. 
4 - زنجیره فراخوانی‌ها (Message chain): زمانیکه یک متد در بدنه خود پیامی به شیء دیگری می‌فرستد که آن شیء نیز به خودی خود پیامی به شیء دیگری می‌فرستد (و الی آخر) یک زنجیره فراخوانی بوجود آمده است. در این روش بیرونی‌ترین استفاده کننده از متد در واقع وابسته به یک زنجیره‌ای از فراخوانی‌ها است که تغییر در هر قدمی از آن باعث خرابی خواهد شد. 
5 - دلال (Middle man): اگر کلاسی تنها کاری که انجام می‌دهد انتقال فراخوانی به کلاس دیگری است، دیگر نیازی به این کلاس وجود نخواهد داشت.

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