مفهوم S.O.L.I.D در برنامه نویسی چیز جدیدی نیست و به سال ها قبل بر میگردد. موضوعی که دانستن آن برای هر برنامه نویسی امری مهم بوده و می تواند شیوه نوشتن کدهای شما را کاملاً تغییر دهد.
من خودم به شخصه مقالات و مطالب زیادی را مطالعه کردم و بتوانم درک دقیقی از این موضوع داشته باشم و تصمیم دارم در این مقاله، اصول S.O.L.I.D رو برای شما با زبانی ساده توضیح دهم. مثال هایی که در این مطلب زده خواهد شد، با زبان سی شارپ می باشد که لازمه درک آن ها داشتن دانش مقدماتی از زبان سی شارپ است. برای آشنایی با زبان سی شارپ می توانید سری آموزش مقدماتی زبان سی شارپ که توسط بنده در وب سایت قرار داده شده را مطالعه کنید.
بعضی افراد میگن که من برنامه نویسی شئ گرا رو بلدم، پس چه نیازی به یادگیری S.O.L.I.D هست؟ ما باید در ابتدا این اصول رو از هم تفکیک کنیم! این سوال مثل این هست که فردی بگه من ابزارهای چوب بری رو در اختیار دارم، پس چه نیازی هست که برم علم چوب بری رو یاد بگیرم!
شما با یاد گیری برنامه نویسی شئ گرا، در حقیقت یاد میگیرد که چگونه از کلاس ها استفاده کنید، چگونه برای یک کلاس فیلد یا خصوصیت تعریف کنیم و ...، اما اینکه بتوانیم از این قابلیت ها در مسیر درست استفاده کنیم موضوع دیگری است، SOLID در حقیقت یکسری قواعد رو برای ما تعریف میکنه که مثل یک مکمل برای برنامه نویسی شئ گرا عمل می کنند و با رعایت این قوانین ما می توانیم به تکنیک های برنامه نویسی شئ گرا رو به صورت صحیح استفاده کنیم.
اما موضوع دیگری نیز وجود دارد! ما مبحثی دیگری به نام الگوهای طراحی داریم که نباید با مبحث S.O.L.I.D اشتباه شود. الگوهای طراحی، مجموعه ای از کدها و راه حل های از پیش نوشته شده هستند که هر کدام مشکلی را حل می کنند، یعنی در شرایطی مشخص می توان از الگوهای طراحی برای حل مشکل استفاده کرد.
اما همانطور که گفته شد، SOLID مجموعه ای از قواعد رو تشکل می دهند.اصول SOLID، مانند برنامه نویسی شئ گرا متعلق به یک زبان برنامه نویسی خاص نیست، یعنی هر زبانی که از اصول شئ گرایی پشتیبانی کند، اصول SOLID در آن قابل اعمال است.اما بهتره به سراغ موضوع اصلی مطلبمون بریم، هدف ما آشنایی با قواعد پنج گانه SOLID است، SOLID مخفف پنج عبارت است که این عبارات به شرح زیر می باشند:
در طول پنج مطلب جداگانه به بررسی هر یک از این مفاهیم خواهیم پرداخت.
برنامه ای که قصد نوشتن آن را دارید باید مانند یک سازمان قانون مند عمل کند! یک سازمان موفق را در نظر بگیرید، تمامی وظایف در این سازمان مشخص است، هر شخص یکسری وظایف دارد و هیچ کس در وظایف شخص دیگر دخالت یا ورود نمی کند.
اگر شخصی نیاز به انجام عملیات در حوزه دیگری را داشته باشد، این وظیفه به شخص مورد نظر در آن بخش محول میشود. برنامه شما هم باید به همین صورت عمل کند. یعنی هر یک از بخش های برنامه شما دقیقاً و صرفاً یک وظیفه انجام یک عملیات خاص را انجام می دهد.
بعضی وقت ها که به گذشته بر میگردم و برنامه هایی که قبلاً نوشتن رو مرور میکنم، ناگهان یک Method و میبینم که حدود 400 خط کد براش نوشته شده! اما با تفکری که الان دارم، به نظرم متدها بیشتر از 10 الی 15 خط کد نیاز ندارن! به این خاطر که هر کلاس و هر Method از برنامه ای که قصد نوشتن اون رو دارم باید فقط یه وظیفه مشخص رو انجام بده و در صورت وابسته بودن به یک عملیات دیگه، انجام اون عملیات باید به بخش مربوطه تو نرم افزار محول بشه. با یک مثال این موضوع رو بیشتر بررسی میکنیم، کد زیر را در نظر بگیرید:
public class CustomersRepository { public void Add() { try { // add customer to database } catch (Exception ex) { System.IO.File.WriteAllText("d:\\errors.txt", ex.ToString()); } } }
در کد بالا، کلاسی با نام CustomersRepository داریم که عملیات اضافه کردن مشتری به Database در آن انجام می شود. اما در صورتی که خطایی در این پروسه رخ دهد، خطای مربوطه در فایلی ثبت می شود. اما مشکل کجاست؟ متد Add در کلاس StudentRepository وظیفه ای غیر از وظیفه اصلی ای که برایش مشخص شده، یعنی اضافه کردن مشتری را انجام می دهد.
با توجه به قاعده Single Reposibility، کد بالا می بایست اصلاح شده و وظیفه ثبت خطا به قسمت دیگری از برنامه محول شود. برای اینکار بتدا کلاس دیگری برای عملیات ثبت خطاها تعریف میکنیم:
public class FileLogger { public void Log(string content) { System.IO.File.WriteAllText("d:\\errors.txt", content); } }
در قدم بعدی کلاس CustomersRepository را طوری تغییر می دهیم که وظیفه ثبت خطا را به کلاس FileLogger محول کنید:
public class CustomersRepository { FileLogger logger = new FileLogger(); public void Add() { try { // add customer to database } catch (Exception ex) { logger.Log(ex.ToString()); } } }
با تغییرات بالا، قاعده Single Reponsibility را در کد خود پیاده کردیم. در قسمت بعدی، با قاعده Open Closed آشنا خواهیم شد.
بحث قواعد SOLID رو با مبحث Open Closed Principle ادامه می دهیم. با یک مثال شروع می کنیم، فرض کنید که یک کلاس برای محصولات داریم:
public class Product { public string Name { get; set; } public string Price { get; set; } }
حال محصول را به سه نوع مختلف تقسیم بندی می کنیم که بر اساس نوع های متفاوت، درصدی تخفیف برای محصول حساب می شود:
public class Product { public int ProductType { get; set; } public string Name { get; set; } public int Price { get; set; } public double GetDiscount() { if (ProductType == 1) { return (Price/100)*5; } if (ProductType == 2) { return (Price/100)*10; } if (ProductType == 3) { return (Price/100)*15; } return 0; } }
حال فرض کنید که بخواهیم نوع چهارمی از تخفف را برای کلاس Product در نظر بگیریم، می بایست یک دستور if دیگر به متد GetDiscount اضافه کنیم و با هر بار تغییر، کلاس Product تغییر می کند. در اینجا ما باید قاعده Open Closed را اعمال کنید
در حقیقت این قاعده به ما می گوید که یک کلاس برای تغییرات باید بسته باشد، یعنی ما اجازه تغییر کلاس برای افزودن امکانات جدید را نداریم، اما راه برای ایجاد Extension یا افزونه جدیدی برای کلاس باز است. در مثال بالا و بر اساس قاعده Open Closed، ما یک کلاس پایه تعریف می کنیم و به ازای هر نوع محصول، یک کلاس فرزند برای آن ایجاد می کنیم:
public class Product { public string Name { get; set; } public int Price { get; set; } public virtual double GetDiscount() { return 0; } } public class ProductType1 : Product { public override double GetDiscount() { return (Price/100)*10; } } public class ProductType2 : Product { public override double GetDiscount() { return (Price/100)*15; } }
حال، در صورت اضافه شدن نوع محصول یا نوع جدیدی از تخفیف، به جای تغییر کلاس Product، به راحتی یک کلاس فرزند ایجاد کرده و متد GetDiscount را در آن Override می کنیم. در نکته بعدی در مورد قاعده Liskov Substitution صحبت خواهیم کرد.
در ادامه توضیحات قواعد 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 است و دیگر آن را نقض نمی کند. در قسمت بعدی در مورد IS یا Interface Segregation صحبت خواهیم کرد.
زمانی که در حال نوشتن کدی هستید و در این کد از 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 را نیز پشتیبانی می کند. در قسمت بعدی که قسمت پایانی می باشد، در مورد قاعده Dependency Inversion توضیح خواهیم داد.
در این قسمت با آخرین قاعده 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 را در کد خود اعمال کردیم. امیدوارم که مطالعه این پنج قسمت که راجع SOLID بود، به دوستان کمک کند تا کدهایی که می نویسیند به استانداردهای برنامه نویسی شئ گرا نزدیکتر بشه.
بنیانگذار توسینسو و برنامه نویس و توسعه دهنده ارشد وب
حسین احمدی ، بنیانگذار TOSINSO ، توسعه دهنده وب و برنامه نویس ، بیش از 12 سال سابقه فعالیت حرفه ای در سطح کلان ، مشاور ، مدیر پروژه و مدرس نهادهای مالی و اعتباری ، تخصص در پلتفرم دات نت و زبان سی شارپ ، طراحی و توسعه وب ، امنیت نرم افزار ، تحلیل سیستم های اطلاعاتی و داده کاوی ...
زمان پاسخ گویی روز های شنبه الی چهارشنبه ساعت 9 الی 18
فقط به موضوعات مربوط به محصولات آموزشی و فروش پاسخ داده می شود