Шаблонный метод - паттерн поведения объектов, определяющий функциональность конктерных методов в рамках лишь абстрактных сущностей.
Шаблонный метод определяет основу алгоритма в рамках абстрактных классов и методов, а подклассам позволяет переопределять отдельные шаги этого алгоритма (или все сразу), не изменяя, таким образом, его структуру и последовательность в целом.
Приложения, построенные на базе этого каркаса, могут порождать подклассы от классов Application и Document, отвечающие конкретным потребностям.
Например, графический редактор определит подклассы DrawApplication и DrawDocument, а электронная таблица — подклассы SpreadsheetApplication и SpreadsheetDocument.
В абстрактном классе Application определен алгоритм открытия и считывания документа в операции OpenDocument:
void Application::OpenDocument (const char* name) { if (!CanOpenDocument(name)) { // работа с этим документом невозможна return; } Document* doc = DoCreateDocument(); if (doc) { _docs->AddDocument(doc); AboutToOpenDocument(doc); doc->0pen(); doc->DoRead(); } }
Операция OpenDocument определяет все шаги открытия документа. Она проверяет, можно ли открыть документ, создает объект класса Document, добавляет его к набору документов и считывает документ из файла.
Операцию вида OpenDocument мы будем называть шаблонным методом, описывающим алгоритм в терминах абстрактных операций, которые замещены в подклассах для получения нужного поведения. Подклассы класса Application выполняют проверку возможности открытия (CanOpenDocument) и создания документа (DoCreateDocument). Подклассы класса Document считывают документ (DoRead).
Шаблонный метод определяет также операцию, которая позволяет подклассам
Application получить информацию о том, что документ вот-вот будет открыт (AboutToOpenDocument). Определяя некоторые шаги алгоритма с помощью абстрактных операций, шаблонный метод фиксирует их последовательность, но позволяет реализовать их в подклассах классов Application и Document.
Схема использования паттерна Шаблонный метод (Template Method)
ConcreteClass предполагает, что инвариантные (зафиксированные) шаги алгоритма будут выполнены в AbstractClass.
Шаблонные методы приводят к инвертированной структуре кода, которую иногда называют принципом Голливуда, подразумевая часто употребляемую в этой киноимперии фразу «Не звоните нам, мы сами позвоним». В данном случае это означает, что родительский класс сам вызывает операции подкласса, а не наоборот.
Шаблонные методы вызывают операции следующих видов:
Важно, чтобы в шаблонном методе четко различались операции-зацепки (которые можно замещать) и абстрактные операции (которые нужно замещать). Чтобы повторно использовать абстрактный класс с максимальной эффективностью, авторы подклассов должны понимать, какие операции предназначены для замещения. Обычно об этом явно говорится и в документации к использованию и в комментариях непосредственно в коде.
Подкласс может расширить поведение некоторой операции, заместив ее и явно
вызвав эту операцию из родительского класса:
void DerivedClass::Operation () { ParentClass::Operation(); // Расширенное поведение класса DerivedClass }
К сожалению, очень легко забыть о необходимости вызывать унаследованную операцию. У нас есть возможность трансформировать такую операцию в шаблонный метод с целью предоставить родителю контроль над тем, как подклассы расширяют его. Идея в том, чтобы вызывать операцию-зацепку из шаблонного метода в родительском классе. Тогда подклассы смогут переопределить именно эту операцию:
В родительском же классе ParentClass эта операция HookOperation не делает ничего:
void ParentClass::HookOperation () { }
Но она замещена в подклассах, которые расширяют поведение:
void DerivedClass::HookOperation () { // расширение в производном классе }
public void process(HttpServletRequest request, HttpServletResponse response) // Wrap multipart requests with a special wrapper request = processMultipart(request); // Identify the path component we will use to select a mapping if (path == null) { return; } if (log.isDebugEnabled()) { log.debug("Processing a '" + request.getMethod() + "' for path '" + path + "'"); } // Select a Locale for the current user if requested processLocale(request, response); // Set the content type and no-caching headers if requested processContent(request, response); processNoCache(request, response); // General purpose preprocessing hook if (!processPreprocess(request, response)) { return; } this.processCachedMessages(request, response); // Identify the mapping for this request ActionMapping mapping = processMapping(request, response, path); if (mapping == null) { return; } // Check for any role required to perform this action if (!processRoles(request, response, mapping)) { return; } // Process any ActionForm bean related to this request ActionForm form = processActionForm(request, response, mapping); processPopulate(request, response, form, mapping); // Validate any fields of the ActionForm bean, if applicable try { if (!processValidate(request, response, form, mapping)) { return; } } catch (InvalidCancelException e) { ActionForward forward = processException(request, response, e, form, mapping); processForwardConfig(request, response, forward); return; throw e; } catch (ServletException e) { throw e; } // Process a forward or include specified by this mapping if (!processForward(request, response, mapping)) { return; } if (!processInclude(request, response, mapping)) { return; } // Create or acquire the Action instance to process this request if (action == null) { return; } // Call the Action instance itself ActionForward forward = processActionPerform(request, response, action, form, mapping); // Process the returned ActionForward instance processForwardConfig(request, response, forward); }
protected HttpServletRequest processMultipart(HttpServletRequest request) { if (!"POST".equalsIgnoreCase(request.getMethod())) { return (request); } if ((contentType != null) && contentType.startsWith("multipart/form-data")) { return (new MultipartRequestWrapper(request)); } else { return (request); } }
HttpServletResponse response) String path; // For prefix matching, match on the path info (if any) if (path == null) { path = request.getPathInfo(); } if ((path != null) && (path.length() > 0)) { return (path); } // For extension matching, strip the module prefix and extension if (path == null) { path = request.getServletPath(); } if (!path.startsWith(prefix)) { log.error(msg + " " + request.getRequestURI()); response.sendError(HttpServletResponse.SC_BAD_REQUEST, msg); return null; } path = path.substring(prefix.length()); int slash = path.lastIndexOf("/"); int period = path.lastIndexOf("."); if ((period >= 0) && (period > slash)) { path = path.substring(0, period); } return (path); }
protected void processLocale(HttpServletRequest request, HttpServletResponse response) { // Are we configured to select the Locale automatically? if (!moduleConfig.getControllerConfig().getLocale()) { return; } // Has a Locale already been selected? HttpSession session = request.getSession(); if (session.getAttribute(Globals.LOCALE_KEY) != null) { return; } // Use the Locale returned by the servlet container (if any) if (locale != null) { if (log.isDebugEnabled()) { log.debug(" Setting user locale '" + locale + "'"); } session.setAttribute(Globals.LOCALE_KEY, locale); } }
protected void processContent(HttpServletRequest request, HttpServletResponse response) { String contentType = moduleConfig.getControllerConfig().getContentType(); if (contentType != null) { response.setContentType(contentType); } }
protected void processNoCache(HttpServletRequest request, HttpServletResponse response) { if (moduleConfig.getControllerConfig().getNocache()) { response.setHeader("Pragma", "No-cache"); response.setHeader("Cache-Control", "no-cache,no-store,max-age=0"); response.setDateHeader("Expires", 1); } }
protected boolean processPreprocess(HttpServletRequest request, HttpServletResponse response) { return (true); }
и т.д.