- Designing classes that can be easily changed.
- Object Dependencies
- Designs based on messages.
Designing classes that can be easily changed.
The aim is to design classes that satisfy current requirements, and are easy to change in the future. Quantitatively defining this ease of change translates as changes will not lead to unexpected side effects, small requirement change leads to small code changes, existing code is easy to reuse.
An acronym to check this is Transparent Reasonable Usable Exemplary.
The first step to this is creating classes that do the smallest possible useful thing i.e. have a single responsibility, this reduces entanglements with other classes which might break when changes occur. “Cohesion” is used to describe this behavior, a class is said to be highly cohesive when everything in a class is related to its central purpose. This isolation allows changes without any consequences or code duplication (Don’t Repeat Yourself).
When deciding on interactions between classes, depend on behavior, and not on data. Abstractions that can help with this are:
- Hide instance variables via accessor methods.
- Hide complex data structures.
- Write methods with a single responsibility.
Another design pattern is called SOLID.
Single Responsibility- Classes should have a single responsibility.
Open Closed- Software entities should be open for extension, but closed for modification.
Liskov Substitution- Superclasses in theory can be replaced by their subclasses without altering program correctness.
Interface Segregation- Multiple specific interfaces are better than one general-purpose interface.
Dependency Inversion- It’s better to depend on abstract entities than concrete ones.
Since objects interact with other objects via messages, they need to know certain information about the object they will be interacting with. The problem this poses is when an object changes all the objects that are interacting it will also have to change. These dependencies take the form of the name of interacting class, name of the message to respond to, arguments required, order of these arguments, message chaining, test-object coupling.
The aim is to reduce such coupling between these two objects, to reduce change propagation to interacting classes. Some ways to manage dependencies are:
- Dependency injection: It segregates the service between client and server by an interface to abstract details of interacting class.
- Isolate dependences: isolate instance creation.
- Isolate vulnerable outgoing messages.
- Remove argument order dependency: use hashes, define defaults, isolate multi-parameter initialization.
- Manage dependency direction: Depend on classes that are less likely to change, and if changes occur will be localized.
Designs based on messages
Class definitions reflect the code, but a system’s functionality is defined by messages. The details of messages that can be passed in a system are characterized via interfaces. Exposed methods of a class form its interface.
The public methods of a class, which are visible to other classes and form the interface must satisfy some constraints. These reveal the primary responsibility of the class, aren’t expected to change easily since these will be invoked by other classes, and are heavily tested.
All other methods of a class, form its private interface. These handle implementation details of a class and may change.
Use sequence diagrams to understand how candidate classes might interact with each other. The nature of the messages helps define what should the interfaces of each class look like. While designing this, aim for “context dependence” i.e. when two objects collaborate with each other without unnecessary knowledge about each other.
Law of Demeter: It restricts objects from communicating with a third object via a second intermediate object. It results in loosely coupled classes.
Source: Agile Ruby Design