Като научи за полиморфизма на човек може да му се стори че всичко трябва да става с наследяване щом полиморфизмът е толкова умен инструмент. Това може да утежни проектите; фактически ако направо изберете наследяването за начин от един клас да се получи друг клас може да стане ненужно сложно.
По-добрият подход е да се използва композиция първо когато не е ясно какво трябва да се използва. Композицията не вкарва проекта в йерархии на наследяване. Но композицията е по-гъвкава понеже позволява динамично да се избира типа (и чрез това-поведението) когато се използва композиция, докато наследяването изисква точният тип да е известен по време на компилацията. Следващия пример илюстрира това:
//: c07:Transmogrify.java
// Dynamically changing the behavior of
// an object via composition.
interface Actor {
void act();
}
class HappyActor implements Actor {
public void act() {
System.out.println("HappyActor");
}
}
class SadActor implements Actor {
public void act() {
System.out.println("SadActor");
}
}
class Stage {
Actor a = new HappyActor();
void change() { a = new SadActor(); }
void go() { a.act(); }
}
public class Transmogrify {
public static void main(String[] args) {
Stage s = new Stage();
s.go(); // Prints "HappyActor"
s.change();
s.go(); // Prints "SadActor"
}
} ///:~
Stage съдържа манипулатор на Actor, който се инициализира с HappyActor обект. Това значи че go( ) произвежда частно поведение. Но доколкото манипулаторът може да бъде пресвързан към друг обект по време на изпълнение, манипулатор на SadActor обект може да бъде заместен в a и тогава поведението давано от go( ) се променя. Така се постига динамична гъвкавост по враме на изпълнение. В контраст не може да решите да наследявате динамично по време на изпълнение; това трява да е напълно определено по време на компилация.
Общо правило е “Използвай наследяване за изразяване на разлики в поведението и член-променливи за изразяване вариациите в състоянието.” В горния пример и двете са използвани: два различни класа са наследени за да се изрази разликата в act( ) метода и Stage използва композиция за да позволи промяна в състоянието си. В този случай промяната в състоянието води промяна в поведението.
Чисто наследяване vs. разширяване
Когато изучаваме наследяването може да ни се стори че най-добрия начин да го осъществим е “чистия” подход. Тоест само методите които фигурират в кореновия клас или interface да бъдат подтискани в извлечения клас, както е на следната диаграма:
…
Това може да се означи като чиста “е” зависимост понеже интерфейса на клас я определя. Наследяването гарантира, че извлечените класове ще имат не по-малко от интерфейса на базовия клас. Ако проследите горната диаграма, извлечените класове също ще имат не повече от интерфейса на базовия клас.
Това може да бъде названо чиста субституция, понеже извлечените класове могат точно да заместят базовия клас, никога няма нужда от допълнителна информация за субкласовете когато ги използвате:
Тоест базовият и извлеченият клас могат да получават едни и същи съобщения, понеже имат един и същ интерфейс. Всичко което е необходимо е ъпкаст и никога не трябва да знаете точния тип на обекта, с който се работи. Всичко става благодарение на полиморфизма.
Ако гледаме по този начин изглежда, че чистата “е” е единствения смислен начин да се направят нещата, всеки друг дизайн индицира междинно мислене и по определение е "фалшив". Това също е капан. Щом стъпите на тази плоскост на мислене ще се огледате наоколо и ще откриете че разширяването на интерфейса (което, за нещастие, ключовата дума extends изглежда да подпомага) е перфектното решение на всеки частен проблем. Това може да бъде окачествено като “прилича-на” зависимост защото извлеченият клас е като базовия клас – има същия основен интерфейс – но има други черти които изискват допълнителни методи за реализацията си:
…
Докато това също е полезен и смислен подход (в зависимост от ситуацията) той има и недостатък. Разширената част на интерфейса на извлечения клас не е достъпна от базовия клас, така че като се направи ъпкаст не може да се викат новите методи:
…
Ако не се прави ъпкаст в този случай, това няма да ви безпокои, но често ще попадате в ситуации където ще трябва да преоткривате точния тип за да може да използвате съответните методи. Следващата секция показва как се прави това.
Даункастинг и идентификация на типа по време на изпълнение
Тъй като се губи специфичната за типа информация чрез upcast (преместване нагоре по йерархията), има смисъл да се възстанови тази информация – тоест да се отиде надолу по йерархията – и се използва downcast. Знае се че ъпкастът е винаги безопасен; базовият клас не може да има по-голям интерфейс от извлечения клас, така че каквито и да са съобщения подавани чрез интерфейса на базовия клас ще бъдат приети. Но с даункаста не се знаече формата (например) е фактически кръг. Би могла да е квадрат, триъгълник или някакъв друг тип.
За да се реши проблема трябва да има начин да се гарантира че даункастът е коректен, така че да не може да се направи каст към неправилен тип и после да се изпрати съобщение, което обектът не може да приеме. Това би било твърде небезопасно.
В някои езици (като C++) трябва да се изпълни специална операция, за да се получи безопасен кастинг, но в Java всеки каст се проверява! Така че даже и да изглежда че се прави само единичен каст в скоби, по време на изпълнение се проверява дали типът е този който се очаква. Ако не е, получава се ClassCastException. Тази дейност за проверка на типовете по време на изпълнение е наречена run-time type identification (RTTI). Следващият пример демонстрира поведението на RTTI:
//: c07:RTTI.java
// Downcasting & Run-Time Type
// Identification (RTTI)
import java.util.*;
class Useful {
public void f() {}
public void g() {}
}
class MoreUseful extends Useful {
public void f() {}
public void g() {}
public void u() {}
public void v() {}
public void w() {}
}
public class RTTI {
public static void main(String[] args) {
Useful[] x = {
new Useful(),
new MoreUseful()
};
x[0].f();
x[1].g();
// Compile-time: method not found in Useful:
//! x[1].u();
((MoreUseful)x[1]).u(); // Downcast/RTTI
((MoreUseful)x[0]).u(); // Exception thrown
}
} ///:~
Както в диаграмата MoreUseful разширява интерфейса на Useful. Но понеже то е наследено, може да се направи ъпкаст към Useful. Може да видите това да става в инициализацията на масива x в main( ). Понеже и двата обекта в масива са от клас Useful, може да се изпратят f( ) и g( ) методите към двете, а ако се опитате да извикате u( ) (който съществува само в MoreUseful) ще получите грешка по време на изпълнение.
Ако искате да използвате разширения интерфейс на обект от клас MoreUseful може да опитате с даункаст. Ако типът е коректен, всичко ще стане. Иначе ще получите ClassCastException. Не е необходимо да пишете специален код във връзка с това, понеже то индицира програмна грешка която се проявява където и да е в програмата.
Има повече за RTTI отколкото прост каст. Например има начин да се види точния тип преди опита за даункаст. Цялата глава 11 е посветена на изучаването на идентификацията на типовете по време на изпълнение в Java.
Сподели с приятели: |