Slide Deck: Design Decomposition
In this lecture, we will focus on decomposing our system into individual classes and functions.
In structured programming, such as languages like C and Javascript, we use Functional Decomposition, where we decompose high-level general functions into lower and lower level functions, each of which performing a more and more specific task. Typically, we think of our functions being executed like a depth-first search in this way.
By contrast, Object-Oriented Decomposition is useful for modern software design. In Object-Oriented decomposition, we break our class down into classes, and describe the interactions of those classes.
Each software module should have one, and only one, reason to change
Consider the following partial class with some selected methods entered:
public class Student {
private String firstName, lastName, email;
private List<Course> currentCourses;
private List<CourseRecord> courseGrades;
private boolean isTA;
private Course courseTA;
private double currentWeeklyHours;
public double generateTranscriptDocument() {
// does something with courseGrades;
}
public void semesterReset() {
currentCourses.clear();
isTA = false;
courseTA = null;
currentWeeklyHours = 0;
}
public void uploadFinalGrade(Grade grade, Course c) {
CourseRecord courseRecord = new CourseRecord(c, grade);
courseGrades.add(courseRecord);
}
public double calculateGPA() {
// iterate through courseRecord
}
public double calculatePay(double hourlyRate) {
return currentWeeklyHours * hourlyRate;
}
}
This class feels awkward, like it is doing too much. That’s because it is! Think of what human-beings using the software are using this class for:
generateTranscriptDocument
uploadFinalGrade
calculatePay
Think of the reasons, however, that this class could change:
1) __generateTranscriptDocument__
may need to change if the printed format changes.
2) __uploadFinalGrade__
and __calculateGPA__
may need to change if the available grades change (as they did during Covid with the addition of CR
, GC
, and NC
grades)
3) __calculatePay__
and fields about TAing may need to change if TAs are moved to a salaried position, or may need to work different if the student is a Graduate student.
In short, there’s a lot of different reasons this class can change. This means that this class is violating the Single Responsibility Principle. As a hint, the use-case of each of the roles mentioned describes a functional module. Using that, we can break this class up:
public class Student {
...
}
public class GPACalculator {
public double calculateGPA(Student student);
}
public class Transcript {
public void generateTranscript(Student student);
}
public class TAPayment {
public double calculatePay(Student student);
}
Now, each of these classes has one responsibility. Yes, each of these classes is heavily dependent on Student, but that was still they case when they were all one-class. The key, however, is that if the software engineer receives a request to change a feature related to GPA calculation, they now know the primary class that they are working from. And because GPACalculation is encapsulated from Student, the developer for GPACalculator is significantly less likely to make changes that could break the other two use-cases.
Note that we probably should decompose this further. For instance, it probably would make sense to separate keeping track of the students current courses, from keeping track of their course history, from keeping track of their TA hours. But this is meant to just be a starting point.
There are 4 kinds of class relationships we will focus on.
This relationship describes a inheritance relationship, where one class is implementing the interface of another. The arrow-head is on the side of the parent class or implemented interface. Generally, we use Realization for implementing interfaces and extending abstract classes, and we use inheritance for extending concrete classes. For example:
public class ElectricCar extends Car {
...
}
Here, EletricCar
realizes Car
This is when one class has another instance(s) of another class as a field(s). For example:
public class Car {
Set<Tire> tires;
}
In the above case, Car aggregates tires.
The different between aggregation and composition is subtle. In composition, we are saying “one thing is composed of” another. For example, I am composed of a skeleton, muscles, etc. They are generally not separable entities (or at least, not meaningfully separable). By contrast, aggregation often implies an impermanent ownership where one class. For example, a Library Patron may check out a book, then adding the data of the book to a List<Book>
called checkedOut
. But the patron isn’t composed of books, they simply have some books (aggregation).
When drawn in a diagram, the diamond is on the owner’s side.
Association is when A is associated with B in some semantic way that implies a bidirectional relationship (That is, A is aware of B, and B is aware of A). For example, “Student enrolls in class”. At a code level, this can take the form of something like a mutual aggregation (although that isn’t explicitly required). For example, a Student
enrolls in a Course
- I would want to, from the Student
access their set of Course
objects, and from a Course
, I would want to access a set of Student
objects. This might look like:
public class Student {
private List<Course> courses;
public void addCourse(Course c);
}
public class Course {
List<Student> students;
public void enrollStudents(Student s);
}
Dependency implies a one-directional association. For example, class A uses class B, however Class B is unaware of Class A. An example might be something like:
public class Notification {
public Email generateStudentNotificiationEmail(Student s, String message) {
String emailAddress = s.getEmailAddress();
Email notificationEmail = new Email(emailAddress, message);
notificationEmail.send();
}
}
Here, Notification
is dependent on Email
and Student
. However, this is not an aggregation, because Notification
doesn’t have fields for Email
or Student
. However, it is still dependent on the interfaces of those two classes (i.e., if the Student interface for getting an email address changed, it might require a change in Notification
).