Защо методите getter и setter са зло

Не възнамерявах да стартирам поредица „е зло“, но няколко читатели ме помолиха да обясня защо споменах, че трябва да избягвате методите get / set в колоната от миналия месец „Защо разширява е зло“.

Въпреки че методите getter / setter са нещо обичайно в Java, те не са особено обектно ориентирани (OO). Всъщност те могат да навредят на поддръжката на вашия код. Освен това, наличието на множество методи за получаване и задаване е червен флаг, че програмата не е задължително добре проектирана от гледна точка на ОО.

Тази статия обяснява защо не трябва да използвате гетери и сетери (и кога можете да ги използвате) и предлага методология за проектиране, която ще ви помогне да излезете от манталитета на гетъра / сетера.

Относно естеството на дизайна

Преди да стартирам в друга колона, свързана с дизайна (с провокативно заглавие, не по-малко), искам да изясня няколко неща.

Бях обезсърчен от някои коментари на читателите, получени в резултат на миналогодишната колона „Защо се простира злото“ (вижте Talkback на последната страница на статията). Някои хора вярваха, че твърдя, че обектната ориентация е лоша, просто защото extendsима проблеми, сякаш двете концепции са еквивалентни. Това със сигурност не е това, което си мислех, че казах, така че нека да изясня някои мета въпроси.

Тази колона и статията от миналия месец са за дизайн. По природа дизайнът е поредица от компромиси. Всеки избор има добра и лоша страна и вие правите своя избор в контекста на общите критерии, определени от необходимостта. Доброто и лошото обаче не са абсолютно. Доброто решение в един контекст може да е лошо в друг.

Ако не разбирате двете страни на даден проблем, не можете да направите интелигентен избор; всъщност, ако не разбирате всички последици от вашите действия, изобщо не проектирате. Спъваш се в тъмното. Не е случайно, че всяка глава от книгата за дизайнерски модели на „ Бандата на четири” включва раздел „Последствия”, който описва кога и защо използването на шаблон е неподходящо.

Да заявяваш, че някои езикови функции или общ идиом за програмиране (като аксесоари) има проблеми, не е същото като да казваш, че никога не трябва да ги използваш при никакви обстоятелства. И само защото дадена функция или идиом се използва често, не означава, че и вие трябва да го използвате. Неинформираните програмисти пишат много програми и простото наемане от Sun Microsystems или Microsoft не подобрява магически способностите на някой за програмиране или дизайн. Пакетите Java съдържат много страхотен код. Но има и части от този код. Сигурен съм, че авторите се притесняват да признаят, че са писали.

По същия начин маркетинговите или политическите стимули често тласкат дизайнерските идиоми. Понякога програмистите взимат лоши решения, но компаниите искат да популяризират това, което технологията може да направи, затова те подчертават, че начинът, по който го правите, е по-малко от идеалния. Те правят най-доброто от лоша ситуация. Следователно, вие действате безотговорно, когато възприемате каквато и да е практика по програмиране, просто защото „така трябва да правите нещата“. Много неуспешни проекти на Enterprise JavaBeans (EJB) доказват този принцип. Базираната на EJB технология е чудесна технология, когато се използва по подходящ начин, но може буквално да срине компания, ако се използва по неподходящ начин.

Моят въпрос е, че не трябва да програмирате на сляпо. Трябва да разберете хаоса, който може да причини характеристика или идиом. По този начин вие сте в много по-добра позиция да решите дали да използвате тази функция или идиом. Вашият избор трябва да бъде едновременно информиран и прагматичен. Целта на тези статии е да ви помогне да подходите към програмирането си с отворени очи.

Абстракция на данни

Основна заповед на системите за ОО е, че обектът не трябва да излага никакви подробности за изпълнението си. По този начин можете да промените изпълнението, без да променяте кода, който използва обекта. От това следва, че в OO системите трябва да избягвате getter и setter функциите, тъй като те осигуряват предимно достъп до подробности за изпълнението.

За да разберете защо, помислете, че може да има 1000 извиквания на getX()метод във вашата програма и всяко повикване приема, че връщаната стойност е от определен тип. Можете да съхранявате getX()връщаната стойност например в локална променлива и този тип променлива трябва да съвпада с типа return-value. Ако трябва да промените начина, по който обектът е реализиран по такъв начин, че типът X да се промени, имате дълбоки проблеми.

Ако X беше int, но сега трябва да е long, ще получите 1000 грешки при компилиране. Ако неправилно отстраните проблема, като хвърлите връщаната стойност int, кодът ще се компилира чисто, но няма да работи. (Възвръщаемата стойност може да бъде съкратена.) Трябва да промените кода около всяко от тези 1000 повиквания, за да компенсирате промяната. Със сигурност не искам да върша толкова много работа.

Един основен принцип на OO системите е абстрахирането на данни . Трябва напълно да скриете начина, по който даден обект изпълнява манипулатор на съобщения от останалата част на програмата. Това е една от причините всичките ви променливи на екземпляра (непостоянните полета на даден клас) да бъдат private.

Ако направите променлива на екземпляр public, тогава не можете да промените полето, тъй като класът се развива с течение на времето, защото бихте разбили външния код, който използва полето. Не искате да търсите 1000 употреби на клас, просто защото го променяте.

Този принцип на скриване на изпълнението води до добър тест за киселинност на качеството на системата на OO: Можете ли да направите огромни промени в дефиницията на клас - дори да изхвърлите цялото нещо и да го замените с напълно различно изпълнение - без да повлияете на кода, който използва това обекти на класа? Този вид модуларизация е централната предпоставка за обектна ориентация и улеснява поддръжката. Без скриване на изпълнението няма смисъл да се използват други функции на OO.

Методите getter и setter (известни също като accessors) са опасни по същата причина, че publicполетата са опасни: Те осигуряват външен достъп до подробности за изпълнението. Какво ще стане, ако трябва да промените типа на достъпното поле? Трябва също да промените типа на връщане на достъп. Използвате тази възвръщаема стойност на много места, така че трябва да промените и целия този код. Искам да огранича ефекта от промяната до определение на един клас. Не искам да се втурват в цялата програма.

Тъй като аксесоарите нарушават принципа на капсулиране, можете разумно да твърдите, че система, която използва интензивно или неподходящо аксесоари, просто не е обектно ориентирана. Ако преминете през процес на проектиране, за разлика от само кодирането, едва ли ще намерите аксесоари във вашата програма. Процесът е важен. Имам още какво да кажа по този въпрос в края на статията.

Липсата на getter / setter методи не означава, че някои данни не протичат през системата. Независимо от това, най-добре е да сведете до минимум движението на данните, доколкото е възможно. Моят опит е, че поддържаемостта е обратно пропорционална на количеството данни, които се движат между обектите. Въпреки че все още може да не виждате как, всъщност можете да премахнете по-голямата част от това движение на данни.

Чрез внимателно проектиране и фокусиране върху това, което трябва да направите, а не върху това как ще го направите, вие елиминирате по-голямата част от методите за получаване / настройване във вашата програма. Не искайте информацията, която ви е необходима, за да свършите работата; помолете обекта, който разполага с информацията, да свърши работата вместо вас.Повечето достъпи се намират в кода, защото дизайнерите не мислят за динамичния модел: обектите за изпълнение и съобщенията, които те изпращат един на друг, за да свършат работата. Те започват (неправилно) чрез проектиране на йерархия на класове и след това се опитват да направят тези класове в динамичния модел. Този подход никога не работи. За да изградите статичен модел, трябва да откриете връзките между класовете и тези връзки точно отговарят на потока от съобщения. Съществува асоциация между два класа само когато обектите от един клас изпращат съобщения до обекти от другия. Основната цел на статичния модел е да улови тази информация за асоцииране, докато динамично моделирате.

Без ясно дефиниран динамичен модел вие само се досещате как ще използвате обектите на даден клас. Следователно методите за достъп често се навиват в модела, защото трябва да осигурите възможно най-голям достъп, тъй като не можете да предскажете дали ще ви е необходим или не. Този вид стратегия за проектиране чрез познаване в най-добрия случай е неефективна. Губите време за писане на безполезни методи (или добавяне на ненужни възможности към класовете).

Аксесоарите също завършват в дизайни по силата на навика. Когато процедурните програмисти приемат Java, те са склонни да започнат с изграждането на познат код. Процедурните езици нямат класове, но имат C struct(помислете: клас без методи). Тогава изглежда естествено да се имитира a structчрез изграждане на дефиниции на класове на практика без методи и нищо друго освен publicполета. Тези процедурни програмисти четат някъде, че полетата трябва да бъдат private, така че те правят полетата privateи предоставят publicметоди за достъп. Но те само усложниха обществения достъп. Със сигурност не са направили системно обектно ориентирано.

Нарисувай себе си

Едно разклонение на пълното капсулиране на полето е в конструкцията на потребителския интерфейс (UI). Ако не можете да използвате аксесоари, не можете да имате клас на конструктор на потребителски интерфейс, който да извиква getAttribute()метод. Вместо това класовете имат елементи като drawYourself(...)методи.

А getIdentity()метод може да работи, разбира се, при условие, че се връща на обект, който реализира Identityинтерфейса. Този интерфейс трябва да включва метод drawYourself()(или дайте ми-това JComponent-представлява-вашата-идентичност) метод. Въпреки че getIdentityзапочва с „get“, това не е аксесоар, защото не само връща поле. Той връща сложен обект, който има разумно поведение. Дори когато имам Identityобект, все още нямам представа как една идентичност е представена вътрешно.

Разбира се, drawYourself()стратегията означава, че аз (ахна!) Влагам UI код в бизнес логиката. Помислете какво се случва, когато изискванията на потребителския интерфейс се променят. Да кажем, че искам да представя атрибута по съвсем различен начин. Днес "идентичност" е име; утре това е име и идентификационен номер; денят след това е име, идентификационен номер и снимка. Ограничавам обхвата на тези промени до едно място в кода. Ако имам JComponentклас дайте ми-това -представлява-вашата-идентичност, тогава съм изолирал начина на представяне на идентичностите от останалата част на системата.

Имайте предвид, че всъщност не съм вложил никакъв потребителски интерфейс в бизнес логиката. Написах слоя на потребителския интерфейс по отношение на AWT (Abstract Window Toolkit) или Swing, които и двете са абстракционни слоеве. Действителният UI код е в изпълнението на AWT / Swing. Това е целият смисъл на един абстракционен слой - за да изолирате вашата бизнес логика от механиката на подсистемата. Лесно мога да пренасям в друга графична среда, без да променя кода, така че единственият проблем е малко бъркотия. Можете лесно да премахнете тази бъркотия, като преместите целия код на потребителския интерфейс във вътрешен клас (или като използвате модела за дизайн на фасадата).

JavaBeans

Може да възразите, като кажете: "Но какво ще кажете за JavaBeans?" Какво за тях? Със сигурност можете да създавате JavaBeans без гетери и сетери. Най- BeanCustomizer, BeanInfoи BeanDescriptorкласове всичко съществува за точно тази цел. Дизайнерите на спецификации JavaBean хвърлиха идиома getter / setter в картината, защото смятаха, че това ще бъде лесен начин за бързо приготвяне на боб - нещо, което можете да направите, докато се научавате да го правите правилно. За съжаление никой не го направи.

Аксесоарите са създадени единствено като начин за маркиране на определени свойства, така че програма за създаване на потребителски интерфейс или еквивалент да може да ги идентифицира. Не трябва сами да извиквате тези методи. Те съществуват за използване на автоматизиран инструмент. Този инструмент използва API за самоанализ в Classкласа, за да намери методите и да екстраполира съществуването на определени свойства от имената на методите. На практика този идиом, базиран на самоанализ, не е разработен. Това направи кода твърде сложен и процедурен. Програмистите, които не разбират абстракцията на данни, всъщност се обаждат на достъпите и в резултат на това кодът е по-малко поддържаем. Поради тази причина в Java 1.5 ще бъде включена функция за метаданни (в средата на 2004 г.). Така че вместо:

частна собственост; public int getProperty () {return свойство; } public void setProperty (int value} {свойство = стойност;}

Ще можете да използвате нещо като:

частна собственост @property int; 

Инструментът за конструиране на потребителски интерфейс или еквивалент ще използва API за интроспекция, за да намери свойствата, вместо да изследва имената на методите и да направи извод за съществуването на свойство от име. Следователно, никакъв достъп по време на работа не уврежда вашия код.

Кога е добре аксесоарът?

Първо, както обсъдих по-рано, добре е методът да върне обект от гледна точка на интерфейс, който обектът изпълнява, защото този интерфейс ви изолира от промени в класа на внедряване. Този вид метод (който връща препратка към интерфейс) всъщност не е "гетер" в смисъла на метод, който просто осигурява достъп до поле. Ако промените вътрешната реализация на доставчика, просто променяте дефиницията на върнатия обект, за да приспособите промените. Все още защитавате външния код, който използва обекта чрез неговия интерфейс.