Повече за гетери и сетери

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

За съжаление, идиомът getter / setter, за който много програмисти смятат, че е обектно ориентиран, нарушава този основен принцип на OO в пика. Да разгледаме примера на Moneyклас, който има getValue()метод, който връща "стойността" в долари. Ще имате код като следния в цялата си програма:

двоен ред Общо; Парична сума = ...; // ... orderTotal + = amount.getValue (); // orderTotal трябва да е в долари

Проблемът с този подход е, че предходният код прави голямо предположение за това как Moneyсе изпълнява класът (че „стойността“ се съхранява в a double). Код, който прави допусканията за внедряване се прекъсва, когато изпълнението се промени. Ако например трябва да интернационализирате приложението си, за да поддържа валути, различни от долари, тогава не getValue()връща нищо смислено. Можете да добавите a getCurrency(), но това би направило целия код около getValue()разговора много по-сложен, особено ако продължавате да използвате стратегията getter / setter, за да получите информацията, от която се нуждаете, за да свършите работата. Типично (недостатъчно) изпълнение може да изглежда така:

Парична сума = ...; // ... стойност = сума.getValue (); валута = сума.getCurrency (); conversion = CurrencyTable.getConversionFactor (валута, USDOLLARS); общо + = стойност * преобразуване; // ...

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

Решението на бизнес-логическо ниво на този проблем е да се извърши работата в обекта, който разполага с информацията, необходима за извършване на работата. Вместо да извличате "стойността", за да извършите някаква външна операция върху нея, трябва да накарате Moneyкласа да извършва всички операции, свързани с парите, включително конвертиране на валута. Правилно структурираният обект би обработил сумата по следния начин:

Общо пари = ...; Парична сума = ...; total.increaseBy (сума);

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

Проблемът

Повечето програмисти нямат затруднения да схванат тази концепция на ниво бизнес логика (макар че може да са необходими известни усилия, за да се мисли последователно по този начин). Проблемите обаче започват да се появяват, когато потребителският интерфейс (UI) влезе в картината. Проблемът не е в това, че не можете да приложите техники като тази, която току-що описах, за изграждане на потребителски интерфейс, а в това, че много програмисти са заключени в манталитета на гетъра / сетера, когато става въпрос за потребителски интерфейси. Обвинявам този проблем върху фундаментално процедурни инструменти за конструиране на кодове като Visual Basic и неговите клонинги (включително Java UI конструкторите), които ви принуждават да влезете в този процедурен начин на мислене.

(Отстъпление: Някои от вас ще говорят на предходното твърдение и ще крещят, че VB се основава на осветената архитектура Model-View-Controller (MVC), така че е свещен. Имайте предвид, че MVC е разработен преди почти 30 години. В началото 70-те години най-големият суперкомпютър беше наравно с днешните настолни компютри. Повечето машини (като DEC PDP-11) бяха 16-битови компютри, с 64 KB памет и тактови честоти, измерени в десетки мегагерци. Вашият потребителски интерфейс вероятно беше стек перфокарти. Ако сте имали късмета да имате видео терминал, може би сте използвали ASCII-базирана конзолна система за вход / изход (I / O). Научихме много през последните 30 години. Дори Java Swing трябваше да замени MVC с подобна архитектура на „разделим модел“, главно защото чистият MVC не изолира достатъчно слоевете на потребителския интерфейс и домейн-модела.)

И така, нека дефинираме проблема накратко:

Ако даден обект не може да изложи информация за изпълнение (чрез методите get / set или по какъвто и да е друг начин), тогава разбира се обектът трябва по някакъв начин да създаде свой собствен потребителски интерфейс. Тоест, ако начинът, по който са представени атрибутите на даден обект, е скрит от останалата част на програмата, тогава не можете да извлечете тези атрибути, за да изградите потребителски интерфейс.

Имайте предвид, между другото, че не криете факта, че съществува атрибут. ( Тук определям атрибута като съществена характеристика на обекта.) Знаете, че човек Employeeтрябва да има атрибут заплата или заплата, иначе не би бил Employee. (Това би било a Person, a Volunteer, a Vagrantили нещо друго, което няма заплата.) Това, което не знаете - или искате да знаете - е как тази заплата е представена вътре в обекта. Може да бъде a double, a String, мащабиран longили двоично кодиран десетичен знак. Това може да е "синтетичен" или "производен" атрибут, който се изчислява по време на изпълнение (например от клас на заплащане или заглавие на длъжността или чрез извличане на стойността от база данни). Въпреки че методът get наистина може да скрие някои от тези подробности за изпълнението,както видяхме сMoney например, не може да скрие достатъчно.

И така, как обектът създава свой собствен потребителски интерфейс и остава да се поддържа? Само най-опростените обекти могат да поддържат нещо като displayYourself()метод. Реалистичните обекти трябва:

  • Показват се в различни формати (XML, SQL, стойности, разделени със запетая и т.н.).
  • Показват различни изгледи на себе си (един изглед може да покаже всички атрибути; друг може да покаже само подмножество на атрибутите; а трети може да представи атрибутите по различен начин).
  • Показват се в различни среди (например от страна на клиента ( JComponent) и обслужване на клиента (HTML)) и се справят както с входа, така и с изхода в двете среди.

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

Изградете решение

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

Тази раздвоеност на методите на обекта се появява в няколко модела на проектиране. Най-вероятно сте запознати със стратегията, която се използва с различните java.awt.Containerкласове за оформление. Можете да разрешите проблема с оформлението с решение за деривация: FlowLayoutPanel,, GridLayoutPanelи BorderLayoutPanelт.н., но това налага твърде много класове и много дублиран код в тези класове. Едно единствено решение в тежка категория (добавяне на методи за Containerхаресване layOutAsGrid()и layOutAsFlow()т.н.) също е непрактично, тъй като не можете да модифицирате изходния код Containerсамо защото имате нужда от неподдържано оформление. В модела Стратегия създавате Strategyинтерфейс ( LayoutManager), реализиран от няколко Concrete Strategyкласа ( FlowLayout, GridLayoutи т.н.). След това казвате на Contextобект (aContainer) как да направя нещо, като му предам Strategyобект. (Вие се премине на Containerпо LayoutManagerкойто дефинира оформление стратегия.)

Моделът на Builder е подобен на стратегията. Основната разлика е, че Builderкласът изпълнява стратегия за конструиране на нещо (като JComponentили XML поток, който представлява състоянието на обекта). Builderобектите обикновено изграждат своите продукти, използвайки и многоетапен процес. Тоест, Builderза завършване на строителния процес са необходими извиквания към различни методи и Builderобикновено не знае реда, в който ще се извършват повикванията, нито колко пъти ще бъде извикан един от неговите методи. Най-важната характеристика на Builder е, че бизнес обектът (наречен the Context) не знае точно какво изгражда Builderобектът. Моделът изолира бизнес обекта от неговото представяне.

Най-добрият начин да видите как работи един прост конструктор е да погледнете такъв. Първо нека разгледаме Contextбизнес обекта, който трябва да изложи потребителски интерфейс. Листинг 1 показва опростен Employeeклас. The Employeeима name, idи salaryатрибути. (Стъблата за тези класове са в долната част на списъка, но тези заглушки са просто заместители за истинското нещо. Можете - надявам се - лесно да си представите как биха работили тези класове.)

Това специално Contextизползва това, което мисля като двупосочен конструктор. Класическата Gang of Four Builder върви в една посока (изход), но също така съм добавил a, Builderкойто Employeeобект може да използва, за да се инициализира. Необходими Builderса два интерфейса. В Employee.Exporterинтерфейс (листинга 1, ред 8) дръжки посока на изхода. Той дефинира интерфейс към Builderобект, който изгражда представянето на текущия обект. На Employeeделегатите действителната UI строежа на Builderв export()метода (по линия 31). В Builderне се предава на действителните полета, но вместо да употреби Stringи да премине представяне на тези области.

Листинг 1. Служител: Контекстът на строителя

1 импортиране на java.util.Locale; 2 3 публичен клас Служител 4 {частно име; 5 частен идентификатор на EmployeeId; 6 частни парична заплата; 7 8 публичен интерфейс Износител 9 {void addName (име на низ); 10 void addID (String id); 11 void addSalary (заплата в низ); 12} 13 14 публичен интерфейс Вносител 15 {String provideName (); 16 String provideID (); 17 String provideSalary (); 18 празнота отворена (); 19 void close (); 20} 21 22 публичен служител (конструктор на вносители) 23 {builder.open (); 24 this.name = ново име (builder.provideName ()); 25 this.id = new EmployeeId (builder.provideID ()); 26 this.salary = нови пари (builder.provideSalary (), 27 нови Locale ("en", "US")); 28 builder.close (); 29} 30 31 публичен невалиден експорт (конструктор на износител) 32 {builder.addName (name.toString ()); 33 builder.addID (id.toString ()); 34 builder.addSalary (заплата.toString ()); 35} 36 37// ... 38} 39 // ---------------------------------------- ------------------------------ 40 // Единични тестови неща 41 // 42 клас Име 43 {private String value; 44 публично име (String стойност) 45 {this.value = value; 46} 47 публичен String toString () {върната стойност; }; 48} 49 50 class EmployeeId 51 {private String value; 52 public EmployeeId (String value) 53 {this.value = value; 54} 55 публичен String toString () {върната стойност; } 56} 57 58 клас пари 59 {private String value; 60 публични пари (стойност на низа, локално местоположение) 61 {this.value = value; 62} 63 публичен String toString () {върната стойност; } 64}

Нека разгледаме един пример. Следният код изгражда потребителския интерфейс на Фигура 1:

Служител wilma = ...; JComponentExporter uiBuilder = нов JComponentExporter (); // Създаване на конструктора wilma.export (uiBuilder); // Изграждане на потребителския интерфейс JComponent userInterface = uiBuilder.getJComponent (); // ... someContainer.add (userInterface);

Листинг 2 показва източника за JComponentExporter. Както можете да видите, целият код, свързан с потребителския интерфейс, е съсредоточен в Concrete Builder( JComponentExporter), а Context( Employee) управлява процеса на изграждане, без да знае точно какво изгражда.

Листинг 2. Експортиране към потребителски интерфейс от страна на клиента

1 импортиране на javax.swing. *; 2 импортиране на java.awt. *; 3 импортиране на java.awt.event. *; 4 5 клас JComponentExporter изпълнява Employee.Exporter 6 {име на частен низ, идентификатор, заплата; 7 8 публична невалидна addName (Име на низ) {this.name = name; } 9 публична невалидна addID (String id) {this.id = id; } 10 публична невалидна addSalary (заплата в низ) {this.salary = заплата; } 11 12 JComponent getJComponent () 13 {JComponent панел = нов JPanel (); 14 panel.setLayout (нов GridLayout (3,2)); 15 panel.add (нов JLabel ("Име:")); 16 panel.add (нов JLabel (име)); 17 panel.add (нов JLabel ("Идентификационен номер на служителя:")); 18 panel.add (нов JLabel (id)); 19 panel.add (нов JLabel ("Заплата:")); 20 panel.add (нов JLabel (заплата)); 21 панел за връщане; 22} 23}