Open/Closed Principle در قوانین Solid
در مقاله قبل٬ سرفصل های قانون پنج گانه SOLID را تعریف و پیرامون Single Responsiblity توضیحاتی ارائه کردیم. در این مقاله قصد داریم تا به دومین قانون SOLID بپردازیم که آن را با عنوان Open/Closed Principle و یا به اختصار OCP میشناسیم.
بر اساس تعریفی که برای Open/Closed Princle ارئه میشود٬ یک کلاس باید قابلیت گسترش را داشته باشد٬ بدون اینکه در ساختار و کد های فعلی آن تغییری ایجاد شود. مفهومی که از این تعریف بر میآید این است که کلاس ما باید قابلیت این را داشته باشد که امکانات جدیدی به آن اضافه شود٬ اما ساختار فعلی آن تغییر نکند. علت آن هم این است که ممکن است چندین کلاس دیگر به این بخش از کلاس نیاز داشته باشند و یا با آن در تعامل باشند و تغییر در ساختار فعلی٬ ممکن است زنجیرهای از تغییرات را به همراه داشته باشد و ممکن است سیستم را از حالت stable خارج نماید.
یکی از نمونه هایی که بسیار مشهور است و ساختار OCP را در آن به خوبی نشان میدهد٬ plugin ها هستند. وقتی یک plugin را به یک سیستم اضافه میکنیم٬ در حقیقت به ساختار آن٬ یک قابلیت جدید اضافه کردهایم. اما این عمل بدون تغییر در ماهیت و ساختار فعلی سیستم انجام شده است. این قابلیتی است که OCP میتواند به پروژه های ما القا کند. در اینجا یک سیستم حداقل هایی را برای تعامل در اختیار plugin قرار میدهد اما از جزئیات آن آگاه نیست. این مفهوم در ساختار فنی آن به این معنا است که سیستم با abstraction آن plugin در تعامل است. میتوان از این مفهوم چنین نتیجهای را دریافت کرد که یک سیستم چیزی از plugin نمیداند بلکه plugin سیستم را میشناسد.
استفاده از ارث بری
یکی از شیوه های پیاده سازی که برای این قانون میتوان در نظر گرفت استفاده از ارث بری است. وقتی از ارث بری استفاده میکنیم که یک کلاس٬ به موجودیت های کلاس فعلی نیاز داشته باشد و یا در بسیاری از ساختار ها مشابه کلاس اصلی باشد. حال با توجه به اینکه کلاس اصلی ما باید ثابت باشد (براساس تعریف OCP) و بخواهیم امکانات جدیدی به آن اضافه کنیم٬ میتوانیم از آن ارث بری کنیم و خواص جدیدی را به آن اضافه کنیم. به این ترتیب ما اصل تعریف شده را رعایت کردهایم. اما این روش (استفاده از ارث بری) با مشکلاتی نیز روبرو است. برای آشنایی با این مشکلات میتوانید این مقاله را مطالعه نمایید. در این مقاله به این مشکلات نمیپردازیم و در مقاله ای به تفصیل آنرا مورد بررسی قرار خواهیم داد.
استفاده از polymorphism
روش دوم برای رعایت قانون OCP استفاده از Polymorphism است. به این صورت که ما یک interface تعریف میکنیم و پیاده سازی های گوناگونی را از روی آن انجام میدهیم. نکتهای که در اینجا مطرح است این است که ویژگی های interface آنرا برای اعمال قانون OCP مناسب مینماید. interface ها بالاترین سطح abstraction را دارند و به این ترتیب کمترین میزان وابستگی به پیاده سازی را به همراه میآورند. پیاده سازی های interface ها مستقل از یکدگیر هستند. در نتیجه نیازی به اشتراک گذاری داده ها با یکدگیر ندارد. بنابراین بهترین راه برای پیاده سازی پروژه ها این است که پروژه بر اساس interface ها ساخته شود تا وابستگی به پیاده سازی ها کاهش یابد.
اکنون چند مثال را ارائه میدهیم تا با چند روش٬ شیوه رعایت OCP را بررسی کنیم. فرض کنید یک کلاس Calculator داریم که از Calculate interface استفاده میکند:
public interface Calulate { void add(int a, int b); }
کلاس Calculator:
public class Calculator implements Calulate{ @Override public void add(int a, int b) { System.out.println(a + b); } }
اکنون میخواهیم این امکان را به کلاس اضافه کنیم تا سه عدد را نیز با یکدگیر جمع کند٬ به این ترتیب به interface یک متد جدید اضافه میکنیم:
public interface Calulate { void add(int a, int b); void add(int a, int b, int c); }
همانطور که مشاهده میکنید٬ یک خاصیت جدید به پروژه اضافه شده است اما کد های موجود دچار تغییر نشدهاند:
public class Calculator implements Calulate { @Override public void add(int a, int b) { System.out.println(a + b); } @Override public void add(int a, int b, int c) { System.out.println(a + b + c); } }
مثال بعدی را با ساختاری متفاوت پیاده سازی میکنیم. فرض کنید میخواهیم داده را از پایگاه داده بخوانیم و مدل دادهای ما به این صورت است:
public class UserBasicInfo { private String firstName; private String lastName; // getter and setters }
و برای دریافت اطلاعات از پایگاه داده چنین مینویسیم:
public class UserInfoLocalRepository { public UserBasicInfo getUserBasicInfo(String username) { Database database = new Database(); return database.getUserInfo(username); } public UserAddressInfo getUserAddressInfo() { } public String userAvatarPath() { } public UserCompanyInfo getUserCompanyInfo() { } }
همانطور که مشاهده میکنید٬ چندین متد وجود دارد که انواع اطلاعات کاربر را برای ما برمیگرداند. این اطلاعات از طریق پایگاه داده خوانده میشوند. حال اگر بخواهیم این اطلاعات از طریق یک سرویس RESTful دریافت شوند باید تغییر در ساختار فعلی ایجاد کنیم و یا یک کلاس جدید ایجاد نماییم. ایجاد تغییر در ساختار فعلی با اصول OCP مغایرت دارد. بنابراین یک کلاس جدید ایجا میکنیم:
public class UserInfoRestRepository { public UserBasicInfo getUserBasicInfo(String username) { RestApi api = new RestApi("api.zerotohero.ir"); api.addHeaders("auth", "abcd"); return api.getUserInfo(username); } public UserAddressInfo getUserAddressInfo() { } public String userAvatarPath() { } public UserCompanyInfo getUserCompanyInfo() { } }
اکنون دو کلاس داریم که اطلاعات کاربر را از دو منبع مستقل از هم تامین میکنند. اما اکنون این مشکل در پروژه وجود دارد که پروژه را درگیر دو Dependency با دو پیاده سازی متفاوت کردهایم. برای رفع این مشکل پیاده سازی را به این صورت تغییر میدهیم:
public interface UserRepository { UserBasicInfo getUserBasicInfo(); UserAddressInfo getUserAddressInfo(); String userAvatarPath(); UserCompanyInfo getUserCompanyInfo(); }
و ساختار کلاس ها را نیز اینگونه تغییر میدهیم:
public class UserInfoRepository implements UserRepository{ @Override public UserBasicInfo getUserBasicInfo() { return null; } @Override public UserAddressInfo getUserAddressInfo() { return null; } @Override public String userAvatarPath() { return null; } @Override public UserCompanyInfo getUserCompanyInfo() { return null; } }
public class UserInfoRestRepository implements UserRepository{ @Override public UserBasicInfo getUserBasicInfo() { return null; } @Override public UserAddressInfo getUserAddressInfo() { return null; } @Override public String userAvatarPath() { return null; } @Override public UserCompanyInfo getUserCompanyInfo() { return null; } }
اکنون ما یک Dependency و دو پیاده سازی داریم. تفاوت این دو روش در Dependency Injection به خوبی قابل مشاهده است. هر کلاسی که میخواهد داده هارا دریافت کند٬ میتواند تنها به UserRespository وابسته باشد. به این ترتیب به صورت Runtime میتوانیم پیاده سازی مورد نیاز آن کلاس را مشخص کنیم.
در این مقاله سعی بر این شد تا با مفهوم OCP و یا Open/Cloesed Principle آشنا شویم. سه قانون دیگر را در مقالات بعدی مورد بررسی قرار خواهیم داد.
با ما همراه باشید.
دیدگاهتان را بنویسید
برای نوشتن دیدگاه باید وارد بشوید.