HamidReza Ireh

حمیدرضا ایره

HamidReza Ireh

حمیدرضا ایره

مقدمه
SOLID مخفف چند مفهوم مختلف هست که فهم این مطالب میتواند شما را در هنر برنامه نویسی به سطح پیشرفته‌تری برساند.
S مخفف Single responsibility principle یا SRP به معنی اینکه هر کلاس بایستی فقط یک کار انجام دهد نه بیشتر.
O مخفف Open/closed principle یا OCP به معنی اینکه کلاس‌ها جوری نوشته بشن که قابل گسترش باشند اما نیاز به تغییر نداشته باشند.
L مخفف Liskov Substitution Principle یا LSP به مفهوم اینکه هر کلاسی که از کلاس دیگر ارث بری می‌کند هرگز نباید رفتار کلاس والد را تغییر دهد.
I مخفف Interface Segregation Principle با ISP به مفهوم اینکه چند اینترفیس کوچک و خورد شده همیشه بهتر از یک اینترفیس کلی و بزرگ است.
D مخفف Dependency inversion principle یا DIP به معنی اینکه از اینترفیس‌ها به خوبی استفاده کن!
ما  این اصول رو یاد می‌گیریم تا بتوانیم نرم افزار بهتری را ایجاد کنیم یا به عبارتی اصولی‌تر کد نویسی کنیم.

SRP - Single Responsibility Principle
"به دنبال ماژول‌های تک مسئولیتی باش"
هر کلاس بایستی فقط و فقط یک وظیفه را برعهده داشته باشد. وقتی دو دلیل مختلف برای تغییر یک کلاس وجود داشته باشد بنابراین ممکن است دو تیم مختلف این کار را انجام دهند. در نهایت یک کلاس توسط دو تیم مختلف ویرایش می شود و این سبب می شود تا پروسه سوال و جواب برای هماهنگی و … طولانی شود. برای درک بیشتر مسئله مثال زیر را در نظر بگیرید.
فرض کنید کلاسی با نام Book دارید که وظیفه مدیریت عناوین و محتوای کتاب را بر عهده دارد مانند کلاس زیر:
public class Book {
public String getTitle() {
return "A Great Book";
}

public String getAuthor() {
return "John Doe";
}

public String getCurrentPage() {
return "current page content";
}

public void printCurrentPage() {
System.out.println("current page content");
}
}
این کلاس به هر حال کاری که از آن انتظار داریم انجام می‌دهد و میتوان گفت به ظاهر کلاس خوبی نوشته ایم. اما مشکلی که وجود دارد این است که وظیفه‌ی پرینت کردن صفحه با وظیفه‌ی مدیریت یک کتاب وظایف بسیار متفاوتی هستند. لذا با این ساختار در واقع SRP را نادیده گرفته‌ایم. اما بیایید همین مثال را در قالب SRP پیاده سازی کنیم:
public class Book {
public String getTitle() {
return "A Great Book";
}

public String getAuthor() {
return "John Doe";
}

public String getCurrentPage() {
return "current page content";
}
}

public interface Printer {
public void printCurrentPage(String currentPage);
}

public class PlainTextPrinter implements Printer {
public void printCurrentPage(String currentPage) {
System.out.println("PlainTextPrinter" + currentPage);
}
}

public class HtmlPrinter implements Printer {
public void printCurrentPage(String currentPage) {
System.out.println("HtmlPrinter:" + currentPage);
}
}
با روش جدید ما ساختار مدیریت کتاب را از بخش پرینت جدا کردیم و همچنین ممکن است به انواع فرمت‌های مختلف بخواهیم خروجی پرینت داشته باشیم. با تعریف یک اینترفیس برای کلاس‌های پرینت می‌توانیم ساختار مشترکی برای آنها ایجاد کنیم و مجددا وظیفه پرینت هر فرمت را به کلاس مستقلی بسپاریم. به نظر شما با این روش انعطاف پذیری برنامه نویس بیشتر نمی شود؟ اگر روزی قرار باشد پرینت با فرمت جدیدی به پروژه افزوده شود نیازی به تغییر کلاس های دیگر نخواهد بود و فقط کلاس جدیدی به مجموعه افزوده خواهد شد که همین باعث حفظ ساختار، تفکیک وظایف، مدیریت بهتر و خطای کمتر خواهد بود.
هنگام طراحی نرم افزار بایستی وظایف پایه از وظایف فرعی تشخیص داده شود. یک کتاب همواره نیازمند عنوان و نام نویسنده است اما همیشه نیاز به خروجی ندارد. اینطور در نظر بگیرید که اگر دیگران بخواهند از کلاس شما در پروژه های خودشان استفاده کنند آیا متد پرینت یک متد الزامی برای آنها خواهد بود؟ به این وسیله می‌توانید وظایف اصلی را از وظایف فرعی جدا نمایید. بنابراین پس از رعایت SRP عملا کلاس Book یک کلاس پایه خواهد بود و در هر پروژه ای میتوانید آنرا استفاده نمایید.
در نهایت بایستی به این نکته اشاره کنیم که ما در ایران معمولا مصرف کننده تکنولوژی هستیم و می‌توان گفت تولید تکنولوژی در بخش نرم افزار در ایران در حد صفر است. شاید یکی از علت‌ها این باشد که ما از آخرین تکنولوژی‌ها استفاده نمی‌کنیم. به روز نیستیم و در نهایت با معایب تکنولوژی‌های جدید آشنا نخواهیم شد. تکنولوژی زمانی تولید می‌شود که نیاز آن احساس شود و یا برطرف کننده معایب تکنولوژی‌های قدیمی تر باشد. اما تا وقتی که ما از جدید ترین
تکنولوژی ها استفاده نکنیم و به روز نباشیم چطور خواهیم توانست روش جدید و بهتری ابداع کنیم؟

OCP - Open/Close Principle
"پذیرای توسعه و بازدارنده از تغییر هر آنچه که هست، باشید"
منظور از OCP این است که برنامه نویس بایستی کلاس‌ها و اشیاء را طوری ایجاد نماید که همواره امکان گسترش (Extend) آن وجود داشته باشد اما برای گسترش نیازی به تغییر در کلاس‌های اصلی نباشد. یعنی اینکه کدها و کلاس‌ها بایستی همواره برای گسترش باز باشند اما برای ویرایش و تغییر بسته باشند. البته منظور از بسته بودن برای ویرایش این است که نیازی به تغییر کدها نباشد. در بهترین حالت این اصل در برنامه نویسی شی گرا باعث کاهش ارتباطات کلاس‌ها و ماژول‌ها خواهد شد. در نهایت امکان توسعه از طریق افزودن کلاس‌های جدید و نه تغییر کلاس‌های موجود میسر خواهد شد.
با کمی دقت متوجه می‌شویم که OCP و SRP مکمل همدیگر هستند. البته به این معنی نیست که هر جا SRP را رعایت کرده باشیم پس OCP را نیز رعایت کرده‌ایم. اما با رعایت هر یک از اصول دستیابی به مورد دیگر راحت تر و ساده تر خواهد بود.
به عنوان مثال کلاس‌های زیر را در نظر بگیرید:
public class TXTFile {
public Integer length;
public Integer sent;
}
public class Progress {
private TXTFile txtFile;

public Progress(TXTFile txtFile) {
this.txtFile = txtFile;
}

public Integer getAsPercent() {
return txtFile.sent * 100 / txtFile.length;
}
}
این طرز کد نویسی اصل OCP را نقض میکند. چرا؟ علت این است که کلاس Progress متغیر file از نوع TXTFile را به عنوان ورودی دریافت می‌کند و نوع دیگری از فایل ها را نمی‌شناسد. پس اگر در آینده بخواهیم به جز فایل txt فایل دیگری مثل mp3 به این بخش اضافه کنیم باید کلاس Progress را تغییر دهیم تا این نوع از فایل را نیز شناسایی کند.
برای حل این مشکل بهتر است مشخص کنیم تمام ورودی هایی که به کلاس Progress خواهیم داد از یک استاندارد پیروی می‌کنند. وقتی صحبت از استاندارد بین کلاس‌ها می‌شود معمولا پای یک اینترفیس یا یک کلاس انتزاعی(abstract) در میان است!
public interface MeasurableInterface {
Integer getLength();
Integer getSent();
}

public class File implements MeasurableInterface {
private Integer length;
private Integer sent;
private String filename;

public Integer getLength() {
return length;
}

public void setLength(Integer length) {
this.length = length;
}

public Integer getSent() {
return sent;
}

public void setSent(Integer sent) {
this.sent = sent;
}

public String getFilename() {
return filename;
}

public void setFilename(String filename) {
this.filename = filename;
}
}

public class Progress {
private MeasurableInterface measurableInterface;

public Progress(MeasurableInterface measurableInterface) {
this.measurableInterface = measurableInterface;
}

public Integer getAsPercent() {
return measurableInterface.getSent() * 100 / measurableInterface.getLength();
}
}
این روش در حال حاضر بهترین روشی است که هم SRP و هم OCP را به خوبی رعایت می‌کند. البته برخی ترجیح می‌دهند به جای استفاده از اینترفیس از یک کلاس انتزاعی استفاده کنند. بستگی به نوع الگوی طراحی دارد که انتخاب می‌کنند. در نهایت این روش باعث میشود که بدون تغییر کلاس اصلی بتوان انواع فایل‌های دیگر را به پروژه اضافه کرد.

LSP - Liskov Substitution Principle
"ارث بری باید به صورتی باشد که زیر نوع را بتوان بجای ابر نوع استفاده کرد"
یکی از اصول دیگر برنامه نویسی شی گرا و اصل سوم SOLID مفهوم LSP یا Liskov Substitution Principle می باشد، به این معنی که هیچ کلاسی نباید رفتار کلاس والد را تغییر دهد. برای رعایت این اصل باید در نظر داشته باشیم که هر کلاسی میتواند از کلاس دیگر ارث بری کند به شرطی که رفتار کلاس والد را تغییر ندهند. مثال زیر را در نظر بگیرید:‌
public class Rectangle {
protected int width;
protected int height;

public int getWidth() {
return width;
}

public void setWidth(int width) {
this.width = width;
}

public int getHeight() {
return height;
}

public void setHeight(int height) {
this.height = height;
}

public int area() {
return this.width * this.height;
}
}
کلاس Rectangle (مستطیل) را به عنوان یک کلاس پایه در نظر بگیرید. در دنیای واقعی شکل مربع هم نوعی مستطیل است. اما آیا در دنیای برنامه نویسی هم این موضوع صحیح است؟
public class Square extends Rectangle {
@Override
public void setHeight(int height) {
this.height = height;
this.width = height;
}

@Override
public void setWidth(int width) {
this.width = width;
this.height = width;
}
}
کلاس Square (مربع) از کلاس پایه ارث بری می‌کند. اما تفاوت این کلاس با کلاس پایه در این است که اگر هر یک از ابعاد طول و عرض مقدار دریافت کنند بعد دیگر نیز همان مقدار را خواهد داشت. یعنی اگر مقدار طول را 4 در نظر بگیریم، قطعا مقدار عرض نیز 4 خواهد بود.(خاصیت مربع)
حالا کلاس زیر را در نظر بگیرید:
public class Client {
public void areaVerifier(Rectangle rectangle) throws Exception {
rectangle.setWidth(5);
rectangle.setHeight(4);
if (rectangle.area() != 20)
throw new Exception("Bad area!");
else
System.out.println("Ok");
}
}
این کلاس ورودی از نوع Rectangle را دریافت کرده و طول و عرض آنرا مقدار دهی می‌کند و در نهایت بررسی می‌کند که آیا مساحت درست حساب شده است یا خیر. در ظاهر همه چیز خیلی خوب کار خواهد کرد. اما وقتی کلاس Square که نوعی از Rectangle هست را به عنوان ورودی به تابع areaVerifier بدهیم پاسخ کمی تغییر می کند. وقتی به متد setHeight(4) می رسیم بر اساس کاری که Square انجام می‌دهد مقدار Width را نیز برابر ۴ قرار می‌دهد. بنابراین مساحت ۱۶ خواهد شد نه ۲۰.
این موضوع هدف اصلی LSP است و به ما آموزش می‌دهد که هنگام ارث بری از یک کلاس نباید رفتار کلاس والد را تغییر دهیم.
فرض کنید در یک کلاس پایه متدی با نام writeToFile دارید. هر کسی از این متد انتظار ذخیره اطلاعات در یک فایل را دارد. اما فرض کنید که  یکی از کلاس‌های زیر مجموعه با فراخوانی این متد کار دیگری انجام دهد! LSP بر روی این نکته تمرکز کرده است تا این مشکلات به وجود نیایند.

ISP - Interface Segregation Principle
"واسط‌های کوچک بهتر از واسط‌های حجیم است"
یکی از اصول دیگر برنامه نویسی شی گرا و اصول SOLID، مفهوم ISP یا Interface Segregation Principle می‌باشد، به این معنی که برای استفاده از اینترفیس ها آنها را باید به اجزای کوچکتری تقسیم کرد. وقتی یک کلاس از یک اینترفیس بزرگ استفاده می‌کند ممکن است برخی از این متدها در کلاس مورد نظر قابل استفاده نباشند. اما وقتی یک اینترفیس بزرگ به چند اینترفیس کوچک تقسیم می‌شود هر کلاس می‌تواند در صورتی که به اینترفیس خاصی نیاز داشت از آن استفاده نماید. با این امکان اگرچه تعداد اینترفیس‌ها بیشتر می‌شوند و ممکن است تکرار رخ دهد اما به دلیل اینکه منطق برنامه ما در اینترفیس ها اجرا نمی شود میتوان این مسئله را نادیده گرفت. در نهایت با رعایت این اصل امکان دیباگ و بررسی کد‌ها سرعت بیشتری خواهد داشت.
تصویر زیر را در نظر بگیرید :

اینترفیس VEHICLE برای کلاس‌های مرتبط با حمل و نقل ایجاد شده است. این در صورتی است که کلاس‌هایی مانند HighWay و BusStation که از این اینترفیس استفاده می‌کنند نیازی به متدهایی مانند stopRadio و یا brake ندارند. این اصل SOLID تاکید بر این موضوع دارد که اینترفیس‌های بزرگ به اینترفیس‌های کوچک تر تبدیل شوند. تصویر زیر را در نظر بگیرید:
در تصویر بالا به جای یک اینترفیس بزرگ دو اینترفیس کوچک ایجاد کردیم. گرچه برخی از متدها تکرار شده اند اما همانطور که در ابتدای موضوع مطرح شد این اینترفیس‌ها قرار نیست منطق برنامه ما را تشکیل دهند لذا اصل اول SOLID را نقض نمی‌کند. استفاده از اینترفیس‌های کوچک به ما کمک می‌کند تا مشکلات را سریع تر شناسایی کنیم، راحت تر تست بگیریم و راحت تر کدهای نوشته شده را درک کنیم.

 DIP - Dependency Inversion Principle
"وابستگی بین ماژول‌ها  را به وابستگی آن‌ها به انتزاع (واسط) تغییر بده"
DIP مفهومی است که وابستگی مستقیم کلاس‌های سطح بالا را به کلاس‌های سطح پایین منع می‌کند. به این منظور که اگر کلاس خاصی(high-level) که از کلاس های دیگر(low-level) استفاده می‌کند وابستگی مستقیمی با کلاس های low-level داشته باشد سبب بروز این مشکل خواهد شد که اگر کلاس low-level دیگری به مجموعه افزوده شود اجبارا کلاس high-level نیز بایستی تغییر کند. DIP برای حل این مشکل به وجود آمده است. برای مثال تصویر زیر را در نظر بگیرید:
فرض کنید کلاسی با نام Copy داریم که وظیفه دریافت ورودی از KeyboardReader و ارسال آن به PrinterWriter را دارد. در این مثال، کلاس Copy از نوع high-level است. فرض کنید کدی که برای حل مسئله استفاده شده است مانند زیر باشد:
public class KeyboardReader {
public String Read() {
Scanner scanner = new Scanner(System.in);
return scanner.nextLine();
}
}

public class PrinterWriter {
public void Write(String output) {
System.out.println(output);
}
}

public class Copy {
private KeyboardReader reader;
private PrinterWriter writer;

public void DoWork() {
reader = new KeyboardReader();
writer = new PrinterWriter();
writer.Write(reader.Read());
}
}

public class App {
public static void main(String[] args) {
Copy badCopy = new Copy();
badCopy.DoWork();
}
}
همانطور که مشاهده می‌کنید کلاس Copy مستقیما از KeaboardReader و PrinterWriter استفاده کرده است، حال فرض کنید اگر علاوه بر PrinterWriter قرار باشد FileWriter نیز به پروژه افزوده شود اجبارا کلاس Copy نیز بایستی تغییر کند. یکی از راه های ممکن برای حل این مشکل خارج کردن وابستگی کلاس Copy به کلاس‌های low-level است. بدین منظور از interface ها و یا کلاس‌های abstract استفاده می‌کنیم:
در تصویر فوق کلاس Copy به جای وابسته شدن به کلاس‌های low-level از اینترفیس ها استفاده می‌کند و این باعث می‌شود که وابستگی Copy به کلاس‌های سطح پایین به حداقل ممکن برسد. کد بهینه شده را می‌توانید مشاهده نمایید:
public interface IReader {
String Read();
}

public class KeyboardReader implements IReader {
public String Read() {
Scanner scanner = new Scanner(System.in);
return scanner.nextLine();
}
}

public interface
IWriter {
void Write(String output);
}

public class
FileWriter implements IWriter {
public void Write(String output) {
System.out.println("FileWriter:" + output);
}
}

public class PrinterWriter implements IWriter {
public void Write(String output) {
System.out.println("PrinterWritter:" + output);
}
}

public class Copy {
private IReader reader;
private IWriter writter;

public Copy(IReader iReader, IWriter iWriter) {
this.reader = iReader;
this.writter = iWriter;
}

public void DoWork() {
writter.Write(reader.Read());
}
}
public class App {
public static void main(String[] args) {
IReader reader = new KeyboardReader();
IWriter writerP = new PrinterWriter();
Copy copyP = new Copy(reader, writerP);
copyP.DoWork();

IWriter writerF = new FileWriter();
Copy copyF = new Copy(reader, writerF);
copyF.DoWork();
}
}
همانطور که مشاهده می‌کنید کلاس Copy تنها به اینترفیس‌ها وابستگی پیدا کرده است که سبب می شود مشکلاتی که در بالا اشاره شد ایجاد نشود.

نظرات  (۰)

هیچ نظری هنوز ثبت نشده است

ارسال نظر

کاربران بیان میتوانند بدون نیاز به تأیید، نظرات خود را ارسال کنند.
اگر قبلا در بیان ثبت نام کرده اید لطفا ابتدا وارد شوید، در غیر این صورت می توانید ثبت نام کنید.
شما میتوانید از این تگهای html استفاده کنید:
<b> یا <strong>، <em> یا <i>، <u>، <strike> یا <s>، <sup>، <sub>، <blockquote>، <code>، <pre>، <hr>، <br>، <p>، <a href="" title="">، <span style="">، <div align="">
تجدید کد امنیتی