دوشنبه , خرداد ۷ ۱۳۹۷
خانه / مقالات / معماری SOLID
معماری SOLID

معماری SOLID

معماری SOLID  – پنج اصل SOLID برای طراحی شی گرا

S.O.L.I.D مخفف شده از:

  • Single-responsiblity principle – S
  • Open-closed principle – O
  • Liskov substitution principle – L
  • Interface segregation principle – 
  • Dependency Inversion Principle – D

معماری SOLID – بخش اول Single-responsiblity principle

یک کلاس باید یک و تنها یک دلیل برای تغییر داشته باشد، به این معنی که یک کلاس فقط باید یک شغل داشته باشد.

به عنوان نمونه به مثال زیر دقت کنید ما چند شکل داریم و میخواهیم تمام زیمنه های شکل را جمع کنیم.

class Circle {
    public $radius;

    public function __construct($radius) {
        $this->radius = $radius;
    }
}

class Square {
    public $length;

    public function __construct($length) {
        $this->length = $length;
    }
}
ابتدا  ما کلاس های اشکال را ایجاد می کنیم و سازندگان  مورد نیاز را تنظیم می کنیم . سپس با ایجاد کلاس AreaCalculator حرکت می کنیم و سپس منطق ما را برای جمع کردن مناطق تمام اشکال ارائه می دهیم.
class AreaCalculator {

    protected $shapes;

    public function __construct($shapes = array()) {
        $this->shapes = $shapes;
    }

    public function sum() {
        // logic to sum the areas
    }

    public function output() {
        return implode('', array(
            "",
                "Sum of the areas of provided shapes: ",
                $this->sum(),
            ""
        ));
    }
}
برای استفاده از کلاس AreaCalculator، ما به سادگی یک شی از کلاس ها گرفته و در آرایه ای از اشکال منتقل می کنیم و خروجی را در پایین صفحه نمایش می دهیم.
$shapes = array(
    new Circle(2),
    new Square(5),
    new Square(6)
);

$areas = new AreaCalculator($shapes);

echo $areas->output();
AreaCalculator مشکلش با روش خروجی این است که منطق را برای خروج داده ها کنترل نمی کند. بنابراین، اگر کاربر خواستار خروجی داده ها به عنوان json یا چیزی دیگری باشد، چه؟ تمامی logic مورد نظر ما باید در کلاس AreaCalculator پشتیبانی شود. کلاس AreaCalculator در واقع فقط باید جمع ببندد ناحیه های شکل ایجاد شده را و لازم نیست توجه داشته باشید که آیا کاربر  خروجی json یا HTML میخواهد. بنابراین شما میتواند کلاس دیگری را مثلا بنام SumCalculatorOutputter ایجاد کنید و نحوه خروجی فایل ها را در آن مشخص کنید که کلاسی مشابه نمونه زیر خواهد بود.
$shapes = array(
    new Circle(2),
    new Square(5),
    new Square(6)
);

$areas = new AreaCalculator($shapes);
$output = new SumCalculatorOutputter($areas);

echo $output->JSON();
echo $output->HAML();
echo $output->HTML();
echo $output->JADE();

معماری SOLID – بخش دوم Open Close Principle

فرض میکنیم که شما میخواهید یک طبقه بین طبقه‌ی اول و دوم خانه‌ی 2 طبقه‌ی خود اضافه کنید. فکرمیکنید امکان پذیر است؟

راه حل هایی که ممکن است به ذهن شما خطور کنند :

1- زمانی که برای اولین بار در حال ساخت خانه هستید آن را 3 طبقه بسازید و طبقه‌ی وسط را خالی نگه دارید.اینطوری هر زمان که شما بخواهید میتوانید از آن استفاده کنید. به هر حال این هم یک راه حل است.
2- خراب کردن طبقه دوم و ساخت دو طبقه‌ی جدید که خوب اصلا معقول نیست.
کد زیر را مشاهده کنید :
public class EmployeeDB
{
    public void Insert(Employee e)
    {
        //Database Logic written here
    }
    public Employee Select()
    {
        //Database Logic written here
    }
}
متد Select در کلاس EmployeeDB توسط کاربران و مدیر مورد استفاده قرار میگیرد. در این بین ممکن است مدیر نیاز داشته باشد تغییراتی را در آن انجام دهد. اگر مدیر این کار را برای برآورده کردن نیاز خود انجام دهد ،روی دیگر قسمت‌ها نیز تاثیر میگذارد، به علاوه ایجاد تغییرات در راه حل‌های تست شده‌ی موجود ممکن است موجب خطاهای غیر منتظره ای شود.
چگونه ممکن است که رفتار یک برنامه تغییر کند بدون اینکه کد آن ویرایش شود؟ چگونه می‌توانیم بدون تغییر یک موجودیت نرم افزاری کارکرد آن را تغییر دهیم؟
اصل OCP میگوید : “ماژول‌های نرم افزار باید برای تغییرات بسته و برای توسعه باز باشند.”
 راه حل استفاده از وراثت (inheritance):
ایجاد یک کلاس جدید به نام EmployeeManagerDB که از کلاس EmployeeDB ارث بری کند و متد Select آن را جهت نیاز خود بازنویسی کند.
public class EmployeeDB
{
    public virtual Employee Select()
    {
        //Old Select Method
    }
}
public class EmployeeManagerDB : EmployeeDB
{
    public override Employee Select()
    {
        //Select method as per Manager
        //UI requirement
    }
}
این انتخاب خیلی خوبی است در صورتی که این تغییرات در زمان طراحی اولیه پیش بینی شده باشد و همکنون قابل استفاده باشند.
کد UI هم به شکل زیر خواهد بود :
//Normal Screen
EmployeeDB objEmpDb = new EmployeeDB();
Employee objEmp = objEmpDb.Select();

//Manager Screen
EmployeeDB objEmpDb = new EmployeeManagerDB();
Employee objEmp = objEmpDb.Select();

معماری SOLID – بخش سوم Liskov substitution principle

در ادامه توضیحات قواعد SOLID، قاعده LSP یا Liskov Substitution Principal را بررسی می کنیم. اگر بخواهیم این قاعده رو توضیح دهیم اینطور می توان توضیح داد: «فرض کنید کلاس C از کلاس B مشتق شده است، بر اساس قاعده LSP، در هر قسمت از برنامه که شئ ای از نوع B استفاده شده است، باید بتوان شئ ای از نوع C را جایگزین کرد، بدون اینکه تغییری در روند اجرای برنامه رخ دهد یا پیغام خطایی دریافت کنیم!» جا افتادن مفهوم این قاعده کمی دشوار است، اما سعی می کنیم با مثالی ساده از پیچیدگی این موضوع کم کنیم و بتوانیم توضیح شفافی از LSP ارائه دهیم. ابتدا حالتی را پیاده سازی می کنیم که قاعده LSP را نقض می کند و در قدم بعدی کد را اصلاح می کنیم که مطابق قاعده LSP باشد. کد زیر را در نظر بگیرید:

public class CollectionBase
{
    public int Count { get; set; }
}

public class Array : CollectionBase
{
        
}

بر اساس قواعد OOP، شما از کد بالا می توانید به صورت زیر استفاده کنید:

CollectionBase collection = new Array();
var items = collection.Count;

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

public class CollectionBase
{
    public int Count { get; set; }

    public virtual void Add(object item)
    {
            
    }
}

public class List : CollectionBase
{
    public override void Add(object item)
    {
        // add item to list
    }
}

public class Array : CollectionBase
{
    public override void Add(object item)
    {
        throw new InvalidOperationException();
    }
}

دقت کنید، متد Add را داخل CollectionBase تعریف کردیم، کلاس List از متد Add پشتیبانی می کند اما کلاس آرایه به دلیلی که بالا گفتیم زمان فراخوانی متد Add، ایجاد خطا می کنید:

CollectionBase array = new Array();
CollectionBase list = new List();
list.Add(2); // works
array.Add(3); // throw exception

کد بالا بدون مشکل کامپایل می شود، اما زمانی که برنامه اجرا شود، زمان اضافه کردن آیتم به آرایه پیغام خطا دریافت می کنیم، با این اوصاف کد بالا قاعده LSP را نقض کرده! زیرا همانطور که در بالا گفتیم در صورت استفاده از کلاس پایه به عنوان Data Type و قرار دادن شئ ای از نوع فرزند در آن، برنامه بدون مشکل باید کار کند. راه حل این مشکل چیست؟ به روش های مختلف می توان مشکل را حل کرد، اما در اینجا مکانیزم استفاده از interface ها را مطرح می کنیم، برای رفع مشک، کد بالا را به صورت زیر تغییر می دهیم:

public interface IList
{
    void Add(object item);
}

public class CollectionBase
{
    public int Count { get; set; }
}

public class List : CollectionBase, IList
{
    public void Add(object item)
    {
        // add item to list
    }
}

public class Array : CollectionBase
{
}

همانطور که مشاهده می کنید در کد بالا، متد Add، به جای تعریف در کلاس CollectionBase، داخل یک interface به نام IList تعریف شده و کلاس List این interface را پیاده سازی کرده است. با این کار، دیگر امکان فراخوانی متد Add برای کلاس Array وجود ندارد. کد بالا مبتنی بر قاعده LSP است و دیگر آن را نقض نمی کند.

معماری SOLID – بخش  چهارم Interface Segregation Principal

زمانی که در حال نوشتن کدی هستید و در این کد از interface ها استفاده می کنید، دقت کنید تنها می بایست اعضایی در دسترس بخش های برنامه باشند که به آن نیاز دارند! برای مثال، فرض کنید interface ای نوشتید که 10 متد در آن تعریف شده، حالا شاید همه قسمت های برنامه به این 10 متد نیازی نداشته باشند یا کسی که قرار است از کد شما استفاده کند، به همه این متدها نیازی ندارد، در اینجا باید interface خود را به interface های کوچکتر بشکنید تا تنها از interface هایی استفاده شود که به آن ها واقعاً نیاز است. برای مثال، کد زیر را در نظر بگیرید:

public interface IDatabaseManager
{
    void Add();
    void Remove(int id);
    void Persisit();
}

سه متد برای Interface بالا تعریف کردیم و در حال استفاده از این کد هستیم، حال برای قسمتی از برنامه نیاز به حذف گروهی موجودیت ها از database داریم، اولین کاری که می توانیم انجام دهیم اضافه کردن متدی با نام RemoveBatch به interface بالا است:

public interface IDatabaseManager
{
    void Add();
    void Remove(int id);
    void RemoveBatch(params int[] ids);
    void Persisit();
}

با این کار قاعده Interface Segregation را نقض کردیم، برای اصلاح کد بالا، می توانیم Interface جدیدی ایجاد کنیم که از interface قبلی مشتق شده است:

public interface IDatabaseManager
{
    void Add();
    void Remove(int id);
    void Persisit();
}

public interface IDbBatchOperations : IDatabaseManager
{
    void RemoveBatch(params int[] ids);
}

قسمت هایی از کد که در حال حاضر از interface اولی، یعنی IDatabaseManager استفاده می کنند بدون مشکل کار خود را انجام داده و متد RemoveBatch نیز برای آن ها قابل دسترس نیست، چون نیازی به آن ندارند، اما interface دوم، علاوه بر پشتیبانی از متدهای interface اول، متد RemoveBatch را نیز پشتیبانی می کند.

معماری SOLID – بخش آخر Dependency Inversion Principal

در این قسمت با آخرین قاعده SOLID، یعنی Dependency Inversion آشنا می شویم. زمانی که شما مبتنی بر تکنیک های شئ گرایی برنامه می نویسید، به طور حتم، کلاس هایی خواهید داشت که وابسته به کلاس های دیگر هستند. قاعده Single Responsiblity رو به یاد دارید؟ گفتیم هر کلاس باید تنها و تنها یک وظیفه خاص را انجام دهد و سایر وظایف را به کلاس های مربوطه محول کند. اما نباید ارتباط مستقیمی بین کلاس ها وجود داشته باشد! اصطلاحاً گفته میشه که باید ارتباط بین کلاس ها Loosely Coupled باشد. به مثال زیر دقت کنید:

public class EmailNotification
{
    public void Send(string message)
    {
        // send email
    }
}

public class DatabaseManager
{
    private EmailNotification notification;

    public DatabaseManager()
    {
        notification = new EmailNotification();
    }

    public void Add()
    {
        notification.Send("Record added to database!");
    }

    public void Remove()
    {
        notification.Send("Record removed to database!");
    }

    public void Persisit()
    {
        notification.Send("Changes submitted to database!");
    }
}

کلاسی داریم با نام DatabaseManager که با فراخوانی هر یک از متدهای آن، ایمیلی برای یک آدرس مشخص ارسال می شود. در کد بالا وظایف تقسیم بندی شده، یعنی قاعده SRP در نظر گرفته شده، اما ارتباطی که میان کلاس DatabaseManager و کلاس EmailNotification وجود دارد، مستقیم است. فرض کنید بخواهیم به جای ارسال رویداد بوسیله Email از پیامک استفاده کنیم، باید کلاس جدیدی تعریف شود و کلاس DatabaseManager تغییر کند تا رویدادها بوسیله پیامک ارسال شوند. اما با پیاده سازی مبتنی بر قاعده Dependency Inversion، این کار به راحتی امکان پذیر خواهد بود، برای این کار ابتدا یک interface با نام INotification تعریف می کنیم:

public interface INotification
{
    void Send(string message);
}

حال، کلاس هر کلاسی که عملیات ارسال رویداد را انجام می دهد، می بایست interface ای که تعریف کردیم را پیاده سازی کند، در زیر دو کلاس EmailNotification و SMSNotification را تعریف میکنیم:

public class EmailNotification : INotification
{
    public void Send(string message)
    {
        // send email
    }
}

public class SMSNotification : INotification
{
    public void Send(string message)
    {
        // send sms
    }
}

حال کلاس DatbaseManager را جوری تغییر می دهیم تا وابستگی آن نسبت به یک کلاس از بین رفته و وابسته به interface تعریف شده باشد:

public class DatabaseManager
{
    private INotification notification;

    public DatabaseManager(INotification notification)
    {
        this.notification = notification;
    }

    public void Add()
    {
        notification.Send("Record added to database!");
    }

    public void Remove()
    {
        notification.Send("Record removed to database!");
    }

    public void Persisit()
    {
        notification.Send("Changes submitted to database!");
    }
}

با تغییر بالا، کلاس DatabaseManager هیچ وابستگی به کلاس خاصی ندارد و می توان زمان ساخت شئ از روی آن، Dependecy مربوطه را برای آن مشخص کرد:

DatabaseManager manager = new DatabaseManager(new SMSNotification());

و در صورتی که بخواهیم از سرویس ایمیل استفاده کنیم:

DatabaseManager manager = new DatabaseManager(new EmailNotification());

با تغییرات انجام شده، قاعده Dependecy Inversion را در کد خود اعمال کردیم.

  • آیا این مقاله مفید بود؟
  • بله   خیر

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

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد.