📜 ⬆️ ⬇️

Pattern Observer: lists and matryoshka from listeners

In this habrastia the example of patterns Observer and Linker is considered how to apply the principles of object-oriented programming, when it is necessary to use composition, and when inheritance. And it is also considered what are the ways to reuse the code, except copy-paste.

The article is quite simple, it deals with obvious things, but I hope that it will be interesting to novice programmers who have so far met with the words from the first paragraph only in programming lectures. (In fact, this article is a piece of practical programming.)


So, let us have the following interface for observing objects.
')
interface AListener { public void aChanged(); public void bChanged(); } 


And any such class for observable objects.

 class AEventSource { private List<AListener> listeners = new ArrayList<AListener>(); public void add(AListener listener) { listeners.add(listener); } private void aChanged() { for (AListener listener : listeners) { listener.aChanged(); } } private void bChanged() { for (AListener listener : listeners) { listener.bChanged(); } } public void f() { ... aChanged(); ... bChanged(); ... } ... } 


Here, in the aChanged() and bChanged() methods, all listeners are notified that this event has occurred. And in the f() method at the right moment these methods are invoked.

All this works, but suppose at some point we understand that we need the opportunity to have different ways of notifying listeners. For example, to notify listeners in different orders or, depending on some conditions, some listeners should not be notified of the changes that have occurred. There are at least two solutions to this problem. First, you can complicate the AEventSource class by adding different notification methods and some code that will choose one way or another. Secondly, everything that relates to the notification of listeners can be taken out in a separate class. Then the desired notification method can be set, for example, by passing the desired object as a parameter to the constructor.

The second method is better for two reasons. If we have more than one class for the observed objects AEventSource , but two (or more) classes AEventSource1 , AEventSource2 , and most likely it will be, then choosing the first method will have to duplicate the new complex code for notification in two classes, and this is already copy -paste, that is, evil. The second reason is that the second approach respects the principle of shared responsibility (the one that SRP). In fact, now AEventSource two reasons for changing the AEventSource class: if we want to change the notification order, and if we want to change the reason for the notification, that is, the f() function from the example above.

So we choose the second method - the code for notifying listeners is placed in a separate class. At the same time, we’ll get a little more interesting, namely, we will use the Linker pattern. This will give us not only choose different ways of notifying listeners, but also combine them.

 class ACompositeListener implements AListener { private List<AListener> listeners = new ArrayList<AListener>(); public void add(AListener listener) { listeners.add(listener); } @Override public void aChanged() { for (AListener listener : listeners) { listener.aChanged(); } } @Override public void bChanged() { for (AListener listener : listeners) { listener.bChanged(); } } } 


Then the class AEventSource will look something like this:

 class AEventSource { private AListener listener; AEventSource(AListener listener) { this.listener = listener; } public void f() { ... listener.aChanged(); ... listener.bChanged(); ... } } 


In this case, you can create whole dolls from the audience.

 ACompositeListener compositeListener1 = new ACompositeListener(); compositeListener1.add(new AConcreteListener1()); ACompositeListener compositeListener2 = new ACompositeListener(); compositeListener1.add(compositeListener2); compositeListener2.add(new AConcreteListener2()); compositeListener2.add(new AConcreteListener3()); AEventSource source = new AEventSource(compositeListener1); 


True, this solution has a drawback. Having obtained such flexibility in creating an aggregate of observing objects, it is possible to create aggregates with such an order and rules of notification that it will be very difficult to figure this out.

And again this code copes with its task perfectly, but suddenly it turns out (it’s not for nothing that I added the letter A to the class names everywhere), that you also need to know how to work with listeners who have one more method besides the AListener interface methods, let's call it dChanged() That is, there is a second interface for DListener listeners:

 interface DListener extends AListener { public void dChanged(); } 


So we need to reuse the code somehow. There are at least three ways to reuse code. This is, firstly, copy-paste, secondly, inheritance and, finally, composition + delegation. We will immediately reject the first for ideological reasons.

Let us analyze why simple inheritance also does not suit us, despite the fact that the DListener interface extends the AListener interface. But really, as soon as we write

 class DCompositeListener extends ACompositeListener { ... } 


we immediately have a problem with the add(AListener listener) method, since it allows you to add an object that can implement the AListener interface, but not the DListener . If we want to override this method as follows:

 @Override public void add(DListener listener) { ... } 

then the compiler will carefully say: method does not override or implement a method from a supertype (thanks to the @Override annotations).

Okay, why don't we do a check inside this method, and if the object being added doesn't implement the DListener interface, then throw an exception? That is, why not write something like this:
 @Override public void add(final AListener listener) { if (listener instanceof DListener) { listeners.add(listener); } else { throw new IllegalArgumentException(); } } 


Besides the fact that it looks somehow clumsy, it also violates the Liskov substitution principle (the one that is LSP). Which can be formulated as follows: functions that use references to base classes should be able to use objects of derived classes without knowing it . In fact, the function

 void g(ACompositeListener composite) { composite.add(new AConcreteListener1()); } 


it looks quite harmless and expects (and justifiably) that you can add a listener to the composite . But here we call the g() function like this:
 g(new DCompositeListener()); 

and everything will fall with the exception. And our goal is to make it even impossible to compile.

Yes, the option to completely abandon the AListener interface, and to all listeners who did not have the dChanged() method add an empty or throwing an exception to the implementation of the dChanged() method, we discard it, because this would violate the interface separation principle (also known as ISP).

So, there is a composition + delegation. To begin with, from the ACompositeListener class ACompositeListener all the code responsible for adding, deleting and iterating listeners will be moved to the new ListenerList class. And since we have listeners of different types, it will be a parameterized class.

 class ListenerList<T> implements Iterable<T> { protected List<T> listeners = new ArrayList<T>(); public void add(T listener) { listeners.add(listener); } public void remove(T listener) { listeners.remove(listener); } public Iterator<T> iterator() { return listeners.iterator(); } } 


Classes which will notify listeners, will look as follows.

 class ATranslator<T extends AListener> { protected ListenerList<T> listeners; public ATranslator(ListenerList<T> listeners) { this.listeners = listeners; } public void aChanged() { for (AListener listener : listeners) { listener.aChanged(); } } public void bChanged() { for (AListener listener : listeners) { listener.bChanged(); } } } class DTranslator<T extends DListener> extends ATranslator<T> { public DTranslator(ListenerList<T> listeners) { super(listeners); } public void dChanged() { for (DListener listener : listeners) { listener.dChanged(); } } } 


It uses restrictive types ( <T extends XListener> ), so that only those listeners that implement the corresponding interface can be added to the list. Attempting to add a listener that does not implement the desired interface will result in a compilation error. What we sought.

Now ACompositeListener and ACompositeListener written simply:
 class ACompositeListener extends ListenerList<AListener> implements AListener { private ATranslator<AListener> aTranslator = new ATranslator<AListener>(this); @Override public void aChanged() { aTranslator.aChanged(); } @Override public void bChanged() { aTranslator.bChanged(); } } 


 class DCompositeListener extends ListenerList<DListener> implements DListener { private DTranslator<DListener> dTranslator = new DTranslator<DListener>(this); @Override public void aChanged() { dTranslator.aChanged(); } @Override public void bChanged() { dTranslator.bChanged(); } @Override public void dChanged() { dTranslator.dChanged(); } } 


That's all.

In general, it is clear about patterns and OOP that it is worth reading in GoF. You can read about the Linker pattern in Habrastia. The Linker / Composite design pattern of the spiff habraiser . You can read about universal types in Java in Chapter 13 of the first volume of Core Java by Horstmann and Cornell. You can read about similar problems for the Builder pattern in Extensible Classes - Extendable Builders! Habrayuzer gvsmirnov and comments to that article. What can lead to an excessive fascination with the ideas of the PLO, it is told in habrastatiya So we put a factorial .

Source: https://habr.com/ru/post/128827/


All Articles