This notes gather guidance and principles of software design (OO design in particular).
This is a work in progress.
It is hard to create a perfect structure of orthogonal principles as some relate to each other.
Links:
wikipedia
Good: http://www.cmcrossroads.com/bradapp/docs
A Java study: http://javaboutique.internet.com/Case_Study/
Some principles expained: http://www.cs.sjsu.edu/faculty/pearce/cs251b/principles
0. Serenity Principle (SP?): Separate what can change from that that cannot.
God, give us grace to accept with serenity the things that cannot be changed, courage to change the things that should be changed, and the wisdom to distinguish the one from the other.
Common Closure Principle (CCP) Classes that change together must be placed in the same package.
See: Policy and mechanism
1. Liskov Substitution Principle (LSP): All uses of A can be substituted by A' (A' subtype of A).
All base class usages (use define nature, interface) can be substituted by derived classes without external behavior change.
Guides proper inheritance usage. Inheritance should be additive, never restrictive.
If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.
When extending: Preconditions cannot be straightened, Postconditions cannot be loosened, visible Invariants cannot be changed (?). Invariants: users depend on this both before and after sending a message.
Use a proper set-based inheritance relationship. Not following set semantics is very risky.
Subsumption Rule: A reference to a subtype can be used in any context where a reference to a super type is expected.
This principle extremely limits what SHOULD be done with the pure extension (inheritance) mechanism. Do not follow at your own risk.
Squares aren't Rectangles!
See: http://www.cs.sjsu.edu/faculty/pearce/cs251b/principles/liskov2.htm
2. Dependency Inversion Principle (DIP): Abstractions should not depend upon details. Details should depend upon abstractions.
High-level modules should not depend upon low-level modules. Both should depend upon abstractions.
Depend on abstractions, not implementations.
Program to an interface, not to an implementation. Separate interface from concrete implementations. Depend upon abstractions. Decouple helpers with interfaces.
Do not make high-level (general) classes directly dependent on the implementation of lower-level ones (its helpers).
Abstractions(core client visible, testable, components) should not depend upon details (helpers). Details(helpers) should depend upon abstractions (interfaces): this is the "inversion" , the helper depends upon a new abstraction, and the abstraction no longer depends on the helper directly.
High level or low level modules should not depend upon each other, instead they should depend upon abstractions.
Frees you from having to implement in either a down-top (always runnable and useful) approach or a top-down approach (whole view, structured, progressive refinement) with mock implementations. You are free to decide you implementation strategy guided by: risks, weights, scenarios, ... allows TDD and others.
Allows you to use compositional techniques for program creation: components.
Allows you to test each element in isolation (unit tests) as well as a group of them (functional and scenario tests).
Program to an interface not to an implementation. Corollary: Do not access/modify directly the internal state of a client.
If you depend on a concrete client, you will probably end up depending also on the concrete clients of the client, ad nauseum.
Reify design in code (by not tying yourself to an concrete "future" implementation).
Minimize concrete implementation dependencies.
Note that interfaces in Java are clean but a bit unwieldy and underpowered (see traits in Scala). Requires more concepts.
The Inverted Tree: shown core modules down and and future usage layers on top of it. Inversion: Client (user) code invokes base code. This is the basis of frameworks, independent of implementation technique. Inversion appears when older code will call newer, more refined one (call-backs).
Reify structure and dependencies.
Related: see Facade, Interface, TDD, component, composition, IoC, DI, framework, Spring, Guice
See:
Protocol Stack Example: http://www.eventhelix.com/RealtimeMantra/Object_Oriented/dependency_inversion_principle.htm
3. Open/Closed Principle (OCP): modules should be open for extension, but closed for modification.
Modification can easily break the design of a piece of sw.
Modification requires deep internal knowledge.
Solving a family of problems instead of the concrete one you have to face.This can conflict with YAGNI.
You need to modify if the current design cannot accommodate your extension/use easily.
Embed design decisions and restrictions in the source code (final, protected).
Open/Closed object
Dynamic Dispatch Rule: Objects should decide how to handle the messages they receive.
Open/Closed class
It should be possible to change the implementation of an object without modifying its class.
It should be possible to change the internals of a class without needing to even recompile its clients.
Abstraction Principle: The interface of a module should be independent of its implementation
See: Strategy, State, and Bridge Patterns
Open closed system
Service-Oriented Architecture (SOA) is an Open/Closed architecture for systems.
Reflective systems allow the system to dynamically discover the interfaces implemented by third party components. Each component is expected to implement a meta-interface that describes the application-specific interfaces that it implements.
Separate policy (max change) from mechanism (stable).
Open/Closed principle in HCI:
The most open sw artifact is a language or other building-blocks based solution.
The better closed a sw piece, the easier it is to experiment with it without risk.
Good GUI design uses the Open/Closed principle to guide and present the possible transitions in an intuitive way, without forcing analytical thought: see flow.
4. Design by contract and responsibilities (DBC and RDD)
It is a series of techniques for OOD: guiding the distribution of functionality and its definition and evolution.
DbC involves: preconditions, postconditions, invariants. You get: well-defined interfaces, closed designs (see Open/Closed) easily extensible, a basis for some types of testing, the capacity to fail fast, a way to warrant consistency.
Responsibility Design involves: Objects, responsibilities and collaborations. You get well-decoupled, highly cohesive and easily comprehended classes, well-defined associations and a balanced overall class design.
High Cohesion by responsibilities - Low Coupling by good delegation and concept reification.
See: OOSC for DbC, DOOS for responsibilities
5. Composite Reuse Principle (CRP)
Favor composition over inheritance to achieve polymorphism.
Favor delegation over inheritance. Delegation can be seen as a reuse mechanism at the object level, while inheritance is a reuse mechanism at the class level.
Implementation inheritance is the goto of OOP.
Favor composition over inheritance.
When in doubt, do not inherit.
See: Strategy pattern
6. Modularity Principle (MP): High cohesion and low coupling
Modularity Principle tells us that modules should be cohesive and loosely coupled.
The Facade pattern can lower the coupling degree between modules.
The Mediator pattern can reduce the dependencies between the members of a package.
The members of a subpackage can be considered to be members of the super package (transparent packages) or not (opaque packages, like in Java.)
Acyclic Dependency Principle(ADP): Avoid cycles in the package-level dependency graph
It is sometimes difficult (impossible) to avoid cycles in the class-level dependency graph. But reject packages with cycles!
Interface Segregation Principle (ISP): Many specific interfaces are better than a single, general interface
Release Reuse Equivalency Principle (REP) The granule of release is the granule of reuse.
Common Reuse Principle (CReP) The classes in a package are reused together. If one of the classes in a package is reused, all the classes in that package are reused.
Stable Dependencies Principle (SDP) The dependencies between packages must always be in the direction of stability. Less stable packages should depend on more stable packages.
Stable Abstraction Principle (SAP) The stable packages must be abstract packages. The instable packages should contain the concrete implementations.
Note: Java modularity is incomplete. Both NB & Eclipse add higher abstraction 'packages' (see OSGi)
7. Law of Demeter (LoD) or Principle of Least Knowledge (PLK): Only talk to your friends
A method should only send messages to itself, its parameters, objects it creates (in the method), global objects (of the object/class), and objects it contains (inner of the method or of the object).
Law of Demeter for Functions/Methods (LoD-F): Object A can request a service (call a method) of an object instance B, but object A cannot “reach through” object B to access yet another object to request its services.
A method M of an object O may only invoke the methods of the following kinds of objects:
- O itself
- M's parameters
- any objects created/instantiated within M
- O's direct component objects
Objects are less dependent on the internal structure of other objects, object containers can be changed without reworking their callers.
Use only one dot (approx).
Delegation is an effective technique to avoid Law of Demeter violations, but only for behavior, not for attributes.
Law of Demeter can degrade performance:
Demeter Transmogrifiers: requires writing a large number of small “wrapper” methods to propagate method calls to the components. A class’s interface can become bulky as it hosts methods for contained classes resulting in a class without a cohesive interface.
Law of Demeter for Concerns (LoDC): Only talk to your friends who share your concerns, requires aspects (or traits).
Designing for extension: (frameworks)
Strong Adherence to LoD may be required for reusable class frameworks (usable by client subclasses as well as "normal" clients). Some of the rules they propose are:
- Whenever an object needs to request a service of some other external object, this external service request should be encapsulated in an internal non-public method of the object. This allows derived classes to override the service request with a more specialized one (it is also a use of the GoF Template pattern).
- Whenever an object needs to instantiate some other externally associated object, it should do so using a non-public method to perform the instantiation. This allows derived classes to instantiate a more specialized object if needed.
9. End to End Argument (EEA)
It should be possible to defer work as late as possible (YAGNI)
Avoid intermediate steps.
Lazy evaluation is the general case!
The best optimization is done when all possible affected information is available
See: JIT
OO Tips
Naïve OOAD:
Start with the domain, language analysis helps:
names -> domain objects
adjectives -> derived classes
verbs -> methods
patterns, structure -> design objects
Polymorphism:
- Subsumption Rule: A reference to a subtype can be used in any context where a reference to a super type is expected. (LSP)
- Dynamic Dispatch Rule: Objects decide how to handle the messages they receive.
Divide an app into 1 core and N helper reusable frameworks
Favor immutability for small objects (over get-set beans).
Model classes with complex behavior as state machines
Choose the most restrictive access possible. You will not be able to close it up when there are users.
Structured Programming smells:
- Control-Driven Solutions (switch, case, if, instanceof) point to structured programming, Data-driven solutions are more OO.
- Dynamic checking should be subsumed in the design of the application (avoid instanceof).
- Control modules that hold the logic of multiple components. Control and algorithms are distributed in OO !
Incomplete delegation smells:
- big classes: split into helpers
- multiple get-set: clone, update operations
Saturday, May 31, 2008
Sunday, May 11, 2008
Some design koans
A good design is worth ten implementations.
A bad architecture can´t be saved by a million developers (in the long run).
Software rots.
Software flexibility comes from the blood of its creators.
Design patterns in a language/framework are code idioms in a better one.
Refactoring is trying to get design out of entropy-full code.
Sw process is how it is done, not what is written or talked about.
You get farther striving for simplicity than for cleverness or features.
Design is what can be said about software without having to read each line of code. The more that needs to be said the worse the design.
Reuse is a dream, but we are allowed to dream. Aren´t we?
Reuse is now at the copy-and-paste level.
Software reuse is what a developer has learned after completing a project.
A software factory is an oxymoron.
Good code can only be appreciated by developers, the effects of bad code are detected by anyone.
Abstraction is the last refuge of the reckless.
As in real life, good looks can hide mean souls.
Those are mine (as far as I know), there are great sw koan writers around, I´ll add links latter.
Thursday, May 1, 2008
About roaches & Java: Climbing with grace (with Scala!)
There is some tension around about Java: what is the Next Big Thing(tm)?, is Java mature? Is Java dead? .... The fact is, as has been said elsewhere, that Java has reached cockroach status: meaning there is no way back. So Java is now on par with Cobol, Fortran and C: code (and work) is not going away! Also Java has set the bar higher than any other roach before it:
- Cobol: legibility, closeness to user model
- Fortran: numerical and algorithmic expressiveness
- C: portability, simplicity, closeness to underlying model, speed
- Java: garbage collection, objects, dynamic loading, virtual machine, language interoperability, APIs, IDE ...
For a future aspiring roach it has to at least be able to have the characteristics of all previous roaches. (When I say Java of course you can put .Net if you are living in that closed world).
It seems the dev world is, at last, ready for FP (wether newcomers are ready for it is another question. But newcomers are forced to do what they have to do). And of all the proposed New Things it seems Scala is the thing that retains most good Java things and adds most good "new" things.
What Scala has got now:
1. Exceptional Java interoperability
2. Great (for a non pure FP) functional constructs
3. Better OO than Java (traits, companion objects)
4. Nice core community (and I mean nice, being nice to newcomers and to other players is a must some Gollums have learned the hard way ...)
5. Academic soundness
What Scala is missing right now (to be on a par with what Java has got):
1. Good tutorial: there is a book in the works that may be of the right level for Java developers. There are a bunch of scattered blog entries around. Missing: use cases and idioms, more Java frameworks-Scala samples
2. API stability and development: more Java helpers-wrappers, better documentation.
3. Killer applications?: Java, and the JVM, is the "killer application" of Scala.
4. Patronage: Academia is behind Scala but some further industry support is needed. Plugins for main IDEs are on the way, but Scala is a more complex language to support that others.
5. Clearer roadmaps: Although stability is settling up lately, the only way to know where we are heading in the next moths is to check the bug
6. Better visibility: There is a need for more "in the wild" tools, programs, frameworks, applications, API. And this is where most of us should be working on. Start using Scala for some small projects now !
Subscribe to:
Posts (Atom)