И композицията и наследяването позволяват да се слага подобекти в клас. Може да се чудите за разликата помежду им и кога да изберете едното или другото.
Изобщо композицията се използва когато искате реализацията на един клас в друг, но не искате интерфейса. Тоест вграждате обект за да го използвате в новия си клас, но потребителят на новия клас вижда интерфейса който вие сте определили, а не интерфейса на онзи клас. За тази цел вграждате private обекти от съществуващи класове във вашите нови класове.
Понякога има смисъл да се позволи на потребителя на новия клас направо да има достъп до композицията му; тоест да се направят член-обектите public. Член-обектите използват реализацията скривайки се, така че това е безопасно да се направи и когато потребителят знае, че събирате много части заедно това прави интерфейса лесен за разбиране. car обектът е добър пример:
//: c06:Car.java
// Composition with public objects
class Engine {
public void start() {}
public void rev() {}
public void stop() {}
}
class Wheel {
public void inflate(int psi) {}
}
class Window {
public void rollup() {}
public void rolldown() {}
}
class Door {
public Window window = new Window();
public void open() {}
public void close() {}
}
public class Car {
public Engine engine = new Engine();
public Wheel[] wheel = new Wheel[4];
public Door left = new Door(),
right = new Door(); // 2-door
Car() {
for(int i = 0; i < 4; i++)
wheel[i] = new Wheel();
}
public static void main(String[] args) {
Car car = new Car();
car.left.window.rollup();
car.wheel[0].inflate(72);
}
} ///:~
Понеже композицията на кола е част от анализа на проблема (а не просто част от подлежащото проектиране), правенето на членовете публични помага на клиента за разбирането и изисква по-малко сложен кад от създателя на класа.
Когато се наследява се взема съществуващ клас и се прави специална негова версия. Изобщо това означава че се взема клас за обща употреба и се специализира за конкретни нужди. С малко размисъл ще видите, че е безсмислено да се композира кола с обект "превозно средство" – колата не съдържа превозно средство, тя е превозно средство. Тази е- зависимост се изразява с наследяването, а има- зависимостта се изразява с композицията.
protected
Сега като сте запознати с наследяването ключовата дума protected най-накрая има значение. В идеалния свят private биха били винаги непроменимо private, но в реалните проекти понякога искаме да направим нещо скрито от широкия свят и същевременно да оставим достъп на наследниците. Ключовата дума protected е съгласие с прагматизма. Тя казва “Това е private що се отнася до потребителя на класа, но е достъпно за наследниците на класа или за друг от същия package.” Тоест protected в Java автоматично е “приятелски.”
Най-добрия начин е да оставим членовете-данни private – винаги ще запазвате правото си да променяте подлежащата реализация. Може тогава да се позволи на потребителите контролиран достъп чрез protected методи:
//: c06:Orc.java
// The protected keyword
import java.util.*;
class Villain {
private int i;
protected int read() { return i; }
protected void set(int ii) { i = ii; }
public Villain(int ii) { i = ii; }
public int value(int m) { return m*i; }
}
public class Orc extends Villain {
private int j;
public Orc(int jj) { super(jj); j = jj; }
public void change(int x) { set(x); }
} ///:~
Може да видите че change( ) има достъп до set( ) понеже е protected.
Постъпкова разработка
Едно от предимствата на наследяването е че то поддържа incremental development чрез възможността да се добавя код без да се засяга съществуващ код. Това също изолира новите грешки в новия код. Чрез наследяване на съществуващ, функционален клас и добавяне на данни и методи (и предефиниране на съществуващи методи) оставяте съществуващия код – който някой друг може би още използва – недокоснат и небъгиран. Ако се случи да има грешка, знае се че тя е в новия код, който е много по-лесно да се прочете отколкото съществуващия такъв.
Малко изумяващо е колко чисто класовете се разделят. Даже не ви трябва сорсът за да използвате кода отново. Най-много да импортирате пакет. (Това е в сила и за композицията и за наследяването.)
Важно е да се разбере че разработката на програми е постъпков процес, точно както човешкото учене. Може да направите всичкия анализ на който сте способни, но още не знаете всичките отговори когато седнете над проекта. Ще имате много по-голям успех – и по-непосредствена обратна връзка – ако започнете да “израствате” своя проект като органическо, еволюционно творение, отколкото ако го направите изведнъж както се прави небостъргач от остъклени кутии.
Макар и наследяването за експериментиране да е полезна техника, в някаква точка когато нещата се постабилизират трябва да обгърнете цялата йерархия с намерение да се опрости и реорганизира. Запомнете че зад наследяването се крие отношение което казва “Този нов клас е от типа на онзи стар клас.” Вашата програма не трябва да се занимава със сетване и ресетване на битове, а със създаване на обекти, които се определя какви да бъдат от същината на проблемното пространство.
Upcasting
Най-важният аспект на наследяването не е, че дава методи на новия клас. Това е зависимостта между новия и стария клас. Тази зависимост може да бъде резюмирана като се каже “новият клас е от типа насъществуващия клас.”
Това описание не е просто фантазьорски начин да се изрази наследяването – то се поддържа направо от езика. Като пример да вземем базов клас наречен Instrument който представя музикалните инструменти и извлечен клас Wind. Понеже наследяването значи че всичките методи на базовия клас са достъпни и за извлечения клас, всяко съобщение изпратено до базовия клас също може да бъде изпратено до извлечения клас. Ако класа Instrument има play( ) метод, и Wind инструментите ще имат. Това значи че можем правилно да кажем че Wind обектът е също тип Instrument. Следващия пример показва как компилаторът поддържа това нещо:
//: c06:Wind.java
// Inheritance & upcasting
import java.util.*;
class Instrument {
public void play() {}
static void tune(Instrument i) {
// ...
i.play();
}
}
// Wind objects are instruments
// because they have the same interface:
class Wind extends Instrument {
public static void main(String[] args) {
Wind flute = new Wind();
Instrument.tune(flute); // Upcasting
}
} ///:~
Интересното в този пример е tune( ) методът, който приема Instrument манипулатор. Обаче в Wind.main( ) tune( ) се вика като му се дава Wind манипулатор. Като знаем че Java е особено внимателен за проверката на типовете, изглежда странно че метод, който приема един тип приема с готовност и друг тип, докато не разберем че Wind обектът е също Instrument обект и няма метод който tune( ) може да извика за Instrument който не е също и Wind. Вътре в tune( ) кодът работи за Instrument и всичко извлечено от Instrument и актът на обръщане на Wind манипулаторът в Instrument манипулатор се нарича upcasting.
Защо “upcasting”?
Причината за термина е историческа и е свързана с начина на чертане на диаграмите на наследяването отгоре надолу, растейки надолу. (Разбира се, може да чертаете вашите диаграми както си искате.) Диаграмата на наследяването за Wind.java е тогава:
(диаграмата липсва в тази ревизия - б.пр.)
Кастингът от извлечения към базовия премества нагоре по диаграмата на наследяването, така че обикновено се говори за upcasting. Пкастингът е винаги безопасен понеже се отива от по-частен тип към по-общ. Тоест извлеченият клас е надмножество на базовия клас. Той може да съдържа повече методи от базовия клас, но трябва да съдържа най-малко методите на базовия клас. Единственото нещо което може да се случи при ъпкастинга е да се загубят методи, не да се придобият (за използване и от там — да се получат разминавания - бел.пр.). Поради това компилаторът въобще нищо не казва.
Може също да се направи обратното на ъпкастинг, наречено downcasting, но това довежда до дилема която е обект на глава 11.
Отново композиция vs. наследяване
В ОО програмиране най-вероятният начин на работа е да пакетирате данни и методи в клас и да създавате обекти от този клас. От време на време ще използвате съществуващи класове чрез композиция. Още по-рядко ще използвате наследяване. Така че макар и наследяването да изисква най-много усилие при изучаването на ООП, това не значи, че ще го използвате навсякъде, където е възможно това да стане. Напротив, ще го използвате в единични сучаи, само там, където е ясно, че наследяването е полезно. Един от най-добрите начини да се познае е да се прецени дали ще се налага ъпкастинг от извлечените класове към базовите. Ако трябва да се прави ъпкастинг, наследяването е необходимо, но ако ъпкастинг няма да е наложителен ще трябва по-добре да огледате дали е необходимо наследяване. Следващата глава (полиморфизъм) дава един от най-властните подтици за наследяване, но ако помните да се запитате “Трябва ли ми ъпкастинг?”ще имате добър инструмет за избор между композицията и наследяването.
Сподели с приятели: |