Maintainability
Concept:
- Maintainable design allows for easy understanding, correction, adaptation, and enhancement.
In Practice:
- Writing clean, well-documented Java code and adhering to architectural patterns.
BCS1430
Dr. Ashish Sai
đ Week 2 Lecture 1 & 2
đ» BCS1430.ashish.nl
đ EPD150 MSM Conference Hall
Learning how to design OO software


Object Interaction Analysis is a technique used to understand how objects in the system will communicate and collaborate.
Interaction diagrams are used to visualize these interactions 1.
Class-Responsibility-Collaborator (CRC) Cards:
CRC cards is a simple tool used to define the behaviors and interactions of classes.
Components of a CRC Card:
Class Name: The entity or concept being modeled.
Responsibilities: What the class should know or do (its behavior).
Collaborators: Other classes this class interacts with or uses.
Class Name: ShoppingCart
Responsibilities:
Add Item: Include a new product in the cart.
Calculate Total: Compute the total cost of items in the cart.
Remove Item: Take out a product from the cart.
Checkout: Initiate the purchasing process.
Collaborators:
Product: Items that can be added to the shopping cart.
User: The customer who owns the shopping cart.
class ShoppingCart {
private List<Product> products;
private User owner;
void addItem(Product product) { /*...*/ }
double calculateTotal() { /*...*/ }
void removeItem(Product product) { /*...*/ }
void checkout() { /*...*/ }
}
class Product { /* Product details */ }
class User { /* User details */ }The ShoppingCart class has clear responsibilities like managing items and calculating totals, and it collaborates with Product and User classes to fulfill these responsibilities.
Beyond aesthetics, good software design is about crafting solutions that are effective, efficient, and maintainable.
Simplicity, Consistency, Modularity, Usability, Maintainability, Robustness
In Practice:
// Method to add two numbers
public class Addition {
// Entry method to demonstrate addition complexity
public static void main(String[] args) {
int number1 = 5;
int number2 = 10;
int result = Add(number1, number2);
System.out.println("The result is: " + result);
}
// addition method
public static int Add(int a, int b) {
// Initialize sum
int sum = 0;
// Convert integers to strings
String strA = Integer.toString(a);
String strB = Integer.toString(b);
// Convert strings back to integers
int intA = convertStringToInt(strA);
int intB = convertStringToInt(strB);
// Perform addition in a loop
for (int i = 0; i < intA; i++) {
sum = increment(sum);
}
for (int i = 0; i < intB; i++) {
sum = increment(sum);
}
// Return the calculated sum
return sum;
}
// Method to convert string to integer
private static int convertStringToInt(String number) {
try {
return Integer.parseInt(number);
} catch (NumberFormatException e) {
System.err.println("Error converting: " + e.getMessage());
return 0;
}
}
// Method to increment a number
private static int increment(int number) {
return number + 1;
}
}In Practice:
CamelCase naming conventions.// The Variable Naming Convention Extravaganza
int numberOfCatsOwnedByAunt = 5;
// Clearly, someone loves their aunt... and cats
double BitcoinInvestmentValue2024 = 42069.42;
// PascalCase:To the moon, they said
boolean is_this_variable_named_correctly = false;
// snake_case: A philosophical inquiry
String favorite-ice-cream-flavor = "MintChocolateChip";
// kebab-case: A contentious choice (Syntax Error!)
// And in a bold move to challenge the very fabric of naming conventions...
String uniFied_NamingConvention2024 = "AbsolutelyNotRecommended";
// An optimist's futile attempt
// The Variable Naming Convention Harmonization
int numberOfCatsOwnedByAunt = 5;
// Consistent CamelCase: Reflecting a fondness for one's aunt... and cats
double bitcoinInvestmentValue2024 = 42069.42;
// CamelCase: Optimistically aiming for the moon
boolean isThisVariableNamedCorrectly = false;
// CamelCase: Pondering the philosophical depths of naming correctness
String favoriteIceCreamFlavor = "MintChocolateChip";
// CamelCase: A choice that's as bold as it is divisive
// Unifying the naming convention to restore order from chaos
String unifiedNamingConvention2024 = "HighlyRecommended";
// CamelCase: A testament to the power of consistency
In Practice:
// Package for animal management
package com.zoo.animalcare;
public class AnimalFeeder {
// Feed the animals, or they start considering you as the next meal
}
// Package for zoo staff management
package com.zoo.staffmanagement;
public class StaffScheduler {
// Schedule staff or face the chaos of a zoo without keepers
}
// Separate package for gift shop inventory
package com.zoo.giftshop;
public class InventoryManager {
// Keep the plushies stocked, or brace for a toddler tantrum apocalypse
}
// And for the adventurous souls...
package com.zoo.emergencyprotocols;
public class EscapeProtocol {
// In case someone decides to reenact a scene from a dinosaur theme park movie
}Impact: - Enhances flexibility, manageability, and scalability.
In Practice:
// Ultra-simple UI for the ultimate couch potato experience
public class LazyLoggerUI {
public void logInactivity() {
if (isUserAwake()) {
displayMessage("Welcome! Just press any key to log today's laziness.");
waitForAnyKeyPress();
logLaziness("Another day successfully wasted.");
} else {
// Auto-log for those too lazy to even press a key
logLaziness("User too lazy to interact. Laziness logged automatically.");
}
}
private boolean isUserAwake() {
// Implement complex algorithm to detect user's consciousness
return Math.random() > 0.5; // 50/50 chance
}
private void waitForAnyKeyPress() {
// Wait for any key, but really, who's in a hurry?
}
private void logLaziness(String message) {
System.out.println(message + " Enjoy your achievements in idleness!");
}
}
Concept:
In Practice:
// Movie recommendation for the world's most indecisive person
/**
* Suggests a movie based on the user's mood and weather.
* This method embodies the art of overthinking movie night.
* @param mood The user's current mood, e.g., "happy", "sad".
* @param weather The current weather, e.g., "rainy", "sunny".
* @return A string suggesting a movie, because choosing is hard.
*/
public String suggestMovie(String mood, String weather) {
// If it's raining and the user is sad, recommend a comedy
if ("rainy".equals(weather) && "sad".equals(mood)) {
return "Watching 'Monty Python and the Holy Grail' will lift your spirits!";
}
// If it's sunny and the user is happy, recommend an adventure movie
else if ("sunny".equals(weather) && "happy".equals(mood)) {
return "It's a perfect day for 'Indiana Jones'!";
}
// For all other cases, recommend something random
// because who doesn't love a surprise movie pick?
else {
return "How about a wildcard? 'The Grand Budapest Hotel'";
}
}Impact:
Concept:
In Practice:
// "FutureSeer", attempts to predict daily events with a mix of technology and,
// let's say, less scientific methods.
/**
* Attempts to predict the user's future by combining high-tech algorithms
* with a touch of mystical randomness.
*/
public String predictFuture() {
try {
// Pretend to perform some complex calculation involving astrology, machine learning, and a magic 8-ball
if (Math.random() > 0.5) {
return "Good fortune awaits you today!";
} else {
throw new UncertainFutureException("The future is cloudy. Try again.");
}
} catch (UncertainFutureException e) {
// Gracefully handling the uncertainty of future predictions
return "Even the app is puzzled today. Maybe just do what feels right?";
}
}
/**
* Custom exception for when the future is just too murky to predict.
*/
class UncertainFutureException extends Exception {
public UncertainFutureException(String message) {
super(message);
}
}Impact: - Keeps systems stable and functional under various conditions, enhancing reliability.
DRY (Donât Repeat Yourself): Avoid duplication.
KISS (Keep It Simple, Stupid): Keep the design simple and straightforward.
YAGNI (You Arenât Gonna Need It): Donât implement something until it is necessary.

Definition: Avoid duplication in code. Every piece of knowledge should have a single, unambiguous representation in the system.
Impact:
Definition: Simplicity in design. Avoid unnecessary complexity and keep things straightforward.
Impact:

Pre-KISS
// Pre-KISS: "Mission Control"
class BeverageDispenser {
String selectBeverage(int buttonPressed) {
// Check if it's a leap year to decide on the extra espresso shot
if ((buttonPressed == 1) && ((Year.now().getValue() % 4) == 0)) {
return "Extra espresso shot, because leap year!";
} else if (buttonPressed == 2) {
// Calculate the gravitational pull of the moon to adjust sugar levels
return "Sugar adjusted for the moon's pull";
}
return "Beverage selected";
}
}Post-KISS
// Post-KISS: "Just Press Play"
class BeverageDispenser {
String selectBeverage(int buttonPressed) {
// Simply dispense the chosen beverage
switch (buttonPressed) {
case 1: return "Espresso, straight up!";
case 2: return "Sugar? We got you, just as you like!";
default: return "Beverage selected, enjoy!";
}
}
}Definition: Implement only those features that are necessary. Avoid adding functionality until it is required.
Impact:
Separation of Concerns: Different functionality managed by separate code.
Principle of Least Astonishment: Software behaves how users will expect it to.
Law of Demeter: Object should assume as little as possible about the structure or properties of anything else.
Definition:
Impact:
Before:
// Before: Mixing data access with business logic
class UserHandler {
void createUser(String username) {
// Database connection code
// User creation logic
}
}After:
// After: Separated data access and business logic
class UserDataAccess {
void saveUser(User user) { /* Database code to save user */ }
}
class UserHandler {
UserDataAccess dataAccess = new UserDataAccess();
void createUser(String username) {
User user = new User(username);
dataAccess.saveUser(user); // Separated concern
}
}Before:
// Pre-Separation: The "Cluttered Kitchen" Approach
class MealPreparation {
void prepareMeal(String ingredient) {
// Look for ingredients in the fridge
// Mix ingredients in a bowl
// Cook the mixed ingredients
}
}After:
// Post-Separation: The "Organized Chef" Method
class IngredientStorage {
String fetchIngredient(String name) { /* Retrieve from storage */ }
}
class Mixer {
Bowl mixIngredients(List<String> ingredients) { /* Mixing logic */ }
}
class Stove {
Meal cook(Bowl bowl) { /* Cooking logic */ }
}
class MealPreparation {
IngredientStorage storage = new IngredientStorage();
Mixer mixer = new Mixer();
Stove stove = new Stove();
void prepareMeal(String ingredientName) {
String ingredient = storage.fetchIngredient(ingredientName);
Bowl mixed = mixer.mixIngredients(Arrays.asList(ingredient));
Meal meal = stove.cook(mixed); // Cleanly separated concerns
}
}Definition:
Impact:
Before:
// Before: Confusing method name
class FileProcessor {
void erase(File file) { /* Actually saves the file */ }
}After:
Definition:
Impact:
Before:
// Before: Violating Law of Demeter
class Customer {
Wallet wallet;
//...
}
class Shop {
void chargeCustomer(Customer customer) {
double amount = customer.wallet.getMoney(); // Directly accessing customer's wallet
// Charge logic
}
}After:
What is GRASP?
General Responsibility Assignment Software Patterns (GRASP) are guidelines for assigning responsibilities in object-oriented design to improve robustness and maintainability.
Principles Overview:
Information Expert: Responsibilities go to the class with the most related information.
Creator: The class that needs an object or has initializing data should create it.
Controller: Designate a class to handle system events and user input.
Low Coupling: Minimize class interdependencies for flexibility.
High Cohesion: Keep related functions together for focused class design.
Polymorphism: Handle alternatives based on object types dynamically.
Pure Fabrication: Create classes for better design, even if they donât represent real-world entities.
Principle: Assign responsibility to the class that has the necessary information to fulfill it.
Benefits:
Reduces redundancy and complexity.
Enhances maintainability and cohesion.
Makes the system more modular and easier to navigate.
Application:
class Pizza {
private List<String> toppings;
// Other pizza-related methods...
}
class PizzaPriceCalculator {
// This class eagerly jumps in to calculate the pizza price
double calculatePrice(Pizza pizza) {
double price = 10; // base price
for(String topping : pizza.getToppings()) {
price += 0.50; // Assuming each topping adds 50 cents
}
return price;
}
}OrderCalculator is taking the responsibility that naturally belongs to the Order class, leading to a less intuitive and more fragmented design.Order class.Principle: Assign class B the responsibility to create an instance of class A if B closely uses A or holds the data that will initialize A.
Benefits:
Enhances encapsulation and clarity.
Reduces dependencies and coupling between classes.
Simplifies the systemâs structure.
Application:
// Order is responsible for creating Item instances,
// but it doesn't directly contain or closely use them
class Order {
void addNewItem() {
Item newItem = new Item(); // Order creates Item instances
// ...
}
}
class ShoppingCart {
private List<Item> items;
// ShoppingCart logic...
}Problem:
Order class is creating Item instances, but it doesnât have a logical or direct relationship with Item, leading to poor encapsulation and design.Item instances to the ShoppingCart class, which logically contains and manages items.Principle: Assign the responsibility of handling a system event to a class representing the overall system, a root object, or a subsystem.
Benefits:
Centralizes control logic.
Decouples the UI from the underlying business logic.
Simplifies maintenance and enhances scalability.
Application:
class UserInterface {
// Directly handling user input and business logic
void onLoginButtonClick(String username, String password) {
// Validate credentials
// Directly access the database to verify user
// Manage session
}
}UserInterface class is overloaded with responsibilities, handling UI events, business logic, and data access, making the system hard to maintain and scale.Solution:
class UserInterface {
// Delegates handling of the login event to the LoginController
void onLoginButtonClick(String username, String password) {
LoginController controller = new LoginController();
controller.handleLoginRequest(username, password);
}
}
class LoginController {
AuthenticationService authService;
void handleLoginRequest(String username, String password) {
// Handle the login process
User user = authService.authenticate(username, password);
// Manage session and other login-related tasks
}
}Definition:
Principle: Design to minimize the dependencies between classes to reduce the impact of changes and improve reusability.
Benefits:
Increases the flexibility of the system.
Makes the codebase more resilient to changes.
Facilitates easier testing and maintenance.
Application:
class Toaster {
// Directly wired to the coffee maker
CoffeeMaker coffeeMaker;
void toast() {
coffeeMaker.brew(); // Toasting bread shouldn't brew coffee
// ...toast bread
}
}Solution:
interface Appliance {
void activate();
}
class Toaster implements Appliance {
void activate() { /* Toast bread */ }
}
class CoffeeMaker implements Appliance {
void activate() { /* Brew coffee */ }
}
class KitchenManager {
Appliance appliance;
void useAppliance() {
appliance.activate(); // Chooses which appliance to use, independently
}
}Definition: - Cohesion refers to the degree to which elements of a module or class belong together.
Principle: Keep related and similar functionalities together in a class, ensuring that each class has a clear, narrowly focused role.
Benefits:
Simplifies understanding of the system.
Enhances the ability to manage and modify code.
Promotes single responsibility and focused class design.
Application: - Essential in ensuring that the system remains organized and each part is as independent and focused as possible, promoting better design and easier future changes.
class UserManager {
void createUser() { /*...*/ }
void deleteUser() { /*...*/ }
void generateReport() { /* Unrelated to user management */ }
}UserManager is handling both user management and report generation, making it less cohesive and more complex.class UserManager {
void createUser() { /*...*/ }
void deleteUser() { /*...*/ }
// Removed report generation
}
class ReportGenerator {
void generateReport() { /* Focused on report generation */ }
}UserManager and ReportGenerator makes each class more focused, understandable, and maintainable.Principle: Use polymorphism to handle alternatives based on object type, where the behavior varies depending on the class of the object.
Benefits:
Enhances flexibility and reusability by allowing different classes to be used interchangeably.
Simplifies code by eliminating the need for multiple conditional statements.
Application:
class AnimalSound {
void makeSound(Animal animal) {
if (animal instanceof Dog) {
System.out.println("Woof");
} else if (animal instanceof Cat) {
System.out.println("Meow");
}
// More conditions for other animal types
}
}
class Dog extends Animal { /*...*/ }
class Cat extends Animal { /*...*/ }AnimalSound class becomes cumbersome and difficult to maintain as more animal types are added.abstract class Animal {
abstract void makeSound();
}
class Dog extends Animal {
void makeSound() { System.out.println("Woof"); }
}
class Cat extends Animal {
void makeSound() { System.out.println("Meow"); }
}
class AnimalSound {
void makeSound(Animal animal) {
animal.makeSound(); // Polymorphism in action
}
}AnimalSound and making the code more scalable and maintainable.Principle: Create a class that doesnât represent a concept in the problem domain, particularly to achieve low coupling, high cohesion, or to encapsulate change.
Benefits:
Enhances maintainability and reusability.
Allows for better separation of concerns and encapsulation.
Application:
class Order {
// Order related data and methods...
void saveOrder() {
// Direct database access code to save the order
// This mixes business logic with data access logic
}
}Order class is directly handling database operations, which is not its primary responsibility, leading to a design thatâs hard to maintain and scale.class Order {
// Order related data and methods...
}
class OrderRepository {
void saveOrder(Order order) {
// Specific database access code to save the order
}
}OrderRepository is a pure fabrication that takes over database responsibilities, leading to a cleaner separation of concerns and a more maintainable design.What is SOLID?
SOLID represents five fundamental principles in object-oriented programming and design that promote software maintainability and extensibility.
Key Objectives:
Single Responsibility: Encourage classes to have one reason to change.
Open/Closed: Design modules that are open for extension but closed for modification.
Liskov Substitution: Ensure subclasses can replace their superclasses without altering the programâs correctness.
Interface Segregation: Favor client-specific interfaces over general-purpose ones.
Dependency Inversion: Depend on abstractions rather than concrete implementations.

Definition:
A class should have one, and only one, reason to change, promoting modularity and separation of concerns.
Importance:
Simplifies understanding and modification of classes.
Enhances cohesion and reduces the impact of changes.
class UserManager {
void createUser() { /*...*/ }
void sendEmail(String message) { /*...*/ } // Unrelated to user management
}UserManager handling both user creation and email sending mixes different concerns, making it less cohesive and more prone to errors.class UserManager {
void createUser() { /*...*/ }
}
class EmailService {
void sendEmail(String message) { /*...*/ }
}
Definition:
Software entities should be open for extension but closed for modification, promoting flexible and scalable systems.
How?:
class GraphicEditor {
void drawShape(Shape shape) {
if (shape.type == 1) {
drawRectangle(shape);
} else if (shape.type == 2) {
drawCircle(shape);
}
// Adding a new shape requires modifying this method
}
}GraphicEditor, violating the open/closed principle.abstract class Shape {
abstract void draw();
}
class Rectangle extends Shape {
void draw() { /* Draw rectangle */ }
}
class Circle extends Shape {
void draw() { /* Draw circle */ }
}
class GraphicEditor {
void drawShape(Shape shape) {
shape.draw(); // No modification needed for new shapes
}
}GraphicEditor, adhering to the open/closed principle and making the system more extensible.
Definition:
Subclasses should be substitutable for their base classes without affecting the programâs correctness.
Java Example:
class Bird {
void fly() { /*...*/ }
}
class Ostrich extends Bird {
// Ostriches can't fly, but they're subclassed from Bird
void fly() { throw new UnsupportedOperationException(); }
}Ostrich object where a Bird is expected can cause unexpected errors due to the overridden fly method.abstract class Bird {
// Some common bird behavior
}
class FlyingBird extends Bird {
void fly() { /* Implement flying */ }
}
class Ostrich extends Bird {
// Ostrich-specific behavior, no fly method
}FlyingBird and Ostrich are now both substitutable for Bird without causing unexpected behavior, adhering to the Liskov Substitution Principle.
Definition:
Clients should not be forced to depend on interfaces they do not use, encouraging fine-grained interfaces over general-purpose ones.
interface Worker {
void work();
void eat();
void takeBreak();
}
class Robot implements Worker {
void work() { /*...*/ }
void eat() { /* Robots don't eat */ }
void takeBreak() { /* Robots don't take breaks */ }
}Robot is forced to implement eat and takeBreak, which are irrelevant to its functionality.interface Worker {
void work();
}
interface HumanWorker extends Worker {
void eat();
void takeBreak();
}
class Robot implements Worker {
void work() { /*...*/ }
}
class Human implements HumanWorker {
void work() { /*...*/ }
void eat() { /*...*/ }
void takeBreak() { /*...*/ }
}Robot now only implements the relevant Worker interface, and Human implements the extended HumanWorker interface. This segregation makes the system more flexible and maintainable.
Definition:
High-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.
class OrderProcessor {
MySQLDatabase database; // Directly dependent on a specific database implementation
void processOrder(Order order) {
database.save(order); // Tightly coupled to MySQLDatabase
}
}OrderProcessor is directly dependent on MySQLDatabase, making it hard to switch to a different database or test the order processing independently.interface Database {
void save(Order order);
}
class MySQLDatabase implements Database {
void save(Order order) { /*...*/ }
}
class OrderProcessor {
Database database; // Depends on the abstraction
void processOrder(Order order) {
database.save(order); // Not tied to a specific implementation
}
}OrderProcessor now depends on the Database abstraction, not the concrete MySQLDatabase implementation. This makes the system more flexible and easier to modify or test.