Технически погледнато, ООП е точно за абстрактните типове дании, наследяването и полиморфизма, но и други неща са също поне толкова важни. Останалата част от тази секция ще се занимае с тях.
Един от най-важните фактори е начинът по който се създават и разрушават обекти. Къде са данните на обекта и как се управлява времето на живот на обекта? Има различни философии на въпроса, които работят. C++ има за най-важни въпросите за управление на ефективността, така че дава на програмиста избор. За максимална скорост на изпълнение още по време на писане на програмата обектите се слагат на стека (понякога са наричани автоматични или с обхват променливи) или в поле от статична памет. Това слага приоритета на въпросите за алокиране и освобождаване на памет и този контрол може да бъде извънредно ценен в някои ситуации. По този начин, обаче, се убива гъвкавостта, понеже трябва да се знае всичко за обектите по време на писането на програмата. Ако се опитвате да решавате по-общ проблем като CAD например, складово стопанство или контрол на въздушния транспорт, това е твърде ограничаващо.
Другият подход е да се създават обектите в област, наречена heap. При този подход не се знае точно колко обекти ще има по време на изпълнение, колко е тяхното време на живот и точният им тип. Тези неща се определят по време на изпълнение, както се случи в програмата. Ако трябва нов обект, той просто се създава на хийпа щом стане нужен. Понеже паметта се управлява динамично, значително повече време трябва да се алокира или освободи памет, отколкото в случая, когато тя е върху стека. (Често алокирането на памет върху стека е асемблерска инструкция за запис в стековия указател, респективно освобождаването - запис в указателя променящ го в противната посока.) Динамичният подход има тенденция да са усложнени, така че допълнителното време за отделяне и освобождаване на памет ще е малко в сравнение с общото време на изпълнение. В добавка голямата гъвкавост е основна предпоставка за решаването на общия програмен проблем.
C++ позволява на програмиста да определи дали обектите ще се създават по време на написването на програмата (т.е. по време на компилацията - б.пр.) или по време на изпълнение, позволявайки по този начин управление на ефективността. Би могло да се помисли, че, понеже е по-гъвкаво, обектите трябва винаги да се създават в хийпа, а не на стека. Има обаче и друго нещо и то е времето на живот на обекта. Ако се създаде обект на стека или в статичната памет, компилаторът може да определи времето на живот на този обект и да определи кога да го разруши. Ако обаче се създаде обект в хийпа, компилаторът не знае това. Програмистът ема две възможности за разрушаване на обекта: може програмно да се определи кога да се разруши обекта или програмната среда може да доставя услугата събиране на боклука която автоматично открива кога обектът е вече ненужен и го разрушава. Разбира се, събирачът на боклук е много по-удобен, но се изисква всички програми да са съгласувани с него и има допълнителен разход на ресурси за неговата работа. Това не отговаря на изискванията към езика C++ и затова не е включено, но Java има събирач на боклук (както и Smalltalk; Delphi няма, но може да се добави такъв. Съществуват събирачи за C++ произведени от "странични" доставчици).
Останалата част от тази секция разглежда допълнителни фактори, засягащи обектите и времето им на живот.
Колекции и итератори
Ако не знаете колко обекта ще ви трябвата за решаването на даден проблем или колко време те ще просъществуват вие също не знаете къде да ги сложите. Как да знаете колко място да отделите? Това не може да стане, понеже информацията ще е достъпна чак по време на изпълнение.
Решението на повечето проблеми в ООП изглежда лекомислено: създавате друг тип обект. Новият обект който решава конкретния проблем съдържа манипулатори на други обекти. Разбира се, може да се направи нещо подобно и с масив, което е достъпно в повечето езици. Но наличното тук е повече. Този нов обект, наречен изобщо колекция (също наричан контейнер, но Swing GUI библиотеката използва този термин в друг смисъл, така че в тази книга ще се използва“колекция”), ще се разширява от самосебе си за да поеме всичко, което ще решите да сложите в нея. Така че не е необходимо да се знае колко обекта ще се слагат в колекцията. Само се създава обекта колекция и се оставя да се грижи за детайлите.
За щастие един добър ООП език идва с набор добри колекции. В C++ това е Standard Template Library (STL). Object Pascal има колекции в неговата Visual Component Library (VCL). Smalltalk има много завършено множество от колекции. Java също има колекции в стандартната си библиотека. В някои библиотеки общата колекция се счита за добра за всички нужди, в други (C++ в частност) има различни типове колекции за различните нужди: виктор за смислен достъп до всеки елемент, свързан лист за смислена итерация по елементите, например да можете да изберете подходящ тип за своите нужди. Може да включват мрежи, опашки, хеш таблици, дървета, стекове и т.н.
Всички колекции имат начин да слагат неща вътре и да ги вадят навън. Начинът да се сложи нещо во колекцията е очевиден. Има функция наречена “push” или “add” или нещо подобно. Извличането на неща от колекцията не е винаги толкова непосредствено; ако е нещо подобно на масив, като вектор например, може да е възможно да се използува индексиращ оператор или функция. Но в много ситуации това няма значение. Също, функция за избиране само на един елемент е много ограничаваща. Ако искате да работите с или да сравнявате няколко неща в колекцията?
Решението е итератор, чието предназначение е да избира елементи от колекцията и да ги представя на потребителя на итератора. Като клас итераторът също дава някакво ниво на абстракция. Тази абстракция може да се използува за разделяне на детайлите на колекцията от кода, който извлича елементите. Колекцията, чрез итератора, се свежда просто до последователност (от елементи -б.пр.). Итераторът позволява да се работи с тази последователност без да се познават детайлите на истинската структура – тоест дали е вектор, свързан списък, стек или нещо друго. Това дава гъвкавостта лесно да се променя подлежащата структура без да се проминя кода на приложната програма. Java започна (във версии 1.0 и 1.1) със стандартен итератор, наречен Enumeration, за всичките си класове-колекции. Java 2 добави Iterator който прави много повече от стария Enumeration.
От гледна точка на проектирането всичко, което е нужно, е последователност, която решава конкретния пробем. Ако една единствена последователност решава проблема, няма нужда от повече. Има две причини за необходимост от избор на колекция. Първо, колекциите дават различни интерфейси и поведение. Стекът има различен интерфейс и поведение от опашката, която е различна от множеството или списъка. Един от тези типове би могло да дава по-добро решение на вашия проблем от другите. Второ, различните колекции имат различна ефективност в различните ситуации. Най-добрият пример са векторът и списъкът. Двете са прости последователности, които могат да имат еднакви интерфейси и поведение. Но някои операции могат да имат много различна цена. Достъпът до случайни елементи от масива е за едно и също време винаги. За свързания списък (лист -б.пр.) такава операция би отнела много време, ако елементът е дълбоко в списъка. От друга страна, вмъкването на елемент някъде по средата е лесно в списъка и отнемащо много време в масива. Тези и други операции имат различна ефективност в зависимост от подлежащата структура. Във фазата на проектирането може да се започне със списък и после, когато се настройва производителността, да се премине към масив. Поради абстракцията чрез итераторите това може да стане чрез минимална промяна на кода.
Накрая, запомнете, че колекцията е просто шкаф от памет за слагане на обекти вътре. Ако шкафът удовлетворява всичките нужди, няма значение как е направен (основна концепция с повечето типове обекти). Ако работите в програмна среда, която има допълнителни разходи на ресурси, сължащи се на други фактори (работата под Windows, например, или цената на събирача на боклук), тогава разликата в ефективността на свързания списък и масива може да няма значение. Може да се нуждаете само от един тип последователност. Може даже да си въобразите “перфектна” абстракция на колекция, която може автоматично да сменя типа в зависимост от използваната подлежаща система.
Йерархия с един корен
Едно от нещата в ООП, което стана доста забележително след въвеждането на C++ е въпросът дали всичко в края на краищата ще е наследник на един единствен клас . В Java (както и в практически всички останали ООП езици) отговорът е "да" и прародителят на всички класове е просто Object. Това показва, че ползите от йерархията с един корен са много.
Всички обекти в такава йерархия имат общ интерфейс, така че те в края на краищата са от един тип. Алтернативата (като в C++) е, че не може да се каже, че всичко е от някакъв фундаментален тип. От гледна точка на обратната съвместимост това повече подхожда за преход от C и може да изглежда по-малко ограничаващо, но когато се иска да се направи напълно ООП се налага самостаятелно да се направи същото, което вече го има в другите ООП езици. И каквато и нова библиотека да се вземе, тя ще бъде с някакъв нов несъвместим интерфейс. Това изисква усилието (и може би многократното наследяване) да се вмести новия интерфейс. Заслужава ли си това допълнителната “гъвкавост” на C++ ? Ако се нуждаете от нея – ако имате голяма инвестиция в C – много даже си заслужава. Ако започвате от нулата, други варианти като Java често могат да бъдат по-перспективни.
All objects in a singly-rooted hierarchy (such as Java provides) can be guaranteed to have certain functionality. You know you can perform certain basic operations on every object in your system. A singly-rooted hierarchy, along with creating all objects on the heap, greatly simplifies argument passing (one of the more complex topics in C++).
Йерархията с един корен прави много по-лесно вграждането на събирач на боклука. Необходимата поддръжка може да се постави в базовия клас и тогава трябва само събирачът да изпрати съответните съобщения до всички обекти в системата. Без йерархията с един корен и система, която управляма обектите чрез манипулатори е много трудно да се направи събирач на боклука.
Доколкото информацията по време на изпълнение е във всички обекти, никога програмата не може да завърши с обект, чийто тип не може да се определи. Това е важно специално при системните операции като обработката на изключения и за правене на по-гъвкави програми.
Може да се чудите защо, като е толкова полезна, йерархията с един корен не е представена в C++. Това е старата надпревара между ефективността и управлението. Йерархията с един корен налага ограничения върху проектирането и в частност на съществуващия C код. Тези ограничения представляват проблем само в някои случаи, но като цяло не е наложена йерархията с един корен в C++. В Java, който започна от нулата и нямаше изисквания за обратна съвместимост с никой език, логично бе представена йерархията с един корен, както и в повечето други ООП езици.
Библиотеки от колекции и поддръжка за лесно използване
Тъй като колекциите са нещо, което се използва често, полезно е да има библиотеки от повторно използваеми колекции, от където се взема каквото е подходящо и се използува. Java има такава колекция, въпреки че едоста ограничена в Java 1.0 и 1.1 (Библиотеката от колекции на Java 2 обаче удовлетворява повечето нужди).
Downcasting и templates/generics
За да бъдат колекциите повторно използваеми те съдържат фундаменталния тип в Java който беше споменат по-рано: Object. Йерархията с един корен значи, че всичко е Object, така че колекция която държи Object-и може да държи каквото и да е. Това я прави лесна за повторно използване.
За да се използва такава колекция просто се добавят манипулатори към нея, а след това се изискват обратно. Но, доколкото колекцията съдържа само Object-и, когато се добавя манипулатор става upcasting към Object, като по този начин се губи идентичността. Когато се взема обратно се полечава манипулатор на Object , а не този, който е бил добавен. Как се връщаме към нещото с полезен интерфейс, което сме сложили в колекцията?
Пак се използва casting, но този път не е нагоре към по-общ тип, а надолу по йерархията към по-специфичен тип. Това се нарича downcasting. С upcasting, например, се знае, че Circle е от типа на Shape така че може безопасно да се направи upcast, но не се знае дали Object е непременно Circle или Shape така че манипулаторът не може сигурно да се преобразува, ако не знаете точно с какъв тип имате работа.
Това не е чак толкова опасно, понеже ако се направи опит за неправилно преобразуване, по време на изпълнение ще се получи изключение, което ще опишем накратко. Когато извличате манипулатори от колекцията трябва да има начин да се помни типът им, за да се правят подходящи преобразувания.
Downcasting-ът и проверките по време на изпълнение довеждат до изразходване на допълнително време и допълнителни усилия на програмиста. Би ли имало смисъл да се създаде колекция, която да помни типовете и т.н., та да не се налага всеки път да се прави? Решението е параметризирани типове, които са класове, които компилаторът може автоматично да приспособява за конкретния случай. Например, с параметризирана колекция, компилаторът може да я направи да работи само с Shape-ове и за извлича само Shape-ове.
Параметризираните типове са важна част от C++, частично защото C++ няма йерархия с един корен. В C++ ключовата дума за прилагане на параметризираните типове е template. Java в момента няма параметризирани типове, но те могат да се направят – тромаво, обаче – използвайки йерархията с един корен. От една страна думата generic (ключовата дума използвана в Ada за неговите templates) беше в списъка на думите “резервирани за бъдещо приложение.” Някои от тези думи мистериозно се плъзнаха в “Бермудския триъгълник” на ключовите думи и е трудно да се каже какво би могло да се случи.
Домакинската дилема:
кой ще чисти?
Всеки обект се нуждае от ресурси за да съществува, от които първа е паметта. Когато обектът вече не е необходим тези ресурси трябва да се освободят за повторно използване. В прости програмни ситуации въпросът колко съществува обекта не изглежда голямо предизвикателство: създавате обекта, той съществува колкото е необходимо и после трябва да се разруши. Не е много трудно, обаче, да се намерят ситуации в които отговорът е много по-сложен.
Да предположим, например, че се проектира система за управление на трафика за летище. (Същият модел е за управление на палетите в складово стопанство, за видеоленти под наем и за боксовете за пансион на домашни любимци.) Отначало изглежда просто: прави се колекция да сложим аеропланите, после се прави обект и се вкарва в колекцията за всеки самолет, който влиза в зоната на управление на трафика. За почистване просто се изтрива обекта за всеки аероплан, който напуска зоната.
Но може да има и друга система, която записва данни за самолетите; може би тя не изисква такава бърза намеса като прякото управление на полетите. Може да е запис на маршрутите на всички малки самолети, които напускат летището. Така че имаме втора колекция от малки самолети и винаги, когато се създава обект свързан с аероплан, той се вкарва и във втората колекция, ако самолетът е малък. После някакъв фонов процес върши необходимото в моменти на бездействие на инсталацията.
Сега проблемът е по-тежък: как бихте могли да знаете кога да се разрушат обектите? Когато главният процес е приключил с даден обект, някаква друга част от системата може да не е. Същия проблем може да възникне в множество ситуации и в програмни системи (като C++) в които обектът трябва явно да се унищожи, когато не е необходим, може да стане много сложен.6
В Java събирачът на боклук е проектиран да се грижи за освобождаването на паметта (въпреки че това не включва други аспекти от унищожаването на обектите). Събирачът “знае” когато един обект вече не се използва и автоматично освобождава паметта на обекта. Това комбинирано с факта, че всички обекти са наследници на единствен клас Object и че може да се създават обекти по единствен начин: на хийпа, прави процеса на програмиране на Java много по-прост от този на C++. Имате много по-малко решения за вземане и препятствия за преодоляване.
Събирачите на боклук срещу
ефективността и гъвкавостта
Ако всичко това е толкова добра идея, защо не са направили и C++ така? Разбира се, има цена за всичкото това програмистко удобство и тази цена е допълнителния разход на ресурси по време на изпълнение. Както се спомена преди, в C++ и в този случай те автоматично се почистват (но нямате гъвкавостта да създавате колкото ви трябват по време на изпълнение). Създаването на обекти на стека е най-ефективния начин за заемане и освобождаване на паметта. Създаването на обекти в хййпа може да бъде много по-скъпо. Наследяването винаги на базовия клас и правенето на всички извиквания на функции полиморфни също събира своя малък данък. Но събирачът на боклук е отделен проблем, защото никога не се знае кога ще се включи и колко ще работи. Това значи, че има неопределено време на реакцията на Java програма, така че не може да я използвате в някои ситуации, където времето на реакция е критично. (Такива ситуации се наричат изобщо програми в реално време, макар и не всички изисквания на програмирането в реално време да са толкова строги.)7
Проектантите на езика C++, опитвайки се да ухажват C програмистите (и най-успешно - докато го правеха), не искаха да добавят черти, които могат да засегнат скоростта или използването на C++ в каквито и да са ситуации, където C би могъл да бъде използван. Тази цел беше постигната, но на цената на по-голама сложност на програмирането на C++. Java е по-прост от C++, но недостатъкът е в ефективността и понякога в приложимостта. За значителна част от програмните задачи, обаче, Java често е най-добрият избор.
Сподели с приятели: |