DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
  1. DZone
  2. Data Engineering
  3. Big Data
  4. Object Orientation: Maintaining Relationships Amongst Objects

Object Orientation: Maintaining Relationships Amongst Objects

Want to learn how to establish and maintain strong relationships amongst objects in your project? Check out this post to learn more about object orientation.

Rohit Kumar user avatar by
Rohit Kumar
·
Aug. 27, 18 · Presentation
Like (3)
Save
Tweet
Share
7.10K Views

Join the DZone community and get the full member experience.

Join For Free

Any successful programming language lives on long after it is first created. In its lifespan, this language goes through multiple changes that fix different issues as well as adds more and more useful features. In order to go through these changes smoothly, the software should be designed to make it more maintainable and extensible.

The Code Smells! The Code Rots!

Oftentimes, we lack internal quality, and the focus is primarily on "making it work" and not so much on "doing it the right way." Here, I will explain my thoughts on getting software craftsmanship to "do it right" in the object-oriented world.

Why Another Article on OOP?

There is tons of information available on design principles and patterns. Here, the focus will be on programming fundamentals (classes and its relationship). We will attempt to work out design flaws by dealing with these simple elementary concepts, without getting into the list of patterns and principles. During this process, we will begin to understand the kind of relationship that leads to greater flexibility in design. I hope this will give you additional insight while designing or implementing your software.

The content of this post is based on my studies and experience with object-oriented design, as well as some references from the object-oriented design training provided by Subramanian Sivakumar from Pratian Technologies.

Let's Begin

Let's begin with chanting what I call as the Bija Mantra of object-oriented programming:

"High cohesion and low coupling!"

Cohesion is all about how different classes are being defined, and coupling is about how different classes are being related. If you get this right, then, most likely, you have created a design that is of great internal quality and is highly maintainable and extensible.

So, the primary design decisions in any object-oriented software development are to:

  1. Define what constitutes different classes,
  2. Define relationships between all the classes.

With a comprehensive understanding of the requirements, it is intuitive to come up with a software component design that has a set of classes and relationships in a solution space, which will be in coherence with objects in the problem space. To begin with, the design may be perfect and the software may work great. But, as I said before, any programming language that lives long after it is created requires a lot of changes throughout its life to support new platforms and features. The design of classes and its relationships should not only be in coherence with objects in the problem space, but it should also be flexible enough to support extensibility and maintainability.

The class relationship is the most fundamental and one of the most important decisions that drive extensibility and maintainability of a design. The content here will provide you with some insight into the fundamentals of class relationships and how to achieve design flexibility.

Class Relationships

In a procedural programming paradigm, a function or procedure defines a behavior. To get this behavior in an application, the procedure can simply be called, if it is available in the same process space.

Similarly, in an object-oriented programming paradigm, a function or method of a class also defines behavior. But, to get that behavior in another class, unlike procedural paradigm, it is not possible to directly call the method. To do that, first, the two classes must be related.

Here are different ways in which two classes can be related:

Image title

Is-A: Should be read as "class A Is-A class B." In this case, any method of class B can be called from class A, as if the method is available in class A itself.

Image title

Has-A: This should be read as "class A Has-A class B." This means class A has an object of class B as a member variable. In this case, any method of class B can be called from class A using the class B member variable.

Uses: If class A uses class B, it means that the methods in class A have a local instance of class B, which is used to call a method of class B. This should be used only for localized need.

How Do we Relate Classes?

Given two classes, how do I decide on which relationship to choose? One approach could be to relate classes based on the relationship of corresponding objects in the problem space. But, that isn’t enough, as it doesn’t take extensibility and maintainability into consideration.

To further develop our understanding, here, we will take a problem statement and analyze its evolving design with a focus on maintenance and extensibility. These evolving designs will also give us insight into the usage of these different types of class relationships.

Problem Statement

Design reader classes should be able to read from a file, socket, or pipe. It should be able to read byte-by-byte or a number of bytes together. These requirements are just for the first version. We may have more requirements for the future, e.g. there may be more source to read from, or we may want to read data types ( String, boolean, int, etc). Based on what we decide to build in the future, these classes should accommodate the new changes as and when we want to build them.

Design #1

Image title





In this design, the read() method in the InputStream class is left for the subclass to implement. And, the read(buffer : byte []) method is implemented using the read() method in the InputStream class. All the subclasses implement the read() method. The example pseudo code explains how to use these classes.

Overall, this is a good design that uses the Is-A relationship for code reuse. This way, it makes the design extensible, as a new class,  ByteArrayInputStream, can be added here, which has Is-A relationship with InputStream, and automatically, it will have the read(buffer) method available. The design is maintainable as it avoids duplicate code for the (buffer) method in each class, thus any change in this method in future would require change only in the InputStream class.

Issues

Applications using these libraries may need to read large data. As the read method is reading it byte-by-byte, overall performance will be slow.

The other issue is that it provides methods to read a byte or an array of bytes. Whereas, the application may need to read different data-types and not just bytes. If that facility is not available, then each application will have to do data conversion in their implementation, which would be an overhead.

"A Is-A B" means that class A can reuse code from class B. This also provides extensibility and maintainability.

Design #2

The solution to any performance problem would be to implement buffering in methods. This buffer is maintained in the class. Each time the read method is called, it looks into the buffer and returns from there. If the data is not available in the buffer, then it fills the entire buffer instead of reading just one byte. Changing the same read method in each class may not be advisable as some applications or customers may not want this overhead of extra memory getting utilized as an internal buffer for these classes. So, look at a design where new classes are introduced to get this new behavior. This will ensure that the application will only need to take care of extra memory requirement, whereas all other application will be unaffected.

Image title

Here, we notice that there are two different streams in the design. Some streams will read from different sources and implement the read method, e.g.,FileInputStreamSocketInputStream, and PipeInputStream. We will call themBaseStreamclasses. The other type of streams work with base streams and provide extra functionality on top of that, for example,  BufferedFileInputS, DataFileInputS, etc. We will call them the DecoratorStream classes.

The above design is done using A Is-A B and overcomes the design issues discussed above. We use this Is-A relationship to create buffered and data streams. Pseudo code that uses BufferedFileInputS shows that the only change required from design #1 is to instantiate BufferedFileInputS instead of  FileInputStream. This is possible since the Is-A relationship provides type-compatibility.

Issues

We realize that to add any new base stream class, we need to create a corresponding decorator stream classes, as well. Things will be more complicated when all these decorator stream classes need to work together. This will lead to an explosion in the number of classes and the extensibility problem in the design. Also, it leads to maintainability problems, as changing the readBoolean method will require changes in all data related decorator stream classes. This problem is known as Class Proliferation.

Is-A relationship provides type-compatibility, which allows the use of variable implementation in an easier way. Extensive use of Is-A for code reuse may lead to problems like class proliferation.

Design #3

When we look at it closely, we realize that all buffered and data decorator stream classes do similar things. All buffered decorator stream classes read the buffer, and all data stream classes use the read() method from the base stream classes and construct different data-types based on the method being called, e.g. readInt() or readBoolean() , etc. So, quite clearly, the design should avoid having the repetitive code.

Image title

The above design solves the problem of class proliferation. Here, we use the Has-A relationship instead of Is-A. In this design, adding a new base stream class will not require the addition of new decorator stream classes.

Issues 

When a new base stream class is added, all the decorator stream classes need modification to add a reference to this new class, which is a maintenance problem.

A Has-A B' means class A can reuse code from class B. This provides extensibility and maintainability and also solves the problem of class proliferation.

Design #4

Image title

The above design solves the maintenance problem above. In the new design, all the decorator stream classes keep a reference of InputStream instead of each base stream classes. So, when a new base stream class is added, it is automatically referenced in decorator stream classes through the InputStream reference. This design allows all the decorator stream classes to work together, e.g. we can create DataInputStream, which uses a  BufferedInputStream, which internally uses  FileInputStream. It’s a very flexible design!

Issues 

If a new decorator stream class is added, it provides extra functionality to the basic stream classes. The designer of the new decorator stream class would need to keep a reference to the InputStream. A good design should hide these complexities for better extensibility.

A Has-A B' means class A can reuse code from any class of type B. This provides greater extensibility and maintainability.

Design #5

Image title

The above design makes a further improvement over the existing one. With the new design, a new decorator stream class can be added by just extending to the FilterInputStream, and automatically, it will reference the InputStream  class, and thus, it will have the reference of all the base and decorator stream classes.

A 'Has-A B' means any class of type A can reuse code from any class of type B. This provides greater extensibility and maintainability.

Conclusion

Progression of Thought

If you go through the above five designs, it shows a transition on how Has-A provides better implementation reuse.

Image title

'A Is-A B' Class A can reuse implementation of Class B.  And, Is-A provides implementation reuse.

Image title

'A Has-A B' Class A can reuse implementation of Class B. Has-A also provides implementation reuse. Along with that, it also avoids the class proliferation problem.

Image title

'A Has-A B' Class A can reuse any implementation of type B (class B and all its subclasses). Has-A provides variable implementation reuse.

Image title

'A Has-A B' any class of type A (Class A and all its subclasses) can reuse any implementation of type B (class B and all its subclasses). Has-A provides variable implementation reuse.

This is a very powerful thought, as you can build any hierarchy of Class A and reuse any implementation of type B in any implementation of type A without changing anything in the relationship.

Takeaways

So, in conclusion, Is-A should be used when type compatibility is the requirement. And, Has-A should generally be used for implementation reuse. Has-A must be used when reuse of variable implementation is required with or without type compatibility. The right use of both provides highly maintainable and extensible design.

Image title



Design Data stream Object (computer science) Implementation application Extensibility Software development Object-oriented programming

Published at DZone with permission of Rohit Kumar. See the original article here.

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • 19 Most Common OpenSSL Commands for 2023
  • Chaos Engineering Tutorial: Comprehensive Guide With Best Practices
  • 10 Things to Know When Using SHACL With GraphDB
  • A Guide to Understanding XDR Security Systems

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: