470 likes | 629 Views
Class-level Design. Reminder: Software is Recursive. Civil engineering: Room, Flat, building, Street, City, … Software Class, Class, Class, ... Or: Object, Object, Object, … => A class can represent both low level concerns and high level concerns => Design of classes ~ design of a program.
E N D
Reminder: Software is Recursive • Civil engineering: • Room, Flat, building, Street, City, … • Software • Class, Class, Class, ... • Or: • Object, Object, Object, … • => A class can represent both low level concerns and high level concerns • => Design of classes ~ design of a program
Interfaces: Visibility • PublicThe group of methods (of a class) that any other class in the system can invoke • PublishedA class interface that is used outside the code base that it is defined in Source: Martin Fowler; Public versus Published interfaces; IEEE Software 2002
Interfaces: Width • Thin • Support minimal set of necessary services • Humane • Support common usage • Fat • Support every imaginable service • Textbook example: • Java’s List inteface has 25 methods • Ruby’s Array class has 78 methods Source: http://martinfowler.com/bliki/HumaneInterface.html
Humane Interface public class HumaneList<T> { public T get(int i); public void remove(int n); public int size(); public void set(int index, T value); public void add(T value); public T first(); public T last(); public void sort(); public int indexOf(T value); }
Thin Interface public class ThinList<T> { public T get(int i); public void remove(int n); public int size(); public void set(int index, T value); } // Client code – add a value list.set(list.size(), newValue); // Client code – get last List.get(list.size() – 1);
Thin Interfaces Encourage Foreign Methods public class Lists { public static<T> void add(List<T> list, T value) { list.set(list.size(), value); } public static<T> T getLast(List<T> list) { list.get(list.size() – 1); } public static<T> void sort(...) public static<T> int IndexOf (...) ... }
Thin vs. Humane • Thin interfaces – pros: • Coherency • of implementing class • of the interface itself • Easier subclassing • Client code is less coupled • Humane Interface – cons: • Less client code
Thin/Humane – Final Thoughts • A spectrum, not a binary issue • An interface can be slightly humane and very much thin • Inside project boundaries? • Start with thin, let it evolve • Unknown users? • Humane seems more useful • OTOH, more difficult to fight coupling
Mission Statement A program which manipulates the location of furniture in a room. A furniture can be a chair a table or a lamp. Bounding rectangles of furniture should not intersect and should be contained inside the room. Doors should be located along the boundaries of the room.
Problem Space Classes • Room • Chair, Table, Lamp, Door • Rectangle • …
Program Space Classes • Range • Placement
Problem Space • Characteristics are dictated externally • Some may change in unanticipated ways • See the axioms • Limited reuse opportunities
Program Space • Characteristics chosen by programmer • High degree of reuse: The Lego principle • => Difficult to change
public class Rect { public final int top; public final int left; public final int bottom; public final int right; public Rect(int top, int left, int bottom, int right) { this.top = top; this.left = left; this.bottom = bottom; this.right = right; } } public class Point { public final int x; public final int y; public Point(int x, int y) { this.x = x; this.y = y; } }
public abstract class Furniture { public abstract Rect getBoundingRect(); } public class Table extends Furniture { private int width; private int height; public Table(int width, int height) { this.width = width; this.height = height; } @Override public Rect getBoundingRect() { return new Rect(0, 0, width, height); } }
public class Lamp extends Furniture { private int base; private int height; public Lamp(int base, int height) { this.base = base; this.height = height; } @Override public Rect getBoundingRect() { return new Rect(0, 0, base, height); } }
public class Furniture { final List<Point> points = new ArrayList<Point>(); protected void add(int x ,int y) { points.add(new Point(x, y)); } public Rect getBoundingRect(){ List<Integer> xs = new ArrayList<Integer>(); List<Integer> ys = new ArrayList<Integer>(); for(Point p : points) { xs.add(p.x); ys.add(p.y); } return new Rect(min(ys), min(xs), max(ys), max(xs)); } }
public class Furniture { ... static int max(List<Integer> values) { int result = Integer.MIN_VALUE; for(int n : values) result = Math.max(result, n); return result; } static int min(List<Integer> values) { int result = Integer.MAX_VALUE; for(int n : values) result = Math.min(result, n); return result; } }
public class Table extends Furniture { public Table(int width, int height) { add(0, 0); add(width, 0); add(width, height); add(0, height); } } public class Lamp extends Furniture { public Lamp(int base, int height) { add(0, 0); add(base, 0); add(base / 2, height); } }
Public class Furniture { ... // Same code as design #2 public Furniture newTable(int width, int height) { Furniture result = new Furniture(); result.add(0, 0); result.add(width, 0); result.add(width, height); result.add(0, height); return result; } public Furniture newLamp(int base, int height) { Furniture result = new Furniture(); result.add(0, 0); result.add(base, 0); result.add(base / 2, height); return result; } }
Summary • Design #1: • getBoundingRect() over-ridden • Subclasses do most of the work • Elegant if computations vary greatly across subclasses • Design #2, #3: • Superclass does most/all of the work • Variation among subclasses expressed by state • Elegant if computations can be generalized • Without too many special cases crippling it • More efficient as number of variants grows
Immutability public class Driver { ... }// Mutable:public class Car { private Driver d; public Driver getDriver() { return d; } public void setDriver(Driver d_) { d = d_; }}// Immutable:public class Car { private final Driver d; public Car(Driver d_) { d = d_; } public Driver getDriver() { return d; }}
Immutability (cont.) • Pros: • Compiler-checked contract • Covariant sub-classing • Thread safety • Caching, Sampling • Only the constructor throws (usually) • Less exception handling on the client's side • Better exception safety • Performance: Less copying • Cons: • Mutations
An Interface or a Classes? public void g(List<Nameable> list) { for(Nameable n : list) System.out.println(n.getName());}// Opt.1: Person is an interfacepublic interface Nameable { public String getName();}// Opt.2: Person is a classpublic abstract class Nameable { public abstract String getName(); }
Interfaces Vs. Classes • Interfaces • Do not fill the inheritance spot • Easier to provide alternative implementations • Promise less – client is less coupled • => Client is more reusable • => Client is more complicated • Classes • Easier to read the code • Promise more
Interfaces vs. Classes (one more time) public interface MyList { public int getHead(); public List getTail();}public class MyList implements List { private final int head; private final MyList tail; public MyList(int head_, MyList tail_) { head = head_; tail = tail_; } public final int getHead() { return head; } public final MyList getTail() { return tail; } }public static boolean exist(int n, MyList lst) { return lst == null ? false : (lst.getHead() == n ? true : exist(n, lst.getTail())); }
public class Bank { private Map<Integer,Account> accounts = new HashMap<Integer,Account>(); public Account getAccount(int id){ return accounts.get(id); } } public class Account { private final Owner owner; private int amount; public Account(Owner o) { owner = o; } public Owner getOwner() { return owner; } public void deposit(int n) { amount += n; } public int getAmount() { return amount; } }
public class Owner { private final String name; private final Branch branch; public Owner(String name_, Branch branch_) { branch = branch_; name = name_; } public String getName() { return name; } public Branch getBranch() { return branch; } }
public class Branch { String address; Map<Date,String> meetings = new HashMap<Date,String>(); public Branch(String address_) { address = address_; } public String getAddress() { return address; } public Date requestMeeting(String subject) { Date d = ... // Choose a date for the meeting meetings.put(d, subject); return d; } } public class Reporter { public void printAddress(Bank b, int id) { System.out.println(id + ": " + b.getAccount(id).getOwner().getBranch().getAddress()); } }
Tell vs. Ask • AskAccount a = ...; a.getOwner().getBranch().requestMeeting("…"); • TellAccount a = ...; a.requestMeeting("…"); • Guideline: Tell, Don’t ask • Better encapsulation • Let Account control the behaviour of requestMeeting • Related Smell: Long Message Chains • “Watch out for long sequences of method calls...”
Law of Demeter • A method M of an object O may only invoke the methods of the following kinds of objects • 1. O itself • 2. M's parameters • 3. any objects created within M • 4. O's direct fields • Embodies the "tell, don’t ask" guideline
Class Account (Demeterized) public class Account { private final Owner owner; private int amount; public Account(Owner o) { owner = o; } public void deposit(int delta) { amount += delta; } public int getAmount() { return amount; } public String getOwnerName() { return owner.getName(); } public String getBranchAddress() { return owner.getBranchAddress(); } public Date requestMeeting(String subject) { return owner.requestMeeting(subject) : null; } }
Class Owner (Demeterized) public class Owner { private final String name; private final Branch branch; public Owner(String name_, Branch branch_) { branch = branch_; name = name_; } public String getName() { return name; } public Branch getBranch() { return branch; } public String getBranchAddress() { return branch.getAddress(); } public Date requestMeeting(String subject) { return branch.requestMeeting(subject); } }
Benefits of the "Tell" Approach // Class Account can control the requestMeeting behavior public class Account { private final Owner owner; private int amount; private int limit = 5; public Account(Owner o) { owner = o; } public void deposit(int delta) { amount += delta; } public int getAmount() { return amount; } public String getOwnerName() { return owner.getName(); } public String getBranchAddress() { return owner.getBranchAddress(); } public Date requestMeeting(String subject) { return limit-- > 0 ? owner.requestMeeting(subject) : null; } }
Law Of Demeter • Pros • Better Encapsulation • Easy to intercept/modify requests • Cons • Low Cohesion • High Coupling • Smells: • Combinatorial explosion • Divergent change (lack of cohesion) • Shotgun surgery • Middle man
Defensive Setters/Getters • (Relevant under the “Ask” approach) • Instead of returning an object… • …Return a copy thereof • Expected to increase encapsulation
Class Bank: Duplicated Data public class Bank { private Map<Integer,Account> accountFromId = new HashMap<Integer,Account>(); private Map<String,Integer> idFromName = new HashMap<String,Integer> public Account getAccount(int id){ return accountFromId.get(id); }public void addAddcount(int id, Account a) { accountFromId.put(id, a); idFromName.put(a.getOwner().getName(), id); }public Account getAccount(String name) { return getAccount(idFromName.get(name)); } }
Huston, We Have a Consistency Problem //// Let's assume Owner has a setName() method...// public void f1(Bank b) { b.getAccount(1).getOwner().setName("new-name"); } public void f2(Owner o) { o.setName("new-name"); }
Defensive Copies public class Account { private final Owner owner; private int amount; public Account(Owner o) { owner = o; } public Owner getOwner() { return owner.clone(); } public void deposit(int delta) { amount += delta; } public int getAmount() { return amount; } }
Class Owner (Defensive Copies) public class Owner implements Cloneable { private String name; private final Branch branch; public Owner(String name_, Branch branch_) { branch = branch_; name = name_; } @Override protected Owner clone() { try { return (Owner) super.clone(); } catch(CloneNotSupportedException e) { throw new AssertionError(); } } public void setName(String arg) { name = arg; } public String getName() { return name; } public Branch getBranch() { return branch; } }
Defensive Copies: The Pitfall public void g(Account a) { a.getOwner().setName("new-name"); a.getOwner().getBranch().setAddress("new-address"); }
Defensive Copies: Summary • Never underestimate the importance of DRY • No duplicated data => no need to be defensive • E.g.: caching, data computed from raw data • Useful when returning objects that were not passed in from the outside • E.g.: Collections • Textbook example: Class.getMethods() • Can be used in either setters or getters