آشنایی با مفهوم Solid در برنامه نویسی اندروید

نویسنده : سید ایوب کوکبی ۲۵ فروردین ۱۳۹۸

آشنایی با مفهوم Solid در برنامه نویسی اندروید

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

اجازه دهید برای پرهیز از اشتباه و فهم بهتر مطلب، همان واژگان انگلیسی را به کار بریم و از آوردن معادل فارسی دوری کنیم. SOLID سرنام ۵ اصل کلیدی در مهندسی نرم‌افزار است:

    • Single Responsibility Principle
    • Open-Closed Principle
    • Liskov Substitution Principle
    • Interface Segregation Principle
  • Dependency Inversion Principle

اصل اول (Solid – The Single Responsibility Principle (SRP

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

این اصل می‌خواهد بگوید که هر کلاسی، هر تابعی یا هر موجودیت دیگری فقط باید یک مسئولیت داشته باشد. مثلاً کلاسی که کارش ذخیرۀ داده‌ها است فقط باید همین یک کار را انجام دهد و کاری به واسط کاربری نداشته باشد. بیایید مثال ملموس اندرویدی‌اش را ببینیم. فرض کنید برای یک RecyclerView به یک adapter جهت دریافت داده‌ها نیازمندیم. همانطور که می‌دانید آدابتور، داده‌ها را از دیتاست دریافت کرده و آن را برای نمایش در ویو قالب‌بندی می‌کند. قطعه کد پایین پیاده‌سازی رایجی است که اغلب توسعه‌دهندگان از آن استفاده می‌کنند:

// violation of single responsibility principle
public class MovieRecyclerAdapter extends RecyclerView.Adapter<MovieRecyclerAdapter.ViewHolder> {
 
  private List<Movie> movies;
  private int itemLayout;   

    public MovieRecyclerAdapter(List<Movies> movies, int itemLayout)
    {
         this.movies = movies;
         this.itemLayout = itemLayout;
    }
 
    @Override 
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) 
    {
      View v = LayoutInflater.from(parent.getContext())
                             .inflate(itemLayout, parent, false);         
      return new ViewHolder(v);
    }
 
    @Override 
    public void onBindViewHolder(ViewHolder holder, int position) {
     final Movie movie = movies.get(position);  
     holder.itemView.setTag(movie);
     holder.title.setText(movie.getTitle());  
     holder.rating.setText(movie.getRating()); 
     String genreStr = "";  
     for (String str: movie.getGenre()) { 
           genreStr += str + ", ";        
     }     
     genreStr = genreStr.length() > 0 ? 
            genreStr.substring(0, genreStr.length() - 2) : genreStr;  
     holder.genre.setText(genreStr);           
     holder.releaseYear.setText(movie.getYear()); 
     Glide.with(holder.thumbNail.getContext())
          .load(movies.get(position)
          .getThumbnailUrl())
          .into(holder.thumbNail);
    }
 
    @Override
    public int getItemCount() {
        return movies.size();
    }
 
    public static class ViewHolder extends RecyclerView.ViewHolder {
      @Bind(R.id.title) TextView title; 
      @Bind(R.id.rating) TextView rating;        
      @Bind(R.id.genre) TextView genre;
      @Bind(R.id.releaseYear) TextView releaseYear;   
      @Bind(R.id.thumbnail) ImageView thumbNail;
 
      public ViewHolder(View itemView) {
        super(itemView);
        ButterKnife.bind(this, itemView);            
      }
    }
}

این کد اصل SRP را نقض کرده است. چرا؟

چون متد onBindViewHolder علاوه بر مپ کردن آبجکت Movie، کار قالب‌بندی داده را هم انجام داده است (gereStr) که به آن ربطی ندارد. آداپتورف فقط مسئول اتصال داده‌ها به ویو است. هر کار دیگری، خارج از چارچوب وظایف آن قلمداد شده و نقض‌کنندۀ اصل SRP است. اصلاح‌شدۀ این کد که مبتنی بر اصل SRP است:

// single responsibility principle - Fix it example
public class MovieRecyclerAdapter extends RecyclerView.Adapter<MovieRecyclerAdapter.ViewHolder> {
 
  private List<Movie> movies;
  private int itemLayout;
 
    public MovieRecyclerAdapter(List<Movie> movies, int itemLayout)
    {
         this.movies = movies;
         this.itemLayout = itemLayout;
    }
 
    @Override 
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) 
    {
      View v = LayoutInflater.from(parent.getContext())
                             .inflate(itemLayout, parent, false);       
      return new ViewHolder(v);
    }
 
    @Override 
    public void onBindViewHolder(ViewHolder holder, int position) {
     final Movie movie = movies.get(position);  
     holder.itemView.setTag(movie);
     holder.title.setText(movie.getTitle());  
     holder.rating.setText(movie.getRating()); 
     holder.genre.setText(  
            ArraysUtil.convertArrayListToString(movie.getGenre()));           
     holder.releaseYear.setText(movie.getYear()); 
     Glide.with(holder.thumbNail.getContext())
          .load(movies.get(position)
          .getThumbnailUrl())
          .into(holder.thumbNail);
    }
 
    @Override
    public int getItemCount() {
        return movies.size();
    }
 
    public static class ViewHolder extends RecyclerView.ViewHolder {
      @Bind(R.id.title) TextView title; 
      @Bind(R.id.rating) TextView rating;        
      @Bind(R.id.genre) TextView genre;
      @Bind(R.id.releaseYear) TextView releaseYear;   
      @Bind(R.id.thumbnail) ImageView thumbNail;
 
      public ViewHolder(View itemView) {
        super(itemView);
        ButterKnife.bind(this, itemView);            
      }
    }
}

به قول عمو باب که پیش‌تر معماری تمییز او را معرفی کردیم:

در اصل SRP مسئولیت را دلیلی برای تغییر معرفی کرده‌ایم. اگر برای کلاسی توانستید بیشتر از یک دلیل برای تغییر پیدا کنید یعنی آن کلاس چندین مسئولیت متعدد دارد.

اصل دوم (Solid – The Open-Closed Principle (OCP

هر موجودیت نرم‌افزاری اعم از کلاس، تابع، ماژول و … باید برای توسعه باز و برای اصلاح بسته باشد.

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

// violation of Open closed principle
// Rectangle.java
public class Rectangle {
    private double length;
    private double height; 
    // getters/setters ... 
}
// Circle.java
public class Circle {
    private double radius; 
    // getters/setters ...
}
// AreaFactory.java
public class AreaFactory {
    public double calculateArea(ArrayList<Object>... shapes) {
        double area = 0;
        for (Object shape : shapes) {
            if (shape instanceof Rectangle) {
                Rectangle rect = (Rectangle)shape;
                area += (rect.getLength() * rect.getHeight());                
            } else if (shape instanceof Circle) {
                Circle circle = (Circle)shape;
                area += 
                (circle.getRadius() * cirlce.getRadius() * Math.PI);
            } else {
                throw new RuntimeException("Shape not supported");
            }            
        }
        return area;
    }
}

این کد اصل OCP را نقض کرده است. دلیلش این است که اگر کلاس دیگری مثلاً Triangle به کدها اضافه کنیم ناچاریم کلاس AreaFactory را اصلاح کنیم تا در بین دستورات شرطی، اشیائی از کلاس Triangle را هم تشخیص دهد. این یعنی کلاس AreaFactory برای اصلاحات بسته نبوده و برای توسعه باز نیست.  بیایید مشکل را حل کنیم:

// Open closed principle: good example
// Shape.java
public interface Shape {
    double getArea(); 
}
// Rectangle.java
public class Rectangle implements Shape{
    private double length;
    private double height; 
    // getters/setters ... 
    @Override
    public double getArea() {
        return (length * height);
    }
}
// Circle.java
public class Circle implements Shape{
    private double radius; 
    // getters/setters ...
   @Override
    public double getArea() {
        return (radius * radius * Math.PI);
    }
}
// AreaFactory.java
public class AreaFactory {
    public double calculateArea(ArrayList<Shape>... shapes) {
        double area = 0;
        for (Shape shape : shapes) {
            area += shape.getArea();
        }
        return area;
    }
}

همانطور که می‌بینید Shape را در قالب یک اینترفیس پیاده‌سازی کرده‌ایم که حاوی متد getArea است. این یعنی هر کلاسی که اینترفیس فوق را پیاده‌سازی کند یک شکل محسوب شده و انتظار دریافت مساحت آن از طریق getArea را خواهیم داشت. کلاس AreaFactory با اشیائی که این اینترفیس را پیاده‌سازی کرده باشند کار دارد. بنابراین اگر کلاس جدیدی مثل Triangle به کدهای قبلی اضافه کنیم، تنها کافی است اینترفیس Shape را پیاده‌سازی کنیم؛ کار دیگری هم لازم نیست. همانطور که می‌بینید کلاس AreaFactory برخلاف گذشته نیازی به تغییرات ندارد یعنی برای اصلاح بسته است و به خاطر اینترفیس Shape قابل توسعه هم هست.

بخوانید  تجربیاتی ارزشمند از یک سِنیور دولوپر اندروید

اصل سوم (Solid – The Liskov Substitution Principle (LSP

اگر S زیرکلاسی از T باشد، اشیائی از نوع T را باید بتوان با اشیائی از نوع S جایگزین کرد.

بیایید با یک مثال ساده توضیح دهیم:

public class Bird{
    public void fly(){}
}
public class Duck extends Bird{}

اردک (Duck) کلاس پرنده (Bird) را اکستند کرده است. اردک می‌تواند پرواز کند بنابراین از متد fly می‌تواند استفاده کند. اما شترمرغ چه؟ شترمرغ هم زیرمجموعه‌ای از پرندگان محسوب شده ولی قادر به پرواز نیست، یعنی نمی‌تواند از متد fly استفاده کند. در واقع اگر به جای شی Duck، شی Ostrich (شترمرغ) را جایگزین کنیم با مشکل مواجه خواهیم شد. این یعنی اصل LSP را نقض کرده‌ایم:

public class Ostrich extends Bird{}

راه‌حل مشکل:

public class Bird{
}
public class FlyingBirds extends Bird{
    public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{} 

متدهای مشترک بین همۀ پرندگان چه آن‌هایی که پرواز می‌کنند و چه آن‌هایی که پرواز نمی‌کنند را داخل کلاس Bird قرار می‌دهیم. سپس زیرمجموعۀ خاصی از Bird تحت عنوان FlyingBirds تعریف می‌کنیم که مختص پرندگان قابل پرواز است. این کلاس حاوی متد fly است.

اصل چهارم (Solid – The Interface Segregation Principle (ISP

کلاینت‌ها نباید به متدهایی وابسته باشند که نیازی به آن‌ها ندارند

این اصل بیان می‌کند که با بزرگ شدن یک اینترفیس، کلاس‌هایی که اینترفیس را پیاده‌سازی می‌کنند ناچارند همۀ متدهای آن را پیاده‌سازی کنند حتی گر نیازی به همۀ آن‌ها نداشته باشند. برای حل این مشکل باید اینترفیس چاق را به چندین اینترفیس کوچکتر بشکنیم تا کلاینت‌ها فقط متدهای لازم را پیاده‌سازی کنند. مثال جالب و مشهوری در کدهای SDK اندروید وجود دارد:

public interface OnClickListener { 
    void onClick(View v);
    void onLongClick(View v); 
    void onTouch(View v, MotionEvent event);
}

همانطور که می‌بینید، اینترفیس فوق حاوی سه متد است. حالا فرض کنید بخواهیم برای یک دکمه از آن استفاده کنیم:

// Violation of Interface segregation principle
Button valid = (Button)findViewById(R.id.valid);
valid.setOnClickListener(new View.OnClickListener {
    public void onClick(View v) {
       // TODO: do some stuff...
       
    }
    
    public void onLongClick(View v) {
        // we don't need to it
    }

    public void onTouch(View v, MotionEvent event) {
        // we don't need to it
    } 
});

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

بخوانید  مهم‌ترین سوالات مصاحبه استخدامی از توسعه‌دهندگان اندروید

برای حل مشکل می‌توانیم اینترفیس را بشکنیم:

// Fix of Interface Segregation principle
public interface OnClickListener { 
    void onClick(View v);
}
public interface OnLongClickListener { 
    void onLongClick(View v);
}
public interface OnTouchListener { 
    void onTouch(View v, MotionEvent event);
}

اکنون بدون نیاز به پیاده‌سازی همۀ متدها، فقط آن‌هایی را پیاده‌سازی می‌کنیم که نیاز داریم.

اصل پنجم (Solid – The Dependency Inversion Principle (DIP

ماژول‌های سطح بالا نباید به ماژول‌های سطح پایین وابسته باشند. هر دو باید به انتزاعات وابسته باشند.
انتزاعات نباید به جزئیات وابسته باشد. جزئیات باید وابسته به انتزاعات باشد.

کد پایین را در ببینید:

// violation of Dependency's inversion principle
// Program.java
class Program {

 public void work() {
  // ....code
 }
}

// Engineer.java

class Engineer{

 Program program;

 public void setProgram(Program p) {
  program = p;
 }

 public void manage() {
  program.work();
 }
}

این کد اصل DIP را نقض کرده است چرا که Engineer به عنوان یک کلاس سطح بالا به کلاس سطح پایین Program وابستگی دارد. حالا فرض کنید کلاس Engineer پیچیده و دارای و جزئیات زیادی باشد و اکنون لازم است به جای Program از کلاس دیگری تحت عنوان SuperProgram استفاده شود. چه مشکلاتی پیش روی ما است:

  • باید کلاس Engineer را تغییر دهیم. به خاطر داشته باشید که این کلاس پیچده است و تغییر آن زمان زیادی می‌برد؛
  • ممکن است در اثر اشتباه برخی از عملکردهای فعلی کلاس Engineer تحت تاثر قرار بگیرد؛
  • آزمون واحد باید دوباره انجام شود.

اما با تغییر کوچکی می‌توانیم نقیصۀ فوق را حل کنیم:

// Dependency Inversion Principle - Good example
interface IProgram {
 public void work();
}

class Program implements IProgram{
 public void work() {
  // ....code
 }
}

class SuperProgram implements IProgram{
 public void work() {
  //....code
 }
}

class Engineer{
 IProgram program;

 public void setProgram(IProgram p) {
  program = p;
 }

 public void manage() {
  program.work();
 }
}

بر اساس اصل پنجم Solid در طراحی جدید، با کمک اینترفیس IProgram لایۀ انتزاعی جدیدی به برنامه اضافه کرده‌ایم. اکنون مشکلات قبلی از بین رفته‌اند:

  • کلاس Engineer هنگام اضافه کردن SuperProgram نیازی به تغییر ندارند؛
  • ریسک تغییر عملکردهای قبلی کلاس Engineer کاهش پیدا می‌کند چون به کدهای آن دست نمی‌زنیم؛
  • نیازی به انجام مجدد آزمون‌های واحد نیست.
بخوانید  15 توصیه به توسعه‌دهندگان جوان اندروید

به صورت خلاصه Single Responsibility Principle در رابطه با معماری سطح بالای کدهاست. Open/Closed Principle دربارۀ طراحی کلاس و توسعه‌پذیری آن است. Liskov Substitution Principle دربارۀ ارث‌بری و زیرکلاس‌ها صحبت می‌کند. Interface Segregation Principle مرتبط با منطق تجاری کدهاست و نهایتاً Dependency Inversion Principale وابستگی ماژول‌های برنامه را توضیح می‌دهد. امیدواریم این مطلب اصول SOLID را در حد ابتدایی به شما آموخته باشد.

سید ایوب کوکبی

نویسنده و مترجم...

3 دیدگاه

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *




    سعید رضایی

    سه شنبه ۰۷ خرداد ۱۳۹۸

    سلام
    تشکر از مطلب مفیدتون.
    فقط خواهش میکنم که اینقدر اسامی انگلیسی رو ترجمه نکنید، مگه ما به فارسی کد می زنیم که نیاز داشته باشیم معادل فارسی اسامی رو بدونیم ؟
    “چندریختی، کلاس‌های انتزاعی و کپسوله‌سازی” واقعا ؟
    اگر کسی هم یاد نداشته باشه مطمئنا با جستجو اون کلمه انگلیسی می تونه معنی و تعریفش رو پیدا کنه.

    تشکر.

      محمد فرکاریان

      پنج شنبه ۰۹ خرداد ۱۳۹۸

      سلام
      تشکر از نظرتون حتمااا

    Javad

    شنبه ۰۵ مرداد ۱۳۹۸

    تنها سایت ایرانی هستید که دارید مطالب حرفه ای برنامه نویسی رو منتشر می کنید. ممنونم. شما عالی هستید!