There is no formal and agreed upon definition of Object-Oriented Design. The original definition is:
- Encapsulation refers to the logic, resources, and information contained within the service boundary.
Service loose coupling
- Services maintain a relationship that minimizes dependencies and only requires that they maintain an awareness of each other.
- Services adhere to a communications agreement, as defined collectively by one or more service description documents.
- Beyond what is described in the service contract, services hide logic from the outside world.
- Logic is divided into services with the intention of promoting reuse.
- Collections of services can be coordinated and assembled to form composite services.
- Services have control over the logic they encapsulate.
- All else equal, high-quality services are generally considered preferable to low-quality ones.
- Services are designed to be outwardly descriptive so that they can be found and assessed via available discovery mechanisms.
- Functionality is presented at a granularity recognized by the user as a meaningful service.
Separate commands and queries.
- Queries return a result but do not change the visible properties of the object.
- Commands might change the object but do not return a result.
Separate basic queries from derived queries.
- Derived queries can be specified in terms of basic queries.
For each derived query, write a postcondition that specifies what result will be returned in terms of one or more basic queries.
- Then, if we know the values of the basic queries, we also know the value of the derived queries.
For each command, write a postcondition that specifies the value of every basic query.
- Taken together with the principle of defining derived queries in terms of basic queries, this means that we now know the total visible effect of every command.
For every query and command, decide on a suitable precondition.
- Preconditions constrain when clients may call the queries and commands.
Write invariants to define unchanging properties of objects.
- Concentrate on properties that help the reader build an appropriate conceptual model of the abstraction that the class embodies.
Add physical constraints where appropriate.
- Typically, these will be constraints that variables should not be void.
Make sure that queries used in preconditions are cheap to calculate.
- If necessary, add cheap-to-calculate derived queries whose post-conditions verify them against more expensive queries.
Constrain attributes using an invariant
- When a derived query is implemented as an attribute, it can be constrained to be consistent with other queries by an assertion in the class's invariant section.
To support redefinition of features, guard each postcondition clause with its corresponding precondition.
- This allows unforeseen redefinitions by those developing subclasses.
Place constraints on desired changes and frame rules in separate classes.
- This allows developers more freedom to extend your classes.
If there are privacy requirements, queries that compromise privacy can be used in contracts and then made private.
- The phrase then made private means they are made private in layers below the one in which they are used in contracts, but above the layers to which clients have full access.
- Initially hide the distinction between data and behavior. Think of objects responsibilities for knowing, doing, and deciding.
- Promote a delegated control architecture. Make objects smart -- have them behave intelligently, not just hold bundles of data.
- Design objects so interior details can be readily changed.
Coordinators (versus controllers)
Inherited behavior (versus inherited attributes)
Delegated behavior (versus centralized control)
Few high-level messages (versus many low-level messages)
Few smart objects that blend role stereotypes (versus many simplistic information holders)
Keep behavior with related information.
- This makes objects efficient.
Don't make any one role too big.
- This makes objects understandable.
- This makes objects smart.
Keep information about one thing in one place.
- This reduces complexity.
The Single Responsibility Principle (srp)
- A class should have one, and only one, reason to change.
The Open-Closed Principle (ocp)
- You should be able to extend a classes behavior, without modifying it.
The Liskov Substitution Principle (lsp)
- Derived classes must be substitutable for their base classes.
The Dependency Inversion Principle (dip)
- Depend on abstractions, not on concretions.
The Interface Segregation Principle (isp)
- Make fine grained interfaces that are client specific.
The Release Reuse Equivalency Principle (rep)
- The granule of reuse is the granule of release.
The Common Closure Principle (ccp)
- Classes that change together are packaged together.
The Common Reuse Principle (crp)
- Classes that are used together are packaged together.
The Acyclic Dependencies Principle (adp)
- The dependency graph of packages must have no cycles.
The Stable Dependencies Principle (sdp)
- Depend in the direction of stability.
The Stable Abstractions Principle (sap)
- Abstractness increases with stability.
All data should be hidden within its class.
Users of a class must be dependent on its public interface, but a class should not be dependent on its users.
Minimize the number of messages in the protocol of a class.
Implement a minimal public interface that all classes understand.
Do not put implementation details such as common-code private functions into the public interface of a class.
Do not clutter the public interface of a class with things that users of that class are not able to use or are not interested in using.
Classes should only exhibit nil or export coupling with other classes.
A class should capture one and only one key abstraction.
Keep related data and behavior in one place.
Spin off unrelated information into another class.
Be sure the abstractions that you model are classes and not simply the roles objects play.
Distribute system intelligence horizontally as uniformly as possible.
Do not create god classes/objects in your system.
Beware of classes that have many accessor methods defined in their public interface. Having many implies that related data and behavior are not being kept in one place.
Beware classes that have too much non-communicating information.
In applications that consist of an object-oriented model interacting with a user interface, the model should never be dependent on the interface.
Model the real world whenever possible.
Eliminate irrelevant classes from your design.
Eliminate classes that are outside the system.
Do not turn an operation into a class.
Agent classes are often placed in the analysis model of an application.
Minimize the number of classes with which another class collaborates.
Minimize the number of message sends between a class and its collaborator.
Minimize the amount of collaboration between a class and its collaborator.
Minimize fanout in a class.
If a class contains objects of another class, then the containing class should be sending messages to the contained objects.
Most of the methods defined on a class should be using most of the data members most of the time.
Classes should not contain more objects than a developer can fit in his or her short-term memory.
Distribute system intelligence vertically down narrow and deep containment hierarchies.
When implementing semantic constraints, it is best to implement them in terms of the class definition.
When implementing semantic constraints in the constructor of a class, place the constraint test in the constructor as far down the constructor hierarchy as the domain allows.
The semantic information on which a constraint is based is best placed in a central, third-party object when that information is volatile.
The semantic information on which a constraint is based is best decentralized among the classes involved in the constraint when that information is stable.
A class must know what it contains, but it should never know who contains it.
Objects which share lexical scope should not have uses relationships between them.
Inheritance should be used only to model a specialization hierarchy.
Derived classes must have knowledge of their base class by definition, but base classes should not know anything about their derived classes.
All data in a base class should be private.
In theory, inheritance hierarchies should be deep.
In practice, inheritance hierarchies should be no deeper than an average person can keep in her or her short-term memory.
All abstract classes must be base classes.
All base classes should be abstract classes.
Factor the commonality of data, behavior, and/or interface as high as possible in the inheritance hierarchy.
If two or more classes share only common data (no common behavior), then the common data should be placed in a class which will be contained by each sharing class.
If two or more classes have common data and behavior (i.e. methods), then those classes should each inherit from a common base class which captures those data and behavior.
If two or more classes share only common interface (i.e. messages, not methods), then they should inherit from a common base class only if they will be used polymorphically.
Explicit case analysis on the type of an object is usually an error. The designer should use polymorphism in most of these cases.
Explicit case on the value of an attribute is often an error.
Do not model the dynamic semantics of a class through the use of the inheritance relationship.
Do not turn objects of a class into derived classes of the class.
If you think you need to create new classes at runtime, take a step back and realize that what you are trying to create are objects.
It should be illegal for a derived class to override a base class method with a no-operation method.
Do not confuse optional containment with the need for inheritance. Modeling optional containment with inheritance will lead to a proliferation of classes.
When building an inheritance hierarchy, try to construct reusable frameworks rather than reusable components.
If you have an example of multiple inheritance in your design, assume you have made a mistake and prove otherwise.
Whenever there is inheritance in an object-oriented design, ask yourself two questions:
- Am I a special type of the thing from which I'm inheriting?
- Is the thing from which I'm inheriting part of me?
Whenever you have found multiple inheritance relationship in an object-oriented design, be sure that no base class is actually a derived class of another base class.
When given a choice in an object-oriented design between a containment relationship and an association relationship, choose the containment relationship.
Do not use global data to perform bookkeeping information on the objects of a class. Class variables or methods should be used instead.
Object-oriented designers should not allow physical design criteria to corrupt their local design.
Do not change the state of an object without going through its public interface.
Make the common-case fast
- In making a design trade-off, favor the frequent case over the infrequent case.
Fail fast, fail hard
- Do not allow improper state to leak into and throughout the system. It is better to fail hard at the first possible opportunity and force the error to be fixed rather than to silently allow it to propagate.
Make the right thing easy, the hard thing possible (Opinionated Software)
- Restrict the number of options and customizable features to promote best practice usage. Breaking out may be possible, but it should feel dirty.
- If clients need to do hard things then they should be coerced to reconsider their approach. Likely they are abusing the software and an alternative design is a better choice.
Convention over Configuration
- Seek to decrease the number of decisions that developers need to make to gain simplicity, but not necessarily lose flexibility.
Don't Repeat Yourself
- Avoid multiple and possibly diverging ways to express every piece of knowledge.
Amdahl's law (link)
- The speedup of a program using multiple processors in parallel computing is limited by the time needed for the sequential fraction of the program.
Gustafson's law (link)
- The influence of the serial part of a program is constant and parallelism can be achieved by increasing the number of processes.
Don't do it.
- Take advantage of locality, in both the forms of data and process communications, to avoid making unnecessary operations.
Do it, but don't do it again.
- Lazily compute and memorize/cache results.
- Pre-compute data to save time later and consider the algorithmic complexity of a task.
Do it cheaper.
- Take advantage of partitioning to spread out hotspots, keep frequently accessed data together, duplicate data if needed to achieve better locality, and make sure all of the new data isn't being generated by a single process.
Do it less.
- Perform cheap queries to check whether an expensive operation is required.
- For example, using a bloom filter for membership tests before making unnecessary I/O operations.
Do it later.
- Perform tasks that are not time critical asynchronous to a user's request and favor doing them during off-peak hours.
Do it when they're not looking.
- Perform tasks asynchronously and take advantage of user's idle time to prefetch data that has a high probability to be requested.
- For example, an email application may asynchronously prefetch unread mail after the user logs in by anticipating the user's next request.
Do it concurrently.
- Perform tasks in parallel to reduce latency, increase throughput, and avoid starvation (idle processing units).
- Consider performing an idempotent operation on multiple processing units to avoid slow units from impacting the process's throughput by leveraging idle units.
- Partition data so that it can be re-partitioned quickly and efficiently if something fails. Make the unit of distribution appropriate to the number of processing units that you have, e.g. 5 requests : 1 processing unit ratio.
The network is reliable.
- A component's MTBF indicates that a failure should be expected. A design should expect failures and handle them gracefully.
- Never assume that only one thing fails at a time and don't rely too heavily on statistical predictions of downtime. Opt for a minimum of N+2 redundancy.
- Avoid sending a query to all leaves that can cause a leaf to crash deterministically. Attempt the query on a single component and, if successful, then it is probably okay to send it to all.
Latency is zero.
- Latency: The time between initiating a request for data and the beginning of the actual data transfer.
Bandwidth is infinite.
- Bandwidth: A measure of the capacity of a communications channel. The higher a channel's bandwidth, the more information it can carry.
The network is secure.
- Security must be designed into the system at the network, infrastructure, and application levels.
Topology doesn't change.
- The physical network topology may be be configured as ring, bus, star, meshed, etc. layouts.
- The logical network topology determines if processes are organized to communicate on a single machine, within the same rack, within the same data center, etc.
There is one administrator.
- The network may be segregated for internal management (e.g. security zones) and connectivity to external providers.
Transport cost is zero.
- The marshaling of messages (serialization), network protocol, and data validation (checksums) consume computer resources and increases latency.
- The physical network must be built, leased, maintained, and supported. This consumes an operational budget.
The network is homogeneous.
- Homogeneous network: A network running a single network protocol (transport and serialization protocols).