Annotated lecture notes and other miscellaneous thoughts

| No Comments | No TrackBacks
CAVEATS: This is 110% opinion with heaps of experience thrown in.
As an aside, in the real world there is far less conscious design than you realise. Sometimes this is a very good thing because it reduces confidence - mantras are like car sat-navs, rely on them totally and one day you might find yourself driving off a cliff because you trust it perfectly. YMMV (especially if you leave the road and plummet towards the ocean).


"The new keyword isn't inherently evil, but it may complicate your design later." Emphasis on may. (Context: using new Connection(db) over Connection.get(db) )

For example, say you have a database connection object and you want to set up a pool of X objects that you want to reuse. Rather than use the factory method, you could edit the Connection object such that is composed of e.g. a DatabaseSocket which interfaces with the database directly. Connection's constructor grabs a DatabaseSocket from the pool, and existing methods on the Connection object forward themselves to the DatabaseSocket. (delegation!)

From outside it's essentially the same, the methods do what you want them to, no knowledge of the DatabaseSocket required... the only difference is each Connection object can hold additional state as well as a reference to the (possibly shared) DatabaseSocket.

This could be very desirable, especially in multi-threaded scenarios. Consider the case where there is contention for a limited resource. With a factory method that directly hands you an object, the onus is usually on you to properly handle sharing and locking for that object. (It's not as simple as adding 'synchronised' to all the methods!) In addition, if other objects in the pool become unused but the object you were given is highly contended, you can't switch to another object unless your code specifically accounts for this. And, if your code accounts for this, the entire factory pattern is less of an abstraction and more of a distraction.

However, if you construct an object unique to you, one that delegates your commands to a shared resource, that object can alter its behaviour with respect to the resource, even storing command history and seamlessly replacing it with another resource if need be (consider distributed systems and a failing link, or simply a process that has crashed), you maintain the abstraction that you have *a* instance of the resource, which is often all you want.

Sharing objects is nice, but instantiating a brand-new object is always more powerful because in the worst case it can be a perfect proxy for another object, and thus replace the factory pattern entirely.  Personally I see the factory pattern as an obtuse version of early optimisation, and as we all know it is a fool's game. We have refactoring for this kind of thing anyway, and at the very least, you can always s/new Connection/Connection.get/

My rule of thumb generally (and completely ignoring most uses of the factory pattern) is: 'do you want a brand new object of your very own, or are you happy being given an object to achieve what you want?' If you're doing anything with mutable state outside your program logic, you probably don't need to track any extra data outside of whatever exists externally, and you should ask whatever subsystem you are dealing with for appropriate objects to this effect. However, if you want additional data with your ice-cream sundae, the former is actually a sensible thing to do, and doesn't preclude you from changing the implementation of the created class so that it grabs the 'real' object elsewhere behind the scenes.

Besides which, factory methods won't magically make everything work if you change the underlying implementation. Integration testing still needs to be done, you almost certainly need to fix bugs in the edge cases.  Anyone who has done multi-threaded programming will attest to the nightmare caused when you start changing things like this.

-----

Symmetric operations like Equals are not solved satisfactory by pure-OO approaches. Verbs are verbs, they exist without nouns if need be - Java makes this hard, but if you can, treat verbs like first-class beings.  c.f. CLOS multiple dispatch. (Not that this is a perfect solution either, but I scribed this for future reference.)

Composition is often a good thing to use, but that really depends on what you're trying to achieve.

Liskov substitution principle - if you can adhere to that you can safely derive your class.  Otherwise you will almost certainly run into errors later down the line, some quite gnarly and subtle - imagine if you program all Animals to jump(), then derive an Elephant from Animal...

Inheritance has its uses, but you need to be careful about what it really means. It's not about extending functionality as much as it is about telling people "Hey, I have a cool object here with special features I can use, but since it behaves exactly like this more basic object, you guys get to play with the same instance as me!".

Aside: as far as defensive programming is concerned, you can learn a lot by programming to a closed-source API like Windows. By the third time they screw you over by making some subtle change, you learn not to trust implementation details too much. (And besides which, classes change, code does, libraries... that's what integration testing is for, surely?)


Imagine you buy some skittles from the shop one day. Next week, you go in, and you try and do the same thing, but the shop keeper punches you in the nose before telling you "oh, if you want to purchase your item, you have to say 'Blooblebloo' and twirl the golden baton three times". No, you'd call the police for him being a menace to society and your now-bloody nose. Do you honestly expect your software to be any different?

If you change your interface such that invariants, preconditions and postconditions change, strengthen or weaken respectively, you will negatively effect other code up if they rely on it. This is not a fault with inheritance, interfaces, OOP or whatever - you told the programmer you'd always do something one way, deal with the consequences of changing it to something completely different.  This is a seriously bad design flaw.

Aside: The Old New Thing is a blog about backwards-compatibility in Windows among other subjects, and reading that will prove many times over that taking interfaces at more than face value causes enormous problems. To be fair to the zillions of win32 developers, they had no power to change the APIs to expose hidden features they needed, so this kind of trickery was sadly inevitable for many years. You, on the other hand, have no excuse in open source-land.


Incidentally, Eiffel needs a big mention because it hasn't done in the lectures yet. Look up 'design by contract'. It's like first-class unit testing. It also eases a lot of the above issues by tightly coupling guarantees about your code with the code itself - you can't break them without it knowing about it, and likewise, calling code knows exactly what to expect from you.

Anyway, the trick is to design your interfaces so you give code as little guarantee as is possible while allowing useful things to be done. This gives you more freedom to change the implementation, add new methods, etc. assuming the calling code is bug-free and makes no wrong assumptions about your interface.

Think about real-life contracts which lock down so much and try to close loopholes. There's a good reason why they do that, annoying as it is, so perhaps you should consider closing the ambiguity in your own software so that everybody is playing to the same rules.

What annoys me about interfaces though is that you're forced to do a fair amount of donkey work to use them. Think of the high-water-mark stack example and my suggestion to override all the push() methods so that it works. Doable, not pleasant, though, is it?


A superior solution to interfaces - and to the occasional twisted desire to have multiple inheritance - is traits, as implemented by Squeak Smalltalk and Scala on the JVM. Traits promote behaviour reuse rather than the combined state+behaviour reuse of inheritance. Mixins are similar, slightly less powerful and harder to manipulate in complicated cases, but at least are available in languages like Python and Lisp. Scala lies between these two with its own traits implementation (and, as a JVM language, has the benefit of being compilable, which I am sure will please Ian immensely and hopefully encourage him to look at it if he hasn't already done so!)
More on traits: http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf
Hopefully someone will find that enlightening.



And most importantly, there is more to program design than OO!

No TrackBacks

TrackBack URL: http://www.cookingcoder.com/cgi-bin/mt/mt-tb.cgi/12

Leave a comment