OOAD Principles
OOAD Principles
Is it Open or Closed?
This is the first question you should ask yourself after you have finished writing your application
module. Beware if the answer is closed. It means you need to take a fresh look at your design.
Object Oriented software does not only mean that your application is a combination of several
objects but it must satisfy the requirements that the design is flexible, and OPEN for extension.
At this point of time, we will define the Open-Closed principle (OCP) formally as:
A software module which should be closed for modification but open for extension.
You may wonder how it is possible to extend an application without modifying it. Precisely, a
good OO design gives you this power. Remember, bug fixing your application is not an
extension. Think about how Object1 uses Object 2 and Object 2 provides some business logic to
Object1. This business logic may be customer specific. To meet different customer needs, if you
have to change the business logic embedded in Object 2, your application is violating OCP.
So far so good but the example speaks more than a thousand words and I will present examples to
illustrate the points about OCP. If you follow the examples, you should be able to understand and
apply OCP in your own application design.
Strong Coupling
What it is? It means that one application module is totally dependent on the implementation of
another module. This is the first sign of violation of OCP. Consider this example. You are going
to design a banking application where a particular loan request is passed through a LoanValidator
to approve the loan request. The LoanValidator looks to see if the balance is above certain values
and then approves the request. Given this problem, one guy comes up with the following classes.
The LoanRequestHandler can assess a loan request. This class holds the balance and period of
existence for a particular bank account. I have simplified all other features that this class should
normally bear to represent a physical bank account. But still this is sufficient to make the point.
Listing 1 LoanRequestHandler.java
This program can tell you if your loan request is approved or not. It uses another object
PersonalLoanValidator to decide if the loan request is valid or not. The PersonalLoanValidator
just checks to see if the balance is more than 1000 and approves the loan or else rejects it.
Listing 2 PersonalLoanValidator.java
Problem
Do you see the problem with the above design? Probably yes. The LoanRequestHandler object is
strongly coupled with PersonalLoanValidator object. What happens if the LoanRequestHandler
object should be used to verify a business loan. Certainly, your business logic to approve a
business loan will be different and PersonalLoanValidator is not capable of handling this
business logic.
One solution may to be add another method to the PersonalLoanValidator class to verify the
business loan approval process. Also we need to modify the LoanRequestHandler object with an
if-else loop to apply different Validator objects for personal loans and business loans. This means
every time the bank decides to provide a different type of loan, we need to make change in the
LoanRequestHandler and PersonalLoanValidator objects.
This application is certainly not closed for modification. This is where the OCP comes into the
picture. This application has violated the OCP and therefore it is not properly designed.
The strong coupling between LoanRequestHandler and any Validator objects should be avoided.
Individual validator objects implementing different business logics for approving a loan request
should represent an abstract type and the LoanRequestHandler should use this abstract type rather
than any specific sub-type to avoid any strong coupling.
Bearing these facts in mind, we come up with the following design. I will only show the skeleton
of the modified design and the code inside each of them remains same as before. Only the design
changes, not the implementation.
Listing 3
The LoanRequestHandler now uses an interface type of object named Validator, which defines
the method for approving the loan. Each individual Validator object will implement this method,
with the object specific business logic within the method inherited from the defined Validator
interface.
Listing 4
Now each individual Validator object will implement this interface and make use of their
individual business logic to approve or reject a loan request. Based on this the
PersonalLoanValidator object changes to the following.
Listing 5
The BusinessLoanValidator only approves the loan request if the balance is more than 5000,
whereas the PersonalLoanValidator will approve it if the balance is more than 1000. But the
LoanRequestHandler remains unaffected by this change in the business logic change. So long it is
supplied the correct Validator, it will operate correctly.
Clearly, this is a better design. Any new loan type can be handled by adding a new XXXValidator
object, implementing the Validator interface. This is OCP compliant solution.
by Samudra Gupta
That Java is an Object Oriented language does not necessarily mean that the code
written in Java is always Object Oriented. If this statement surprises you, this series is
for you. In this series, I will try to demonstrate some design aspects, both good and bad,
that are the key to well written software in Java. The first of these is the Liskov’s
Substitution Principle (LSP), which will lead to the Design by Contract and Dependency
Inversion Principle. We will then see how all of them conform to one most vital principle
of OO design, the Open-Closed Principle.
The Liskov’s Substitution Principle provides a guideline to sub-typing any existing type.
Stated formally it reads: 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 behaviour of P is unchanged
when o1 is substituted for o2 then S is a subtype of T.
In a simpler term, if a program module is using the reference of a Base class, then it
should be able to replace the Base class with a Derived class without affecting the
functioning of the program module.
An example
When I was in the beginning of my IT career, I was working on a banking project and I
was asked to design an account-handling module. To simplify the scenario, let us
assume that I had two types of accounts to handle. One is the "Current Account" and the
other is a special type of current account with a better interest rate, but with some
restrictions that the account cannot be closed before a certain time period. Having done
the preliminary analysis, I decided to come up with two account objects
"CurrentAccount" and "SpecialCurrentAccount," and also decided to write another class
that would offer the interfaces to operate on these "XXXAccount" objects. I also
analysed that the "SpecialCurrentAccount" and "CurrentAccount" share lot in common.
Delighted with such a straightforward relationship, I came up with the following class
diagram. The SpecialCurrentAccount is a sub-type of the CurrentAccount.
The program specification for the module specified the following constraints.
The closing of Current Account should check for a balance greater than zero. If
satisfied, proceed to close the account.
The closing of Special Current Account should check for a balance greater than
zero and also that the minimum period for the account is covered. If satisfied,
proceed to close the account.
From the above specification and my own class diagram, I decided that I will override a
closeAccount() method in the derived SpecialAccount class. The following programs
CurrentAccount.java (Listing 1) and SpecialCurrentAccount.java (Listing 2) describe the
designed classes.
/*
* Account.java
*
*/
package sam.oo.bad;
/**
* open a current account with the given balance
*/
public boolean openAccount(int balance)
{
this.balance = balance;
return true;
}
/**
*closes the account
*/
public boolean closeAccount()
{
if(balance >0)
return true;
else
return false;
}
}
Listing 1 CurrentAccount.java
/*
* SpecialCurrentAccount.java
*
*/
package sam.oo.bad;
}
Listing 2 SpecialCurrentAccount.java
Also I decided to offer an interface through another class to operate on these account
objects, which looked like:
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
AccountTest test = new AccountTest();
test.closeAnAccount(ac);
test.closeAnAccount(sac);
Everything went fine, and I was happy, until a user of this module discovered an
unexpected behaviour with the system. When he tried to do the above, the result was
contrary to his expectation.
The closing of a SpecialCurrentAccount object failed and the user has no clue why,
without looking into the source code of the SpecialCurrentAccount class. But looking into
interface closeAnAccount(CurrrentAccount), he is justified to make an assumption that
any type of CurrentAccount object will behave in a similar manner. But in reality it did
not. That made me think a while and I came to the following conclusions:
The above discussion leads us to the discussion of the Design by Contract principle. In
Design by Contract, each method in a class can have a pre-condition and a post-
condition attached to it. The pre-condition defines the criteria to be met before the
method offers a certain behaviour and the post-condition is the state or behaviour
offered by the method once the pre-conditions are met. Following that, if we note down
the pre-conditions and post-conditions followed by the CurrentAccount and the
SpecialCurrentAccount classes, they will look like the following:
CurrentAccount.java
/**
* pre condition: the balance >0
* post-condition: the account is closed
**/
public boolean closeAccount()
SpecialCurrentAccount.java
/**
* pre condition: the balance >0
* the period > default period
* post condition: the account is closed
**/
public boolean closeAccount();
A sub-type can only have weaker pre-conditions and stronger post-conditions than its
base class.
Clearly, my previous design violated the Design by Contract principle.Having these facts
in mind, I had a clear idea what was going wrong and approached a better LSP and
Design by Contract compliant solution.
One of the main lessons I learned is that sub-typing needs to be done with respect to the
behaviour of the types and not with respect to the data only. In this aspect, the
SpecialCurrentAccount IS NOT A CurrentAccount. A banker will shout at me for sure if I
say this to him. But for you the intelligent programmers this would make sense. This is
because they do not exhibit the same behaviour against the same message. I decided to
break the hierarchy and come up with is new design with Account as an abstract base
type and CurrentAccount and SpecialCurrentAccount as its sub-types.
The abstract Account class declares an abstract method closeAccount(), which is
implemented by both CurrentAccount and SpecialCurrentAccount. I have now changed
the interface class to accept an Account type of Object as opposed a particular type of
Account Object. The new interface class looked like this.
With this hierarchy, what may surprise you is that we have not changed the
implementation of the closeAccount() method in any of the classes. The pre-conditions
and the post-conditions remain the same. But in essence, what has happened is that the
user will not make any assumption about the behaviour of the account object he is
dealing with. This makes the module more maintainable and reusable in the sense that
now it is very easy to add another type of account which may impose some other pre-
condition and post-condition without breaking the LSP and Design by Contract.
No overriding!!!
It may seem from the above discussion that overriding is the main problem with LSP and
in that case inheritance makes no sense. To some extent, it is puzzling but always keep
the Design by Contract principle in mind before you override. If you are unable to comply
with the same, then it might be worth revisiting your class diagrams. Always make sure
that the derived class must enforce less strict pre-conditions while overriding any base
class method. Try to think about the situation on your own and surely you will come up
with some logic why LSP is so important for a good OO design.
The OCP is the guiding principle for good Object-Oriented design. It is easy and it is
difficult. The main problem is how you look at your design. The design typically has two
aspects with it. One, you design an application module to make it work and second, you
need to take care whether your design, and thereby your application, module is
reusable, flexible and robust.
The OCP tells you that the software module should be open for extension but closed for
modification. As you might have already started thinking, this is a very high level
statement and the real problem is how to achieve this. Well, it comes through practice,
experience and constant inspection of any piece of design, understanding that how it
performs and works tackles with the expanding requirements of the application. But even
though you are a new designer, you can follow certain principles to make sure that your
design is a good one.
One of these principles is the Dependency Inversion Principle, which helps you make
your design OCP compliant. Formally stated, the principle makes two points:
High level modules should not depend upon low level modules. Both should
depend upon abstractions
Abstractions should not depend upon details. Details should depend upon
abstractions.
What this means? Think about a typical software design process. You typically start with
high-level modules. For example, if you are designing a module, which collects data
from a data source and writes to a database, you will try to break down the architecture
in the following way.
There is a component which accepts certain objects and writes data to the
database
There is a component which reads data from a certain data source and creates
certain suitable objects for the data writing component.
Look at it like this, your idea started from the point that you need to write some data to
the database and then you think about getting the data in an acceptable format. There is
nothing wrong in this process. This is what your application looks like from the high-level.
But there is a danger if you fail to transform your thinking into a right design.
Consider, given the above problem, you come up with the following design in Figure 1.
The DataWriter class uses the DataCollector class. The DataCollector class is
responsible for collecting the data from some data source and passing the required
object to the DataWriter class. The DataWriter class in turn accepts the passed object
and uses another class DatabaseWriter to write data to the database.
Assuming this architecture, the pseudo-code for the above classes may look like this.
Listing 6 DataCollector.java
The pseudo-code for the DataWriter class may look like this.
Listing 7 DataWriter.java
The pseudo code for the DatabaseWriter can in turn look like this.
Listing 8 DatabaseWriter.java
Look at this design, implement it and your application will run fine. Now there is a
problem, your high level component is dependent on the details of the low level
implementation. Clearly, it is tied to the implementation of the DatabaseWriter object.
This makes your design inflexible. Really, what you want your DataWriter object to be
able to write to any destination. This gives you the ability to reuse your DataWriter class
to write data to any other destination should the application require it in the future.
One solution may be to put an if-else loop within the DataWriter, so that it can decide
which destination to write to. We have discussed in OCP that this is the point not at all
desirable. This makes your software open for modification. A better approach of thinking
would be:
How can we do this? Abstraction is the answer. This is the key for flexible design. Make
the abstractions depend on each other, not the concrete implementations. Look at the
following modified design in Figure 2.
In this design, we have abstracted the implementation of the data writing components.
Now the DataWriter class uses the <<interface>> Writer. There are two concrete
implementations of this interface viz. DatabaseWriter and FleWriter. You can pass any of
the implementations depending on the requirement. Notice, that this design is flexible
because you can add any other Writer to the application structure should it be required.
All said and done, why is it called Dependency Inversion Principle? The answer is again
in thought thinking patterns. In the traditional approach, we start thinking from the high-
level modules and cascade down to the lower levels. For example, we initially thought,
we need an object to write data to the database and then created another object to write
the data to the database and strongly coupled the high-level writer object to the low-level
database writer object. This led to the bad design.
You might at this point question and might have truly experienced that designing with
abstraction is sometimes overkill. In many cases, the requirement is rigid and unlikely to
change too often. It is a trade-off, as to how far we go and how much time we spend on
a good design. It may be quite important to get a reasonable designs out to the
developers and let the project get on. True, but again if you look at any project, you will
often find distinct layers of application components. They may typically be like this:
High-level policy objects holding different piece of business logic or common
functionality across different modules.
The Policy level components use some middle layer objects to perform different
business level operations such as sending messages to other components,
writing data to some data source etc.
The lowest-level objects are Utility objects, which hold the concrete
implementation of different operations.
In my opinion, for a reasonable design, you need to concentrate on the Policy level
objects. It is important to get them well designed as a policy can be reused by many
other applications. The rest of the layers can be left for now if you have run out of time
and resource.
Conclusion
Hope you have understood the power of OO designs following the OCP and DIP. It
might need some practice in the initial days of design but once you get it by heart, this
becomes easy. The important thing is to consider the alternate scenarios that can arise
to break your application design. If you find them, try to see if your design can handle
them. If not, think about these principles and you might see some light.
Previous examples and articles have shown that interface-based designs are much
more effective in making a software module flexible. In the Java language, interface
provides a way of inheriting the methods, without inheriting the implementation. The
alternative is the inheritance mechanism (used by the extends keyword in Java), where
you inherit all the public and protected method implementations by default. The previous
choice between inheritance or composition is a delicate one. Remember that the
interface-based composition gives you more flexibility in terms of design.
While designing with interfaces is a grand idea, care should be taken when designing
the interfaces themselves. In Java, interface is the way to guarantee certain behaviors
through the objects that implement the interface. The problem comes when multiple
objects implement the same interface. The Interface Segregation Principle (ISP) helps
you to achieve appropriate separation of interfaces. Formally stated, the ISP reads:
Why is this? Look at the following example. Imagine that in your application you are
required to write some Data Access Objects (DAO). These data objects should support a
variety of data sources. Let's consider that the two main data sources are file and
database. You must be careful enough to come up with an interface-based design,
where the implementation of data access can be varied without affecting the client code
using your DAO object. The following design is a good example of the above
requirements (Figure 3).
Figure 3: The initial DAO class hierarchy
There's another aspect that needs be to considered. What happens if the data source is
read-only? The methods for inserting and updating data are not needed. On the other
hand, if the DAO object should implement the DAO interface, it will have to provide a null
implementation for those methods defined in the interface. This is still acceptable, but
the design is gradually going wrong. What if there is a need to rotate the file data source
to a different file once a certain amount of data has been written to the file? That will
require a separate method to add to the DAO interface. This is just to add the flexibility
to the clients using this FileDAO object to enable them to choose either the normal
append feature to the file data source or to make use of the improved file rotation
feature.
With the DatabaseDAO implementation now broken, we'll need to change it, to provide a
null implementation of the new method added to the interface. This is against the Open-
Closed Principle.
So, what went wrong? In the basic design, the fact that the file data access operation
and database access operation can differ fundamentally must be considered. We
defined the behaviors for both the data access operation, and the database access
operation together in a single interface. This caused problems at a later stage in the
development. It is not necessary to be a guru in Object Oriented System Design, to
solve this problem nor is vast experience in designing software applications needed.
What is necessary is to think of interfaces as the behaviors to be provided through
particular objects. If two or more objects implementing the interface depict different sets
of behaviors, then they probably cannot subscribe to a single interface.
When a single interface is designed to support different groups of behaviors, they are, by
virtue, inherently poorly designed, and are called Fat interfaces. They are called Fat
because they grow enormously with each additional function required by clients using
that interface.
Thus, for the problem with the Data Access Objects, follow the Interface Segregation
Principle, and separate the interfaces based on the behaviors. The database access
classes and file access classes should subscribe to two separate interfaces. The
following design is obtained by applying the Interface Segregation Principle (Figure 4).
Figure 4: The final DAO class hierarchy
With this design, the Fat interface symptom is avoided and the interfaces clearly
delineate their intended purpose. If any imaginary data access object requires a
combination of operations defined in both of these interfaces, they will be able to do so
by implementing both the interfaces.
Conclusion
This article summarizes the concepts of the Composite Reuse Principle and Interface
Segregation Principle. The first principle addresses a good interface-based system
design, while the second addresses the issues of designing interfaces which are more
cohesive, grouped on the behaviors they support.
This ties up the loose ends in our discussion about the design of class structure. In my
next article, I will explore how package structure is as important as class structures, and
examine a few principles governing proper package structure.
Polymorphism, in Object Oriented terminology, means that any particular class can exist
as multiple distinct sub-classes (sub-types). The following is an example of
polymorphism:
Animal is the super class, and it can exist in the form of either a Dog or a Cat. When
objects exist in the real world, they exhibit some behavior to the external world. Thus,
the Dog and Cat objects expose the behavior of talking and, as they talk differently, they
are polymorphic.
The above example demonstrates how polymorphism can easily be achieved with
inheritance. However, inheritance based polymorphism often is ineffective, as an
explosion of subtypes may cause the system to run out of flexibility. Take a look at the
following example:
Imagine that you have been assigned the task of designing a payroll system for an
organization. As Christmas is near, your first job is to make some provision for employee
bonus payments . The company has three types of employees: Permanent, Temporary
and Part-time.
In your initial design, you have considered each employee to be a polymorph of the base
type Employee. You have also determined that the bonus for all employees is calculated
in a similar manner and defined the calculateBonus() method in the Employee class. The
other operations, such as leave and insurance premium calculations, are specific to the
type of Employee. Thus those methods are declared as abstract in the super class and
provided the implementation in the specific Employee classes. The initial design is
shown in Figure 1.
The manager is impressed. Then he asks that the company consultants get bonuses,
calculated at the same rate as part-time employees.
You could create a new class called Consultant and make it a sub-class of
PartTime to inherit the calculateBonus() implementation. However, this causes a
problem with the class hierarchy, because Consultants are also Employees. The
class hierarchy will always say that Consultants are part time employees, which
they aren't. In addition, if on a future date, the Consultant receives a Permanent
employee bonus, the hierarchy will not withstand the change.
Another possibility would be to override the calculateBonus() method in the
Consultant class, and copy the same implementation of PartTime class there.
However, duplicate code is not the reusability we desire.
The original class structure is highly limiting when new parameters are added. Lets now
examine the following solution and explanation.
To date, we've made a fundamental assumption that the bonus calculation is a frozen
arithmetic function and pushed it to the super class. In reality, the calculation of bonus
can change algorithms more frequently, and each type of employee can have a unique
bonus calculation algorithm. Inheritance is only applicable in the context of a generalized
relationship where the sub-type is a super-type. Or, in other words, there is an ISA
relationship. In Liskov’s Substitution Principle, the main criterion for the ISA relationship
is whether the sub-class exposes the same behavior as the super-class. Each time one
has to override the methods from the super class, one violates this principle and the
super-class becomes a specialized version of the sub-class rather than a generalized
version of the sub-class. Further, each time that occurs one runs the risk of having the
same problem we are faced with in the previous example.
The elegant solution is to define an abstract BonusCalculator and attach the appropriate
BonusCalculator instance to each Employee instance. The following design depicts the
proposed solution (Figure 2):
Figure 2: The CRP based Employee class hierarchy
The diagram above demonstrates that the BonusCalculator is a composite of all the
different Employee classes, and thus an extremely flexible polymorphism has been
achieved. The algorithm can be changed at any time for bonus calculation by attaching a
different implementation of the BonusCalculator to any of the Employee objects.
It's time to consolidate on what we've been discussing in this series about design
principles governing the class structure and package structure. In this article, I will
present a sample case study in which we will develop a hypothetical application
component and try to employ all the design principles we have learned so far, to come
up with a reasonable robust design solution. You can appreciate that it is well beyond
the scope of the space for this article to present a large-scale project architecture
encompassing all the complexities that can arise in a designing situation. So, I will try to
pick a hypothetical application component and try to present a design for that component
module. Well then, have your cup of coffee ready with you.
In the previous five articles, I have presented the design principles governing the class
structure and packaging of several classes into different packages. In this section, we
will recapitulate the principles that we have discussed so far.
A software module should be closed for modification but open for extension.
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.
High-level modules should not depend upon low-level modules. Both should
depend upon abstractions.
Abstractions should not depend upon details. Details should depend upon
abstractions.
Many specific interfaces are better than one combined general interface.
Design Principles for Package Structure
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.
The dependencies between packages must always be in the direction of stability. Less
stable packages should depend on more stable packages.
The stable packages must be abstract packages. The instable packages should contain
the concrete implementations.
Now that we have recapitulated the principles that we need to keep in mind while
designing any component, let us venture to design such a component. For example, let
us think that we need to develop a Reporting component. This component in short needs
to support the following features:
I would at this point try to avoid any detailed discussion of any other possible features
that the ReportWriter component might provide.
With these features in mind, we can develop a sample Business Type Model (BTM) for
our ReportWriter component. This sample BTM is presented in Figure 1.
Figure 1: The Business Type Model for the ReportWriter component
This BTM shows the different components acting together to produce the report. The
Client component makes a report request to the RequestHandler component. The
RequestHandler component then makes a call to the ReportWriter component supplying
the report parameters to it. The ReportWriter component then makes use of DAO
component to obtain the data from a specific data source, produces the report and
hands it over to the RequestHandler component to send it back to the appropriate client.
Once we have the BTM with us, it is time to get into the real class structure. Deciding on
the class structure is often an iterative process. The initial design may get affected with
slight requirement change or changes in the existing components that the component in
question is going to use. In this context, the challenge is to come up with a design, which
is flexible enough to accommodate such changes.
Well then, lets proceed with designing the classes for our ReportWriter component. First
of all, let us consider the entry point to the ReportWriter component. A Client makes a
request to the ReportWriter. We need a mechanism to handle this request. If you are
developing a web-based application, this request will typically be over HTTP. So, we
need to have a mechanism to handle the HTTP requests. In Java world, this is handled
by using a Servlet.
Designing the RequestHandler
So our initial class diagram for RequestHandler might look like something in Figure 2.
At this point it is clear that the client makes a HTTP call to the ReporterServlet
requesting to produce a report. In other situations, a Client can interact with the
ReportWriter component using some other protocols such as RMI etc. We will see later
in the series how to best accommodate such scenarios. But whatever be the protocol,
the RequestHandler component will ultimately make use of the ReportWriter component
to produce the report.
Now we come to the main bit of the application i.e. designing the ReportWriter
component. The following points are important to note:
The format of reports can vary from client to client. For example, one client may
request an HTML report while another may be looking for an Excel based report.
To produce each individual report format, we might need to implement different
technologies. For example, while HTML formatted reports are easy to produce;
Excel reports may be little more complicated.
We need to come up with a design so that the change in underlying
implementation of ReportWriter will not affect the client code in any form.
Figure 3 represents a possible solution based on the idea that client at the moment
requires two types of reports: HTML and Excel.
Figure 3: One possible class structure for the ReportWriter component
Although this is a possible solution, this is not a good one. We now try to test if this
design passes all the principles stated in the Class Designing principles previously.
OCP compliance
The classes in a package are reused together. If you use one of the classes in a
package, you reuse all the classes in that package.
Figure 4: The OCP compliant design for the ReportWriter component
The design in Figure 4 is OCP compliant and also maintains the principle of Dependency
Inversion. Previously, the high-level module the ReporterServlet had a strong coupling
with the concrete implementation of the individual Reporter. Now both the high-level
module and the low-level XXXReporter implementations depend on the abstraction viz.
Reporter interface.
The ReportWriter component needs to bring back data from a certain data source.
Typically, it may be a database. One possible solution to the problem of bringing back
the data from any data source is to write a separate Data Access Object component.
The DAO component receives the criteria from the ReportWriter component for bringing
the data back, fetches the data back from the data source and returns the data back to
the caller ReportWriter component. Figure 5 represents a possible solution with a DAO
bringing back data from database.
Figure 5: One possible solution for Reporter objects to bring data from database.
Again this solution fails to clear OCP test. We have lost the flexibility to bring data from
any data source other than Database. Of course, we can implement another DAO to
bring back data from say flat file, but that means we need to go back to the Reporter
objects and change the strong coupling with the DBDAO to use the new DAO. Clearly,
the solution is that the Reporter objects should depend on some abstraction of the DAO
classes.
The test against Dependency Inversion Principle also fails as in the previous design the
high-level modules (ReportWriter objects) directly depend on the low-level modules
(DAO objects). The DIP dictates that both should depend on abstraction.
The correct solution
Figure 6 represents the correct solution for the relationship between ReportWriter
objects and the DAO objects.
Now we have data to produce the report. One more thing, we need is to format the
report in a desired way. One possible solution is to have separate Formatter objects.
Again thinking in the line of OCP and DIP, we want to come up with an interface based
solution for the Formatter objects so that we can interchange the format of reports
without having to change the ReportWriter objects.
Conclusion
You can now see that we have a reasonable design for the ReportWriter component.
This design is extendible and OCP compliant. Now it is time to finalize the interaction
mechanism among the components. The questions that we need to ask is how each
component will make use of one out of the many implementations of any particular
abstraction (interface) and also how different components will exchange data amongst
themselves.
Welcome back to our series on design principles. If you recall we have been discussing
a hypothetical case study for developing a reporting component. You would think that at
this time we would talk about how to write the classes and interfaces for the reporting
component, but there is still more we need to focus on to finalize our design.
First, let's look at this class diagram (Figure 1) that we ended with in the last article.
Things have changed since then…
Since we came up with this design, a month has elapsed. We all know that this is too
long a time scale in the world of programming. We now come back to the class diagram,
but there is a new requirement regarding the HTML formatter reports. One of your
managers has come up with a new idea (they always do!!!). If there is tabular data and
the number of rows is more than 25, he wants a scrollable table (damn!). Thinking about
it for a little while, you see a ray of hope; there is a browser incompatibility issue that
Netscape might not support this wonderful scrollable table idea. So in your opinion, it is
better to come up with a solution that is liked by both the browsers IE and Netscape.
You set out for the manager’s room and explain everything. He nods and smiles and
then says "OK, we then need to capture which browser the user is currently using and
depending on that we either display plain table or scrollable table". You return back with
an added layer of complexity. Don’t be too disappointed; it is after all simply a matter of
writing another Formatter object.
Ok, we already have an HTMLFormatter class with us realizing the interface IFormatter.
Now we need to come up with a new class to format the scrolling table. Let us name this
class as MsHTMLFormatter (as this implements features supported by MSIE only). So
we decide to come up with a class design something like this (Figure 2):
This design isn't as righteous as it seems from the surface, it is wrong. How could we get
it wrong, when we had been learning all the design principles for so long? Well, the
answer is we did not follow the design methodology before we designed the
MsHTMLFormatter.
One rule of thumb to me is whenever I decide to subclass any existing class; I follow the
design by contract principle. To do that, we just need to jot down the pre and post
conditions for the super class and the sub class. Figure 3, represents the design by
contract for the HTMLFormatter and MsHTMLFormatter classes.
Figure 3: Design by Contract for MsHTMLFormatter
Inspect the contracts ,the pre-conditions and the post-conditions specified in both the
classes. Clearly, the subclass MsHTMLFormatter has more of a number of pre-
conditions than its super-class HTMLFormatter. Now recall, the Liskov’s Substitution
Principle (LSP), we discussed in the beginning of the series.
It reads: If a program P is using a super class S and sub class T exists for the S, then
the program P should retain the same behavior of S is substituted by T.
In our case, let us assume that our Reporter programs (P) uses an instance of the
HTMLFormatter (S). MsHTMLFormatter (T) is a sub class of the HTMLFormatter (S).
According to LSP, we should be able to substitute any use of HTMLFormatter with
MsHTMLFormatter without the behavior of program P getting changed.
Clearly, in our design it is not going to happen that way. Imagine, we are working with a
data set of size 5. If we are using the HTMLFormatter, this data set will be processed
and formatted. The moment we replace the HTMLFormatter instance with
MsHTMLFormatter, it will break because MsHTMLFormatter will not format the data set
if the size of the data set is less than 25.
Well, we have now got our Reporter objects, Formatter objects and Data Access Objects
all in place. The hierarchy looks reasonably done and the good news is that they
conform to design principles, but surely all of these classes cannot reside in the same
package and we need to come up with a package structure for these classes.
One good idea for package structure is to place the interfaces and the concrete
implementations in a separate package. This enables us to achieve a complete
separation of the specification and the realization of the component/modules we are
developing. Keeping this is mind, we can draw an initial package diagram as shown in
Figure 5.
As you can see from this package diagram, the whole module has got four separate
components. The first is Client component, the second the Reporter component, the
third the Formatter component and the fourth and last is the Data component.
From our package diagram in Figure 5, it is clear that packages do depend on each
other. Careful attention will also reveal that each component depends on the abstraction
(specification) package of the other component. This ensures that direction of
dependencies is towards the stable packages. This is because the specification is less
likely to change compared to the implementation.
In the last series when I discussed about the SAP, I provided a formula for calculating
the stability index of a package based on the incoming and outgoing dependencies of
the individual package. Accordingly, for our trivial example, we will note down the
incoming and outgoing dependencies of each package
I =Outgoing/(outgoing+incoming)
Client 0 1 1/(1+0) = 1
reporter.spec 2 0 0/(0+2) = 0
reporter.impl 0 3 3/(3+0) = 1
formatter.spec 2 0 0/(0+2) = 0
formatter.impl 0 1 1/(1+0) = 1
dao.spec 2 0 0/(0+2) = 0
dao.impl 0 1 1/(1+0) = 1
In the discussion of SAP, we also saw that if the Stability index is nearing 1, then it is a
less stable package and if the Stability index is nearing 0, it is a more stable package.
From our table, we can see that the specification packages (e.g. reporter.spec) have
Stability index 0 where as the implementation packages (e.g. formatter.impl) have
Stability index 1. This implies that the specification packages are more stable than the
implementation packages. This is a correct design. It is also to note that our package
dependencies are in the direction of stable packages as packages depend on each other
via the specification packages.
Thus, our design conforms to the Stable Abstraction Principle.
Note: This example is a very trivial one. In real world, it is hardly possible to design a
package with Stability index equal to 0 or 1. As the number of dependencies grows, they
tend to assume a more fractional value. For example, if say the dao.spec package
depends on java.sql package, then the Stability index will change to 1/(1+2) = 0.33. Still
it is toward the 0 value and thereby tends to be a stable package.
One thing is sure, that if we keep on refining the design forever, the project is going to
miss the deadline. One fundamental concept of today’s modern development
methodologies such as Rational Unified Process (RUP) is that they rely on iteration. It is
good practice to accept that things have a natural course of evolution and any system
and for that matter any design will need to go through iteration.
As time goes by we start implementing one of the Formatter classes and realize that this
particular Formatter object will require putting some heading into the formatted report.
We turn our attention towards the ReportUtility class and discover to our joy that there is
a method to do just such a job. So we happily use it from within our Formatter class.
The code works but unknowingly we have introduced a problem. To understand the
problem, we will redraw the package diagram, this time, in a different way in Figure 6.
Figure 6: The package diagram with the utility package.
What you notice here is a problem of Cyclic Dependency between the packages reporter
and formatter. The problem arises because one of the classes from the formatter
package is willing to use classes from the reporter package.
Don’t be discouraged. This happens and it can be avoided with experience and adhering
to the design methodologies and by checking the design at a regular time period. You
know how to solve this cyclic dependency by factoring out the class to a different
package or by placing it in the formatter package. The reporter package can still
continue to use the utility classes as it already depends on the formatter package.
Conclusion
This brings us to the end of our example design. A larger example, would simply be too
complicated to present. I hope this simple example will help you grasp the concepts and
will have you applying them in your own designs.
I have received many emails from readers who liked the topics I have discussed so far.
I'd like to say thanks to all of you, and as always it is good to hear from the readers.
Please e-mail with particular topics you'd like me to cover and I'll try to get to them all.
With this I say good-bye to this series. Till I come back next time, happy-coding.