Object Oriented Programming
- Software design approaches and patterns, to identify reusable solutions to commonly occurring problems
- Apply an appropriate software development approach according to the relevant paradigm (for example object-oriented, event-driven or procedural)
Object Oriented Programming (OOP) is a coding style focused around objects which encapsulate all the data (information) and code (behaviour) in an application. The majority of computer programs written today use OOP as part of their programming style, normally the main part and almost all mainstream languages support object orientation.
This article considers OOP specifically in the context of Java, but the principles apply to any language.
Classes
You are hopefully already familiar with the concept of a class
. This is the unit in which you write Java code, normally putting one class in a file. In OOP, you can consider a class to be a blueprint to create objects. The class has two key types of content:
- Data – information that’s stored in the class
- Behaviour – methods that you can call on the class
We’ll use as an example a Zoo Management System application – a simple app that helps zookeepers keep tabs on their animals. Here are some classes you might find in the Zoo app:
Lion
– represents a lion. You might want to store data about the lion’s age, and you might want to implement some program behaviour to deal with the time when the lion is fed.Keeper
– represents a zookeeper. A zookeeper probably has a list of animals she’s responsible for.FeedingScheduler
– a class that is responsible for managing the animals’ feeding schedule. This one is less recognisable as a “thing” in the real world, but it’s still very much a type of thing in your program.Config
– stores configuration information about your program. This is a class storing a useful conceptual object that we will explore later.
Fork a copy of the Zoo Management System repo and follow the instructions laid out in the README.
Open up the project directory in your IDE and take a look at the classes described above – for each one, make sure you can follow its responsibilities, both in terms of data and behaviour.
Instances
An instance of a class is an object that follows the blueprint of the class it belongs to. So, the class Lion
represents lions generally; an instance of that class represents a single specific lion. A class can have many objects that belong to it. You create a new instance of a class using the new
keyword:
Lion myLion = new Lion();
As mentioned, these instances are referred to as “objects” – hence the term Object Oriented Programming.
Once you have an object, then you call methods on the object itself:
if (myLion.isHungry()) {
myLion.feed();
}
Static vs nonstatic
By default, data (stored in fields) and behaviour (stored in methods) are associated with the instance. The Rabbit
class defines lastGroomed
, and each individual Rabbit
instance has a value for this field.
You can alternatively associate data and behaviour with the class itself. To do this, use the static
keyword. Now this field or method is called on the class itself, not an instance of the class.
Take a look at the FeedingScheduler
class. We’ve added a static field and a static method to the class – to access the instance
property, you call FeedingScheduler.getInstance()
. In contrast, to call the nonstatic method assignFeedingJobs()
you’d invoke it on a specific object, as in myScheduler.assignFeedingJobs(etc)
.
When should you use a static field or method? On the whole, not very often. OOP works best if you create instances of classes because then you unlock the power of inheritance, polymorphism, etc. which we’ll come onto below. It also makes testing your code easier. But occasionally static is useful – here are some of the main examples:
- Where you have unchanging data which can be shared by all instances of a class. Perhaps the zoo has a policy on lion feeding times that applies to all lions; this could go as a static field on the
Lion
class because it’s shared. - The Singleton pattern. This is where there is strictly only one instance of a particular class allowed; the
FeedingScheduler
is an example of how this pattern is used. The single instance of the class is stored in a private static field, and anyone who wants “the”FeedingScheduler
can get it via a static property. (You might well ask, why not just make all the fields and methods on theFeedingScheduler
static, and not bother with instances at all? That’s because of the above point about how the power of OOP will eventually benefit you most if you use instances). - The
main
method. This must be static by definition. Again it’s best practice to keep this short, though, and start using instances as soon as possible.
As a rule, aim to make everything nonstatic, and make a conscious decision to make things static where appropriate – not the other way around.
Inheritance
Inheritance is where a lot of the power of OOP comes from. If a child class “inherits” from a parent class, the child class gets all the data and behaviour definitions from its parent, and can then add additional behaviour on top.
Look at the models
folder: There’s a hierarchy of classes. Lion
inherits from AbstractAnimal
, and hence inherits the dateOfBirth
property, age
function, and feeding behaviour. This can be most naturally phrased as “A Lion is an Animal”. Rabbit
is implemented similarly, but unlike Lion
contains some rabbit-specific behaviour – rabbits can be groomed.
Inheritance also allows behaviour to be modified, not just added – take a look at the Rabbit’s feed
method. If someone calls the feed method on a rabbit, this method is invoked instead of the one on Animal
. Key things to note:
- The
Rabbit
has afeed
method which is marked@Override
. That says this is replacing the base class (parent)’s implementation. - The
Rabbit
’sfeed
method callssuper.feed()
. That’s entirely optional, but invokes the behaviour in the base class – i.e. the rabbit makes a munching noise, and then does the normal stuff that happens when you feed any animal.
Note that the rabbit’s implementation of feed
will be invoked regardless of the data type of the variable you’re calling it on. So consider the following example:
Rabbit rabbit = new Rabbit();
Animal animal = rabbit;
rabbit.feed();
animal.feed();
animal = new Lion();
animal.feed();
Both the first two feedings will call the rabbit version because the object (instance) in question is a rabbit – even though animal.feed()
doesn’t appear to know that. However, the final feeding will call the general animal version of the method, becuase Lion
doesn’t override the feed
method.
Interfaces
An interface is a promise by the class to implement certain methods. The interface doesn’t contain any actual behaviour (each class that implements the interface defines its own behaviour), it just contains method names and their parameters.
Take a look at the CanBeGroomed
interface definition – it specifies a single groom
method. Note that we don’t say public, even though the method will always be public, because all interface methods are public by default.
You cannot create an instance of CanBeGroomed
– it’s not a class. Notice that:
Zebra
andRabbit
implement this interfaceKeeper.groom
accepts anything of typeCanBeGroomed
If it wasn’t for the CanBeGroomed
interface, it would be impossible to define a safe Keeper.groom
method.
- If
Keeper.groom
took aRabbit
, you couldn’t pass in aZebra
, because a Zebra is not a Rabbit. - If
Keeper.groom
took anAnimal
(likefeed
does), you could pass in anything but then you can’t callgroom
on that animal afterward. This applies even if the animal is, in fact, a zebra – the Java compiler cannot know that you’ve passed in a zebra on this particular occasion because the variable is of typeAnimal
and hence might be any animal.
Note that there’s no direct link between animals and things-that-can-be-groomed. The keeper can now groom anything implementing that interface – it doesn’t have to be an Animal
! This is perfectly reasonable – if all the keeper does is call the groom
method, it really doesn’t care whether it’s an animal or not. The only thing that matters is that the groom
method does actually exist, and that’s precisely what the interface is for.
Polymorphism
Polymorphism means the ability to provide instances of different classes to a single method. Take a look at the Keeper
class. The method feed
takes a single parameter with type Animal
and the method groom
takes a single parameter with type CanBeGroomed
. Thanks to polymorphism, we can provide any class extending Animal
to feed
and any class implementing CanBeGroomed
to groom
.
In particular, we could pass any of the following to feed
:
- A
Rabbit
, because it inherits fromAnimal
- A
Lion
, because it inherits fromAnimal
- An animal we’ve never heard of, but that someone else has defined and marked as inheriting from
Animal
(this might be relevant if you were writing an application library, where you don’t know what the user will do but want to give them flexibility when writing their own programs) - An
Animal
itself (i.e. an instance of the base class). Except actually right now you can’t create one of those because that class is markedabstract
, which means you’re not allowed to create instances of it and it’s only there for use as a base class to inherit other classes from
Polymorphism is pretty powerful. As an experiment, try adding a GuineaFowl
to the zoo, you can decide for yourself if a GuineaFowl
can be groomed.
Choosing between interfaces and inheritance
It’s worth pointing out that our use of the interface CanBeGroomed
here leads to a slightly unfortunate side-effect, which is that the Rabbit
and Zebra
classes have a lot of duplicated code. They don’t just share a groom
method; they have completely identical implementations of that method. One way of implementing this behaviour to avoid this:
- Create a new subclass of
Animal
calledAnimalThatCanBeGroomed
- Change
Zebra
andRabbit
to inherit fromAnimalThatCanBeGroomed
, not fromAnimal
- Get rid of the interface, and replace all other references to it with
AnimalThatCanBeGroomed
Since their common code lives in AnimalThatCanBeGroomed
, this would make the individual animals (Rabbit
and Zebra
) much shorter. However, there’s a potential disadvantage lurking too: You can only inherit from a single parent. Suppose we now add in some scheduling for keepers sweeping muck out of the larger enclosures. That applies to Lion
and Zebra
, the larger animals, but not Rabbit
(no doubt we’ll get a separate hutch-cleaning regime in place soon enough). Now what do we do? We can repeat the above steps to create a CanHaveMuckSweptOut
interface or an AnimalThatCanHaveMuckSweptOut
class. But if we do the latter, we can’t also have AnimalThatCanBeGroomed
because Zebra
can’t inherit from both at once. At this stage, it starts looking like we’re better off with the interface approach. We’ll have to find another way to minimise code duplication. What options can you come up with?
As another exercise, take a look at the main
method. This currently creates two schedulers, one for feeding and one for grooming, and then calls them both with very similar calls. The methods (assignFeedingJobs
and assignGroomingJobs
) currently have different names, but we could rename them both to just assignJobs
and then try to implement some common code that takes a list of schedulers and calls assignJobs
on each in turn. Should we do this by creating a Scheduler
interface, or by creating a common Scheduler
base class? Think about the pros and cons.
Composition
Composition is an OOP pattern that is an alternative to inheritance as a way of sharing common characteristics and behaviour. We’ll look at how it could be used as a different approach to the issue of grooming and sweeping out muck.
- Inheritance expresses an “is a” relationship, as in “a
Zebra
is anAnimalThatCanBeGroomed
, which is anAnimal
” - Composition expresses a “has a” relationship, as in “an
Animal
has aGroomability
characteristic”
In the latter case, Groomability
would be an interface that has at least two implementations, GroomableFeature
and UngroomableFeature
:
public interface Groomability {
boolean isGroomable();
void groom();
}
public class GroomableFeature implements Groomability {
@Override
public boolean isGroomable() {
return true;
}
@Override
public void groom() {
lastGroomed = LocalDateTime.now();
}
}
public class UngroomableFeature implements Groomability {
@Override
public boolean isGroomable() {
return false;
}
@Override
public void groom() {
throw new Exception("I told you I can't be groomed");
}
}
So then the classes might be composed in the following way.
public abstract class Animal {
private Groomability groomability;
private Muckiness muckiness;
protected Animal(Groomability groomability, Muckiness muckiness) {
this.groomability = groomability;
this.muckiness = muckiness;
}
}
public class Zebra extends Animal {
public Zebra() {
super(new GroomableFeature(), new NeedsMuckSweeping());
}
}
public class Rabbit extends Animal {
public Rabbit() {
super(new GroomableFeature(), new NonMucky());
}
}
public class Lion extends Animal {
public Lion() {
super(new UngroomableFeature(), new NeedsMuckSweeping());
}
}
As you can see, composition enables much more code reuse than inheritance when the commonality between classes doesn’t fit a simple single-ancestor inheritance tree. Indeed, when you are designing an OOP solution, there is a general principle of preferring composition over inheritance – but both are valuable tools.
Default methods (Advanced)
In modern versions of Java, interfaces may include implementations of methods marked with default
. These are mainly useful when you need to extend an interface while retaining backward compatibility. For example, the Iterable
interface has a default implementation of forEach
which accepts a functional interface (covered later in the course) – this is usable on all existing implementations without changing any code.
However, if you find yourself with lots of large default methods, an abstract class might be more suitable!
Object oriented design patterns
Design patterns are common ways of solving problems in software development. Each pattern is a structure that can be followed in your own code, that you implement for your specific situation. Following established design patterns is advantageous because:
- in general they have evolved as best practice, and so avoid the need for you to solve every problem anew – although “best practice” changes over time so some established patterns fall out of favour; and
- other developers will probably be familiar with the patterns so it will be easier for them to understand and maintain your code.
Although design patterns aren’t specifically tied to object oriented programming, most of the common design patterns that have arisen fit the OOP paradigm. The following are some common OOP design patterns.
-
Singleton: This pattern ensures that there is only ever one instance of a class. This could be important if your application needs a single shared configuration object or an object manages access to an external resource. This would be implemented by:
- Having a static property in the class that can hold the singleton
- Making the class constructor private so that new objects of that class cannot be created
- Having a static public method that is used to access the singleton; if the shared object doesn’t exist yet then it is created
-
Factory: This is a pattern for creating objects that share an interface, without the caller needing to know about the various classes that implement that interface. For example, a factory pattern for creating objects that have the interface
Product
could look like:- Two classes that implement
Product
arePen
andBook
- The interface
ProductFactory
has the methodcreateProduct
, which creates and returnsProduct
objects - The class
PenFactory
implementsProductFactory
, and itscreateProduct
method creates and returns a newPen
- The class
BookFactory
implementsProductFactory
, and itscreateProduct
method creates and returns a newBook
- If code elsewhere in the application is given a
ProductFactory
for creating products, it doesn’t need to know what the products are or be told when new products are created
- Two classes that implement
-
Model-View-Controller: You’ll be familiar with the MVC pattern from the Bootcamp Bookish exercise; it’s a very common way to structure user interfaces.
- Model objects contains the data that is to be displayed (e.g., an entity that has been fetched from a database)
- The View is a representation of the Model to the user (such as a table or graphical representation)
- The Controller processes commands from the user and tells the Model objects to change appropriately
-
Adapter: This pattern would be suitable for the Bootcamp SupportBank exercise, in which you had to process data from files in different formats (CSV, JSON and XML). An adapter is a class that allows incompatible interfaces to interact. In the case of SupportBank, you might decide that:
TransactionReader
is an interface that has the methodloadTransactions()
, which returns an array ofTransaction
objects- There are three implementations of
TransactionReader
, each of which knows how to parse its particular file type, convert values where necessary and produceTransaction
objects - Note that you might use a Factory pattern to create the
TransactionReader
s, so it’s easy to add support for new file types in future
Strengths of OOP
The following are strengths of the object oriented programmming paradigm.
Abstraction refers to the fact that object oriented code hides information that you don’t need to know, so if you’re using an object all you know about it is its publicly visible characteristics. Your code doesn’t know about how the object’s data and methods are implemented. This is especially clear when dealing with interfaces – code that interacts with an interface knows only the contract that the interface publishes, and that interface might be implemented by lots of different classes that have completely different structures and behaviour.
Encapsulation refers to the fact that the data and behaviour of an object are tied together into a single entity. If you think about programming without encapsulation, if you have a simple data structure with a set of fields in it and then want to run a function on it, you need to make sure that you find the function that correctly handles that specific data structure. With encapsulation, you just call the object’s own method.
Polymorphism refers to the fact that an object can be interacted with as if it were an instance of an ancestor class, while its behaviour will come from its actual class. When a new descendent class is defined with its own implementation of ancestor class methods, code that interacts with the base class will automatically call the new implementations without having to be updated.