390 likes | 513 Views
Satish Annapureddy Director, Technology Myrio Corporation. Practical object oriented design techniques. Introduction. Focus Practical techniques and guidelines, that can be used daily, to create “good” object-oriented designs. How to design objects – fields and methods
E N D
Satish Annapureddy Director, Technology Myrio Corporation Practical object oriented design techniques
Introduction • Focus • Practical techniques and guidelines, that can be used daily, to create “good” object-oriented designs. • How to design objects – fields and methods • How to design relationships and interactions • Inheritance, composition, interfaces • Thread-safe design with OO. • Java design idioms
Proper intialization • Objects can be seen as finite-state machines. • Instance variables store state. Methods change state by changing instance variables. • The challenge is in keeping the object in a valid state at all times. • Data hiding is imperative! • Proper initialization ensures that the object starts in a valid state.
Proper Initialization (contd) • Design constructors and initializers so that there is no way the object can start in an invalid state. • Throw exception to indicate invalid constructor/method parameters eg., java.lang. IllegalArgumentException • If for some reason, it is needed to allow the object to start in an invalid state, throw exception to indicate improper usage. eg., java.lang.IllegalStateException.
Proper finalization • Finalizers – what they are NOT for - • System resources – file handles, sockets, memory etc. are finite. Free them when no longer needed. • In Java, garbage collector runs finalizer when it frees unreferenced object (not when running out of some non-memory resource). • Provide an API to allow clients to release resources. eg., open() and close().
Proper finalization (contd) • Finalizers – what are they for? • In Java, for releasing memory allocated in native methods via JNI. • Last attempt at releasing non-memory resources. • “Attempt” since finalizers may not run on live objects at time of exit. • In Java, java.lang.Runtime.runFinalizersOnExit() ensures that finalizers are run on all live objects at exit.
Designing fields • Use a variable per purpose. • In version 1, let's say that the temperature sensor is capable of tracking temps >= 0; public class TemperatureSensor { ... public int getTemperature() { return temp; } public boolean isSensorWorking() { return temp < 0; // dual-use } private int temp; }
Designing fields (contd) • In version 2, the sensor can track negative temperatures too. public class TemperatureSensor { ... public int getTemperature() { return temp; } public boolean isSensorWorking() { return working == true; } private int temp; private boolean working; }
Designing methods • Minimizemethod coupling as much as possible. • The less a method (and hence a class) knows about other classes, the better. • Take in the most relevant object and output the object actually affected. • Least coupled are utility methods (static methods that depend only on input parameters and class constants)
Designing methods (contd) // encode x-www-form-urlencoded strings public class XWWWFormURLEncoder { public void encode(URLString str) { ... } } Note: x-www-form-urlencoded is applied to a String, usually in the context of a URL, but not necessarily.
Designing methods (contd) • Maximize Cohesion – each method must focus on a single conceptual task. eg., insert(..), delete(..), open(..) etc. • Why? • Changes localized. Removes side-effects. • More readable. • How? • Avoid passing control parameters. Create multiple methods instead.
Designing methods (contd) public class Account { // low cohesion: control data (type) is passed in. // split into multiple methods: creditAccount(..), debitAccount(..), clearAccount(..) public void updateBalance(int type, float amount) { if(type == CREDIT) {...} else if(type == DEBIT) {...} else if(type == CLEAR) {...} else if... } }
Encapsulation and information hiding • Encapsulation and information hiding are two different concepts! • Encapsulation refers to bundling data and operations that use that data. • Information hiding refers to hiding implementation details of the class • You can have encapsulation without information hiding. • Good OO requires both done right! • Objects should be intelligent entities. Do not separate methods from related data.
Encapsulation and information hiding (contd) • Information hiding guidelines • Don't expose data. • Don't expose the fact that certain attribute is derived – use getDuration(...) instead of computeDuration(...) • Don't expose details on internal data structures – getMap() instead of getTreeMap(). • Don't give out mutable handles to internal data.
Encapsulation and information Hiding (contd) public class SortedList { public void insert(ListElement obj) { // insert at appropriate position so that // the list remains sorted. } public ListElement getElement(int position) { return elementAt(position); // direct access to internal data. } } Notes: client code can use getElement().setX(...) so that the list is no longer sorted.
Encapsulation and Information Hiding (contd) • get/set expose implementation details via interfaces • For example, code that uses int getX() breaks when the return type is changed to float. • Objects should be designed to be intelligent. ie., request services not data. • By understanding how the class will be used, you can eliminate most get/set methods by providing services instead.
UI Design without getters and setters • Problem: If get/set are removed, an object must somehow know how to present its UI. • But, it is not feasible for an object to support all possible UIs. • Solution: Use the Builder pattern • Use a Builder helper object and provide for export and import via interfaces. • This basically moves the get/set to the Builder (UI) object from the business class object.
UI design without getters and setters (contd) public class TestingStats { public interface Exporter { void setScores(float[] scores); void setAvg(float avg); .. } public void export(Exporter builder) { builder.setScores(scores); builder.setAvg(avg); ... } private float[] scores; private float avg, median, mean; // and others ... }
UI Design without getters and setters (contd) public class TestingStatsUI extends JPanel implements Exporter { public void setScores(float[] scores) { graph.setData(scores); } public void setAvg(float avg) { add(new JtextField(“”+avg)); } ... private SuperPowerfulGraphWidget graph; } public void showUI(...) { ... testingStats.export(testingStatsUI); ... testingStatsUI.show(); ... }
Thread-safe design • Heap (and method area) is common, stacks are local among threads. • Methods cause state transitions. But during the transition, the state becomes invalid. Atomicity is required to ensure that the invalid state is not exposed to other threads. • Guarding critical sections prevents race conditions – read/write and write/write conflicts.
Thread-safe design (contd) • For example, insert() below has both Read/Write and Write/Write conflicts. public class LinkedList { public void insert(LinkElement new_element) { tmp = cur.next; cur.next = new_element; new_element.next = tmp; } public void printList() { for(LinkElement elem = start; elem != null; elem=elem.next) print(elem); } ... }
Thread-safe design (contd) • Three approaches • Protect critical sections with mutexes. • Another reason why fields MUST be private! • Identify critical sections and use lock/unlock to enter and leave critical sections. • Synchronizing everything degrades performance and may also result in deadlocks. • Most common and most powerful approach. Thread coordination (wait and notify) are impossible without locks. • Use immutable objects. • Inherently thread-safe - object state does not change after creation.
Thread-safe design (contd) • Identify critical sections but no locking. • Critical sections that read are left alone. • Critical sections that write are changed to create new immutable objects to reflect new states. • eg., java.lang.String • Thread-safe wrappers • Front-end the object with a thread-safe wrapper that has the same interface. (Decorator pattern) • Flexible – make objects thread-safe only when really needed. • Eg., Collections.synchronizedSet(Set set)
Exceptions • When to throw exceptions? • To indicate an abnormal event: If your method encounters an “error condition”, throw an exception. • What constitutes an “error condition”? • Is EOF an error condition? • While reading byte by byte into a buffer? • When only 3 bytes of a 4-byte int are read? • To indicate broken contract: caller violates pre-condition. • What about calling java.util.Iterator.next() when java.util.Iterator.hasNext() returns false?
Exceptions (contd) • A Runtime exception – NoSuchElementException is thrown. • What about a method that expects non-null String but is passed null instead? • A runtime exception – IllegalArgumentException is thrown. • What exception to throw? • The client programmer should handle “abnormal” conditions by throwing specific checked exceptions. These are declared and hence enforced by the compiler. • Broken contracts indicate bad code and should be fixed before release. Runtime exceptions are appropriate for this purpose.
Inheritance vs Composition • Inheritance makes use of dynamic binding and polymorphism. • Great for adding new behaviors via subclassing. • Bad when superclass's interface needs modifications. • Changes to public method signatures ripple to all the clients that use the superclass or any of its subclasses. • Composition delegates actual implementation to a back-end class.
Inheritance vs Composition (contd) • Composition: Advantages • Better isolates back-end changes: The front-end implementation will change to reflect the back-end change, but the front-end interface may remain the same. • Subclasses are more rigid than front-end classes. eg., changing return type on an inherited method is not possible. • Composition allows optimizations – lazy instantiation of back-end objects and dynamically changing back-end objects.
Inheritance vs Composition (contd) • Composition: Disadvantages • Polymorphism makes inheritance far more effective than composition for adding new implementations (new subclasses). • Composition with interfaces solves this problem. • Delegation approach in composition might affect performance. • How to choose between the two? • Inheritance ONLY for permanent is-a relationships. Otherwise, use composition. • Do not use inheritance just to reuse implementation or for polymorphism
Inheritance vs Composition (contd) Consider the following classes: public class MediaAsset { public String getURL() { ... } } public class MovieAsset extends MediaAsset { ... } public class MoviePlayer { public void setupMovie(MovieAsset movie) { BandwidthController.reserveBandwidth(authenticationInfo, movie.getURL()); ... } }
Inheritance vs Composition Let us say that getURL() method in MediaAsset is changed to return URL. public class MediaAsset { public StringURL getURL() { } ... } • Now, MoviePlayer object is broken since it uses MovieAsset which extends MediaAsset. If we used composition instead, the change would not ripple past MovieAsset (the front-end class) public class MovieAsset { public String getURL() { return mediaAsset.getURL().getPath(); } private MediaAsset mediaAsset; }
Composition with interfaces • Interfaces allow for more polymorphism than inheritance. • Inheritance with polymorphism allows for using a subclass in place of a superclass. • With interfaces you are not limited to one inheritance hierarchy. Any class that implements the interface can be substituted. • Composition with interfaces is just as powerful and flexible as inheritance with polymorphism. • Interfaces do not suffer from the “diamond” problem.
Composition with interfaces(contd) • No multiple inheritance of implementation (methods and non-private fields) and hence no ambiguity. • Interfaces allow implementation to be totally decoupled from interface. • How to use interfaces: • Various subsystems should communicate with each other via interfaces. • Use interfaces to abstract out subsystems that can have many implementations. • Use interfaces to represent functionality common to different class hierarchies.
Composition with interfaces (contd) • In the previous example, MovieAsset cannot be substituted for MediaAsset if it is implemented using composition. By combining composition with interface, we can easily solve this problem! public interface Asset { String getAssetURL(); ... } public class MediaAsset implements Asset { .. } public class MovieAsset implements Asset { // delegate to mediaAsset instance. }
Implementation inheritance issues • Implementation inheritance couples subclass method implementation to superclass even if no data is exposed. • Any superclass implementation changes may break the subclasses. • Protected member variables are worse! • Don't make any assumptions about or use artifacts of superclass implementation • Better yet, consider using interfaces.
Implementation inheritance issues (contd) public class Base { public void foo() { ... } ... } public class Foo extends Base { public void foo() { ... } public boolean equals(Object obj) { ... } ... } // foo1 and foo2 are two instances of Foo and foo1.equals(foo2) returns true. Hashtable htbl = new Hashtable().put(foo1, “Foo 1”); Now, htbl.get(foo2) should return “Foo 1”. But it returns null. Why?
Abstract base classes and interfaces • Combine interfaces with abstract base classes to get the best of both worlds • Implement an interface with default implementation in an abstract base class. • Class hierarchy is used as a common code repository. If the implementation changes radically, you can go back to using the interface.
Abstract base class and interfaces (contd) public interface RTSPClient { void setup(..); void teardown(..); void play(..); void pause(..); void describe(..); ... } public abstract class AbstractRTSPClient implements RTSPClient { abstract void setup(...); // other methods as well. // connection management, keep alive timer impl. Etc. }
Abstract base classes and interfaces (contd) public class NcubeRTSPClient extends AbstractRTSPClient { void setup(...) { // compose request headers // send request and receive response // activate keep-alive based on session timeout in response // update state machine } } public class BitBandRTSPClient implements RTSPClient { void setup() { bitband_setup(...); } native void bitband_setup(...); // RTSP and more is done in native library }
References • OODP/Java design articles by Bill Venners at http://www.artima.com • OODP/Java design articles by Allen Holub at http://www.holub.com • OODP series at http://www.javaworld.com/channel_content/jw-oop-index.shtml