Шаблоны проектирования Java J2EE

Обзор Это четвертая и последняя статья из короткой серии, посвященной шаблонам проектирования в Java [/ design-patterns-in-java /], и является прямым продолжением предыдущей статьи - Шаблоны поведенческого проектирования в Java [/ behavior-design- шаблоны-в-java /]. Шаблоны J2EE Шаблоны J2EE заботятся о предоставлении решений, касающихся Java EE. Эти шаблоны широко применяются в других фреймворках и проектах. Например: Spring [https://spring.io/]. Паттерны J2EE, которые покрывают

Обзор

Это четвертая и последняя статья из короткой серии, посвященной шаблонам проектирования в Java , и прямое продолжение предыдущей статьи - Шаблоны проектирования в Java .

Паттерны J2EE

Шаблоны J2EE заботятся о предоставлении решений, касающихся Java EE. Эти шаблоны широко применяются в других фреймворках и проектах. Например: Весна .

В этой статье рассматриваются следующие шаблоны J2EE:

Шаблон MVC

Это один из самых известных и часто используемых паттернов из этой категории. Он основан на идее Модель-Представление-Контроллер , отсюда и происходит аббревиатура.

Модели - это в основном объекты, точнее POJO, используемые в качестве чертежей / моделей для всех объектов, которые будут использоваться в приложении.

Представления представляют собой презентационный аспект данных и информации, находящихся в моделях.

Контроллеры контролируют и то, и другое. Они служат связующим звеном между ними. Контроллеры создают, обновляют и удаляют модели, заполняют их информацией, а затем отправляют данные в представления для представления конечному пользователю.

Выполнение

При этом давайте начнем с первого из трех компонентов этого паттерна - модели:

 public class Employee { 
 private int employeeId; 
 private String name; 
 
 public int getEmployeeId() { 
 return employeeId; 
 } 
 public void setEmployeeId(int id) { 
 this.employeeId = id; 
 } 
 public String getName() { 
 return name; 
 } 
 public void setEmployeeName(String name) { 
 this.name = name; 
 } 
 } 

Нам нужен способ представления данных из модели, поэтому мы определяем представление именно для этой цели:

 public class EmployeeView { 
 public void printEmployeeInformation(String employeeName, int employeeId) { 
 System.out.println("Employee information: "); 
 System.out.println("ID: " + employeeId); 
 System.out.println("Name: " + employeeName); 
 } 
 } 

Представление отвечает за форматирование информации в удобном для пользователя виде.

Как только это будет решено, давайте определим контроллер. Этот контроллер будет использовать как модель, так и представление, чтобы создать экземпляр модели, заполнить его некоторыми данными, а затем передать его в представление, чтобы клиент мог увидеть:

 public class EmployeeController { 
 private Employee employee; 
 private EmployeeView employeeView; 
 
 public EmployeeController(Employee employee, EmployeeView employeeView) { 
 this.employee = employee; 
 this.employeeView = employeeView; 
 } 
 
 public String getEmployeeName() { 
 return employee.getName(); 
 } 
 public void setEmployeeName(String name) { 
 employee.setEmployeeName(name); 
 } 
 public int getEmployeeId() { 
 return employee.getEmployeeId(); 
 } 
 public void setEmployeeId(int id) { 
 employee.setEmployeeId(id); 
 } 
 public void updateView() { 
 employeeView.printEmployeeInformation(employee.getName(), employee.getEmployeeId()); 
 } 
 } 

Завершив все три компонента этого шаблона, мы можем завершить этот пример.

Чтобы проиллюстрировать суть этого паттерна:

 public class Main { 
 public static void main(String[] args) { 
 Employee employee = getEmployeeFromDatabase(); 
 EmployeeView view = new EmployeeView(); 
 EmployeeController controller = new EmployeeController(employee, view); 
 
 controller.updateView(); 
 
 controller.setEmployeeId(5); 
 
 controller.updateView(); 
 } 
 
 // simulating a database 
 public static Employee getEmployeeFromDatabase() { 
 Employee employee = new Employee(); 
 employee.setEmployeeName("David"); 
 employee.setEmployeeId(1); 
 return employee; 
 } 
 } 

Выполнение этого фрагмента кода даст:

 Employee information: 
 ID: 1 
 Name: David 
 Employee information: 
 ID: 5 
 Name: David 

Шаблон бизнес-делегата

Шаблон Business Delegate используется для отделения уровня представления от бизнес-уровня, чтобы минимизировать количество запросов между клиентом (презентацией) и бизнес-уровнями.

Выполнение

Начнем с определения интерфейса для наших бизнес-сервисов:

 public interface BusinessService { 
 public void process(); 
 } 

После этого давайте определим два конкретных класса, реализующих этот интерфейс:

 public class EJBService implements BusinessService { 
 @Override 
 public void process() { 
 System.out.println("Processing using the EJB Service."); 
 } 
 } 
 
 public class JMSService implements BusinessService { 
 @Override 
 public void process() { 
 System.out.println("Processing using the JSM Service."); 
 } 
 } 

Определим службу поиска. Объект службы поиска должен предоставлять относительные бизнес-реализации и доступ бизнес-объекта к логике бизнес-делегата:

 public class BusinessLookUp { 
 public BusinessService getBusinessService(String type) { 
 if (type.equalsIgnoreCase("ejb")) { 
 return new EJBService(); 
 } else if (type.equalsIgnoreCase("JMS")) { 
 return new JMSService(); 
 } else { 
 return null; 
 } 
 } 
 } 

Теперь мы можем определить нашего бизнес-делегата:

 public class BusinessDelegate { 
 private BusinessLookUp lookupService = new BusinessLookUp(); 
 private BusinessService businessService; 
 private String type; 
 
 public void setServiceType(String type) { 
 this.type = type; 
 } 
 
 public void process() { 
 businessService = lookupService.getBusinessService(type); 
 businessService.process(); 
 } 
 } 

Он действует как точка доступа к бизнес-сервисам для использования Client :

 public class Client { 
 BusinessDelegate businessDelegate; 
 
 public Client(BusinessDelegate businessDelegate) { 
 this.businessDelegate = businessDelegate; 
 } 
 
 public void process() { 
 businessDelegate.process(); 
 } 
 } 

А теперь, чтобы проиллюстрировать суть этого паттерна:

 public class Main { 
 public static void main(String[] args) { 
 BusinessDelegate businessDelegate = new BusinessDelegate(); 
 businessDelegate.setServiceType("EJB"); 
 
 Client client = new Client(businessDelegate); 
 client.process(); 
 
 businessDelegate.setServiceType("JMS"); 
 client.process(); 
 } 
 } 

Выполнение этого фрагмента кода даст:

 Processing using the EJB Service. 
 Processing using the JSM Service. 

Шаблон составного объекта

Шаблон составной сущности представляет собой граф объектов, который при обновлении запускает обновление для всех зависимых сущностей в графе.

Он в основном используется в Enterprise JavaBeans (EJB), который не является очень популярным API, поскольку был заменен другими фреймворками и инструментами, такими как Spring Framework и его многочисленные инструменты.

Выполнение

Давайте определим два класса, данные функций которых потребуются для обновления другого класса:

 public class Employee { 
 private String name; 
 private String jobSuccess; 
 
 public void setJobSuccess(String jobSuccess) { 
 this.jobSuccess = jobSuccess; 
 } 
 
 public String getJobSuccess() { 
 return jobSuccess; 
 } 
 } 

 public class Manager { 
 private String name; 
 private String satisfaction; 
 
 public void setSatisfaction(String satisfaction) { 
 this.satisfaction = satisfaction; 
 } 
 
 public String getSatisfaction() { 
 return satisfaction; 
 } 
 } 

Если Employee работает хорошо, Manager доволен, и наоборот.

Так как точка этого шаблона, чтобы не позволить бобы действовать в качестве «мелкозернистый» объекты в одиночку, мы введены с объектом крупнозернистый. Этот объект управляет своими отношениями с другими объектами:

 public class CoarseGrainedObject { 
 Employee employee = new Employee(); 
 Manager manager = new Manager(); 
 
 public void setData(String jobSuccess, String satisfaction) { 
 employee.setJobSuccess(jobSuccess); 
 manager.setSatisfaction(satisfaction); 
 } 
 
 public String[] getData() { 
 return new String[] {"Employee : " + employee.getJobSuccess(),"Manager: " + 
 manager.getSatisfaction()}; 
 } 
 } 

После этого нам нужно определить класс CompositeEntity Этот класс сам по себе является крупнозернистым объектом и может ссылаться на другой:

 public class CompositeEntity { 
 private CoarseGrainedObject cgo = new CoarseGrainedObject(); 
 
 public void setData(String jobSuccess, String satisfaction) { 
 cgo.setData(jobSuccess, satisfaction); 
 } 
 
 public String[] getData() { 
 return cgo.getData(); 
 } 
 } 

После этого нам просто нужен Client для использования CompositeEntity :

 public class Client { 
 private CompositeEntity compositeEntity = new CompositeEntity(); 
 
 public void print() { 
 for (int i = 0; i < compositeEntity.getData().length; i++) { 
 System.out.println(compositeEntity.getData()[i]); 
 } 
 } 
 
 public void setData(String jobSuccess, String satisfaction) { 
 compositeEntity.setData(jobSuccess, satisfaction); 
 } 
 } 

И чтобы проиллюстрировать суть этого паттерна:

 public class Main { 
 public static void main(String[] args) { 
 Client client = new Client(); 
 client.setData("Successful", "Satisfied"); 
 client.print(); 
 client.setData("Failed", "Unsatisfied"); 
 client.print(); 
 } 
 } 

Выполнение этого фрагмента кода даст:

 Employee : Successful 
 Manager: Satisfied 
 Employee : Failed 
 Manager: Unsatisfied 

Шаблон объекта доступа к данным

Шаблон объекта доступа к данным, который чаще всего сокращается до DAO, представляет собой шаблон, в котором объекты предназначены для взаимодействия с уровнем данных.

Эти объекты часто создают для этой цели экземпляры «SessionFactories» и обрабатывают всю логику взаимодействия с базой данных.

Стандартная практика - создать интерфейс DAO, за которым следует конкретный класс, реализующий интерфейс и все методы, определенные в нем.

Выполнение

Следуя стандартной практике, давайте определим наш интерфейс DAO:

 public interface EmployeeDAO { 
 public List<Employee> getAllEmployees(); 
 public Employee getEmployeeById(int id); 
 public void addEmployee(Employee e); 
 public void updateEmployee(Employee e); 
 public void deleteEmployee(Employee e); 
 } 

И наш конкретный класс реализации вместе с ним:

 public class EmployeeDAOImpl implements EmployeeDAO { 
 List<Employee> employeeList; 
 
 public EmployeeDAOImpl() { 
 employeeList = new ArrayList<Employee>(); 
 Employee david = new Employee(5, "David"); 
 Employee scott = new Employee(7, "Scott"); 
 Employee jessica = new Employee(12, "Jessica"); 
 Employee rebecca = new Employee(16, "Rebecca"); 
 employeeList.add(david); 
 employeeList.add(scott); 
 employeeList.add(jessica); 
 employeeList.add(rebecca); 
 } 
 
 @Override 
 public List<Employee> getAllEmployees() { 
 return employeeList; 
 } 
 @Override 
 public Employee getEmployeeById(int id) { 
 return employeeList.get(id); 
 } 
 @Override 
 public void addEmployee(Employee e) { 
 employeeList.add(e); 
 System.out.println("Successfully added " + e.getName()); 
 } 
 @Override 
 public void updateEmployee(Employee e) { 
 employeeList.get(e.getEmployeeId()).setEmployeeName(e.getName()); 
 System.out.println("Successfully update name of employee with id: " + e.getEmployeeId()); 
 } 
 @Override 
 public void deleteEmployee(Employee e) { 
 employeeList.remove(e.getEmployeeId()); 
 System.out.println("Successfully removed employee: " + e.getName() + "with the ID: " + e.getEmployeeId()); 
 } 
 } 

Мы будем использовать эти два класса для добавления, извлечения, обновления или удаления пользователей из нашей базы данных:

 public class Employee { 
 private int employeeId; 
 private String name; 
 
 public Employee(int id, String name) { 
 this.employeeId = id; 
 this.name = name; 
 } 
 
 public int getEmployeeId() { 
 return employeeId; 
 } 
 public void setEmployeeId(int id) { 
 this.employeeId = id; 
 } 
 public String getName() { 
 return name; 
 } 
 public void setEmployeeName(String name) { 
 this.name = name; 
 } 
 } 

И чтобы проиллюстрировать суть этого паттерна:

 public class Main { 
 public static void main(String[] args) { 
 EmployeeDAO employeeDao = new EmployeeDAOImpl(); 
 
 for(Employee employee : employeeDao.getAllEmployees()) { 
 System.out.println("Employee info: |Name: " + employee.getName() + ", ID: " + employee.getEmployeeId() + "|"); 
 } 
 } 
 } 

Выполнение этого фрагмента кода даст:

 Employee info: |Name: David, ID: 5| 
 Employee info: |Name: Scott, ID: 7| 
 Employee info: |Name: Jessica, ID: 12| 
 Employee info: |Name: Rebecca, ID: 16| 

Шаблон переднего контроллера

После отправки запроса передний контроллер становится первым контроллером, к которому он обращается. На основе запроса он решает, какой контроллер наиболее подходит для его обработки, после чего передает запрос выбранному контроллеру.

Front Controller чаще всего используется в веб-приложениях в форме сервлета диспетчера .

Выполнение

Для этой реализации мы определим два простых представления, FrontController и Dispatcher :

 public class MainView { 
 public void showView() { 
 System.out.println("Showing main view."); 
 } 
 } 
 
 public class EmployeeView { 
 public void showView() { 
 System.out.println("Showing Employee view."); 
 } 
 } 

Запрос на любой из этих двух может появиться в любой момент. Мы используем Dispatcher для обработки запроса, указывая на правильное представление, после того, как FrontController обработал запрос изначально:

 public class Dispatcher { 
 private MainView mainView; 
 private EmployeeView employeeView; 
 
 public Dispatcher() { 
 mainView = new MainView(); 
 employeeView = new EmployeeView(); 
 } 
 
 public void dispatch(String request) { 
 if(request.equalsIgnoreCase("EMPLOYEE")) { 
 employeeView.showView(); 
 } else { 
 mainView.showView(); 
 } 
 } 
 } 

 public class FrontController { 
 private Dispatcher dispatcher; 
 
 public FrontController() { 
 dispatcher = new Dispatcher(); 
 } 
 
 private boolean isAuthenticUser() { 
 System.out.println("User has successfully authenticated."); 
 return true; 
 } 
 
 private void trackRequest(String request) { 
 System.out.println("Request: " + request); 
 } 
 
 public void dispatchRequest(String request) { 
 trackRequest(request); 
 
 if(isAuthenticUser()) { 
 dispatcher.dispatch(request); 
 } 
 } 
 } 

И чтобы проиллюстрировать суть паттерна:

 public class Main { 
 public static void main(String[] args) { 
 FrontController frontController = new FrontController(); 
 frontController.dispatchRequest("MAIN"); 
 frontController.dispatchRequest("EMPLOYEE"); 
 } 
 } 

Выполнение этого фрагмента кода даст:

 Request: MAIN 
 User has successfully authenticated. 
 Showing main view. 
 Request: EMPLOYEE 
 User has successfully authenticated. 
 Showing Employee view. 

Шаблон перехватывающего фильтра

Фильтры используются еще до того, как запрос будет передан соответствующим контроллерам для обработки. Эти фильтры могут существовать в форме цепочки фильтров и включать несколько фильтров или просто существовать как один фильтр.

Тем не менее, они проверяют авторизацию, аутентификацию, поддерживаемые браузеры, не нарушает ли путь запроса какие-либо ограничения и ограничения и т. Д.

Выполнение

Мы сделаем простую цепочку фильтров с парой фильтров для перехвата запроса после достижения цели.

Начнем с определения интерфейса самого Filter :

 public interface Filter { 
 public void execute(String request); 
 } 

И пара конкретных реализаций:

 public class AuthenticationFilter implements Filter { 
 @Override 
 public void execute(String request) { 
 System.out.println("Authentication request: " + request); 
 } 
 } 
 
 public class DebuggingFilter implements Filter { 
 @Override 
 public void execute(String request) { 
 System.out.println("Logging request: " + request); 
 } 
 } 

И, наконец, Target запроса:

 public class Target { 
 public void execute(String request) { 
 System.out.println("Executing request: " + request); 
 } 
 } 

Определив FilterChain , мы можем добавить несколько фильтров для перехвата запроса. Давайте определим один для наших двух фильтров:

 public class FilterChain { 
 private List<Filter> filters = new ArrayList<>(); 
 private Target target; 
 
 public void addFilter(Filter filter) { 
 filters.add(filter); 
 } 
 
 public void execute(String request) { 
 for (Filter filter : filters) { 
 filter.execute(request); 
 } 
 target.execute(request); 
 } 
 
 public void setTarget(Target target) { 
 this.target = target; 
 } 
 } 

Теперь нам нужен класс менеджера, который поможет управлять этой FilterChain :

 public class FilterManager { 
 FilterChain filterChain; 
 
 public FilterManager(Target target) { 
 filterChain = new FilterChain(); 
 filterChain.setTarget(target); 
 } 
 
 public void addFilter(Filter filter) { 
 filterChain.addFilter(filter); 
 } 
 
 public void filterRequest(String request) { 
 filterChain.execute(request); 
 } 
 } 

И, наконец, Client будет использовать FilterManager для отправки запроса приложению:

 public class Client { 
 FilterManager filterManager; 
 
 public void setFilterManager(FilterManager filterManager) { 
 this.filterManager = filterManager; 
 } 
 
 public void sendRequest(String request) { 
 filterManager.filterRequest(request); 
 } 
 } 

Теперь, чтобы проиллюстрировать суть этого паттерна:

 public class Main { 
 public static void main(String[] args) { 
 FilterManager filterManager = new FilterManager(new Target()); 
 filterManager.addFilter(new AuthenticationFilter()); 
 filterManager.addFilter(new DebuggingFilter()); 
 
 Client client = new Client(); 
 client.setFilterManager(filterManager); 
 client.sendRequest("Index"); 
 } 
 } 

Выполнение этого фрагмента кода даст:

 Authentication request: Index 
 Logging request: Index 
 Executing request: Index 

FilterChain через оба фильтра из FilterChain перед переадресацией в Target .

Шаблон локатора услуг

Шаблон, часто встречающийся в веб-приложениях , шаблон Service Locator используется для разделения потребителей службы и конкретных классов, таких как реализации DAO.

Шаблон ищет соответствующий сервис, сохраняет его в кеш-хранилище, чтобы уменьшить количество запросов и, следовательно, нагрузку на сервер, и предоставляет приложению их экземпляры.

Выполнение

Давайте начнем эту реализацию с определения общего интерфейса Service

 public interface Service { 
 public String getServiceName(); 
 public void execute(); 
 } 

Пара конкретных классов будет реализовывать этот интерфейс:

 public class EmployeeService implements Service { 
 @Override 
 public String getServiceName() { 
 return "Employee Service"; 
 } 
 
 @Override 
 public void execute() { 
 System.out.println("Executing Employee Service..."); 
 } 
 } 
 
 public class CustomerService implements Service { 
 @Override 
 public String getServiceName() { 
 return "Customer Service"; 
 } 
 
 @Override 
 public void execute() { 
 System.out.println("Executing Customer Service..."); 
 } 
 } 

Согласно шаблону, при поиске этих сервисов мы должны кэшировать их, чтобы снизить нагрузку на сервер:

 public class Cache { 
 private List<Service> services; 
 
 public Cache() { 
 services = new ArrayList<Service>(); 
 } 
 
 public Service getService(String serviceName) { 
 for(Service service : services) { 
 if(service.getServiceName().equalsIgnoreCase(serviceName)) { 
 System.out.println("Returning cached " + serviceName); 
 return service; 
 } 
 } 
 return null; 
 } 
 
 public void addService(Service newService) { 
 boolean exists = false; 
 
 for(Service service : services){ 
 if(service.getServiceName().equalsIgnoreCase(newService.getServiceName())) { 
 exists = true; 
 } 
 } 
 if(!exists) { 
 services.add(newService); 
 } 
 } 
 } 

Нам также нужен класс для поиска и создания экземпляров наших сервисов:

 public class InitialContext { 
 public Object lookup(String jndiName) { 
 if(jndiName.equalsIgnoreCase("EmployeeService")) { 
 System.out.println("Looking up and initializing Employee Service..."); 
 return new EmployeeService(); 
 } else if(jndiName.equalsIgnoreCase("CustomerService")) { 
 System.out.println("Looking up and initializing Customer Service..."); 
 return new CustomerService(); 
 } 
 return null; 
 } 
 } 

И, наконец, мы можем определить Locator для предоставления клиенту, который использует InitialContext для поиска служб и Cache для их кэширования для дальнейшего использования.

 public class Locator { 
 private static Cache cache; 
 
 static { 
 cache = new Cache(); 
 } 
 
 public static Service getService(String jndiName) { 
 Service service = cache.getService(jndiName); 
 
 if(service != null) { 
 return service; 
 } 
 
 InitialContext context = new InitialContext(); 
 Service service1 = (Service)context.lookup(jndiName); 
 cache.addService(service1); 
 return service1; 
 } 
 } 

И чтобы проиллюстрировать суть этого паттерна:

 public class Main { 
 public static void main(String[] args) { 
 Service service = Locator.getService("EmployeeService"); 
 service.execute(); 
 service = Locator.getService("CustomerService"); 
 service.execute(); 
 } 
 } 

Выполнение этого фрагмента кода даст:

 Looking up and initializing Employee Service... 
 Executing Employee Service... 
 Looking up and initializing Customer Service... 
 Executing Customer Service... 

Перенести образец объекта

Этот шаблон используется для передачи объектов с большим количеством полей и параметров за один раз. В шаблоне Transfer Object используются новые объекты, используемые только для целей передачи, обычно передаваемые в DAO.

Эти объекты являются сериализуемыми POJO . У них есть поля, соответствующие им геттеры и сеттеры и никакой другой логики.

Выполнение

Объект может выглядеть так:

 public class EmployeeVO { 
 private int employeeId; 
 private String name; 
 
 public EmployeeVO(int employeeId, String name) { 
 this.employeeId = employeeId; 
 this.name = name; 
 } 
 
 public int getEmployeeId() { 
 return employeeId; 
 } 
 
 public void setEmployeeId(int id) { 
 this.employeeId = id; 
 } 
 
 public String getName() { 
 return name; 
 } 
 
 public void setName(String name) { 
 this.name = name; 
 } 
 } 

Обратите внимание, что для краткости объект содержит только несколько полей.

Пример нового объекта, который используется только для целей передачи:

 public class EmployeeBO { 
 List<EmployeeVO> employees; 
 
 public EmployeeBO() { 
 employees = new ArrayList<>(); 
 EmployeeVO david = new EmployeeVO(1, "David"); 
 EmployeeVO scott = new EmployeeVO(2, "Scott"); 
 EmployeeVO jessica = new EmployeeVO(3, "Jessica"); 
 employees.add(david); 
 employees.add(scott); 
 employees.add(jessica); 
 } 
 
 public void deleteEmployee(EmployeeVO employee) { 
 employees.remove(employee.getEmployeeId()); 
 System.out.println("Employee with ID: " + employee.getEmployeeId() + " was successfully deleted."); 
 } 
 
 public List<EmployeeVO> getAllEmployees() { 
 return employees; 
 } 
 
 public EmployeeVO getEmployee(int id) { 
 return employees.get(id); 
 } 
 
 public void updateEmployee(EmployeeVO employee) { 
 employees.get(employee.getEmployeeId()).setName(employee.getName()); 
 System.out.println("Employee with ID: " + employee.getEmployeeId() + " successfully updated."); 
 } 
 } 

И чтобы проиллюстрировать суть паттерна:

 public class Main { 
 public static void main(String[] args) { 
 EmployeeBO employeeBo = new EmployeeBO(); 
 
 for(EmployeeVO employee : employeeBo.getAllEmployees()) { 
 System.out.println("Employee: |" + employee.getName() + ", ID: " + employee.getEmployeeId() + "|"); 
 } 
 
 EmployeeVO employee = employeeBo.getAllEmployees().get(0); 
 employee.setName("Andrew"); 
 employeeBo.updateEmployee(employee); 
 
 employee = employeeBo.getEmployee(0); 
 System.out.println("Employee: |" + employee.getName() + ", ID: " + employee.getEmployeeId() + "|"); 
 
 } 
 } 

Выполнение этого фрагмента кода даст:

 Employee: |David, ID: 1| 
 Employee: |Scott, ID: 2| 
 Employee: |Jessica, ID: 3| 
 Employee with ID: 1 successfully updated. 
 Employee: |Andrew, ID: 1| 

Заключение

При этом все шаблоны проектирования J2EE в Java полностью покрыты с рабочими примерами.

На этом мы завершаем нашу короткую серию статей о шаблонах проектирования Java. Если вы нашли это информативным и пропустили какой-либо из предыдущих, не стесняйтесь проверить их тоже:

comments powered by Disqus