به تفاوت بین لیست ۶-۱ و لیست ۶-۲ توجه کنید. هر دو نمایانگر دادههای یک نقطه بر روی صفحه کارتیزین هستند. با این حال، یکی پیادهسازی خود را آشکار میکند و دیگری بهطور کامل آن را پنهان میکند.
Listing 6-1 - Concrete Point
public class Point {
public double x;
public double y;
}
Listing 6-2 - Abstract Point
public interface Point {
double getX();
double getY();
void setCartesian(double x, double y);
double getR();
double getTheta();
void setPolar(double r, double theta);
}
اما این رابط بیشتر از یک ساختار داده را نمایان میکند. متدها یک سیاست دسترسی را تحمیل میکنند. شما میتوانید مختصات فردی را بهصورت مستقل بخوانید، اما باید مختصات را بهصورت یک عملیات اتمی تنظیم کنید.
از سوی دیگر، لیست ۶-۱ بهوضوح در مختصات مستطیلی پیادهسازی شده است و ما را مجبور میکند که آن مختصات را بهطور مستقل دستکاری کنیم. این پیادهسازی را آشکار میکند. در واقع، حتی اگر متغیرها خصوصی بودند و ما از getter ها و setter های تکمتغیره استفاده میکردیم، باز هم پیادهسازی را آشکار میکرد.
پنهانسازی پیادهسازی تنها یک مسأله از قرار دادن یک لایه از توابع بین متغیرها نیست. پنهانسازی پیادهسازی مربوط به انتزاعات است! یک کلاس بهسادگی متغیرهای خود را از طریق دریافتکنندهها و تنظیمکنندهها بیرون نمیآورد. بلکه رابطهای انتزاعی را نمایان میکند که به کاربرانش اجازه میدهد جوهر دادهها را دستکاری کنند، بدون اینکه نیاز به دانستن پیادهسازی آنها داشته باشند.
به لیست ۶-۳ و لیست ۶-۴ توجه کنید. مورد اول از اصطلاحات عینی برای بیان سطح سوخت یک وسیله نقلیه استفاده میکند، در حالی که مورد دوم این کار را با انتزاع درصد انجام میدهد. در حالت عینی میتوانید بهطور تقریبی مطمئن باشید که این فقط دسترسی به متغیرها است. در حالت انتزاعی، هیچ ایدهای دربارهی شکل دادهها ندارید.
Listing 6-3 - Concrete Vehicle
public interface Vehicle {
double getFuelTankCapacityInGallons();
double getGallonsOfGasoline();
}
Listing 6-4 - Abstract Vehicle
public interface Vehicle {
double getPercentFuelRemaining();
}
این دو مثال تفاوت بین اشیاء و ساختارهای داده را نشان میدهند. اشیاء دادههای خود را پشت انتزاعات پنهان میکنند و توابعی را که بر روی آن دادهها عمل میکنند، آشکار میسازند. در مقابل، ساختارهای داده دادههای خود را نمایان میکنند و هیچ تابع معنیداری ندارند. دوباره به آن برگردید و بخوانید. به ماهیت مکمل این دو تعریف توجه کنید. آنها عملاً ضد یکدیگر هستند. این تفاوت ممکن است ناچیز به نظر برسد، اما پیامدهای گستردهای دارد.
به عنوان مثال، به مثال شکلی رویهای در لیست ۶-۵ توجه کنید. کلاس Geometry بر روی سه کلاس شکل عمل میکند. کلاسهای شکل (shape) ساختار داده های سادهای هستند که هیچ رفتاری ندارند. تمام رفتار در کلاس Geometry وجود دارد.
Listing 6-5 - Procedural Shape
public class Square {
public Point topLeft;
public double side;
}
public class Rectangle {
public Point topLeft;
public double height;
public double width;
}
public class Circle {
public Point center;
public double radius;
}
public class Geometry {
public final double PI = 3.141592653589793;
public double area(Object shape) throws NoSuchShapeException {
if (shape instanceof Square) {
Square s = (Square) shape;
return s.side * s.side;
} else if (shape instanceof Rectangle) {
Rectangle r = (Rectangle) shape;
return r.height * r.width;
} else if (shape instanceof Circle) {
Circle c = (Circle) shape;
return PI * c.radius * c.radius;
}
throw new NoSuchShapeException();
}
}
حال به راهحل شیءگرا در لیست ۶-۶ توجه کنید. در اینجا، متد area() چندریختی (polymorphic) است. نیازی به کلاس Geometry نیست. بنابراین اگر من یک شکل جدید اضافه کنم، هیچیک از توابع موجود تحت تأثیر قرار نمیگیرند، اما اگر یک تابع جدید اضافه کنم، تمام اشکال باید تغییر کنند!
Listing 6-6 - Polymorphic Shapes
public class Square implements Shape {
private Point topLeft;
private double side;
public double area() {
return side * side;
}
}
public class Rectangle implements Shape {
private Point topLeft;
private double height;
private double width;
public double area() {
return height * width;
}
}
public class Circle implements Shape {
private Point center;
private double radius;
public final double PI = 3.141592653589793;
public double area() {
return PI * radius * radius;
}
}
کد رویهای (کدی که از ساختارهای داده استفاده میکند) افزودن توابع جدید بدون تغییر ساختارهای داده موجود را آسان میکند. از طرف دیگر، کد شیءگرا (OO) افزودن کلاسهای جدید بدون تغییر توابع موجود را آسان میکند.
این مکمل نیز صادق است:
کد رویهای افزودن ساختارهای داده جدید را دشوار میکند زیرا همه توابع باید تغییر کنند. کد شیءگرا (OO) افزودن توابع جدید را دشوار میکند زیرا همه کلاسها باید تغییر کنند.
بنابراین، چیزهایی که برای کد شیءگرا (OO) دشوار است برای رویهایها آسان است و چیزهایی که برای رویهایها دشوار است برای کد شیءگرا آسان است! در هر سیستم پیچیده، زمانهایی وجود دارد که میخواهیم انواع داده جدیدی به جای توابع جدید اضافه کنیم. برای این موارد، اشیاء و شیءگرایی(OO) مناسبترین هستند. از طرف دیگر، زمانهایی نیز وجود خواهد داشت که میخواهیم توابع جدیدی در مقابل انواع داده اضافه کنیم. در آن صورت، کد رویهای و ساختارهای داده مناسبتر خواهند بود.
برنامهنویسان با تجربه میدانند که ایده اینکه همهچیز یک شیء است یک افسانه است. گاهی اوقات واقعاً به ساختارهای داده سادهای با رویههایی که بر روی آنها عمل میکنند نیاز داریم.
یک قاعده معروف است که میگوید یک ماژول نباید از جزئیات داخلی اشیایی که با آنها کار میکند، آگاه باشد. همانطور که در بخش قبل دیدیم، اشیاء دادههای خود را پنهان میکنند و عملیاتهایی را نمایان میسازند. این بدین معناست که یک شیء نباید ساختار داخلی خود را از طریق دسترسیدهندهها (accessors) افشا کند، زیرا این کار به معنای افشای ساختار داخلی، به جای پنهان کردن آن است. بهطور دقیقتر، قانون دمر میگوید که یک متد f از کلاس C فقط باید متدهای این موارد را فراخوانی کند:
-
C
-
یک شیء که توسط f ایجاد شده است
-
یک شیء که بهعنوان آرگومان به f منتقل شده است
-
یک شیء که در یک متغیر نمونه از c نگهداری میشود
این اصل به کاهش وابستگیهای بین ماژولها کمک میکند و طراحی نرمافزار را مدولارتر و قابل نگهداریتر میسازد. بهعبارت دیگر، رعایت این قانون میتواند منجر به کدهای تمیزتر و با ثباتتر شود. متد نباید متدهایی را روی اشیائی که توسط هر یک از توابع مجاز بازمیگردند، فراخوانی کند. به عبارت دیگر، باید با دوستان صحبت کنید، نه با غریبهها.
کد زیر بهنظر میرسد که قانون دمر را نقض میکند (علاوه بر موارد دیگر) زیرا ابتدا تابع getScratchDir() را بر روی نتیجهی تابع getOptions() فراخوانی میکند و سپس تابع getAbsolutePath() را بر روی نتیجهی getScratchDir() فراخوانی میکند:
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
این نوع کد معمولاً به عنوان "قطار تصادفی" نامیده میشود زیرا به نظر میرسد که مجموعهای از واگنهای متصل به هم هستند. زنجیرههای فراخوانی مانند این معمولاً به عنوان یک سبک شل و بینظم در نظر گرفته میشوند و باید از آنها پرهیز کرد. بهترین کار معمولاً این است که آنها را به شکل زیر تقسیم کنید:
Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();
اینکه آیا این نقض قانون دمر است یا نه، به این بستگی دارد که آیا ctxt، Options و ScratchDir اشیاء هستند یا ساختارهای داده. اگر آنها اشیاء هستند، پس ساختار داخلی آنها باید پنهان باشد نه اینکه افشا شود و بنابراین آگاهی از جزئیات داخلی آنها نقض واضح قانون دمر است. از سوی دیگر، اگر ctxt، Options و ScratchDir تنها ساختارهای دادهای بدون رفتار باشند، پس بهطور طبیعی ساختار داخلی خود را افشا میکنند و بنابراین قانون دمر اعمال نمیشود.
استفاده از توابع دسترسیدهنده (accessor) موضوع را پیچیده میکند. اگر کد به صورت زیر نوشته شده بود، احتمالاً دربارهی نقض قانون دمر سوالی نمیکردیم:
final String outputDir = ctxt.options.scratchDir.absolutePath;
این سردرگمی گاهی منجر به ایجاد ساختارهای هیبریدی نامطلوبی میشود که نیمی شیء و نیمی ساختار داده هستند. این ساختارها توابعی دارند که کارهای مهمی انجام میدهند و همچنین دارای متغیرهای عمومی یا دسترسیدهندهها و تنظیمکنندههای عمومی هستند که در واقع متغیرهای خصوصی را عمومی میکنند و سایر توابع خارجی را وسوسه میکنند تا از این متغیرها به روشی استفاده کنند که یک برنامه رویهای از یک ساختار داده استفاده میکند.
این هیبریدها افزودن توابع جدید را دشوار میکنند و همچنین افزودن ساختارهای داده جدید را نیز مشکل میسازند. آنها بدترین حالت از هر دو جهان هستند. از ایجاد آنها پرهیز کنید. این ساختارها نشاندهنده طراحی سردرگمی هستند که نویسندگان آن مطمئن نیستند—یا بدتر، از آن بیاطلاع هستند—که آیا به محافظت در برابر توابع یا انواع نیاز دارند.
اگر ctxt، options و scratchDir اشیایی با رفتار واقعی باشند چه؟
در این صورت، از آنجا که اشیاء باید ساختار داخلی خود را پنهان کنند، نباید بتوانیم از طریق آنها عبور کنیم. در این صورت، چگونه میتوانیم مسیر مطلق دایرکتوری موقت را بهدست آوریم؟
ctxt.getAbsolutePathOfScratchDirectoryOption();
ctx.getScratchDirectoryOption().getAbsolutePath()
اگر ctxt یک شیء باشد، باید از آن بخواهیم که کاری انجام دهد؛ نباید از آن دربارهی جزئیات داخلیاش سوال کنیم. پس چرا ما به مسیر مطلق دایرکتوری موقت نیاز داشتیم؟ قرار بود با آن چه کنیم؟ به این کد از (چند خط پایینتر در) همان ماژول توجه کنید:
String outFile = outputDir + "/" + className.replace('.', '/') + ".class";
FileOutputStream fout = new FileOutputStream(outFile);
BufferedOutputStream bos = new BufferedOutputStream(fout);
BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
شکل اساسی یک ساختار داده، کلاسی با متغیرهای عمومی و بدون توابع است. این بهگاههایی به عنوان شیء انتقال داده یا DTO شناخته میشود. DTOها ساختارهای بسیار مفیدی هستند، بهویژه هنگام ارتباط با پایگاههای داده یا تجزیه پیامها از سوکتها و غیره. آنها اغلب اولین مرحله در یک سری مراحل ترجمه هستند که دادههای خام موجود در پایگاه داده را به اشیاء در کد برنامه تبدیل میکنند. شکل رایجتر آن، فرم "bean" است که در لیست ۶-۷ نشان داده شده است. Beans دارای متغیرهای خصوصی هستند که توسط دسترسیدهندهها و تنظیمکنندهها مدیریت میشوند. نیمهپنهانسازی beans به نظر میرسد که برخی از خالصگرایان شیءگرا را راحتتر کند، اما معمولاً هیچ سود دیگری ارائه نمیدهد.
Listing 6-7 - address.java
public class Address {
private String street;
private String streetExtra;
private String city;
private String state;
private String zip;
public Address(String street, String streetExtra,
String city, String state, String zip) {
this.street = street;
this.streetExtra = streetExtra;
this.city = city;
this.state = state;
this.zip = zip;
}
public String getStreet() {
return street;
}
public String getStreetExtra() {
return streetExtra;
}
public String getCity() {
return city;
}
public String getState() {
return state;
}
public String getZip() {
return zip;
}
}
رکوردهای فعال فرمهای خاصی از DTOها هستند. آنها ساختارهای داده با متغیرهای عمومی (یا متغیرهای دسترسیدهنده) هستند؛ اما معمولاً دارای متدهای ناوبری مانند save و find نیز هستند. معمولاً این رکوردهای فعال ترجمههای مستقیم از جدولهای پایگاه داده یا سایر منابع داده هستند.
متأسفانه، اغلب میبینیم که توسعهدهندگان سعی میکنند این ساختارهای داده را بهعنوان اشیاء در نظر بگیرند و متدهای قوانین کسبوکار را در آنها قرار دهند. این کار ناخوشایند است زیرا یک هیبرید بین یک ساختار داده و یک شیء ایجاد میکند. راهحل، البته، این است که رکورد فعال را به عنوان یک ساختار داده در نظر بگیریم و اشیاء جداگانهای ایجاد کنیم که شامل قوانین کسبوکار باشند و دادههای داخلی خود را پنهان کنند (که احتمالاً تنها نمونههایی از رکورد فعال هستند).
اشیاء رفتار را نمایان میکنند و دادهها را پنهان میسازند. این امر افزودن انواع جدید اشیاء بدون تغییر رفتارهای موجود را آسان میکند. همچنین، افزودن رفتارهای جدید به اشیاء موجود را دشوار میسازد. ساختارهای داده، دادهها را نمایان میکنند و هیچ رفتار قابل توجهی ندارند. این امر افزودن رفتارهای جدید به ساختارهای داده موجود را آسان میکند، اما افزودن ساختارهای داده جدید به توابع موجود را دشوار میسازد.
در هر سیستم خاص، گاهی اوقات به انعطافپذیری برای افزودن انواع داده جدید نیاز خواهیم داشت، و بنابراین برای آن بخش از سیستم، اشیاء را ترجیح میدهیم. در مواقع دیگر، به انعطافپذیری برای افزودن رفتارهای جدید نیاز داریم و بنابراین در آن بخش از سیستم، انواع داده و رویهها را ترجیح میدهیم. توسعهدهندگان نرمافزار خوب این مسائل را بدون پیشداوری درک میکنند و رویکردی را انتخاب میکنند که برای کار در دست بهترین باشد.