در این مقاله سعی بر این داریم که اصول SOLID در برنامه نویسی را شرح دهیم . SOLID در واقع به الگوی برنامه نویسی تمیز و بی نقصی اشاره می کند که از کلمات زیر متشکل شده است که هر کدام از آنها بیان گر موضوعی خاص می باشند .

Single Responsibility PrincipleS
Open–Closed Principle - O
Liskov Substitution Principle L
Interface Segregation Principle I
Dependency Inversion Principle D

حال شروع می کنیم به تحلیل موارد بالا :
Single Responsibility Principle یا همان اصل مسئولیت واحد ، به این امر اشاره دارد که هر کلاس ، آبجکت و متد ، حتما و حتما باید یک کار مشخص را انجام داده و یک مسئولیت مشخص داشته باشند نه بیشتر، به اصطلاح هیچ کدام از کلاس ها نباید به صورت یک God Object عمل کنند و یک حالت اسمارت یا هوشمند داشته باشند ، بلکه باید به صورت احمقانه رفتار کرده و یک مسئولیت مشخص را انجام دهند به جای انجام دادن چندین کار مختلف ، در حقیقت باید یک لاجیک مشخص داشته باشند .
کلاس هایی که کارهای متفاوتی را انجام می دهند شبیه چاقوی های سوئیسی عمل می کنند که چندین وظیفه را با هم انجام می دهند ، نگه داری و به اصلاح Maintain کردن این نوع کلاس ها به شدت سخت و پیچیده می باشد .
به کد زیر نوجه کنید :
public class UserSettingService
{
  public void changeEmail(String email)  {
    if(isValidateEmail(email)) {
       //do anything
    }
  }
 
  public static boolean isValidateEmail(String email) {
    //check the email validation by regexp .etc
  }
}

 

در واقع کلاس UserSettingService اصل Single Responsibility Principle را رعایت نکرده است ، مسئولیت این کلاس تنظیمات دیتای یوزرها می باشد ، نه چک کردن validation ایمیل و ... ، در واقع برای چک کردن input validation ، باید یک آبجکت جداگانه طراحی شده و متد های validation را در آن پیاده سازی کرده و از آن در جاهای مختلف برنامه در صورت نیاز استفاده شود . مانند کد زیر :
public class InputValidationService
{
  public static boolean isValidateEmail(String email) {
    //check the email validation by regexp .etc
  }
}​

حال می توان از کلاس بالا در جاهای مختلف برنامه استفاده کرد :
public class UserSettingService
{
  public void changeEmail(String email) {
    if(InputValidationService.isValidateEmail(email))  {
       //do anything
    }
  }
}​


Open Closed Principle : این اصل که به OCP نیز معروف است  و نظریه پرداز این اصل Bertrand Meyer (از بزرگترین نظریه پردازان در حوضه نرم افزار) به این امر اشاره دارد که رفتار sub class ها نباید در تضاد با رفتار super class باشد . در حقیقت طبق گفته Bertrand Meyer این اصل به این موضوع اشاره دارد که کلاس ها ، ماژول ها ، توابع و ... باید برای گسترش و رشد کردن باز باشند ، ولی برای اصلاح یا modification ، بسته باشند .

فرض کنید کلاس هایی با نام های زیر داریم :
public class Car{

  private String name;
  private double price;

  //getters /setters
}


public class Bicycle{

  private String name;
  private double price;
  
  //getters /setters
}​


حال فرض کنید یک کلاس داریم که دارای یک متد به نام calculatePriceCount است و می خواهد قیمت تمامی ماشین ها و دوچرخه های موجود را با یک دیگر جمع کند :

class CaclulateFactory {
  
    public static double calculatePriceCount(List<Object> objects) {
        double priceCount = 0;
        for (Object o : objects) {
            if (o instanceof Car) {
                priceCount = priceCount + ((Car) o).getPrice();
            } else if (o instanceof Bicycle) {
                priceCount = priceCount + ((Bicycle) o).getPrice();
            } else {
                throw new RuntimeException("object not supported");
            }
        }
        return priceCount;
    }
}

 

همان طور که می بینید ، کد بالا یک کد بسیار کثیف و بد می باشد ، زیرا در حلقه for ، نوع تک تک object های ورودی را به صورت دستی چک کرده ایم و اگر یک نوع جدید از وسایل نقلیه ، به پروژه اضافه شود ، مجبور هستیم این کد را دوباره edit کرده و نوع جدید را نیز چک کنیم ، هم چنین حالت های استثنا در RuntimeException چک می شود که ممکن است در آینده دچار مشکلاتی شویم .

حال به جای کد های کثیف بالا ، با راه حل زیر پیش می رویم ، اول یک interface با نام Vehicle می سازیم ، بعد از آن ، تمامی کلاس های دیگر مانند Car, Bicycle و... که از نوع وسایل نقلیه هستند ، اینترفیس Vehicle را پیاده سازی می کنند :

public interface Vehicle {

    public double getPrice();
    
}
public class Bicycle implements Vehicle{
    private String name;
    private double price;
    
    //getters /setters

    @Override
    public double getPrice() {
        return price;
    }
 
}
public class Car implements Vehicle {

    private String name;
    private double price;

    //getters /setters


    @Override
    public double getPrice() {
        return price;
    }

}

 

حال بدنه متد calculatePriceCount را دوباره ، پیاده سازی می کنیم که بسیار تمیزتر است و همچنین ، در صورت اضافه شدن object های مختلف به برنامه ، نیازی نیست به ازای هر object جدید ، بدنه این متد را تغییر دهیم :

public static double calculatePrice(List<Vehicle> vehicles) {
        double priceCount = 0;
        for (Vehicle vehicle : vehicles) {
            priceCount = priceCount + vehicle.getPrice();
        }
    return priceCount;
}

 

همان طور که مشاهده می کنید ، کد های بدنه متد calculatePriceCount بسیار کم تر و خواناتر شده و هم چنین به جای ورودی آرایه ای از object ها ، آرایه ای از Vehicle ها را قبول می کند که همین موضوع سبب می شود که اگر اکسپشن در ورودی متد وجود داشته باشد ، به جای runtime در حالت compileTime رخ  دهد و به نوعی دارای یک compile time safety نیز می باشد .

Liskov Substitution Principle : این اصل که با مخفف LSP نیز شناخته می شود ، در حقیقت به این اصل می پردازد که کلاس child باید از کلاس Parrent خود تبعیت کند .
فرض کنید سوپر کلاسی داریم به نام TrasportationDevice که دارای متدهای زیر است و تمامی کلاس هایی که شامل وسایل نقلیه می شوند ، از این کلاس ارث بری می کنند :

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }
   
   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

 

حالا کلاس هایی که شامل وسایل نقلیه می شوند نیز از این کلاس ارث بری می کنند :

class Car extends TransportationDevice
{
   @Override
   void startEngine() { ... }
}
class Bicycle extends TransportationDevice
{
   @Override
   void startEngine() /*problem!*/
}

 

کلاس Car چون دارای Engine می باشد ، مشکلی ندارد ، اما کلاس Bicycle چون دارای Engine نمی باشد ، به نوعی از رفتار super class خود که متد startEngine است ، تبعیت نمی کند و به نوعی اصل LSP سولید را رعایت نمی کند .
برای حل این مشکل دوباره کلاس TransportationDevice را ریفکتور می کنیم :

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }
}


حال دو super class برای وسایلی که دارای موتور هستند و هم چنین وسایلی که دارای موتور نیستند ، می سازیم تا هر کدام ، از super class مخصوص خود ارث بری کنند :

class DevicesWithoutEngines extends TransportationDevice
{  
   void startMoving() { ... }
}
class DevicesWithEngines extends TransportationDevice
{  
   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

 

حالا می توانیم بر اساس اصل LSP کلاس های Car  و Bicycle را دوباره پیاده سازی کنیم تا هر کدام از super class مخصوص خود ، ارث بری داشته باشند :

class Car extends DevicesWithEngines
{
   @Override
   void startEngine() { ... }
}
class Bicycle extends DevicesWithoutEngines
{
   @Override
   void startMoving() { ... }
}


Interface Segregation Principle : طبق گفته Robert Martin اصل (ISP) به طراحی اشاره می کند که نباید کلاینت های خود را مجبور کنید که اینترفیسی را پیاده سازی کنند که قرار نیست از آن interface استفاده کنند .
فرض کنید یک interface برای event های مختلف داریم که دارای متدهای زیر می باشد :

public interface ClickListener {
    public void onClick();

    public void onLongClick();

    public void onRightClick();
}


یک کلاس دیگر به نام Button داریم که دارایم که می خواهیم متد setOnClickListener آن را صدا بزنیم :

        Button button = ... //initial Button
        button.setClickListener(new ClickListener() {
            @Override
            public void onClick() {
               // do anything
            }

            @Override
            public void onLongClick() {

            }

            @Override
            public void onRightClick() {

            }
  });

در کد بالا ، ما فقط به متد onClick نیاز داشتیم ، در صورتی که 2 متد دیگر نیز برای ما implement شد که ما به آنها هیچ نیازی نداریم و همچنین برای آنها بایت کد اضافی تولید شده است . برای جلوی گیری از این مشکل ، می توانیم interface ها را به صورت جدا از هم درست کنیم :

public interface onClick {
    public void onClick();
}
public interface onLongClick {
    public void onLongClick();
}
public interface onRightClick {
    public void onRightClick();
}


Dipendeny Inversion Principle : این اصل ، به الگویی اشاره دارد که در آن کلاس های HighLevel نباید هیچ اطلاعی از نوع پیاده سازی کلاس های LowLevel خود داشته باشند و هم چنین هیچ وابستگی نباید از طرف کلاس های HighLevel نسبت به کلاس های LowLevel وجود داشته باشد .

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

public class DownloadTask {
    public void start(){
        // ...code
    }
}
public class TaskRunner {
    private DownloadTask downloadTask;

    public void setDownloadTask(DownloadTask downloadTask) {
        this.downloadTask = downloadTask;
    }

    public void run() {
        this.downloadTask.start();
    }
}


حالا فرض کنید که می خواهیم متد run کار های بیشتری را انجام دهد ، مثلا فایل های دانلود شده را در یک سرور ذخیره کند ، آیا باید دوباره به کلاس TaskRunner پراپرتی و مدتهای جدید مثل EmailTask را اضافه کنیم ؟ مسلما جواب خیر است ، چون در صورت اضافه شدن تسک های جدیدتر ، باید این کلاس را ویرایش کرده و کدهای جدیدی را به آن اضافه کنیم که در این صورت وابستگی این کلاس ، به کلاس های سطح پایین خودش زیاد می شود .
برای حل این مشکل می شود از روش ساخت interface استفاده کرد ، در حقیقت یک interface می سازیم که دارای متد start می باشد و Task های مختلف آن را پیاده سازی می کنند .

public interface TaskInterface {
    public void start();
}
public class DownloadTask implements TaskInterface {
    @Override
    public void start() {
        // ...code
    }
}
public class EmailTask implements TaskInterface {
    @Override
    public void start() {
        // ... code
    }
}


حالا به راحتی می توان هر کلاس که اینترفیس Task را پیاده سازی کرده ، متد start آن را در کلاس TaskRunner فراخوانی کنیم ، بدون اینکه کلاس TaskRunner از نوع پیاده سازی متد start آشنایی داشته باشد  و هم چنین با اضافه شدن Task های جدید ، هیچ نیازی نیست که کدهای کلاس TaskRunner را ویرایش نموده و Task های جدیدی را به آن اضافه نماییم :

public class TaskRunner {
    private List<TaskInterface> taskList;

    public void setTasksList(List<TaskInterface> tasks) {
        this.taskList = tasks;
    }

    public void run() {
        for (TaskInterface task : this.taskList) {
            task.start();
        }
    }
}


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