تولید نرم افزار با الگو

وبلاگ شخصی ابراهیم خانی

applying design patterns

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

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

نباید اجازه داده شود که بار منفی موجود در واژه بی نظم بر روش اول سایه بیاندازد. منظور از بی نظمی در این جا، نبود یک فرایند برای به کارگیری الگوها در تولید نرم افزار است. برای پیاده سازی رویکرد نظام مند دو راهکار کلی وجود دارد. راهکار اول استفاده از زبان الگو [1] است. زبان الگو به دنبال معرفی یک مجموعه از الگوهای مرتبط باهم و ارتباطات بین آن‌ها، جهت حل مشکلات موجود در یک دامنه خاص است. این راهکار مصداق مثل "چیزی به عنوان غذای مجانی وجود ندارد" [2] است. چرا که زبان الگو، فرایند ضمنی موجود در خود را مرهون محدود کردن دامنه الگوهای آن به یک حوزه خاص است. برای شناخت دقیق زبان الگو مطالعه کتاب [3] توصیه می شود. راهکار دوم استفاده از یک فرایند به جهت استفاده نظام مند از الگوها در تولید نرم افزار است. متدولوژی های مطرح شده در این حوزه با دو چالش اساسی روبه رو هستند. چالش اول اینکه نمایشی برای الگوها که مورد پذیرش جامعه مهندسین نرم افزار باشد وجود ندارد. چالش دوم که نشات گرفته از چالش اول است، نیاز به روشی جهت ترکیب الگوهاست.

برای ترکیب الگوها دو رویکرد اصلی وجود دارد :

  • رویکرد ساختاری : در این رویکرد طراحی نرم‌ افزار از اتصال نمودار کلاس [4] الگوها ایجاد می ‌شود.
  • رویکرد رفتاری : در این رویکرد از مدل نقش [5] معرفی شده در متدولوژی OORAM [6] جهت ترکیب الگوها استفاده می شود.

رویکرد ساختاری علاقه مند به ترکیب الگوها در سطح پایین تری از انتزاع [7] است. دو نمونه بارز این رویکرد، روش POT [8] و روش POAD [9] است. در روش POT که در سال 1997 معرفی شد، ابتدا مدل شی رسم می شود و سپس بر اساس تعاملات بین اشیاء، مدل رسم شده به گروه هایی تقسیم می شود و در نهایت در این گروه ها الگوهای طراحی شناسایی می شوند. روش POAD که در سال 2003 در معرفی شد شامل سه مرحله است. در مرحله تحلیل الگوهای قابل کاربرد بر اساس دامنه مساله شناسایی می شوند. سپس در مرحله طراحی این الگوها با استفاده از واسط ترکیب می شوند و در مرحله دقیق سازی طراحی الگوهای ترکیب شده نمونه سازی می شوند. روش POT دید پایین به بالا دارد و در طرف مقابل روش POAD از دید بالا به پایین استفاده می کند. برای جزییات بیشتر در مورد این دو روش مطالعه [10] و [11] توصیه می شود.

رویکرد رفتاری علاقه مند به ترکیب الگوها در سطح بالاتری از انتزاع است. دو نمونه بازر این رویکرد، روش مطرح شده در متدولوژی OORAM و روش مطرح شده توسطDirk Rielhe  است. در متدولوژی OORAM از مدل نقش به عنوان سطح بالاتری از انتزاع نسبت به مدل شی استفاده می شود.  درحالی‌که یک کلاس به‌عنوان یک قالب برای ایجاد اشیا نمود پیدا می‌کند، نقش به مسئولیت این اشیا تأکید دارد. این متدولوژی در دو مرحله انجام می شود. ابتدا مدل های نقش ایجاد و در مرحله بعد این مدل ها با یکدیگر ترکیب می شوند. برای جزییات بیشتر مطالعه کتاب [12] پیشنهاد می شود. Dirk Rielhe با الهام از متدولوژی OORAM، روشی مبتنی بر مدل نقش برای ترکیب الگوها ارایه می دهد. در این روش ابتدا الگوهای موجود بر اساس مدل نقش، مدل می شوند. سپس یک مدل شیء از دامنه مساله ترسیم می شود (Rielhe این مدل را مدل نمونه سازی اولیه می نامد). در مرحله بعد با تحلیل مدل نمونه‌سازی اولیه،الگوهای قابل کاربرد شناسایی و نقش‌های موجود در این الگوها به اشیا موجود در مدل نمونه‌سازی اولیه اختصاص داده می‌شود. درنهایت با بررسی روابط بین نقش ها، ترکیب بین الگوها شکل می گیرد. برای جزییات بیشتر این روش، مطالعه مقاله [13] پیشنهاد می گردد. دو ایراد اساسی به مدل نقش وارد است. اول اینکه زبان UML از این مدل پشتیبانی نمی کند. دوم اینکه این مدل قابلیت نمایش ساختار سلسله مراتبی (که عصاره بسیاری از الگوهاست) را ندارد.



[1] - Pattern language
[2] - There is no such thing as a free lunch
[3] - Pattern Oriented Software Architecture Volume 5 : On Patterns And Pattern Languages
[4] - Class diagram
[5] - Role model
[6] - Object oriented role analysis method
[7] - Abstraction
[8] - Pattern Oriented Technique
[9] - Pattern Oriented Analysis And Design
[10] - A pattern oriented technique for software design
[11] - Pattern-Oriented Analysis and Design: Composing Patterns to Design Software Systems
[12] - Working With Objects:The Ooram Software Engineering Method
[13] - Composite design patterns
۰ نظر موافقین ۰ مخالفین ۰
ابراهیم خانی

error handling

در صورتی که در آرمان شهر [1] زندگی نمی کنید، با مواردی که قواعد شما را نقض می کنند رو به رو خواهید شد. تولید نرم افزار نیز از این قاعده مستثنی نیست. از این رو هر سیستمی برای ادامه حیات، نیازمند راهکاری نظام مند به منظور مقابله با اینگونه موارد است.

به طور کلی دو الگو برای کشف خطا در نرم افزار وجود دارد؛ کنش گرا [2] و واکنش گرا [3]. در الگوی کنش گرا ابتدا امکان انجام یک عملیات بررسی می شود و در صورت امکان اجرای آن عمل (بر اساس مقادیر ورودی و حالت) آن عملیات انجام می شود.  هر چند شعار "پیشگیری بهتر از درمان" در سایر حوزه ها مقبولیت عام دارد، مشکل اصلی این الگو (که با نام Tester-Doer در ادبیات .net شناخته می شود) نبود قابلیت Thread-Safety است. در قطعه کد زیر، چگونگی استفاده از این الگو دیده می شود.

if (!dictionary.ContainsKey("Tester"))
{
    dictionary.Add("Tester","Doer");
}

در الگوی واکنش گرا، عملیات بدون بررسی امکان فرخوانی، انجام می شود و در صورت بروز خطا پاسخ مناسب برای آن در نظر گرفته می شود. در دستکاری [4] خطای رخ داده دو حالت وجود دارد؛ یا به این خطا توسط متدی که در آن خطا رخ داده دستکاری می شود و یا به فراخواننده متد، در مورد خطا اطلاع رسانی می شود. برای اطلاع رسانی نیز دو راهکار وجود دارد؛ از طریق استثنا [5] و یا از طریق بازگردادن کد خطا. استفاده از استثنا ها باعث تغییر جریان اجرا می شود که در نهایت منجر به ایجاد Spaghetti code خواهد شد. Meyer در کتاب گرانقدر خود [6] به استفاده نادرست از استثنا در توابع اشاره می کند که به نوعی شبیه سازی Goto و هر چند بدتر از آن است. چرا که استثنا ها به مکانی که جریان اجرا را منتقل می کنند اشاره ای نمی کنند. پیاده سازی این الگو با روش بازگردادن کد خطا با نام Try-Parse در ادبیات .Net وجود دارد. در قطعه کد زیر چگونگی استفاده از این الگو دیده می شود.  در صورتی که متد TryGetValue به صورت Thread-Safe پیاده سازی شود، این الگو مشکل اشاره شده در الگوی Tester-Doer را ندارد.

string value;
var keyExist = dictionary.TryGetValue("TryParse", out value);
if (keyExist)
{
    Console.WriteLine(value);
}

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



[1] - Utopia 
[2] - Proactive
[3] - Reactive
[4] - Handling 
[5] - Exception
[6] - Object oriented software construction
۰ نظر موافقین ۰ مخالفین ۰
ابراهیم خانی

design by contract

برنامه های کامپیوتری به خودی خود، درست یا غلط نیستند، بلکه باید نسبت به توصیف آنها سنجیده شوند. به عبارت بهتر، به جای صحبت از درست یا نادرست بودن برنامه می بایست از سازگاری اش نسبت به توصیف آن صحبت کرد. برای بررسی صحت توصیف نیاز به فرمولی برای تعریف صحت توصیف داریم. Meyer در کتاب ارزشمند خود [2] برای این توصیف از سه تایی Hoare استفاده می کند.  این سه تایی مفهوم اصلی در منطق Hoare است که در سال 1969 توسط Tony Hoare برای ارزیابی درستی برنامه های کامپیوتری ابداع شد. نگارش این سه تایی به این صورت است : 

{p} c {q}

که در آن p , q گزاره هستند و c برنامه (command) است و تفسیر آن چنین است : در صورتی که p درست باشد، با اجرای  c، q درست خواهد بود. قدرت این مفهوم به ظاهر ساده به خاطر قابلیت اعمال آن در سطوح متخلف تجرید است. به عبارت ساده تر هم ابزاری برای توصیف است و هم ابزاری برای بررسی انطباق برنامه با توصیف آن.

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

{pre} body {post}

که با توجه به تعریف مطرح شده در بخش قبل، معنی آن واضح به نظر می رسد؛ در صورتی که فراخواننده تابع شرایطی که در پیش شرط (precondition) مطرح شده است را فراهم کند (از طریق آرگومان های ارسالی به تابع)، با اجرای بدنه تابع (body) و خاتمه یافتن آن، شرایطی که در پس شرط (postcondition) قید شده است حاصل می گردد. اما در مدل شی گرا، توابع در کلاس ها قرار دارند و حالت این کلاس ها نیز در عملکرد توابع تاثیر گذار است. با در نظر گرفتن این موضوع، Meyer مدل مطرح شده را اینگونه گسترش می دهد : 

{pre ^ inv} body {post ^ inv}

که در آن Inv معرف حالت کلاس است که بایستی همواره برقرار باشد.  نکته ای که باید به آن توجه کرد اشتباه کردن طراحی بر اساس قرارداد با اعتبارسنجی داده ها (input validation)  است. تفاوت آشکاری بین این دو مفهوم است. در اعتبارسنجی ورودی ها، انتظار ورود دادهای غلط را داریم و به دنبال فیلتر کردن داده های ورودی هستیم اما در طراحی بر اساس قرارداد، Assertion ها نقاطی هستند برای وارسی کد نوشته شده. در واقع زمانی که ورودی غلط است با نمایش پیغام خطا و ... از کاربر می خواهیم آن را اصلاح کند اما زمانی که یک Assertion نقض می شود، نشان دهنده ایراد در کد نوشته شده است (که و اینکه می بایستی کد تغییر کند)



[1] - design by contract
[2] - object oriented software construction 
۰ نظر موافقین ۰ مخالفین ۰
ابراهیم خانی

Type code

Type Code یا همان Enumeration ها، نوع داده هایی هستند که تعداد محدودی مقدار دارند که این مقادیر برای مشخص کردن یک حالت خاص به کار می روند. به هنگام استفاده از Type code ها سه وضعیت به وجود می آید؛

  •  Type code مبنای تصمیم گیری نیست
  •  Type code مبنای تصمیم گیری است و پس از انتصاب آن به یک شی، تغییر نمی کند
  •  Type code مبنای تصمیم گیری است و پس از انتصاب آن به یک شی، امکان تغییر آن وجود دارد

با توجه به هریک از این وضعیت ها الگوی خاصی برای بازآرایی [1] در کتاب ارزشمند [2] آورده شده است.

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


public class Person
{
    public static int Wealthy => 1;
    public static int Poor => 2;
    public int Status { get; set; }

    private Person()
    {
    }

    public Person(int status) : this()
    {
        Status = status;
    }
}


پیشنهاد Fowler برای غلبه بر این مشکل (محدود کردن دامنه انتخاب مقدار) به این صورت است. در واقع در این راه حل با بهره گیری از Type system دامنه انتخاب مقدار برای وضعیت مالی محدود شده است.


public class Person
{
    public Status Status { get; set; }
    private Person()
    {

    }
    public Person(Status status) : this()
    {
        Status = status;
    }
}

public class Status
{
    public int StatusNumber { get; }
    public static Status Wealthy = new Status(1);
    public static Status Poor = new Status(2);
    private Status()
    {
    }
    private Status(int statusNumber) : this()
    {
        StatusNumber = statusNumber;
    }
}


اما در زبان #C  با توجه به اینکه Enumeration ها strongly typed هستند، برای وضعیتی که Type code تغییر نمی کند و مبنای تصمیم گیری نیست می توان با کد زیر این وضعیت را مدل کرد.


public class Person
{
    public Status Status { get; set; }

    private Person()
    {
    }

    public Person(Status status) : this()
    {
        Status = status;
    }
}

public enum Status
{
    Wealthy,
    Poor
}

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


public class Person
{
    public Status Status { get; set; }
    private Person()
    {
    }
    public Person(Status status) : this()
    {
        Status = status;
    }
    public decimal GetSubsidy()
    {
        switch (Status)
        {
            case Status.Wealthy:
                return 0m;
            case Status.Poor:
                return 100m;
            default:
                throw new ArgumentOutOfRangeException();
        }
    }
}

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


public abstract class Person
{
    public abstract decimal GetSubsidy();
}

public class WealtyPerson : Person
{
    public override decimal GetSubsidy()
    {
        return 0m;
    }
}

public class PoorPerson : Person
{
    public override decimal GetSubsidy()
    {
        return 100m;
    }
}

اما در وضعیت سوم که Type code مبنای تصمیم گیری است و مقدار آن برای شی ای که به آن تعلق دارد تغییر می کند (امکان ثروتمند شدن فقرا و برعکس وجود دارد) استفاده از پیاده سازی قبلی باعث می شود برای هر تغییر حالت، یک شی جدید ایجاد شود. به علاوه با توجه به اینکه شی انسان قاعدتا از نوع Entity است، می بایست mutable باشد (توصیه DDD) که با این پیاده سازی Immutable خواهد بود. در این وضعیت با رفتار وابسته به حالت رو به رو هستیم که معرف Polymorphism در شی گرایی است. برای باز آرایی این حالت از الگوی State و یا Strategy استفاده می شود.  در این حالت تغییر وضعیت مالی با استفاده از Interface از کلاس Person پنهان می شود.


public abstract class Person
{
    private IPersonState _personState;

    private Person()
    {
    }

    protected Person(IPersonState personState) : this()
    {
        _personState = personState;
    }
}

public interface IPersonState
{
    decimal GetSubsidy();
}

public class WealtyPerson : IPersonState
{
    public decimal GetSubsidy()
    {
        return 0m;
    }
}

public class PoorPerson : IPersonState
{
    public decimal GetSubsidy()
    {
        return 100m;
    }
}



[1] - Refactoring

[2] - Refactoring: Improving the Design of Existing Code

۰ نظر موافقین ۰ مخالفین ۰
ابراهیم خانی

software design patterns

الگوها برای اولین بار توسط Christopher Alexander معرفی شدند. او که استاد بازنشته دانشگاه کالیفرنیا در رشته معماری است، در سال 1977 کتاب خود با عنوان [1] مفهوم زبان الگو را اینگونه معرفی می کند : "فرآیند ساخت همه بنا های زیبا یکسان است. برای آشکار کردن آن می بایست دو کار انجام داد. ابتدا باید هر سازه را به کوچکترین جزء سازنده آن تجزیه کرد و سپس فرایندی برای کنار هم گذاشتن این اجزا تعریف کرد." او این اجزاء را الگو و فرایند کنار هم قرار دادن آنها را زبان الگو می نامد. (تاثیر این دیدگاه را به وضوح در کتاب [2] مشاهده کرد.) در نگاه Alexander الگوها منشا زیبایی هستند و زیبایی را نه با ابداع بلکه با ترکیب الگوها به وسیله یک زبان الگو ایجاد کرد که حاصل این ترکیب پدیده ای است که Alexander آن را "کیفیت بدون نام" [3] می نامد. Kent Beck در سال 1987 در مقاله ای به منظور مطرح کردن استفاده از ایده Alexander در نرم افزار، پنج الگو را در زبان Smalltalk پیاده سازی کرد. Eric Gamma در رساله دکترای خود در سال 1991 به کاربرد الگوها در طراحی نرم افزار اشاره کرد. اما نقطه عطف استفاده از الگوها در نرم افزار بدون تردید چاپ کتاب [4] است.

در مهندسی نرم افزار تعاریف مختلفی برای الگوها وجود دارد :

  •      Eric Gamma : پاسخی برای یک مشکل تکرار شونده در یک حوزه خاص است.
  •      Dirk Riehle : انتزاعی از یک شکل محسوس که در یک زمینه خاص به صورت مکرر اتفاق می افتد.
  •      Brad Appleton : قطعه ای از دانش که ماهیت یک خانواده از پاسخ ها به مشکلات تکرار شونده در یک دامنه خاص را در بر می گیرد.

با نگاه مجرد به این تعاریف، می توان یک تعریف ساده و فراگیر برای الگوها ارایه کرد. در این تعریف هر الگو از سه قسمت تشکیل شده است، وضعیت بد [5]، وضعیت خوب [6] و مجموعه ای از الزامات [7]. در واقع الگوها راهکاری برای تبدیل وضعیت بد به وضعیت خوب، تحت الزماتی مشخص است. الگوها در سطوح مختلفی از نظر ریزدانگی [8] وجود دارند.

  •      الگوهای فرایند
  •      الگوهای مهندسی مجدد
  •      الگوهای معماری
  •      الگوهای طراحی
  •      الگوهای پیاده سازی

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

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

  •  آزمودن تصمیم های مطرح شده :  با استفاده از رویکرد تکراری افزایشی [9] و نمونه سازی اولیه [10].
  •   اتخاذ تصمیم های آزموده شده : استفاده مجدد [11] روش دیگر جهت رسیدن به این هدف است.

رویکرد اول در واقع همان رویکرد متدولوژی های چابک [12] است که صحبت در مورد آن موضوع کتاب ها مختلف است. اما در مورد رویکرد دوم باید گفت، استفاده مجدد در سطح پایین انتزاع (استفاده از مولفه [13]) مشکلاتی به دنبال خواهد داشت. از طرفی یافتن مولفه ای که تمامی نیازها را برآورده کند به ندرت عملی می شود و از طرف دیگر تغییر دادن مولفه در صورت امکان هم، خطراتی برای جامعیت مولفه به دنبال خواهد داشت. در واقع استفاده از الگوها، استفاده مجدد در سطح طراحی (سطحی مجرد تر از پیاده سازی) را فراهم می کنند. نکته ای که نباید از آن غافل شد اصالت الگوهاست (راه حل جدید الگو نیست) چرا که همانطور که اشاره شد، الگوها راه حل های شناخته شده هستند.



[1] - The Timeless Way of Building

[2] - Pattern Oriented Software Architecture Volume 5: On Patterns and Pattern Languages

[3] - Quality without a name

[4] - Design Patterns Elements Of Reusable Software Development

[5] - Bad smell

[6] - Good smell

[7] - Force

[8] - Granularity

[9] - Iterative-incremental

[10] - Prototyping 

[11] - Reuse

[12] - Agile

[13] - Module

۰ نظر موافقین ۰ مخالفین ۰
ابراهیم خانی