Na zakończenie cyklu o wzorcach projektowych poznamy dwa
niezwykle ważne wzorce: Command i Model-View-Controller - MVC).
Oba wzorce (a szczególnie wzorzec MVC) są kluczowy dla
tworzenia iniwerslanych, elastycznych, łatwo modyfikowalnych
programów i systemów
import java.util.*; public interface Command { void init(); void setParameter(String name, Object value); Object getParameter(String name); void execute(); List getResults(); void setStatusCode(int code); int getStatusCode(); }Zatem każda klasa działania powinna implementować metody inicjacji, ustalania i pobierania ew. parametrów, wykonania działania, ustalenia i pobrania kodu wyniku, pobrania wyników działania. Umówimy się, że wyniki będą dostępne jako lista.
import java.util.*; import java.io.*; public class CommandImpl implements Serializable, Command { private Map parameterMap = new HashMap(); private List resultList = new ArrayList(); private int statusCode; public CommandImpl() {} public void init() {} public void setParameter(String name, Object value) { parameterMap.put(name, value); } public Object getParameter(String name) { return parameterMap.get(name); } public void execute() {} public List getResults() { return resultList; } public void addResult(Object o) { resultList.add(o); } public void addResult(String s) { addResult(new Object[] { s } ); } public void clearResult() { resultList.clear(); } public void setStatusCode(int code) { statusCode = code; } public int getStatusCode() { return statusCode; } }
import java.util.*; import java.io.*; import java.util.regex.*; public class FindCommand extends CommandImpl implements Serializable { public FindCommand() {} public void execute() { clearResult(); String regex = (String) getParameter("regex"); String input = (String) getParameter("input"); if (regex == null || input == null) { setStatusCode(1); return; } Pattern pattern; try { pattern = Pattern.compile(regex); } catch (PatternSyntaxException exc) { setStatusCode(2); return; } Matcher matcher = pattern.matcher(input); boolean found = matcher.find(); if (!found) setStatusCode(3); else { setStatusCode(0); do { addResult("\"" + matcher.group() + "\"" + " " + matcher.start() + " " + matcher.end()); } while(matcher.find()); } } }
import java.util.*; import javax.swing.*; public class CommandTest { public CommandTest() { String req = JOptionPane.showInputDialog("Podaj zlecenie do wykonania"); List resList = serviceRequest(req); for (Object o : resList) { if (o.getClass().isArray()) { Object[] arr = (Object[]) o; for (int i=0; i<arr.length; i++) System.out.println(arr[i]); } else System.out.println(o); } } // Metoda obsługi zleceń // niezależna od konkretnych zleceń public List serviceRequest(String req) { Command cmd = null; try { Class klasa = Class.forName(req); cmd = (Command) klasa.newInstance(); String data = JOptionPane.showInputDialog( "------------------- Podaj parametry -----------------"); StringTokenizer st = new StringTokenizer(data, "()"); while (st.hasMoreTokens()) { String para = st.nextToken(); String[] parm = para.split("#"); cmd.setParameter(parm[0], parm[1]); } cmd.execute(); } catch (Exception exc) { exc.printStackTrace(); } return cmd.getResults(); }Tutaj stosujemy następującą konwencję:
Taka separacja umożliwia bardziej elastyczne i logiczne tworzenie kodu, m.in. dzięki skupieniu uwagi na modelu danych, który może następnie być prezentowany przez różne widoki.
W dużym stopniu pozwala także na uniezalęznienie fragmentów kodu odpowiedzoalnych za model, widok i kontroler. Dzięki temu modyfikacje kodu stają się znacznie łatwiejsze, zwiększają się możliwości ponownego użycia gotowych klas (modeli, kontrolerów i widoków).
Podstawowe idee MVC przezentuje poniższy rysunek.
import java.awt.event.*; import java.beans.*; import java.io.*; public class Counter implements Serializable { private int count = 0; private PropertyChangeSupport propertyChange = new PropertyChangeSupport(this); private VetoableChangeSupport vetos = new VetoableChangeSupport(this); public Counter() throws PropertyVetoException { this(0); } public Counter(int aCount) throws PropertyVetoException { setCount( aCount ); } public synchronized void addPropertyChangeListener(PropertyChangeListener listener) { propertyChange.addPropertyChangeListener(listener); } public synchronized void removePropertyChangeListener(PropertyChangeListener listener) { propertyChange.removePropertyChangeListener(listener); } public synchronized void addVetoableChangeListener(VetoableChangeListener l) { vetos.addVetoableChangeListener(l); } public synchronized void removeVetoableChangeListener(VetoableChangeListener l) { vetos.removeVetoableChangeListener(l); } public void increment() throws PropertyVetoException { setCount(getCount()+1); } public void decrement() throws PropertyVetoException { setCount(getCount()-1); } public int getCount() { return count; } public synchronized void setCount(int aCount) throws PropertyVetoException { int oldValue = count; vetos.fireVetoableChange("count", new Integer(oldValue), new Integer(aCount)); count = aCount; propertyChange.firePropertyChange("count", new Integer(oldValue), new Integer(aCount)); } }Widok:
import javax.swing.*; import java.awt.*; import java.beans.*; import java.io.*; public class CounterView extends JLabel implements PropertyChangeListener, Serializable { CounterView() { this("0"); } CounterView(String lab) { super(lab); setOpaque(true); setBorder(BorderFactory.createLineBorder(Color.black)); setPreferredSize(new Dimension(75, 40)); setHorizontalAlignment(CENTER); } public void setLabel(String s) { setText(s); } public void propertyChange(PropertyChangeEvent e) { Integer oldVal = (Integer) e.getOldValue(), newVal = (Integer) e.getNewValue(); System.out.println("Value changed from " + oldVal + " to " + newVal); setText("" + newVal + ""); } }
import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.beans.*; import java.io.*; public class CounterControlGui extends JFrame implements ActionListener { private Counter counter; private CounterView counterView; private CounterLimitator clim; private JButton binc = new JButton("Increment"); private JButton bdec = new JButton("Decrement"); private JTextField txt = new JTextField(10); private JButton save = new JButton("Save"); private JButton load = new JButton("Load"); private static final String fname = "counter.xml"; CounterControlGui(Counter c) { installCounter(c); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); binc.addActionListener(this); cp.add(binc); cp.add(counterView); bdec.addActionListener(this); cp.add(bdec); txt.addActionListener(this); cp.add(txt); save.addActionListener(this); cp.add(save); load.addActionListener(this); cp.add(load); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); pack(); show(); } public void actionPerformed(ActionEvent e) { try { if (e.getSource() == txt) { int n = 0; try { n = Integer.parseInt(txt.getText()); } catch (NumberFormatException exc) { return; } counter.setCount(n); return; } String cmd = e.getActionCommand(); if (cmd.equals("Increment")) counter.increment(); else if (cmd.equals("Decrement")) counter.decrement(); else if (cmd.equals("Save")) { try { XMLEncoder enc = new XMLEncoder( new BufferedOutputStream( new FileOutputStream(fname) ) ); enc.writeObject(counter); enc.close(); } catch (Exception exc) { exc.printStackTrace(); } } else if (cmd.equals("Load")) { try { XMLDecoder dec = new XMLDecoder( new BufferedInputStream( new FileInputStream(fname) ) ); Counter c = (Counter) dec.readObject(); dec.close(); installCounter(c); } catch (Exception exc) { exc.printStackTrace(); } } else System.out.println("Unrecognized command"); } catch (PropertyVetoException exc) { System.out.println(""+ exc); } } public void installCounter(Counter c) { if (counter != null) { counter.removePropertyChangeListener(counterView); counter.removeVetoableChangeListener(clim); } counter = c; if (counterView == null) counterView = new CounterView(""+counter.getCount()); else counterView.setLabel(counter.getCount() + "" ); counter.addPropertyChangeListener(counterView); if (clim == null) clim = new CounterLimitator(-5, 10); counter.addVetoableChangeListener(clim); } public static void main(String[] args) throws PropertyVetoException { new CounterControlGui(new Counter()); } }Limitator:
import java.beans.*; public class CounterLimitator implements VetoableChangeListener { private int min, max; CounterLimitator(int minLim, int maxLim) { min = minLim; max = maxLim; } public void vetoableChange(PropertyChangeEvent e) throws PropertyVetoException { Integer newVal = (Integer) e.getNewValue(); int val = newVal.intValue(); if (val < min || val > max) throw new PropertyVetoException("Niedopuszczalna zmiana wartości", e); } }Integracja:
import java.beans.*; public class Main { Counter counter; CounterView counterView; CounterLimitator clim; CounterControlGui gui; public Main() throws PropertyVetoException { installCounter(new Counter()); } public void installCounter(Counter c) { if (counter != null) { counter.removePropertyChangeListener(counterView); counter.removeVetoableChangeListener(clim); } counter = c; if (counterView == null) counterView = new CounterView(""+counter.getCount()); else counterView.setLabel(counter.getCount() + "" ); counter.addPropertyChangeListener(counterView); if (clim == null) clim = new CounterLimitator(-5, 10); counter.addVetoableChangeListener(clim); if (gui == null) { gui = new CounterControlGui(counter); } else gui.installCounter(counter); } public static void main(String[] args) throws PropertyVetoException { new Main(); } }
try { Pattern pattern = Pattern.compile(regex); // Logika Matcher matcher = pattern.matcher(input); // Logika boolean found = matcher.find(); // Logika if (!found) // Logika out.println("<h3>Nie znaleziono żadnego podłańcucha " + // Prezentacja "pasującego do wzorca</h3>"); else { // Logika out.println("<h3>Dopasowano:</h3>"); // Prezentacja out.println("<ol>"); // Prezentacja do { out.println("<li>podłańcuch \"" + matcher.group() + // L + P "\" od pozycji " + matcher.start() + // L + P " do pozycji " + (matcher.end()-1) + "</li>"); // L + P } while(matcher.find()); out.println("</ol>"); // Prezentacja } } catch (PatternSyntaxException exc) { // Logika out.println("<h2>Błąd w wyrażeniu</h2>"); // Prezentacja } finally { // Logika printEndTag(); // Prezentacja out.close(); // Prezentacja }
..... <context-param> <param-name>presentationServ</param-name> <param-value>/presentation</param-value> </context-param> <context-param> <param-name>getParamsServ</param-name> <param-value>/getparams</param-value> </context-param> <context-param> <param-name>commandClassName</param-name> <param-value>FindCommand</param-value> </context-param> .....i pobierając je przy inicjacji serwletu
public class ControllerServ extends HttpServlet { private ServletContext context; private Command command; // obiekt klasy dzialania private String presentationServ; // nazwa serwlet prezentacji private String getParamsServ; // mazwa serwletu pobierania parametrów private Object lock = new Object(); // semafor dla synchronizacji // odwołań wielu wątków public void init() { context = getServletContext(); presentationServ = context.getInitParameter("presentationServ"); getParamsServ = context.getInitParameter("getParamsServ"); String commandClassName = context.getInitParameter("commandClassName"); // Załadowanie klasy Command i utworzenie jej egzemplarza // który będzie wykonywał pracę try { Class commandClass = Class.forName(commandClassName); command = (Command) commandClass.newInstance(); } catch (Exception exc) { throw new NoCommandException("Nie mogę stworzyć obiektu klasy " + commandClassName); } } // ...Zwróćmy uwagę, że piszemy ten kod w kategoriach interfejsu Command. Dynamicznie ładujemy klasę podaną jako parametr kontekstu (tu FindCommand) i tworzymy jej obiekt. Zmiana wykonywanego działania (np. zamiast wyszukiwania jakieś obliczenia matematyczne) wymaga tylko podania innej wartości parametru kontekstu commandClassName. Może się okazać, że podamy niewłaściwą nazwę klasy lub będzie ona źle zbudowana (np. brak konstruktora bezparametrowego). Wtedy - jak już wiemy - wystąpi wyjątek ClassNotFoundException lub InstantiantionException. Obsługujemy oba te wyjątki poprzez zgłoszenie własnego wyjątku NoComamndException.
public class NoCommandException extends RuntimeException { public NoCommandException() { super(); } public NoCommandException(String msg) { super(msg); } }Jak widać, klasę wyjątku uczyniliśmy pochodną od RuntimeException, dzięki czemu nie musimy go ani obsługiwać w tym miejscu ani zgłaszać w klauzuli throws metody init(), co byłoby niedozowolone (bowiem metoda init() w klasie GenericServlet deklaruje możliwość zgłaszania wyjątku klasy ServletException, a jej przedefiniowanie nie może tego zmienić). Ten sposób oprogramowania umożliwia np. przygotowanie strony HTML z komunikatem o przyczynach błędu, która będzie automatycznie ładowana jeśli użyjemy elementu error-pages w pliku deskryptora wdrożenia.
public class ControllerServ extends HttpServlet { // ... private Object lock = new Object(); // semafor // ... // Obsługa zleceń public void serviceRequest(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html"); // Wywolanie serwletu pobierania parametrów RequestDispatcher disp = context.getRequestDispatcher(getParamsServ); disp.include(req,resp); // Pobranie bieżącej sesji // i z jej atrybutów - wartości parametrów // ustalonych przez servlet pobierania parametrów // Różne informacje o aplikacji (np. nazwy parametrów) // są wygodnie dostępne poprzez własną klasę BundleInfo HttpSession ses = req.getSession(); String[] pnames = BundleInfo.getCommandParamNames(); for (int i=0; i<pnames.length; i++) { String pval = (String) ses.getAttribute("param_"+pnames[i]); if (pval == null) return; // jeszcze nie ma parametrów // Ustalenie tych parametrów dla Command command.setParameter(pnames[i], pval); } // Wykonanie działań definiowanych przez Command // i pobranie wyników // Ponieważ do serwletu może naraz odwoływać sie wielu klientów // (wiele watków) - potrzebna jest synchronizacja synchronized(lock) { // wykonanie command.execute(); // pobranie wyników List results = (List) command.getResults(); // Pobranie i zapamiętanie kodu wyniku (dla servletu prezentacji) ses.setAttribute("StatusCode", new Integer(command.getStatusCode())); // Wyniki - będą dostępne jako atrybut sesji ses.setAttribute("Results", results); } // Wywołanie serwletu prezentacji disp = context.getRequestDispatcher(presentationServ); disp.forward(req, resp); }Widzimy, że opisuje on wyłącznie logikę działania, bez elementów prezentacji (drobnym, niestety nieuniknionym wyjątkiem jest ustalenie content-type odpowiedzi na "text/html"; w przeciwnym razie metoda include RequestDispatchera włączy źródło generowanej przez serwlet pobierania parametrów strony, a nie zinterpretowany HTML).
public class RegexParamsDef_pl extends ListResourceBundle { public Object[][] getContents() { return contents; } static final Object[][] contents = { { "charset", "ISO-8859-2" }, { "header", new String[] { "Testowanie wyrażeń regularnych" } }, { "param_regex", "Wzorzec:" }, { "param_input", "Tekst:" }, { "submit", "Pokaż wyniki wyszukiwania" }, { "footer", new String[] { } }, { "resCode", new String[] { "Dopasowano", "Brak danych", "Wadliwy wzorzec", "Nie znaleziono dopasowania" } }, { "resDescr", new String[] { "podłańcuch", "od poz.", "do poz.", "" } }, }; }
import javax.servlet.*; import javax.servlet.http.*; import java.io.*; import java.util.*; class BundleInfo { static private String[] commandParamNames; static private String[] commandParamDescr; static private String[] statusMsg; static private String[] headers; static private String[] footers; static private String[] resultDescr; static private String charset; static private String submitMsg; static void generateInfo(ResourceBundle rb) { synchronized (BundleInfo.class) { // konieczne ze względu // na możliwość odwołań List cpn = new ArrayList(); // z wielu egzemplarzy serwletów List cpv = new ArrayList(); Enumeration keys = rb.getKeys(); while (keys.hasMoreElements()) { String key = (String) keys.nextElement(); if (key.startsWith("param_")) { cpn.add(key.substring(6)); cpv.add(rb.getString(key)); } else if (key.equals("header")) headers = rb.getStringArray(key); else if (key.equals("footer")) footers = rb.getStringArray(key); else if (key.equals("resCode")) statusMsg = rb.getStringArray(key); else if (key.equals("resDescr")) resultDescr = rb.getStringArray(key); else if (key.equals("charset")) charset = rb.getString(key); else if (key.equals("submit")) submitMsg = rb.getString(key); } commandParamNames = (String[]) cpn.toArray(new String[0]); commandParamDescr = (String[]) cpv.toArray(new String[0]); } } public static String getCharset() { return charset; } public static String getSubmitMsg() { return submitMsg; } public static String[] getCommandParamNames() { return commandParamNames; } public static String[] getCommandParamDescr() { return commandParamDescr; } public static String[] getStatusMsg() { return statusMsg; } public static String[] getHeaders() { return headers; } public static String[] getFooters() { return footers; } public static String[] getResultDescr() { return resultDescr; } } // Serwlet włączany wyłącznie z serwletu pobierania parametrów // Ładuje ResourceBundle i przekazuje go klasie BundleInfo, // która odczytuje info i daje wygodną formę jej pobierania // w innych serwletach. // Ładowanie zasobów i ich przygotowanie przez klasę BundleInfo // następuje tylko raz na sesję. public class ResourceBundleServ extends HttpServlet { private String resBundleName; public void init() { resBundleName = getServletContext().getInitParameter("resBundleName"); } public void serviceRequest(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession ses = req.getSession(); ResourceBundle paramsRes = (ResourceBundle) ses.getAttribute("resBundle"); // W tej sesji jeszcze nie odczytaliśmy zasobów if (paramsRes == null) { Locale loc = req.getLocale(); paramsRes = ResourceBundle.getBundle(resBundleName, loc); ses.setAttribute("resBundle", paramsRes); // Przygotowanie zasobów w wygodnej do odczytu formie BundleInfo.generateInfo(paramsRes); } // ... a jeśli sesja się nie zmieniła - to nie mamy nic do roboty } //... }
// SERWLET POBIERANIA PARAMETRÓW public class GetParamsServ extends HttpServlet { private ServletContext context; private String resBundleServ; // nazwa serwletu przygotowującego // sparametryzowaną informacje // Inicjacja public void init() { context = getServletContext(); resBundleServ = context.getInitParameter("resBundleServ"); } // Obsługa zleceń public void serviceRequest(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // Włączenie serwletu przygotowującego informacje z z zasobów // (ResourceBundle). Informacja będzie dostępna poprzez // statyczne metody klasy BundleInfo RequestDispatcher disp = context.getRequestDispatcher(resBundleServ); disp.include(req, resp); // Pobranie potrzebnej informacji // ktora została wczesniej przygotowana // przez klasę BundleInfo na podstawie zlokalizowanych zasobów // Zlokalizowana strona kodowa String charset = BundleInfo.getCharset(); // Napisy nagłówkowe String[] headers = BundleInfo.getHeaders(); // Nazwy parametrów (pojawią się w formularzu, // ale również są to nazwy parametrów dla Command) String[] pnames = BundleInfo.getCommandParamNames(); // Opisy parametrów - aby było wiadomo co w formularzu wpisywać String[] pdes = BundleInfo.getCommandParamDescr(); // Napis na przycisku String submitMsg = BundleInfo.getSubmitMsg(); // Ew. końcowe napisy na stronie String[] footers = BundleInfo.getFooters(); // Ustalenie właściwego kodowania zlecenia // - bez tego nie będzie można własciwie odczytać parametrów req.setCharacterEncoding(charset); // Pobranie aktualnej sesji // w jej atrybutach są/będą przechowywane // wartości parametrów HttpSession session = req.getSession(); // Generowanie strony resp.setCharacterEncoding(charset); PrintWriter out = resp.getWriter(); out.println("<center><h2>"); for (int i=0; i<headers.length; i++) out.println(headers[i]); out.println("</center></h2><hr>"); // formularz out.println("<form method=\"post\">"); for (int i=0; i<pnames.length; i++) { out.println(pdes[i] + "<br>"); out.print("<input type=\"text\" size=\"30\" name=\"" + pnames[i] + "\""); // Jezeli są już wartości parametrów - pokażemy je w formularzu String pval = (String) session.getAttribute("param_"+pnames[i]); if (pval != null) out.print(" value=\"" + pval + "\""); out.println("><br>"); } out.println("<br><input type=\"submit\" value=\"" + submitMsg + "\">"); out.println("</form>"); // Pobieranie parametrów z formularza for (int i=0; i<pnames.length; i++) { String paramVal = req.getParameter(pnames[i]); // Jeżeli brak parametru (ów) - konczymy if (paramVal == null) return; // Jest parametr - zapiszmy jego wartość jako atrybut sesji. // Zostanie on pobrany przez Controller // który ustali te wartości dla wykonania Command session.setAttribute("param_" + pnames[i], paramVal); } } //..metody doGet i doPost - wywołują serviceRequest }
public class ResultPresent extends HttpServlet { public void serviceRequest(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { ServletContext context = getServletContext(); // Włączenie strony generowanej przez serwlet pobierania parametrów // (formularz) String getParamsServ = context.getInitParameter("getParamsServ"); RequestDispatcher disp = context.getRequestDispatcher(getParamsServ); disp.include(req,resp); // Uzyskanie wyników i wyprowadzenie ich // Controller po wykonaniu Command zapisał w atrybutach sesji // - referencje do listy wyników jako atrybut "Results" // - wartośc kodu wyniku wykonania jako atrybut "StatusCode" HttpSession ses = req.getSession(); List results = (List) ses.getAttribute("Results"); Integer code = (Integer) ses.getAttribute("StatusCode"); PrintWriter out = resp.getWriter(); out.println("<hr>"); // Uzyskanie napisu właściwego dla danego "statusCode" String msg = BundleInfo.getStatusMsg()[code.intValue()]; out.println("<h2>" + msg + "</h2>"); // Elementy danych wyjściowych (wyników) mogą być // poprzedzane jakimiś opisami (zdefiniowanymi w ResourceBundle) String[] dopiski = BundleInfo.getResultDescr(); // Generujemy raport z wyników out.println("<ul>"); for (Iterator iter = results.iterator(); iter.hasNext(); ) { out.println("<li>"); int dlen = dopiski.length; // długość tablicy dopisków Object res = iter.next(); if (res.getClass().isArray()) { // jezeli element wyniku jest tablicą Object[] res1 = (Object[]) res; int i; for (i=0; i < res1.length; i++) { String dopisek = (i < dlen ? dopiski[i] + " " : ""); out.print(dopisek + res1[i] + " "); } if (dlen > res1.length) out.println(dopiski[i]); } else { // może nie być tablicą if (dlen > 0) out.print(dopiski[0] + " "); out.print(res); if (dlen > 1) out.println(" " + dopiski[1]); } out.println("</li>"); } out.println("</ul>"); } //..metody doGet i doPost - wywołują serviceRequest }Uruchamiając tę aplikację uzyskamy znany nam już obraz dzialania (stronę z formularzem , która po wpisaniu regularnego wyrażenia i przeszukiwanego tekstu zostanie uzupełniona o listę komunikatów o odnalezionych dopasowaniach).
import java.util.*; public class StringCmdDef_pl extends ListResourceBundle { public Object[][] getContents() { return contents; } static final Object[][] contents = { { "charset", "ISO-8859-2" }, { "header", new String[] { "Działania na Stringach" } }, { "param_input1", "Tekst 1:" }, { "param_input2", "Tekst 2:" }, { "param_cmd", "Polecenie:" }, { "submit", "Wykonaj" }, { "footer", new String[] { } }, { "resCode", new String[] { "Wynik:", "Brak danych", "Wadliwe polecenie, dostępne: upper, lower, words" } }, { "resDescr", new String[] { "" } }, }; }
import java.io.*; import java.util.*; public class StringCommand extends CommandImpl implements Serializable { public StringCommand() {} public void execute() { clearResult(); String input1 = (String) getParameter("input1"); String input2 = (String) getParameter("input2"); String cmd = (String) getParameter("cmd"); if (input1 == null || input2 == null || cmd == null) { setStatusCode(1); return; } String input = input1 + " " + input2; setStatusCode(0); if (cmd.equals("upper")) addResult(input.toUpperCase()); else if (cmd.equals("lower")) addResult(input.toLowerCase()); else if (cmd.equals("words")) { StringTokenizer st = new StringTokenizer(input); while (st.hasMoreTokens()) addResult(st.nextToken()); } else setStatusCode(2); } }
Zad. 2
Zmodyfikowac kody klas w aplikacji z licznikiem, tak by stały się mniej od siebie zależne.