Enhancing Object-Oriented Design in Project Cami-n: Key Lessons from Code Reviews

In Project Cami-n, our team has been developing a system focused on managing logistics for trucks and their cargo. This involves defining how trucks interact with various 'things' they transport, as well as with destinations and routes. Through recent code reviews, we identified several recurring patterns and opportunities to refine our object-oriented design, leading to a more robust and readable codebase.

Challenges in Object Modeling

Initial implementations often presented common challenges in object-oriented programming. We observed patterns that, while functional, could be improved significantly in terms of clarity, maintainability, and adherence to design principles.

Magic Numbers and Unclear Intent

One frequent observation was the use of 'magic numbers' – hardcoded numerical values without clear explanations. For instance, calculating a truck's total weight might involve adding an arbitrary 1000 to the cargo's weight, intended as the truck's tare. This obscurity makes the code difficult to understand and modify.

// Before: Magic number '1000' for tare weight
method totalWeight() {
  return cargo.sum({item => item.weight()}) + 1000
}

Overly Complex Methods and Redundant Logic

Some methods tended to aggregate multiple responsibilities or reimplement logic that could be handled more elegantly. Review comments pointed out instances where a new filtering or iteration loop was created, despite similar operations existing or language features offering more concise alternatives. For example, checking if all items had even weights, or if any item's weight fell within a range, often involved manual checks instead of leveraging built-in collection methods.

Handling Missing Data: The Pitfall of Nulls

Returning null when an object or value cannot be found emerged as another area for improvement. For example, if a specific type of item wasn't present in the cargo, a method might return null. This forces callers to constantly check for null, introducing potential runtime errors and boilerplate code if not handled diligently. A more robust approach would involve returning a default value or signaling absence through other means.

Misplaced Responsibilities and Delegation

A critical aspect of good object-oriented design is ensuring that responsibilities are correctly assigned. We found cases where a Truck object was deciding if it could pass a Route, rather than delegating that decision to the Route itself. Similarly, a Truck might determine if it could unload its cargo, instead of the Destination dictating its own unloading policies. This leads to less cohesive objects and complex conditional logic in the wrong places.

Implementing Best Practices for Cleaner Code

To address these challenges, our code reviews emphasized applying core object-oriented principles, leading to cleaner, more expressive, and maintainable solutions.

Meaningful Naming and Constants

Replacing magic numbers with named constants or methods significantly improves readability and intent. By defining TARA_WEIGHT (tare weight) or MAX_WEIGHT, the purpose of these values becomes immediately clear.

// After: Using a named constant for tare weight
const TARA_WEIGHT = 1000

method totalWeight() {
  return cargo.sum({item => item.weight()}) + TARA_WEIGHT
}

// Or, defining tare as a property/method of the Truck itself
method tare() = 1000
method totalWeightWithTare() {
  return cargo.sum({item => item.weight()}) + self.tare()
}

Method Refactoring and Leveraging Language Features

Breaking down complex methods into smaller, focused subtasks (e.g., totalBundlesStored(), mostDangerousItem()) adheres to the Single Responsibility Principle. Furthermore, utilizing language-specific collection methods like even() or between(min, max) for common checks reduces boilerplate and improves conciseness.

// Before: Manual check for even weight
method allItemsHaveEvenWeight_v1() {
  return cargo.all({item => item.weight() % 2 == 0})
}

// After: Using a built-in collection method 'even()'
method allItemsHaveEvenWeight_v2() {
  return cargo.all({item => item.weight().even()})
}

// After: Using a built-in 'between(min, max)' for range checks
method hasItemInWeightRange(min, max) {
  return cargo.any({item => item.weight().between(min, max)})
}

Empowering Objects through Delegation

Correct delegation ensures objects are responsible for their own concerns. Instead of a Truck knowing the rules of a Route or Destination, it should simply ask these objects if an action is permissible. This creates a more flexible and robust design.

// Before: Truck decides if it can pass a route (misplaced logic)
// Inside Truck class:
// method canPassRoute(route) { return self.cargoDangerLevel() < route.restrictionLevel() }

// After: Delegation - Route decides if a vehicle can pass
// Inside Route class:
method canPass(aVehicle) {
  return aVehicle.cargoDangerLevel() < self.restrictionLevel()
}

// Inside Truck class (now delegates):
method attemptPassRoute(aRoute) {
  return aRoute.canPass(self) // Route decides based on Truck's info
}

Robust Error Handling: Avoiding Nulls

Instead of returning null, options include returning a default, an empty collection, or using validation to ensure the expected object exists before attempting to retrieve it. This improves the stability and predictability of the system.

Key Takeaways

These code review insights underscore the value of continuous improvement in software development. By focusing on principles like meaningful naming, method encapsulation, proper delegation, and robust error handling, we can significantly enhance the quality, readability, and maintainability of our object-oriented systems. It empowers developers to build more reliable and extensible software, even in complex domains like logistics management.


Generated with Gitvlg.com

Enhancing Object-Oriented Design in Project Cami-n: Key Lessons from Code Reviews
ALAN ACUÑA

ALAN ACUÑA

Author

Share: