Object-Oriented Overload (or, how to use O-O ideas tastefully and forcefully) Guiding principles behind O-O: 1) "don't repeat yourself" (DRY) 2) don't shoot yourself in the foot Old school design: - Code and data structures were separate - Code often went along with particular data structures - Naming conventions started binding code and data together, but could get messy, e.g., typdef struct heap { ... } *Heap; Heap* Heap_Create(); void Heap_Insert(Heap* h, int priority, void* value); void* Heap_GetMin(Heap *h); // return value with minimum priority - Early O-O ideas (early 1980's) - associate code directly with data - allow hiding of implementation details (abstraction) class Heap { private .... ; // data structures Heap() { } // constructor void insert(Element e) { ... } element getMin() { ... } } // notice that we're now loading the priority and value together // - is that always a good thing? // - in Java, would you use the hashCode as the priority for a heap? - Pre O-O idea: separate interface from implementation interface IPriorityQueue { void insert(IPriority e); IPriority getMin(); } interface IPriority { int getPriority(); } - Put those ideas together, and you've got a problem: where's the "new Heap"? - Solution #1: expose it and don't worry about it class Heap implements IPriorityQueue { public Heap() { ... } public void insert(IElement e) { ... } public IElement getMin(); private ...; } (other programming languages, including C, let you write down all the signatures without the method bodies -- Java sucks this way) - Solution #2: hide everything and expose a factory (a.k.a. "factory pattern") - and maybe as a singleton / non-public constructor interface IPriorityQueueFactory { IPriorityQueue getNew(); } class PriorityQueueFactory implements IPriorityQueueFactory { IPriorityQueue getNew() { return new Heap(); } } - so where does the HeapFactory singleton go? Another class? - Answer: use solution #1 because there's no *reason* to use a factory here. - So, when do you *need* a factory? - When there's shared state across instances that you need to keep coherent (example below for java.net.URL) =================================================== Digression: use of generic types - Clever new Java feature interface IPriorityQueue { void insert(T t); T getMin(); } - Generic types can get complicated *very* quickly. Java's collection classes don't return an int. They use the Comparable interface: interface Comparable { int compareTo(T t); // returns -1, 0, or 1 } What if we've got class B extends A. Comparable is not a subclass of Comparable, even if we can compare their contents. Java's solution? interface IPriorityQueue> { ... } - While Comparable is still not a superclass of Comparable, IPriorityQueue can take either Comparable or Comparable. And, so long as A and B, themselves, support the comparisons, then this will compile. - Dan's opinion about how to use this generics: - Easier to use a generic type than to define one - Ugly cases mostly only occur when dealing with collections of things (in Java, already implemented, for the most part, in java.util) - Can catch some problems at compile time rather than runtime. - Clever type design != clever software design - Can blow lots of time getting it to compile. (Anecdote: "playing chess with the compiler.") More details here: http://java.sun.com/j2se/1.5/pdf/generics-tutorial.pdf =================================================== - More complex example: using Java to access the web import java.net.*; URL url = new URL("http://www.owlnet.rice.edu/~comp314/"); InputStream is = url.openStream(); // read bytes from is - The URL class is acting as a factory for InputStream. Is it opening a network connection? Is it reading from a file? Do you know? Do you care? - Under the hood: class URLStreamHandler { // no constructor protected abstract URLConnection openConnection(URL u); } class HttpURLConnection extends URLStreamHandler { protected HttpURLConnection(URL u) { ... } // constructor } URLStreamHandler is an abstract base class. Subclasses that want to be constructable need to implement all abstract methods (analogous to implementing an interface). Difference? Code from the base class is inherited by the subclass. (Possible benefit with DRY.) - What if you want to support a new kind of URL that isn't built into Java? (BitTorrent example: magnet:?xt=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C) - Cheesy Java answer - tell the URL to use a particular factory, which maps the front part of the URL ("http", "magnet", etc.) to the proper handler. URL x = new URL("magnet:..."); x.setURLStreamHandlerFactory(new MangetURLStreamHandlerFactory()); InputStream is = x.openStream(); - Java *should* deal with this internally, but how? Have a centralized, singleton (i.e., static variable)? - How to resolve rules for multiple factories? - Option #1: maintain an internal list... class UberURLStreamHandlerFactory implements URLStreamHandlerFactory { private List handlerList; public void add(URLStreamHandlerFactory fac) { handlerList.add(fac); } public URLStreamHandler createURLStreamHandler(String protocol) { for (URLStreamHandlerFactory handler: handlerList) { try { return handler.createURLStreamHandler(protocol); } catch (Exception e) { // assume it can't handle this protocol } } throw new Exception("no handler for " + protocol); } } - Option #2: chain them together... ("delegation", a.k.a., "decorator pattern") class MagnetURLStreamHandlerFactory implements URLStreamHandlerFactory { private URLStreamHandlerFactory otherFactory; public MagnetURLStreamHandlerFactory(URLStreamHandlerFactory otherFactory) { this.otherFactory = otherFactory; } public URLStreamHandler createURLStreamHandler(String protocol) { if(protocol.equals("magnet") { // do real work, return something } else return otherFactory.createURLStreamHandler(protocol); } } ... new MagetURLStreamHandlerFactory(new HTTPStreamHandlerFactory()) ... - Option #3: subclass rather than delegate class MagnetURLStreamHandlerFactory extends HTTPStreamHandlerFactory { public URLStreamHandler createURLStreamHandler(String protocol) { if(protocol.equals("magnet") { // do real work, return something } else return super.createURLStreamHandler(protocol); } - Which is the correct option? - The internal list maintains separation. Handlers don't need to know about each other. Makes it more future-proof. - Delegation/decoration is a great way to add an extension, but it requires that your "new" handler knows about other handlers (i.e., it has to make some attempt to be universal). Compare to option #1 that allows each handler to focus on what it knows how to do and throw an exception if it doesn't. (Side rant: need for a better-named exception.) - Subclassing can get fragile. Ten protocols would require some fairly deep dependencies. Change something in a base class and you rattle all the subclasses. Option #3 is to be avoided. - Annoying side question: should UberFactory *implement* a list? - Choice 1: probably not. Does anybody need all of the list methods? Typical app may register a new handler and have no need to remove or query it. - Choice 2: why not? Not hard to add list methods and pass on method calls to the internal list. - Choice 3: why not have UberFactory extends LinkedList? Benefit: no need to explicitly delegate methods Problem: Fragile. Makes it very difficult to hide list functionality later, if you want to change implementation. - Dan's answer? Choice #1. Support what you *need* now and deal with other features when/if you need them later.