Detailed Table of Contents
Guidance for the item(s) below:
Let's learn the important OOP concept of inheritance this week, together with how to show it in UML class diagrams.
Can explain the meaning of inheritance
The OOP concept Inheritance allows you to define a new class based on an existing class.
For example, you can use inheritance to define an EvaluationReport
class based on an existing Report
class so that the EvaluationReport
class does not have to duplicate data/behaviors that are already implemented in the Report
class. The EvaluationReport
can inherit the wordCount
attribute and the print()
method from the base class Report
.
A superclass is said to be more general than the subclass. Conversely, a subclass is said to be more specialized than the superclass.
Applying inheritance on a group of similar classes can result in the common parts among classes being extracted into more general classes.
Man
and Woman
behave the same way for certain things. However, the two classes cannot be simply replaced with a more general class Person
because of the need to distinguish between Man
and Woman
for certain other things. A solution is to add the Person
class as a superclass (to contain the code common to men and women) and let Man
and Woman
inherit from Person
class.
Inheritance implies the derived class can be considered as a sub-type of the base class (and the base class is a super-type of the derived class), resulting in an is a relationship.
Inheritance does not necessarily mean a sub-type relationship exists. However, the two often go hand-in-hand. For simplicity, at this point let us assume inheritance implies a sub-type relationship.
To continue the previous example,
Woman
is a Person
Man
is a Person
Inheritance relationships through a chain of classes can result in inheritance hierarchies (aka inheritance trees).
Two inheritance hierarchies/trees are given below. Note that the triangle points to the parent class. Observe how the Parrot
is a Bird
as well as it is an Animal
.
Multiple Inheritance is when a class inherits directly from multiple classes. Multiple inheritance among classes is allowed in some languages (e.g., Python, C++) but not in other languages (e.g., Java, C#).
The Honey
class inherits from the Food
class and the Medicine
class because honey can be consumed as a food as well as a medicine (in some oriental medicine practices). Similarly, a Car
is a Vehicle
, an Asset
and a Liability
.
Exercises
Can interpret class inheritance in class diagrams
You can use a triangle and a solid line (not to be confused with an arrow) to indicate class inheritance.
Notation:
Examples: The Car
class inherits from the Vehicle
class. The Cat
and Dog
classes inherit from the Pet
class.
Can explain method overloading
Method overloading is when there are multiple methods with the same name but different type signatures. Overloading is used to indicate that multiple operations do similar things but take different parameters.
Type signature: The type signature of an operation is the type sequence of the parameters. The return type and parameter names are not part of the type signature. However, the parameter order is significant.
Example:
Method | Type Signature |
---|---|
int add(int X, int Y) | (int, int) |
void add(int A, int B) | (int, int) |
void m(int X, double Y) | (int, double) |
void m(double X, int Y) | (double, int) |
In the case below, the calculate
method is overloaded because the two methods have the same name but different type signatures (String)
and (int)
.
calculate(String): void
calculate(int): void
OOP → Inheritance → What
Can explain method overriding
Method overriding is when a sub-class changes the behavior inherited from the parent class by re-implementing the method. Overridden methods have the same name, same type signature, and same return type.
Consider the following case of EvaluationReport
class inheriting the Report
class:
Report methods | EvaluationReport methods | Overrides? |
---|---|---|
print() | print() | Yes |
write(String) | write(String) | Yes |
read():String | read(int):String | No. Reason: the two methods have different signatures; This is a case of overloading (rather than overriding). |
Exercises
Guidance for the item(s) below:
Next, we cover some additional class/object diagram notations used to show different types of relationships in class/object diagrams.
Can explain the meaning of composition
A composition is an association that represents a strong whole-part relationship. When the whole is destroyed, parts are destroyed too i.e., the part should not exist without being attached to a whole.
A Board
(used for playing board games) consists of Square
objects.
Composition also implies that there cannot be cyclical links.
The ‘sub-folder’ association between Folder
objects is a composition type association. That means if the Folder
object foo
is a sub-folder of Folder
object bar
, bar
cannot be a sub-folder of foo
.
Whether a relationship is a composition can depend on the context.
Is the relationship between Email
and EmailSubject
composition? That is, is the email subject part of an email to the extent that an email subject cannot exist without an email?
A common use of composition is when parts of a big class are carved out as smaller classes for the ease of managing the internal design. In such cases, the classes extracted out still act as parts of the bigger class and the outside world has no business knowing about them.
Cascading deletion alone is not sufficient for composition. Suppose there is a design in which Person
objects are attached to Task
objects and the former get deleted whenever the latter is deleted. This fact alone does not mean there is a composition relationship between the two classes. For it to be composition, a Person
must be an integral part of a Task
in the context of that association, at the concept level (not simply at implementation level).
Identifying and keeping track of composition relationships in the design has benefits such as helping to maintain the data integrity of the system. For example, when you know that a certain relationship is a composition, you can take extra care in your implementation to ensure that when the whole object is deleted, all its parts are deleted too.
Composition is implemented using a normal variable. If correctly implemented, the ‘part’ object will be deleted when the ‘whole’ object is deleted. Ideally, the ‘part’ object may not even be visible to clients of the ‘whole’ object.
class Email {
private Subject subject;
...
}
class Email:
def __init__(self):
self.__subject = Subject()
In this code, the Email
has a composition type relationship with the Subject
class, in the sense that the subject is part of the email.
Can explain the meaning of aggregations
Aggregation represents a container-contained relationship. It is a weaker relationship than composition.
SportsClub
can act as a container for Person
objects who are members of the club. Person
objects can survive without a SportsClub
object.
Implementation is similar to that of composition except the containee object can exist even after the container object is deleted.
In the code below, there is an aggregation association between the Team
class and the Person
class in that a Team
contains a Person
object who is the leader of the team.
class Team {
Person leader;
...
void setLeader(Person p) {
leader = p;
}
}
class Team:
def __init__(self):
self.__leader = None
def set_leader(self, person):
self.__leader = person
Can interpret aggregation in class diagrams
UML uses a hollow diamond to indicate an aggregation.
Notation:
Example:
Aggregation vs Composition
The distinction between composition (◆) and aggregation (◇) is rather blurred. Martin Fowler’s famous book UML Distilled advocates omitting the aggregation symbol altogether because using it adds more confusion than clarity.
Exercises
Can explain dependencies among classes
In the context of OOP associations, a dependency is a need for one class to depend on another without having a direct association in the same direction. Reason for the exclusion: If there is an association from class Foo
to class Bar
(i.e., navigable from Foo
to Bar
), that means Foo
is obviously dependent on Bar
and hence there is no point in mentioning dependency specifically. In other words, we are only concerned about non-obvious dependencies here. One cause of such dependencies is interactions between objects that do not have a long-term link between them.
A Course
class can have a dependency on a Registrar
class because the Course
class needs to refer to the Registrar
class to obtain the the maximum number of students it can support (e.g., Registrar.MAX_COURSE_CAPACITY
).
In the code below, Foo
has a dependency on Bar
but it is not an association because it is only a interaction and there is no long term relationship between a Foo
object and a Bar
object. i.e. the Foo
object does not keep the Bar
object it receives as a parameter.
class Foo {
int calculate(Bar bar) {
return bar.getValue();
}
}
class Bar {
int value;
int getValue() {
return value;
}
}
class Foo:
def calculate(self, bar):
return bar.value;
class Bar:
def __init__(self, value):
self.value = value
Can use dependencies in a class diagram
UML uses a dashed arrow to show dependencies.
Two examples of dependencies:
Dependencies vs associations:
Foo
accessing a constant in Bar
but there is no association/inheritance from Foo
to Bar
. Follow up notes for the item(s) above:
After learning the UML syntax in the section above, you can try the worked example given below. It's even better if you do the Week 10 - Quiz (Part I) (on Canvas) before watching the video below:
After watching the above example, you can try this exercise. The answer will be given during the following lecture.
Exercise: Draw class/object diagrams for HouseManager code
Guidance for the item(s) below:
Let's also have a brief look at various types of testing that is done in software projects, as you will be learning how to do one of them using Python in this week too.
Can explain unit testing
Unit testing: testing individual units (methods, classes, subsystems, ...) to ensure each piece works correctly.
In OOP code, it is common to write one or more unit tests for each public method of a class.
Here are the code skeletons for a Foo
class containing two methods and a FooTest
class that contains unit tests for those two methods.
class Foo {
String read() {
// ...
}
void write(String input) {
// ...
}
}
class FooTest {
@Test
void read() {
// a unit test for Foo#read() method
}
@Test
void write_emptyInput_exceptionThrown() {
// a unit tests for Foo#write(String) method
}
@Test
void write_normalInput_writtenCorrectly() {
// another unit tests for Foo#write(String) method
}
}
import unittest
class Foo:
def read(self):
# ...
def write(self, input):
# ...
class FooTest(unittest.TestCase):
def test_read(self):
# a unit test for read() method
def test_write_emptyIntput_ignored(self):
# a unit test for write(string) method
def test_write_normalInput_writtenCorrectly(self):
# another unit test for write(string) method
Resources
Quality Assurance → Testing → Unit Testing → What →
Can explain integration testing
Integration testing : testing whether different parts of the software work together (i.e. integrates) as expected. Integration tests aim to discover bugs in the 'glue code' related to how components interact with each other. These bugs are often the result of misunderstanding what the parts are supposed to do vs what the parts are actually doing.
Suppose a class Car
uses classes Engine
and Wheel
. If the Car
class assumed a Wheel
can support a speed of up to 200 mph but the actual Wheel
can only support a speed of up to 150 mph, it is the integration test that is supposed to uncover this discrepancy.
Can explain system testing
System testing: take the whole system and test it against the system specification.
System testing is typically done by a testing team (also called a QA team).
System test cases are based on the specified external behavior of the system. Sometimes, system tests go beyond the bounds defined in the specification. This is useful when testing that the system fails 'gracefully' when pushed beyond its limits.
Suppose the SUT is a browser that is supposedly capable of handling web pages containing up to 5000 characters. Given below is a test case to test if the SUT fails gracefully if pushed beyond its limits.
Test case: load a web page that is too big
* Input: loads a web page containing more than 5000 characters.
* Expected behavior: aborts the loading of the page
and shows a meaningful error message.
This test case would fail if the browser attempted to load the large file anyway and crashed.
System testing includes testing against non-functional requirements too. Here are some examples:
Can explain acceptance testing
Acceptance testing (aka User Acceptance Testing (UAT): test the system to ensure it meets the user requirements.
Acceptance tests give an assurance to the customer that the system does what it is intended to do. Acceptance test cases are often defined at the beginning of the project, usually based on the use case specification. Successful completion of UAT is often a prerequisite to the project sign-off.
Can explain the differences between system testing and acceptance testing
Acceptance testing comes after system testing. Similar to system testing, acceptance testing involves testing the whole system.
Some differences between system testing and acceptance testing:
System Testing | Acceptance Testing |
---|---|
Done against the system specification | Done against the requirements specification |
Done by testers of the project team | Done by a team that represents the customer |
Done on the development environment or a test bed | Done on the deployment site or on a close simulation of the deployment site |
Both negative and positive test cases | More focus on positive test cases |
Note: negative test cases: cases where the SUT is not expected to work normally e.g. incorrect inputs; positive test cases: cases where the SUT is expected to work normally
Requirement specification versus system specification
The requirement specification need not be the same as the system specification. Some example differences:
Requirements specification | System specification |
---|---|
limited to how the system behaves in normal working conditions | can also include details on how it will fail gracefully when pushed beyond limits, how to recover, etc. specification |
written in terms of problems that need to be solved (e.g. provide a method to locate an email quickly) | written in terms of how the system solves those problems (e.g. explain the email search feature) |
specifies the interface available for intended end-users | could contain additional APIs not available for end-users (for the use of developers/testers) |
However, in many cases one document serves as both a requirement specification and a system specification.
Passing system tests does not necessarily mean passing acceptance testing. Some examples:
Exercises
Can explain alpha and beta testing
Alpha testing is performed by the users, under controlled conditions set by the software development team.
Beta testing is performed by a selected subset of target users of the system in their natural work setting.
An open beta release is the release of not-yet-production-quality-but-almost-there software to the general population. For example, Google’s Gmail was in 'beta' for many years before the label was finally removed.