Coupling and cohesion
The efficiency of the programs you design depends on how well you choose the classes they use.
Well-abstracted classes lead to programs that are easy to maintain and extend. However, if you choose bad abstractions, you could end up with an error-prone application that doesn't model the problem domain effectively.
Well-abstracted classes lead to programs that are easy to maintain and extend. However, if you choose bad abstractions, you could end up with an error-prone application that doesn't model the problem domain effectively.
Simplicity is the key to choosing good classes.
The classes you choose should capture only one key abstraction. If a class performs two functions, then you should split it into two separate classes, each with its own function.
The classes you choose should capture only one key abstraction. If a class performs two functions, then you should split it into two separate classes, each with its own function.
There are five key concepts you should consider when choosing classes:
- coupling
- cohesion
- sufficiency
- completeness
- primitiveness
These allow you to measure how well-designed classes are.
Coupling refers to how tightly connected classes are. If a class is very dependent on other classes to carry out its functions, then it is strongly coupled.
Strongly coupled classes can lead to problems in program design. Because such classes are interdependent, they are difficult to extend, and bugs in one can affect others.
This interdependency can also make it difficult for other programmers to understand programs that contain such classes.
This interdependency can also make it difficult for other programmers to understand programs that contain such classes.
To perform adequately, classes must be connected to other classes. But you should choose a level of connection that doesn't make one class too dependent on another.
This is particularly important when you design for inheritance.
This is particularly important when you design for inheritance.
Though inheritance is a form of coupling, it can make programs easier to design and extend.
So when choosing classes, you need to balance the requirements of inheritance against the need for weak coupling.
So when choosing classes, you need to balance the requirements of inheritance against the need for weak coupling.
Cohesion is a measure of how closely related the elements in a class are. These elements are the states, behaviors, and functionalities of the class.
A class whose members are simply grouped together and have little in common is only coincidentally cohesive. Such a class might have two unlinked functions, or it may group object states with unrelated object behavior.
Because its elements do not cohere to realize a single purpose, such a class is confusing, difficult to use, and often too complex to implement.
Because its elements do not cohere to realize a single purpose, such a class is confusing, difficult to use, and often too complex to implement.
Let's say you used a class named BankEmployee to model how bank staff are paid and how they interact with customers.
Since the elements of these separate functionalities are unrelated, this class would be a bad abstraction.
Since the elements of these separate functionalities are unrelated, this class would be a bad abstraction.
Classes with good cohesion are well defined and contain elements that properly belong together.
Such classes are easy to understand and use because they serve a definite purpose – they have functional cohesion.
Such classes are easy to understand and use because they serve a definite purpose – they have functional cohesion.
Sufficiency, completeness, and primitiveness
Sufficiency, completeness, and primitiveness are related principles.
A good class needs to balance the requirements of each of these principles before it can function well.
A good class needs to balance the requirements of each of these principles before it can function well.
- Sufficiency
- Completeness
- Primitiveness
- Sufficiency
- Sufficiency is the characteristic that ensures classes are able to function effectively.
You must create classes that contain all the attributes and operations necessary to perform the tasks you require of them. So if you create a Car class, you must include an accelerate operation – otherwise no car object will be able to change speed.
Sufficiency means that you should include only those attributes and operations that are needed for the system you are modeling.
If you want to create a class that models a typical car, you need to specify things like its speed and model.
But you don't need to specify what type of suspension the car uses – this would complicate the class without adding any important functionality to it.
Whether a class is sufficient or not depends on its purpose and on the level of detail it is intended to capture.
If you need to represent something in great detail, then you might have to include specifications that would not be needed in a more abstract model. - Completeness
- Completeness determines whether the interface of the class captures all the behavior of the class.
The attributes and operations you give to a class must be general enough to allow other classes to communicate with it. - Primitiveness
- Primitiveness means that you shouldn't make a class unnecessarily complicated. It is a principle that lets you control a large spectrum of behavior with just a few operations.
For any class, you should choose operations and attributes that are very basic.
To create more complex class states, you can simply change the values of the attributes you have chosen.
Or you can combine simple operations to create complex ones.
Let's say that in the Car class, you want to model the way gear changes work.
A vertical rectangle with three compartments depicts the Car class with the class name, Car, in the first compartment. The second compartment contains the attributes which are: engineSize, color, model, maxSpeed, currentSpeed. The third compartment contains the operations which are: accelerate(), brake(), turn(), getSpeed(), shiftUp(), shiftDown(), shiftUpTwoGears().
You will need to create a shiftUp operation, but you won't need a shiftUpTwoGears operation. You can simply use the more primitive shiftUp operation twice.
Primitiveness is very closely related to completeness.
To make a class complete, you need to account for all its behavior. But by defining this with primitive functions, you ensure that you don't have to list every possible permutation of a class's behavior.
The level of primitiveness you choose depends on how you want your program to perform.
If you need a program to run very fast, it may be better to use a complex function once rather than using a primitive one several times.
Summary
Classes should capture only one key abstraction. Classes that depend on other classes to function are strongly coupled. You should choose classes with weak coupling, because this makes programs easier to extend, maintain, and debug. Classes should also be functionally cohesive.
To be sufficient, a class must have enough attributes and operations to capture all the relevant states and behavior of its objects. You should make the set of these properties sufficiently complete to allow the purpose of the class to be easily understood, but you should also try to keep the class as simple as possible. You can do this by observing the principle of primitiveness.
To be sufficient, a class must have enough attributes and operations to capture all the relevant states and behavior of its objects. You should make the set of these properties sufficiently complete to allow the purpose of the class to be easily understood, but you should also try to keep the class as simple as possible. You can do this by observing the principle of primitiveness.