Книга е още в много ранна фаза на написване


Проектиране с наследяване



страница43/73
Дата25.07.2016
Размер13.53 Mb.
#6732
1   ...   39   40   41   42   43   44   45   46   ...   73

Проектиране с наследяване


Като научи за полиморфизма на човек може да му се стори че всичко трябва да ста­ва с наследяване щом полиморфизмът е толкова умен инструмент. Това мо­же да утежни проектите; фактически ако направо изберете наследяването за на­чин от един клас да се получи друг клас може да стане ненужно сложно.

По-добрият подход е да се използва композиция първо когато не е ясно какво тряб­ва да се използва. Композицията не вкарва проекта в йерархии на на­сле­дя­ва­не. Но композицията е по-гъвкава понеже позволява динамично да се избира ти­па (и чрез това-поведението) когато се използва композиция, докато на­сле­дя­ва­нето изисква точният тип да е известен по време на компилацията. След­ва­щия пример илюстрира това:

//: 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.




Сподели с приятели:
1   ...   39   40   41   42   43   44   45   46   ...   73




©obuch.info 2024
отнасят до администрацията

    Начална страница