Zastosowanie
Odwiedzający
(ang. Visitor
) (wzorzec behawioralny) ma za zadanie odsperować implementację algorytmu od struktury istniejących klas bez konieczności modyfikacji ich bieżącego kodu. Klasy rozszerzone o nową funkcjonalność akceptują obiekt Odwiedzającego
(parametr implementowanej metody) i przenoszą na niego odpowiedzialność realizacji zadania. Obiekt Odwiedzający
dostarcza implementacje algorytmu zgodnie z typem obiektu Odwiedzonego
, który został przekazany jako parametr (double dispatch
). Dodanie funkcjonalności do klas wymaga minimalnego nakładu zmian bez modyfikacji ich aktualnej struktury oraz logiki, a implementacja obiektów Odwiedzających
może być wymienna.
Ograniczenia
Należy unikać stosowania wzorca w przypadku niestabilnej, często zmieniającej się struktury i hierarchii klas, ponieważ zmiany te pociągają za sobą modyfikację klas Odwiedzających
. Co więcej wzorzec narusza hermetyzację komponentów.
Użycie
Odwiedzający
ma zastosowanie w sytuacjach, gdy wymagane jest dodanie funkcjonalności dla zbioru klas, a modyfikacja struktury istniejących klas jest utrudniona lub niedozwolona.
Implementacja
Klasy które wymagają rozszerzenia o nową funkcjonalność implementują interfejs Element
. W metodzie akceptującej wizytację obiektu typu Visitor
, przekazują referencje do własnej instancji i delegują wykonanie zadania obiektowi Odwiedzającemu
. Klasy obiektów Odwiedzających
implementują interfejs Visitor
, dostarczając realizację tego samego zadania dla różnych typów obiektów Odwiedzanych
.
Poniższy listing przedstawia wizytację obiektów typu Visitor
dla klas typu Element
.
public class Element1 implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
public void operation() {
//do some work
}
}
public class Element2 implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
public void action() {
//do some work
}
}
public class Element3 implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
public void work() {
//do some work
}
}
public interface Element {
void accept(Visitor visitor);
}
public class ConcreteVisitor1 {
@Override
public void visit(Element1 element) {
//do something specific for ConcreteVisitor1
element.operation();
}
@Override
public void visit(Element2 element) {
//do something specific for ConcreteVisitor1
element.action();
}
@Override
public void visit(Element3 element) {
//do something specific for ConcreteVisitor1
element.work();
}
}
public class ConcreteVisitor2 {
@Override
public void visit(Element1 element) {
element.operation();
//do something specific for ConcreteVisitor2
}
@Override
public void visit(Element2 element) {
element.action();
//do something specific for ConcreteVisitor2
}
@Override
public void visit(Element3 element) {
element.work();
//do something specific for ConcreteVisitor2
}
}
interface Visitor {
void visit(Element1 element);
void visit(Element2 element);
void visit(Element3 element);
}
Klient dokonuje realizacji zadania poprzez wybór obiektu Odwiedzającego
i wywołaniu metody akceptacji dla kolekcji elementów Odwiedzanych
.
//create some elements
List<Element> elements = new ArrayList();
elements.add(new Element1());
elements.add(new Element2());
elements.add(new Element3());
//choose visitor
Visitor visitor = new ConcreteVisitor1();
for(Element element : elements)
element.accept(visitor); //do specific job for ConcreteVisitor1
//change visitor type
visitor = new ConcreteVisitor2();
for(Element element : elements)
element.accept(visitor); //do specific job for ConcreteVisitor2
Przykład
Księgarnia korzysta z oprogramowania Bookstore
, które umożliwia zarządzanie aktualnym stanem zbiorów książek, filmów oraz gier komputerowych. Księgarnia chciałaby umożliwić czytelnikom sprawdzanie stanu swoich dostępnych zasobów przez internet. Ponadto dział techniczny zgłosił potrzebę dostępu do informacji o zasobach księgarni w formacie json
. Ze względu na brak finansów oraz czasu niezbędnych do przetestowania potencjalnych zmian, dyrektor działu technicznego nie wyraził zgody na silną modyfikacje bieżącego kodu. Z myślą o niskim koszcie modyfikacji oraz rozszerzeniu generowanych plików o nowy format, programista podjął decyzję o wykorzystaniu wzorca Odwiedzający. Poniższy listing przedstawia zmiany dokonane w bieżącym kodzie poprzez implementacje interfejsu Product
w klasach zasobów.
public class Book implements Product {
//fields and constructor
//title, volume, description, author, isbn, pages
//the only change
@Override
public void generateFile(FileGenerator visitor) {
visitor.createFile(this);
}
//getter methods
//other methods
}
public class Film implements Product {
//fields and constructor
//title, description, director, scenario, actors, years
//the only change
@Override
public void generateFile(FileGenerator visitor) {
visitor.createFile(this);
}
//getter methods
//other methods
}
public class Game implements Product {
//fields and constructor
//title, description, producer, budget
//the only change
@Override
public void generateFile(FileGenerator visitor) {
visitor.createFile(this);
}
//getter methods
//other methods
}
interface Product {
void accept(FileGenerator visitor);
}
Implementacja sposobu generowania plików została stworzona poza bieżącym kodem co prezentuje poniższy listing.
public class HtmlGenerator implements FileGenerator {
@Override
public void createFile(Book element) {
StringBuilder builder = new StringBuilder();
builder.append("<html>").append("<head>");
builder.append("<title>").append(element.getTitle()).append("</title>")
builder.append("</head>").append("<body>");
builder.append("<h1>").append(element.getTitle()).append("</h1>");
builder.append("<h2>").append(element.getVolume()).append("</h2>");
builder.append("<h3>").append(element.getAuthor()).append("</h3>");
builder.append("<p>").append(element.getDescription()).append("</p>");
builder.append("<p>Pages: ").append(element.getPages()).append("</p>");
builder.append("<p>ISBN: ").append(element.getIsbn()).append("</p>");
builder.append("</body>").append("</html>");
String fileName = element.getTitle() + "_" + element.getVolume() + ".html";
FileManager.write(fileName, builder.toString());
}
@Override
public void createFile(Film element) {
StringBuilder builder = new StringBuilder();
builder.append("<html>").append("<head>");
builder.append("<title>").append(element.getTitle()).append("</title>")
builder.append("</head>").append("<body>");
builder.append("<h1>").append(element.getTitle()).append("</h1>");
builder.append("<h2>").append(element.getDirector())).append("</h2>");
builder.append("<h3>").append(element.getScenario()).append("</h3>");
for(Actor actor : element.getActors())
builder.append("<h4>").append(actor.getName()).append("(").append(actor.getRole()).append(")");
builder.append("<p>Year production: ").append(element.getYear()).append("</p>");
builder.append("</body>").append("</html>");
String fileName = element.getTitle() + ".html";
FileManager.write(fileName, builder.toString());
}
@Override
public void createFile(Game element) {
StringBuilder builder = new StringBuilder();
builder.append("<html>").append("<head>");
builder.append("<title>").append(element.getTitle()).append("</title>")
builder.append("</head>").append("<body>");
builder.append("<h1>").append(element.getTitle()).append("</h1>");
builder.append("<h2>").append(element.getProducer()).append("</h2>");
builder.append("<p>").append(element.getDescription()).append("</p>");
builder.append("<p>Budget: ").append(element.getBudget()).append("</p>");
builder.append("</body>").append("</html>");
String fileName = element.getTitle() + ".html";
FileManager.write(fileName, builder.toString());
}
}
public class JsonGenerator implements FileGenerator {
@Override
public void createFile(Book element) {
StringBuilder builder = new StringBuilder();
builder.append("{"));
builder.append("title:").append("\"").append(element.getTitle()).append("\",");
builder.append("volume:").append(element.getVolume()).append(",");
builder.append("author:").append("\"").append(element.getAuthor()).append("\",");
builder.append("description:").append("\"").append(element.getDescription()).append("\",");
builder.append("pages:").append(element.getPages()).append(",");
builder.append("isbn:").append("\"").append(element.getIsbn()).append("\"};");
String fileName = element.getTitle() + "_" + element.getVolume() + ".json";
FileManager.write(fileName, builder.toString());
}
@Override
public void createFile(Film element) {
StringBuilder builder = new StringBuilder();
builder.append("{"));
builder.append("title:").append("\"").append(element.getTitle()).append("\",");
builder.append("director:").append("\"").append(element.getDirector()).append("\",");
builder.append("scenario:").append("\"").append(element.getScenario()).append("\",");
builder.append("actors:").append("[");
for(int i=0; i<=element.getActors().size()-1; i++) {
builder.append("\"").append(actor.getName() + "(" + actor.getRole() + ")").append("\"")
if(i != element.getActors().size()-1)
builder.append(",");
}
builder.append("],")
builder.append("year:").append(element.getYear()).append("};");
String fileName = element.getTitle() + ".json";
FileManager.write(fileName, builder.toString());
}
@Override
public void createFile(Game element) {
StringBuilder builder = new StringBuilder();
builder.append("{"));
builder.append("title:").append("\"").append(element.getTitle()).append("\",");
builder.append("producer:").append("\"").append(element.getProducer()).append("\",");
builder.append("description:").append("\"").append(element.getDescription()).append("\",");
builder.append("budget:").append(element.getBudget()).append("};");
String fileName = element.getTitle() + "_" + element.getVolume() + ".json";
FileManager.write(fileName, builder.toString());
}
}
public interface FileGenerator {
void createFile(Book element);
void createFile(Film element);
void createFile(Game element);
}
Dział techniczny korzystając z modyfikacji oprogramowania generuje pliki html
oraz json
dla wybranych zasobów.
//get resources from database
List<Product> products = new ArrayList();
//add books
product.add(new Book("Hobbit", 1, "Description", "J.R.R. Tolkien", "15515377", 320));
//add films
List<Actor> actors = new ArrayList();
actors.add(new Actor("Russell Crowe", "Maximus"));
actors.add(new Actor("Joaquin Phoenix", "Kommodus"))
product.add(new Film("Gladiator", "Description", "Ridley Scott", "David Franzoni", actors, 2000));
//add games
product.add(new Game("Heroes of Might and Magic III", "Description", "Ubisoft", 1000000));
//generate html files for customers
FileGenerator visitor = new HtmlGenerator();
for(Product element : products)
element.generateFile(visitor);
//generate json files for IT
visitor = new JsonGenerator();
for(Product element : products)
element.generateFile(visitor);
Biblioteki
Przykładem biblioteki implementującej wzorzec Odwiedzający
są klasy ElementVisitor
oraz Element
(ze standardowego pakietu Java
). Ze względu na ich generyczność, mogą być z powodzeniem zastosowane w wielu przypadkach bez konieczności tworzenia własnych interfejsów.