اصول SOLID برخی از قدیمی ترین قوانین در دنیای نرم افزار هستند. آنها ما را قادر می سازند تا کدهای قابل نگهداری، خوانا و قابل استفاده مجدد بنویسیم. در این متن سعی می کنم با رعایت اصول SOLID یک مثال تا حدودی واقعی را انجام دهم.
هر کلاس باید تنها یک هدف داشته باشد و با قابلیت های بیش از حد پر نشود. به مثال زیر توجه کنید:
public class PasswordHasher { public String hashAndSavePassword(String password) { hashPassword(); savePassword(); } public void hashPassword() { //hashing implementation } public void savePassword() { //save to the db } }
این کلاس همانطور که از نامش پیداست برای هش کردن رمزهای عبور پیاده سازی شده است. این کلاس در ذخیره آنها در پایگاه داده نباید مسئولیتی داشته باشد. هر کلاس باید یک مسئولیت واحد را انجام دهد.
نباید «کلاسهای خدا» وجود داشته باشد که دارای عملکردهای متنوعی باشد که کارهای زیادی برای انجام دادن دارند. در عوض، ما باید کلاس هایمان را تا حد امکان مدولار بنویسیم. عملیات ذخیره سازی را در کلاس دیگری پیاده سازی کنید.
کلاس ها باید برای گسترش باز و برای اصلاح بسته باشند.
به عبارت دیگر، برای پیادهسازی ویژگیهای جدید، نباید کلاس موجود را بازنویسی کنید.
بیایید به مثال رمز عبور خود ادامه دهیم. فرض کنید می خواهیم کلاس ما بتواند با الگوریتم های مختلف هش کند.
public String hashPassword(String password, HashingType hashingType) { if(HashingType.BASE64.equals(hashingType)) { hashedPassword="hashed with Base64" } else if(HashingType.MD5.equals(hashingType)) { hashedPassword="hashed with MD5" } return hashedPassword; }
اگر این روش را اجرا کنیم، O را در SOLID خیلی بد میشکنیم. هر بار که یک الگوریتم جدید پیادهسازی میشود، باید کلاس موجود را اصلاح کنیم، و به نظر زشت است.
به لطف OOP، ما انتزاع داریم. ما باید کلاس اولیه خود را یک کلاس واسط/انتزاعی (interface/abstract) کنیم و الگوریتم ها را در کلاس های مشخص پیاده سازی کنیم.
public class Base64Hasher implements PasswordHasher { @Override public String hashPassword(String password) { return "hashed with Base64" } }
public interface PasswordHasher { String hashPassword(String password); }
public class MD5Hasher implements PasswordHasher { @Override public String hashPassword(String password) { return "hashed with MD5" } }
به این ترتیب می توانیم الگوریتم های جدیدی را بدون دست زدن به کدهای موجود اضافه کنیم.
یک کلاس فرعی باید بتواند هر یک از ویژگی های کلاس والد خود را برآورده کند و می تواند به عنوان کلاس والد خود در نظر گرفته شود.
برای نشان دادن مثال خود، بیایید کلاس های مدل (داده) را برای استفاده از الگوریتم های هش خود ایجاد کنیم.
public abstract class Hashed { PasswordHasher passwordHasher; String hash; public void generateHash(String password) { hash = passwordHasher.hashPassword(password); } }
public class Base64 extends Hashed { public Base64() { this.passwordHasher = new Base64Hasher(); } }
و ما همین کار را برای کدگذاری های دیگر پیاده سازی کردیم…
برای اجرای قانون Liskov، هر یک از برنامههای افزودنی Hashed باید از پیادهسازی معتبر تابع هش استفاده کرده و یک هش را برگردانند.
به عنوان مثال، اگر کلاس Hashed را با کلاسی به نام NoHash گسترش دهیم که از پیادهسازی استفاده میکند که دقیقاً همان رمز عبور را بدون هیچ کدگذاری برمیگرداند، قانون را زیر پا میگذارد، زیرا انتظار میرود که یک زیر کلاس از Hashed مقدار هش رمز عبور را داشته باشد.
اینترفیس ها نباید کلاس ها را مجبور به اجرای کاری کنند که نمی توانند انجام دهند. رابط های بزرگ باید به رابط های کوچک تقسیم شوند.
در نظر بگیرید که ما ویژگی رمزگشایی را به رابط اضافه می کنیم.
public interface PasswordHasher { String hashPassword(String password); String decodePasswordFromHash(String hash); }
این قانون را زیر پا می گذارد زیرا یکی از الگوریتم های ما، SHA256 عملاً قابل رمزگشایی نیست (این یک تابع یک طرفه است). در عوض، میتوانیم رابط دیگری را به کلاسهای کاربردی اضافه کنیم تا الگوریتم رمزگشایی آنها را پیادهسازی کنیم.
public interface Decryptable { String decodePasswordFromHash(String hash); } public class Base64Hasher implements PasswordHasher, Decryptable { @Override public String hashPassword(String password) { return "hashed with base64" } @Override public String decodePasswordFromHash(String hash) { return "decoded from base64" } }
کامپوننت ها باید به انتزاعات بستگی داشته باشند، نه به ادغام.
ما یک سرویس رمز عبور مانند زیر داریم:
public class PasswordService { private Base64Hasher hasher = new Base64Hasher() { void hashPassword(String password) { hasher.hashPassword(password); } }
ما این اصل را نقض کردیم زیرا Base64Hasher و PasswordService را به شدت مرتبط کردیم.
بیایید آنها را جدا کنیم و به مشتری اجازه دهیم سرویس هشر مورد نیاز را با سازنده تزریق کند.
public class PasswordService { private PasswordHasher passwordHasher; public PasswordService(PasswordHasher passwordHasher) { this.passwordHasher = passwordHasher; } void hashPassword(String password) { this.passwordHasher.hashPassword(password); } }
خیلی بهتر. ما به راحتی می توانیم الگوریتم هش را تغییر دهیم. سرویس ما به الگوریتم اهمیتی نمی دهد، این به مشتری بستگی دارد که آن را انتخاب کند. ما به اجرای عینی وابسته نیستیم، بلکه به انتزاع بستگی داریم.