Aula 5 - Class Design Guidelines
Aula 5 - Class Design Guidelines
Lesson 5
Class Design Guidelines
• 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)
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
4
Object-Oriented Programming (OO) - Common Mistake
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 ) ;
}
}
6
Minimum Public Interface
7
Minimum Public Interface
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
11
Hiding the Implementation
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 ; }
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 ;
}
}
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
20
Building Objects with the Intent to Cooperate
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 ) ;
}
}
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 ;
}
• 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 ; }
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 ;
// 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 ;
}
• 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
31
A Class Should Be Responsible for Itself
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
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
38
Testing the Interface
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 ; }
}
p u b l i c v o i d goToL ast ( ) {
pos = 4 ;
}
40
Simulating Database Calls
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
• 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