0% found this document useful (0 votes)
2 views56 pages

Aula 5 - Class Design Guidelines

The document outlines class design guidelines in object-oriented programming (OOP), emphasizing the importance of encapsulating data and behavior within classes to model real-world entities effectively. It highlights common mistakes, such as creating classes with behavior but no data, and stresses the need for a minimal public interface to ensure utility and security. Additionally, it discusses the significance of constructors and destructors in maintaining object integrity and proper initialization.

Uploaded by

danielsfranco346
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views56 pages

Aula 5 - Class Design Guidelines

The document outlines class design guidelines in object-oriented programming (OOP), emphasizing the importance of encapsulating data and behavior within classes to model real-world entities effectively. It highlights common mistakes, such as creating classes with behavior but no data, and stresses the need for a minimal public interface to ensure utility and security. Additionally, it discusses the significance of constructors and destructors in maintaining object integrity and proper initialization.

Uploaded by

danielsfranco346
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 56

MC322 - Object Oriented Programming

Lesson 5
Class Design Guidelines

Prof. Marcos M. Raimundo


Instituto de Computação - UNICAMP
Designing Solid Classes

• OO programming supports the idea of creating classes that are complete packages,
encapsulating the data and behavior of a single entity.
• A class should represent a logical component, such as a taxicab.
• This chapter presents several suggestions for designing solid classes.
• Obviously, no list such as this can be considered complete.
• You will undoubtedly add many guidelines to your personal list as well as incorporate
useful guidelines from other developers.

1
Modeling Real World Systems
Object-Oriented Programming (OO)

• OO programming aims to model real-world systems like


human thought processes.
• Designing classes are fundamental in OO programming,
encapsulating data and behavior into interacting objects.
• Unlike structured approaches, where data and behavior are
separate, OO encapsulates them into interacting objects. A cabbie and a cab are real-world
objects.
• Classes model real-world objects and their interactions,
mirroring human interactions.
• When creating classes, they should represent the true
behavior of the object.
• Mistakes in transitioning to OO include creating classes with
behavior but no class data, resembling structured models.
• Such an approach misses the power of encapsulation, failing
to take advantage of the object-oriented paradigm. 2
Object-Oriented Programming (OO) - Example: Structured Approach

• In a structured approach, data and behavior are often separate.


• In a Rectangle, a structured approach separate variables for width and height, and a
function to calculate the area.
p u b l i c c l a s s RectangleData {
p u b l i c double width ;
p u b l i c double height ;
}
public class RectangleOperations {
p u b l i c s t a t i c double c a l c u l a t e A r e a ( RectangleData r e c t ) { r e t u r n r e c t . width ∗ r e c t . height ; }

p u b l i c s t a t i c void p r i n t R e c t a n g l e D e t a i l s ( RectangleData r e c t ) {
System . o u t . p r i n t l n ( ” Width : ” + r e c t . w i d t h + ” , H e i g h t : ” + r e c t . h e i g h t ) ;
}
}
p u b l i c c l a s s Main {
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {
R e c t a n g l e D a t a myRect = new R e c t a n g l e D a t a ( ) ;
myRect . w i d t h = 5 ;
myRect . h e i g h t = 1 0 ;
d o u b l e a r e a = R e c t a n g l e O p e r a t i o n s . c a l c u l a t e A r e a ( myRect ) ;
System . o u t . p r i n t l n ( ” Area : ” + a r e a ) ;
R e c t a n g l e O p e r a t i o n s . p r i n t R e c t a n g l e D e t a i l s ( myRect ) ;
}
} 3
Object-Oriented Programming (OO) - Example: OO Approach

• In an object-oriented approach, data and behavior are encapsulated within a class.


• The Rectangle class now holds both its dimensions and the methods to operate on
them.
public c l a s s Rectangle {
p r i v a t e double width ;
p r i v a t e double height ;

p u b l i c R e c t a n g l e ( double width , double h e i g h t ) {


t h i s . width = width ;
this . height = height ;
}
p u b l i c double c a l c u l a t e A r e a () { r e t u r n t h i s . width ∗ t h i s . height ; }
public void p r i n t D e t a i l s () {
System . o u t . p r i n t l n ( ” Width : ” + t h i s . w i d t h + ” , H e i g h t : ” + t h i s . h e i g h t ) ;
}
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {
R e c t a n g l e myRect = new R e c t a n g l e ( 5 , 1 0 ) ;
d o u b l e a r e a = myRect . c a l c u l a t e A r e a ( ) ;
System . o u t . p r i n t l n ( ” Area : ” + a r e a ) ;
myRect . p r i n t D e t a i l s ( ) ;
}
}

4
Object-Oriented Programming (OO) - Common Mistake

• A common mistake is creating classes with behavior but no class-level data.


• This often resembles a structured approach within an OO language.
• Consider a class that only performs an operation without holding any state related to
that operation.
public class StringFormatter {
p u b l i c s t a t i c S t r i n g toUpperCase ( S t r i n g t e x t ) {
r e t u r n t e x t . toUpperCase ( ) ;
}

public static String trim ( String text ) { return text . trim () ; }

p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {
S t r i n g message = ” h e l l o world ” ;
S t r i n g upperCaseMessage = S t r i n g F o r m a t t e r . toUpperCase ( message ) ;
S t r i n g trimmedMessage = S t r i n g F o r m a t t e r . t r i m ( m e s s a g e ) ;
System . o u t . p r i n t l n ( ” U p p e r c a s e : ” + u p p e r C a s e M e s s a g e ) ;
System . o u t . p r i n t l n ( ” Trimmed : ” + trimmedMessage ) ;
}
}

How would you guys fix that?


5
Identifying the Public Interfaces
Designing Class Interfaces

• The primary concern in class design is to minimize the public interface.


• The purpose of a class is to offer something useful and concise to the client.
• According to Gilbert and McCarty in ”Object-Oriented Design in Java,” the interface
of a well-designed object describes the services that the client wants accomplished.
• If a class does not provide a useful service to a user, it should not have been built.

Extending the Interface


Even if the public interface of a class is insufficient for a certain application, object
technology easily allows the capability to extend and adapt this interface by means of
inheritance. In short, if designed with inheritance in mind, a new class can inherit from an
existing class and create a new class with an extended interface.

6
Minimum Public Interface

• Providing the minimum public interface makes the class concise.


• The goal is to offer the user the exact interface needed to perform the task effectively.
• Incomplete public interfaces lead to users being unable to accomplish the full task.
• Improperly restricted public interfaces can result in unnecessary or dangerous access to
behavior, leading to debugging and potential system integrity and security issues.
• Class creation involves a business proposition.
• Users should be involved in the design process from the start to ensure utility and
proper interfaces.

7
Minimum Public Interface

• Let’s consider a more complex example: a ShoppingCart.


• We can define an interface for a ShoppingCart with various possible operations:
p u b l i c i n t e r f a c e ShoppingCart {
v o i d a d d I t e m ( S t r i n g item , d o u b l e p r i c e ) ;
v o i d removeItem ( S t r i n g item ) ;
i n t getItemCount () ;
double getTotalPrice () ;
boolean isEmpty ( ) ;
void clear () ;
j a v a . u t i l . L i s t <S t r i n g > g e t I t e m L i s t ( ) ;
j a v a . u t i l . Map<S t r i n g , I n t e g e r > g e t I t e m Q u a n t i t i e s ( ) ;
double getDiscountedPrice ( double discountPercentage ) ;
v o i d a p p l y C o u p o n ( S t r i n g couponCode ) ;
String getShippingAddress () ;
void setShippingAddress ( String address ) ;
void checkout () ;
}

Looking at this ShoppingCart interface, which methods would you consider


essential for the core functionality of adding, removing, and viewing items in a cart?
Which methods might be considered optional extensions or features that could 8
Minimum Public Interface - Example: Incomplete Interface
import java . io . IOException ;
import java . n e t . URL ;
import java . nio . f i l e . F i l e s ;
import java . n i o . f i l e . Paths ;

public c l a s s FileDownloader {
public void downloadFile ( String f i l e U r l , String destinationPath ) {
try {
URL u r l = new URL( f i l e U r l ) ;
F i l e s . copy ( u r l . openStream ( ) , Paths . ge t ( d e s t i n a t i o n P a t h ) ) ;
System . o u t . p r i n t l n ( ” F i l e d o w n l o a d e d s u c c e s s f u l l y t o : ” + d e s t i n a t i o n P a t h ) ;
} catch ( IOException e ) {
System . e r r . p r i n t l n ( ” E r r o r d o w n l o a d i n g f i l e : ” + e . g e t M e s s a g e ( ) ) ;
}
}

p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {
F i l e D o w n l o a d e r d o w n l o a d e r = new F i l e D o w n l o a d e r ( ) ;
d o w n l o a d e r . d o w n l o a d F i l e ( ” h t t p s : / /www . e x a m p l e . com/ s o m e f i l e . t x t ” , ” d o w n l o a d e d f i l e . t x t ” ) ;
}
}

This FileDownloader allows downloading, but what if the user wants to: Check the
download progress? Cancel the download?
9
Minimum Public Interface - Example: Improperly Restricted Interface
p u b l i c c l a s s BankAccount {
p u b l i c d o u b l e b a l a n c e ; // P u b l i c a c c e s s t o b a l a n c e − P r o b l e m a t i c !

p u b l i c BankAccount ( d o u b l e i n i t i a l B a l a n c e ) {
t h i s . balance = i n i t i a l B a l a n c e ;
}
p u b l i c v o i d d e p o s i t ( d o u b l e amount ) {
i f ( amount > 0 ) {
t h i s . b a l a n c e += amount ;
}
}
p u b l i c v o i d w i t h d r a w ( d o u b l e amount ) {
i f ( amount > 0 && amount <= t h i s . b a l a n c e ) {
t h i s . b a l a n c e −= amount ;
}
}
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {
BankAccount a c c o u n t = new BankAccount ( 1 0 0 0 ) ;
System . o u t . p r i n t l n ( ” I n i t i a l b a l a n c e : ” + a c c o u n t . b a l a n c e ) ; // Output : I n i t i a l balance : 1000.0
a c c o u n t . b a l a n c e = −500;
System . o u t . p r i n t l n ( ” B a l a n c e a f t e r d i r e c t m o d i f i c a t i o n : ” + a c c o u n t . b a l a n c e ) ;
account . d e p o s i t (200) ;
System . o u t . p r i n t l n ( ” B a l a n c e a f t e r d e p o s i t : ” + a c c o u n t . b a l a n c e ) ;
}
}

10
What is wrong here?
Illustration: Cabbie Example

• Consider the cabbie example once again.


• If other objects in the system need to get the name of a
cabbie, the Cabbie class must provide a public interface to
return its name, such as the getName() method.
• For instance, if a Supervisor object needs a name from a
Cabbie object, it must invoke the getName() method from
the Cabbie object. In essence, the supervisor is requesting The public interface specifies how
the cabbie for its name. the objects interact.
• Users of your code need to know nothing about its internal
workings. All they need to know is how to instantiate and
use the object. Provide them a way to get there but hide
the details.

11
Hiding the Implementation

• Hiding the implementation is essential.


• Identifying the public interface revolves around the users of
the class. The implementation should not involve the users.
• A class is most useful if the implementation can change
without affecting the users’ code.
• For example, the Cabbie class might contain behavior
related to how it eats breakfast. This behavior is part of the
implementation of the Cabbie object and should not be Objects don’t need to know some
available to other objects in the system. implementation details.
• According to Gilbert and McCarty, the prime directive of
encapsulation is that ”all fields shall be private,” ensuring
that none of the fields in a class is accessible from other
objects.

12
Designing Robust Constructors (and
Perhaps Destructors)
Constructors and Destructors

• A constructor should first and foremost put an object into an initial, safe state. This involves
attribute initialization and memory management.
• It’s important to ensure the object is constructed properly in the default condition. Providing a
constructor to handle this default situation is usually a good idea.
• In languages with destructors, it’s vital that destructors include proper clean-up functions. This
often involves releasing system memory that the object acquired.
• Java and .NET reclaim memory automatically via garbage collection. However, in languages
like C++, the developer must include code in the destructor to properly free memory.
• Ignoring this function can result in a memory leak.

Memory leaks
Memory leaks occur when objects fail to release the memory they acquired during their lifecycle. If
objects are created and destroyed repeatedly without releasing memory, the available system
memory slowly depletes. Eventually, the system may run out of memory, causing applications to
become unstable or even lock up the system. 13
Constructors - Example in Java
public class Car {
private S t r i n g model ;
private String color ;
private i n t speed ;

// No−a r gument c o n s t r u c t o r ( d e f a u l t s t a t e )
p u b l i c Car ( ) {
t h i s . model = ” G e n e r i c ” ;
t h i s . c o l o r = ” Unknown ” ;
t h i s . speed = 0;
}
// C o n s t r u c t o r w i t h model and c o l o r
p u b l i c Car ( S t r i n g model , S t r i n g c o l o r ) {
t h i s . model = model ;
this . color = color ;
t h i s . speed = 0;
}
// C o n s t r u c t o r w i t h model , c o l o r , and s p e e d
p u b l i c Car ( S t r i n g model , S t r i n g c o l o r , i n t s p e e d ) { o m i t t e d by b r e v i t y }

p u b l i c S t r i n g g e t M o d e l ( ) { r e t u r n model ; }

public String getColor () { return color ; }

p u b l i c i n t getSpeed () { r e t u r n speed ; }
}

14
Destructors and Garbage Collection in Java
• Unlike C++, Java does not have explicit destructors that you need to call manually to free memory.
• Java uses a mechanism called Garbage Collection to automatically manage memory.
• The Garbage Collector periodically identifies and reclaims memory occupied by objects that are no
longer referenced by the program.
• This simplifies memory management for the developer, reducing the risk of memory leaks.

No Explicit Destructors
You don’t need to write code to explicitly ”destroy” objects and release the memory they use in Java. The
garbage collector handles this automatically.

• Java has a finalize() method, which can be overridden in a class. This method is called by the
garbage collector before an object is reclaimed.
• However, relying on finalize() for critical cleanup tasks is generally discouraged due to its
unpredictable timing and potential performance impact.
• For resource management (like closing files or network connections), it’s better to use mechanisms like
try-with-resources or explicitly close the resources.
15
finalize() Example
Example Scenario: Imagine a Java object wraps a native system resource (e.g., a file descriptor obtained
through JNI).
p u b l i c c l a s s NativeResourceWrapper {
p r i v a t e l o n g n a t i v e H a n d l e ; // R e p r e s e n t s t h e n a t i v e r e s o u r c e
p u b l i c NativeResourceWrapper ( long handle ) {
t h i s . nativeHandle = handle ;
}
// Method t o e x p l i c i t l y r e l e a s e t h e n a t i v e r e s o u r c e ( p r e f e r r e d )
public void releaseResource () {
i f ( n a t i v e H a n d l e != 0 ) {
// Code t o c a l l n a t i v e f u n c t i o n t o r e l e a s e t h e r e s o u r c e
nativeHandle = 0;
}
}
@Override
p r o t e c t e d v o i d f i n a l i z e ( ) throws Throwable {
try {
i f ( n a t i v e H a n d l e != 0 ) {
releaseResource ()
}
} finally {
super . f i n a l i z e () ;
}
}
}
16
Designing Error Handling into a
Class
Designing Error Handling into a Class

• Designing how a class handles errors is of vital importance, similar to the design of
constructors.
• Every system will encounter unforeseen problems, so ignoring potential errors is not a
good idea.
• A developer of a good class anticipates potential errors and includes code to handle
these conditions when they are encountered.
• The application should never crash. When an error is encountered, the system should
either fix itself and continue or exit gracefully without losing any important user data.

17
Designing Error Handling into a Class - Example: Handling Division by Zero
• When designing a class, consider potential errors and how to handle them gracefully.
• Let’s look at a Divider class with a method to perform division.
public class Divider {
// O p t i o n 1 : R e t u r n i n g a s p e c i a l v a l u e ( l e s s common i n modern J a v a )
p u b l i c Do ub le d i v i d e W i t h S p e c i a l V a l u e ( d o u b l e n u m e r a t o r , d o u b l e d e n o m i n a t o r ) {
i f ( d e n o m i n a t o r == 0 ) {
System . e r r . p r i n t l n ( ” E r r o r : D i v i s i o n by z e r o e n c o u n t e r e d . ” ) ;
r e t u r n Do ub le . NaN ; // Or n u l l , o r some o t h e r s p e c i a l v a l u e
}
r e t u r n numerator / denominator ;
}

// O p t i o n 2 : Throwing an e x c e p t i o n ( p r e f e r r e d a p p r o a c h )
p u b l i c double d i v i d e W i t h E x c e p t i o n ( double numerator , double denominator ) {
i f ( d e n o m i n a t o r == 0 ) {
t h r o w new I l l e g a l A r g u m e n t E x c e p t i o n ( ” D e n o m i n a t o r c a n n o t be z e r o . ” ) ;
}
r e t u r n numerator / denominator ;
}
}

• Returning special values can be ambiguous.


• Throwing exceptions clearly signals an error and allows the calling code to handle it appropriately using
try-catch blocks.
18
Designing Error Handling into a Class - Example: Checked Exception
import java . io . F i l e ;
import java . io . FileReader ;
import java . io . IOException ;

public class FileReaderHelper {


p u b l i c S t r i n g r e a d F i l e ( S t r i n g f i l e P a t h ) throws IOException {
F i l e f i l e = new F i l e ( f i l e P a t h ) ;
S t r i n g B u i l d e r c o n t e n t = new S t r i n g B u i l d e r ( ) ;
t r y ( F i l e R e a d e r r e a d e r = new F i l e R e a d e r ( f i l e ) ) {
int character ;
w h i l e ( ( c h a r a c t e r = r e a d e r . r e a d ( ) ) != −1) {
c o n t e n t . append ( ( c h a r ) c h a r a c t e r ) ;
}
}
return content . toString () ;
}

p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {
F i l e R e a d e r H e l p e r h e l p e r = new F i l e R e a d e r H e l p e r ( ) ;
String filePath = ” nonexistent file . txt ” ;
try {
String fileContent = helper . readFile ( filePath ) ;
System . o u t . p r i n t l n ( ” F i l e c o n t e n t : ” + f i l e C o n t e n t ) ;
} catch ( IOException e ) {
System . e r r . p r i n t l n ( ” E r r o r r e a d i n g f i l e : ” + e . g e t M e s s a g e ( ) ) ;
}
}
} 19
Designing with Reuse in Mind
Documenting a Class and Using Comments

• Comments and documentation are essential aspects of good design.


• Unfortunately, they are often not taken seriously or ignored.
• While most developers know they should document their code, they often don’t want
to invest the time.
• However, good documentation practices are crucial for a good design.
• At the class level, developers might get away with inadequate documentation, but
when the class is passed to others for extension or maintenance, or becomes part of a
larger system, proper documentation becomes vital.
• Lack of documentation and comments can be detrimental in these scenarios.
Too Much Documentation
Overcommenting can be a problem, as excessive documentation or commenting can
become noise and defeat the purpose. Unfocused documentation is often ignored.

20
Building Objects with the Intent to Cooperate

• In Chapter 6, ”Designing with Objects,” we discuss many design issues involved in


building a system (might not be covered in class, please read it).
• Almost no class lives in isolation; typically, there’s no reason to build a class if it won’t
interact with others.
• Interactions between classes are a fundamental aspect of class life.
• Classes will either service other classes, request services from them, or both.
• In later chapters, various ways of class interactions are discussed.
• In the cabbie example, both the cabbie and the supervisor are not standalone entities;
they interact with each other at various levels.
• When designing a class, it’s crucial to consider how other objects will interact with it.

21
Designing for Reuse

• Objects can be reused in different systems, and code should be written with reuse in
mind.
• For instance, once a Cabbie class is developed and tested, it can be used wherever a
cabbie is needed.
• To ensure a class can be reused in various systems, it must be designed with reuse in
mind.
• Designing for reuse requires careful thought during the design process.
• It’s not a trivial task to anticipate all the possible scenarios in which an object must
operate.

22
Building Objects with the Intent to Cooperate - Example

p u b l i c c l a s s Cabbie {
p r i v a t e S t r i n g name ;
private Dispatcher dispatcher ;
p u b l i c C a b b i e ( S t r i n g name , D i s p a t c h e r d i s p a t c h e r ) { o m i t e d }
public void requestFare () {
System . o u t . p r i n t l n ( name + ” : R e q u e s t i n g a new f a r e . ” ) ;
String fare = dispatcher . assignFare ( this ) ;
System . o u t . p r i n t l n ( name + ” : A s s i g n e d f a r e − ” + f a r e ) ;
}
}

public class Dispatcher {


p u b l i c S t r i n g a s s i g n F a r e ( Cabbie cabbie ) {
// L o g i c t o f i n d and a s s i g n a f a r e t o t h e c a b b i e
// F o r s i m p l i c i t y , l e t ’ s j u s t r e t u r n a dummy f a r e
i f ( Math . random ( ) > 0 . 5 ) {
r e t u r n ” P i c k up p a s s e n g e r a t l o c a t i o n A , d r o p o f f a t l o c a t i o n B . ” ;
} else {
r e t u r n n u l l ; // No f a r e a v a i l a b l e
}
}
}

23
Designing for Reuse - Example: Reusing the Dispatcher

• A well-designed class like a Dispatcher can often be adapted and reused in different
parts of a system or even in different systems that require similar functionality.
public class DeliveryDriver {
p r i v a t e S t r i n g name ;
private Dispatcher dispatcher ;

p u b l i c D e l i v e r y D r i v e r ( S t r i n g name , D i s p a t c h e r d i s p a t c h e r ) {
t h i s . name = name ;
this . dispatcher = dispatcher ;
}

p u b l i c S t r i n g getName ( ) {
r e t u r n name ;
}

public void requestOrder () {


System . o u t . p r i n t l n ( name + ” : R e q u e s t i n g a new d e l i v e r y o r d e r . ” ) ;
String order = dispatcher . assignOrder ( this ) ;
i f ( o r d e r != n u l l ) {
System . o u t . p r i n t l n ( name + ” : A s s i g n e d o r d e r − ” + o r d e r ) ;
} else {
System . o u t . p r i n t l n ( name + ” : No o r d e r s a v a i l a b l e r i g h t now . ” ) ;
}
}
} 24
Designing with Extensibility in Mind
Extending Classes with Inheritance

• Adding new features to a class often involves extending an existing class, adding new
methods, and modifying existing behavior.
• Inheritance plays a crucial role here, allowing new classes to inherit attributes and
behaviors from existing ones.
• For example, if you have a Person class, you might later want to create an Employee
class or a Vendor class. In this case, having Employee inherit from Person can be a
good strategy, making Person extensible.
• Design classes to be extensible, so they can be easily subclassed without restrictions on
future functionalities.
• Abstraction guideline: classes should contain only data and behaviors specific to their
purpose, allowing other classes to subclass and inherit appropriate data and behaviors.
Attributes and Methods as Static
It’s crucial to determine which attributes and methods can be declared as static. Static
attributes and methods are shared by all objects of a class. 25
Extending Classes with Inheritance - Example: Vehicles
class Vehicle {
p r i v a t e S t r i n g model ;
p r i v a t e S t r i n g make ;

p u b l i c V e h i c l e ( S t r i n g model , S t r i n g make ) { o m i t e d }

p u b l i c S t r i n g g e t M o d e l ( ) { r e t u r n model ; }

p u b l i c S t r i n g getMake ( ) { r e t u r n make ; }

public void startEngine () {


System . o u t . p r i n t l n ( ” V e h i c l e e n g i n e s t a r t e d . ” ) ;
}
}

c l a s s Car e x t e n d s V e h i c l e {
p r i v a t e i n t numberOfDoors ;

p u b l i c Car ( S t r i n g model , S t r i n g make , i n t numberOfDoors ) {


s u p e r ( model , make ) ; // C a l l t h e c o n s t r u c t o r o f t h e s u p e r c l a s s
t h i s . numberOfDoors = numberOfDoors ;
}
p u b l i c i n t getNumberOfDoors ( ) { r e t u r n numberOfDoors ; }
@Override
public void startEngine () {
System . o u t . p r i n t l n ( ” Car e n g i n e s t a r t e d ( vroom vroom ) . ” ) ;
}
} 26
Abstracting Out Nonportable Code

• If your system requires nonportable code (i.e., code that


runs only on specific hardware platforms), it’s crucial to
abstract this code out of the class.
• Abstraction involves isolating the nonportable code in its
own class or method, which can be overridden if needed.
• For instance, when dealing with code that accesses
hardware-specific features like a serial port, create a
wrapper class to handle it. A serial port wrapper.

• Your primary class should then interact with this wrapper


class to access the necessary information or services, rather
than embedding system-dependent code directly.
• If the class moves to another hardware system, the way to
access the serial port changes, or you want to go to a
parallel port, the code in your primary class does not have
27
to change.
Abstracting Out Nonportable Code
// I n t e r f a c e f o r g e t t i n g OS i n f o r m a t i o n
i n t e r f a c e OSInfo {
S t r i n g getOSName ( ) ;
}

// I m p l e m e n t a t i o n f o r t h e c u r r e n t s y s t e m
c l a s s SystemOSInfo implements OSInfo {
@Override
p u b l i c S t r i n g getOSName ( ) {
r e t u r n System . g e t P r o p e r t y ( ” o s . name ” ) ;
}
}

// C l a s s t h a t u s e s t h e OS i n f o r m a t i o n
c l a s s ReportGenerator {
p r i v a t e OSInfo o s I n f o ;

p u b l i c ReportGenerator ( OSInfo o s I n f o ) {
this . osInfo = osInfo ;
}

public void generateReportHeader () {


System . o u t . p r i n t l n ( ”−−− R e p o r t G e n e r a t e d −−−” ) ;
System . o u t . p r i n t l n ( ” O p e r a t i n g System : ” + o s I n f o . getOSName ( ) ) ;
System . o u t . p r i n t l n ( ”−−−−−−−−−−−−−−−−−−−−−−−−” ) ;
}
}
28
Keeping the Scope as Small as Possible
• Keeping the scope small is essential for
abstraction and hiding the implementation. • In this case, ‘temp‘ is only needed within
• The goal is to localize attributes and the ‘swap()‘ method, so it should be
behaviors as much as possible, making moved within its scope:
maintenance, testing, and extension easier. p u b l i c c l a s s Math {
p u b l i c i n t swap ( i n t a , i n t b ) {
• For instance, if a method requires a i n t temp =0;
temp = a ;
temporary attribute, it’s best to keep it local. a=b ;
b=temp ;
• Consider the example where the attribute r e t u r n temp ;
‘temp‘ is unnecessarily in the class level: }
}
p u b l i c c l a s s Math {
i n t temp =0;
p u b l i c i n t swap ( i n t a , i n t b ) { • Keeping the scope as small as possible
temp = a ;
a=b ; simplifies code and improves its
b=temp ;
r e t u r n temp ; maintainability.
}
}
29
A Class Should Be Responsible for Itself

• In their book ”Java Primer Plus,” Tyma, Torok, and Downing propose the guideline
that all objects should be responsible for acting on themselves whenever possible.
• Consider an example of printing a circle. In a non-OO approach, you might have:
print ( circle ) ;

• Functions like print, draw, etc., would require a case statement or if/else structure to
determine how to handle each shape passed to them.
• For instance, separate print routines for each shape could be called:
printCircle ( circle ) ;
printSquare ( square ) ;

• However, following the object-oriented principle, each class should handle its own
operations whenever feasible, simplifying the code and improving maintainability.

30
A Class Should Be Responsible for Itself

• When adding a new shape, all functions • In an object-oriented example,


handling shapes need to be updated to polymorphism is used to group shapes
accommodate the new shape. like Circle into a Shape category.
• For instance, a switch statement might need • With polymorphism, Shape can figure
to be updated: out what type of shape it is and know
switch ( shape ) {
case 1: p r i n t C i r c l e ( c i r c l e ) ; break ;
how to print itself.
case 2: printSquare ( square ) ; break ; • For example:
case 3: p r i n t T r i a n g l e ( t r i a n g l e ) ; break ;
d e f a u l t : System . o u t . p r i n t l n ( ” I n v a l i d Shape . p r i n t ( ) ; // Shape i s actually a Circle
shape . ” ) ; break ; Shape . p r i n t ( ) ; // Shape i s a c t u a l l y a Square
}

• The key point is that the call is identical;


• This approach leads to code duplication and
increases the complexity of maintenance.
the context of the shape dictates how
the system reacts.

31
A Class Should Be Responsible for Itself

A non-OO example of a print scenario.


An OO example of a print scenario.

32
A Class Should Be Responsible for Itself - Example in Java
a b s t r a c t c l a s s Shape {
public abstract void print () ;
}
p u b l i c c l a s s Main {
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {
c l a s s C i r c l e e x t e n d s Shape {
Shape c i r c l e = new C i r c l e ( 5 ) ;
p r i v a t e double r ad i us ;
Shape s q u a r e = new S q u a r e ( 4 ) ;

p u b l i c C i r c l e ( double r a d i u s ) { omited }
c i r c l e . print () ; // The C i r c l e o b j e c t knows how
@Override
to p r i n t itself
public void print () {
square . p r i n t () ; // The S q u a r e o b j e c t knows how
System . o u t . p r i n t l n ( ” P r i n t i n g a c i r c l e w i t h
to p r i n t itself
radius : ” + radius ) ;
// Code t o a c t u a l l y draw a c i r c l e would go h e r e
// I m a g i n e a l i s t o f d i f f e r e n t s h a p e s
}
j a v a . u t i l . L i s t <Shape> s h a p e s = new
}
j a v a . u t i l . A r r a y L i s t <>() ;
s h a p e s . add ( new C i r c l e ( 3 ) ) ;
c l a s s S q u a r e e x t e n d s Shape {
s h a p e s . add ( new S q u a r e ( 2 ) ) ;
p r i v a t e double s i d e ;

f o r ( Shape s : s h a p e s ) {
p u b l i c Square ( double s i d e ) { t h i s . s i d e = s i d e ; }
s . p r i n t ( ) ; // Each s h a p e i n t h e l i s t knows
@Override
i t s own p r i n t i n g l o g i c
public void print () {
}
System . o u t . p r i n t l n ( ” P r i n t i n g a s q u a r e w i t h
}
side : ” + side ) ;
}
// Code t o a c t u a l l y draw a s q u a r e would go h e r e
}
} 33
A Class Should Be Responsible for Itself - Contrast with Non-OO
c l a s s CircleNonOO {
p u b l i c double r a di us ;
p u b l i c CircleNonOO ( d o u b l e r a d i u s ) { t h i s . r a d i u s =
radius ; }
}

c l a s s SquareNonOO {
p u b l i c double s i d e ;
p u b l i c SquareNonOO ( d o u b l e s i d e ) { t h i s . s i d e =
side ; } p u b l i c c l a s s MainNonOO {
} p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {
CircleNonOO c i r c l e = new CircleNonOO ( 5 ) ;
class ShapePrinter { SquareNonOO s q u a r e = new SquareNonOO ( 4 ) ;
p u b l i c void printShape ( Object shape ) { S h a p e P r i n t e r p r i n t e r = new S h a p e P r i n t e r ( ) ;
i f ( s h a p e i n s t a n c e o f CircleNonOO ) { p r i n t e r . printShape ( c i r c l e ) ;
CircleNonOO c i r c l e = ( CircleNonOO ) s h a p e ; p r i n t e r . printShape ( square ) ;
System . o u t . p r i n t l n ( ” P r i n t i n g a c i r c l e w i t h }
radius : ” + circle . radius ) ; }
} e l s e i f ( s h a p e i n s t a n c e o f SquareNonOO ) {
SquareNonOO s q u a r e = ( SquareNonOO ) s h a p e ;
System . o u t . p r i n t l n ( ” P r i n t i n g a s q u a r e w i t h
side : ” + square . side ) ;
} else {
System . o u t . p r i n t l n ( ” Unknown s h a p e t y p e . ” ) ;
}
}
} 34
Designing with Maintainability in
Mind
Designing for Maintainability

• Designing useful and concise classes promotes a high level of maintainability.


• Just as you design a class with extensibility in mind, you should also design with future
maintenance in mind.
• The process of designing classes forces you to organize your code into manageable
pieces.
• Separate pieces of code are typically more maintainable than larger ones.
• A key strategy to promote maintainability is to reduce interdependent code. Changes
in one class should ideally have minimal or no impact on other classes.
Highly Coupled Classes
Classes that are highly dependent on one another are considered highly coupled. When a
change in one class necessitates a change in another, these classes are deemed highly
coupled. Conversely, classes with no such dependencies exhibit a low degree of coupling.
For further insights on this topic, refer to ”The Object Primer” by Scott Ambler.
35
Designing for Maintainability - Whats wrong here?
c l a s s Student {
p u b l i c S t r i n g name ; // P u b l i c a t t r i b u t e − Bad p r a c t i c e , b u t u s e d h e r e f o r c o u p l i n g e x a m p l e
p u b l i c i n t grade ; // P u b l i c a t t r i b u t e − Bad p r a c t i c e , b u t u s e d h e r e f o r c o u p l i n g e x a m p l e

p u b l i c S t u d e n t ( S t r i n g name , i n t g r a d e ) {
t h i s . name = name ;
t h i s . grade = grade ;
}
}

c l a s s GradeReport {
p u b l i c void generateReport ( Student student ) {
System . o u t . p r i n t l n ( ”−−− Grade R e p o r t −−−” ) ;
System . o u t . p r i n t l n ( ” S t u d e n t Name : ” + s t u d e n t . name ) ;
System . o u t . p r i n t l n ( ” Grade : ” + s t u d e n t . g r a d e ) ;
System . o u t . p r i n t l n ( ”−−−−−−−−−−−−−−−−−−−−” ) ;
}
}

p u b l i c c l a s s Main {
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) {
S t u d e n t a l i c e = new S t u d e n t ( ” A l i c e ” , 8 5 ) ;
G r a d e R e p o r t r e p o r t = new G r a d e R e p o r t ( ) ;
report . generateReport ( a l i c e ) ;
}
}

36
Maintaining Low Coupling

• Properly designed classes should require changes only to the implementation, avoiding
modifications to the public interface whenever possible.
• Changes to the public interface can lead to ripple effects throughout all systems using
the interface.
• For instance, altering the getName() method of the Cabbie class would necessitate
changes in every system utilizing this interface, which can be a daunting task.
• To ensure a high level of maintainability, strive to keep the coupling level of your
classes as low as possible.

37
Using Iteration

• In both design and programming functions, employing an iterative process is highly


recommended.
• This approach aligns well with the concept of providing minimal interfaces.
• A robust testing plan helps identify areas where interfaces may be insufficient, allowing
the process to iterate until the class has the appropriate interfaces.
• Testing isn’t limited to coding; conducting design walkthroughs and other review
techniques is also valuable.
• Testers benefit from iterative processes as they are involved early in the process, rather
than receiving a system hastily at the end of development.

38
Testing the Interface

• Minimal implementations of interfaces are often referred to as stubs.


• Stubs allow you to test interfaces without writing actual code.
• Instead of connecting to a real database, stubs can be used to verify that interfaces are
functioning correctly from the user’s perspective.
• At this stage, the implementation is not necessary as the design of the interface may
still be evolving, and completing the implementation prematurely could waste time and
energy.
• When a user class interacts with the DataBaseReader class, the information returned is
provided by code stubs instead of the actual database, which may not yet exist.
• Once the interface is complete and implementation begins, the database can be
connected, and the stubs can be replaced.

39
Code Example: Simulated Database

p u b l i c c l a s s DataBaseReader { p u b l i c i n t howManyRecords ( ) {
p r i v a t e S t r i n g db [ ] = { ” R e c o r d 1 ” , i n t numOfRecords = 5 ;
” Record2 ” , r e t u r n numOfRecords ;
” Record3 ” , }
” Record4 ” ,
” Record5 ” }; p u b l i c S t r i n g getRecord ( i n t key ){
p r i v a t e b o o l e a n DBOpen = f a l s e ; /∗ DB S p e c i f i c I m p l e m e n t a t i o n ∗/
p r i v a t e i n t pos ; r e t u r n db [ k e y ] ;
}
p u b l i c v o i d open ( S t r i n g Name ) {
DBOpen = t r u e ; p u b l i c S t r i n g getNextRecord (){
} /∗ DB S p e c i f i c I m p l e m e n t a t i o n ∗/
r e t u r n db [ p o s ++];
public void close (){ }
DBOpen = f a l s e ; }
}

public void goToFirst (){


pos = 0 ;
}

p u b l i c v o i d goToL ast ( ) {
pos = 4 ;
}

40
Simulating Database Calls

• The methods in the code example simulate various database operations.


• The strings within the array represent the records that would be retrieved from a real
database.
• For instance, methods like ‘getRecord()‘ and ‘getNextRecord()‘ return the records
stored in the array, mimicking database queries.
• Once the actual database is integrated into the system, the array will be replaced with
real database calls.
• As you find problems with the interface design, make changes and repeat the process
until you are satisfied with the result.
Keeping the Stubs Around
When you’re finished with stubs, avoid deleting them. Instead, keep them in the code for
potential future use, ensuring that users cannot access them. Well-designed programs
often integrate test stubs into the design and retain them in the program for later use. In
essence, design testing directly into the class! 41
Using Object Persistence
Object Persistence

• Object persistence is a crucial concern in many object-oriented (OO) systems.


• Persistence involves maintaining the state of an object over time.
• When a program is run, if the object’s state is not saved in some manner, the object
ceases to exist and cannot be recovered.
• While transient objects may suffice for some applications, in most business systems, it’s
essential to save the state of objects for later use.

Object Persistence
Object persistence, along with the topics in the next section, may not be traditional design
guidelines, but they are crucial considerations when designing classes. Introducing them
early emphasizes their importance and underscores the need to address them during the
class design phase.

42
Object Persistence Mechanisms

• In its simplest form, object persistence involves serializing an object and writing it to a
flat file.
• Modern technology favors XML-based persistence methods.
• While an object can theoretically persist in memory as long as it’s not destroyed, we’ll
focus on storing persistent objects on storage devices.
• There are three primary storage devices to consider:
• Flat file system: Objects can be stored in a flat file by serialization, although this has
limited use.
• Relational database: Middleware is typically required to convert objects to a relational
model for storage.
• Object-oriented (OO) database: This is the ideal method for persisting objects, but many
companies still rely on legacy systems, necessitating interface with legacy data.

43
Serializing and Marshaling Objects

• Using objects in environments originally designed for structured programming poses


challenges.
• Middleware examples, like writing objects to relational databases, highlight this issue.
• Problems also arise when attempting to write an object to a flat file or send it over a
network.
• To send an object over a wire (e.g., to a file or over a network), the system must
deconstruct the object (flatten it out), transmit it, and then reconstruct it on the
receiving end. This process is known as serializing an object.
• The act of sending the serialized object across a wire is called marshaling.
• A serialized object can theoretically be written to a flat file and later retrieved in the
same state in which it was written.
• A key concern is ensuring that the serialization and deserialization processes use the
same specifications, much like an encryption algorithm. Java provides the
Serializable interface to facilitate this translation. 44
Conclusion
Conclusion

• This chapter presents numerous guidelines for designing classes, although it’s not an
exhaustive list.
• Additional guidelines will likely be encountered as you delve deeper into object-oriented
(OO) design.
• While this chapter focuses on design issues concerning individual classes, it’s important
to recognize that classes do not exist in isolation.
• Classes must be designed to interact with other classes, forming systems that
ultimately deliver value to end users.
• Chapter 6, ”Designing with Objects,” delves into the topic of designing complete
systems, providing further insights into OO design.

45
MC322 - Object Oriented Programming
Lesson 5
Class Design Guidelines

Prof. Marcos M. Raimundo


Instituto de Computação - UNICAMP

You might also like