This refactoring pattern involves grouping parameters that naturally go together into a single object. When you see a group of data items that regularly travel together, appearing in function after function, it's a sign they should be combined into a single object.
Benefits:
- Reduces the number of parameters (improved readability)
- Groups related data together (better organisation)
- Makes relationships between data explicit
- Easier to add new related data without changing function signatures
- Enables moving behaviour related to the data into the new class
- Reduces errors from parameter ordering mistakes
When to use:
- When you have a group of parameters that often appear together
- When you see the same parameters in multiple function signatures
- When parameters represent a cohesive concept
- When you need to add more related parameters
- When parameter ordering becomes confusing
See https://refactoring.com/catalog/introduceParameterObject.html
BEFORE: Multiple primitive parameters passed around
Problems:
- Too many parameters (hard to remember and maintain)
- Parameters are related, but the relationship is not explicit
- Easy to mix up parameter order
- Adding new related data requires changing all function signatures
- Difficult to add validation or behaviour for the group
<?php
declare(strict_types=1);
namespace RefactoringPatterns;
class IntroduceParameterObject
{
public function amountInvoicedBefore(
\DateTimeImmutable $startDate,
\DateTimeImmutable $endDate,
string $customerName,
string $customerId,
string $customerEmail
): float {
$amount = 0;
foreach ($this->getInvoices() as $invoice) {
if ($this->isInDateRangeBefore($invoice, $startDate, $endDate) &&
$this->isForCustomerBefore($invoice, $customerName, $customerId, $customerEmail)) {
$amount += $invoice['amount'];
}
}
return $amount;
}
private function isInDateRangeBefore(
array $invoice,
\DateTimeImmutable $startDate,
\DateTimeImmutable $endDate
): bool {
return $invoice['date'] >= $startDate && $invoice['date'] <= $endDate;
}
private function isForCustomerBefore(
array $invoice,
string $customerName,
string $customerId,
string $customerEmail
): bool {
return $invoice['customerId'] === $customerId;
}
public function demonstratePattern(): void {
$startDate = new \DateTimeImmutable('2024-01-01');
$endDate = new \DateTimeImmutable('2024-03-31');
$customerName = 'John Doe';
$customerId = 'C001';
$customerEmail = '[email protected]';
echo "BEFORE (5 separate parameters):\n";
$amountBefore = $this->amountInvoicedBefore(
$startDate,
$endDate,
$customerName,
$customerId,
$customerEmail
);
echo "Amount invoiced: $" . number_format($amountBefore, 2) . "\n";
echo "Issues: Too many parameters, unclear relationships\n\n";
}
}
AFTER: Using parameter objects for related data
Benefits:
- Reduced the chance of parameter ordering errors
- Clear, self-documenting function signatures
- Related data is explicitly grouped
- Easy to add new related fields without changing signatures
- Can add behaviour and validation to the parameter objects
<?php
declare(strict_types=1);
namespace RefactoringPatterns;
class IntroduceParameterObject
{
public function amountInvoicedAfter(DateRange $dateRange, Customer $customer): float
{
$amount = 0;
foreach ($this->getInvoices() as $invoice) {
if ($this->isInDateRangeAfter($invoice, $dateRange) &&
$this->isForCustomerAfter($invoice, $customer)) {
$amount += $invoice['amount'];
}
}
return $amount;
}
private function isInDateRangeAfter(array $invoice, DateRange $dateRange): bool
{
return $dateRange->contains($invoice['date']);
}
private function isForCustomerAfter(array $invoice, Customer $customer): bool
{
return $invoice['customerId'] === $customer->id;
}
public function demonstratePattern(): void
{
echo "AFTER (2 parameter objects):\n";
$dateRange = new DateRange($startDate, $endDate);
$customer = new Customer($customerId, $customerName, $customerEmail);
$amountAfter = $this->amountInvoicedAfter($dateRange, $customer);
echo "Amount invoiced: $" . number_format($amountAfter, 2) . "\n";
echo "Benefits: Clear grouping, can add validation and behavior\n\n";
}
}
/**
* Parameter Object: DateRange
* Encapsulates a range of dates with validation and behavior
*/
class DateRange
{
public function __construct(
private readonly \DateTimeImmutable $startDate,
private readonly \DateTimeImmutable $endDate
) {
if ($endDate < $startDate) {
throw new \InvalidArgumentException('End date must be after start date');
}
}
public function getStartDate(): \DateTimeImmutable
{
return $this->startDate;
}
public function getEndDate(): \DateTimeImmutable
{
return $this->endDate;
}
/**
* Behavior: Check if a date is within the range
*/
public function contains(\DateTimeImmutable $date): bool
{
return $date >= $this->startDate && $date <= $this->endDate;
}
/**
* Behavior: Get the duration of the range in days
*/
public function getDurationInDays(): int
{
return $this->startDate->diff($this->endDate)->days;
}
}
/**
* Parameter Object: Customer
* Encapsulates customer-related data with validation
*/
class Customer
{
public readonly string $id;
public readonly string $name;
public readonly string $email;
public function __construct(string $id, string $name, string $email)
{
if (empty($id)) {
throw new \InvalidArgumentException('Customer ID cannot be empty');
}
if (empty($name)) {
throw new \InvalidArgumentException('Customer name cannot be empty');
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('Invalid email address');
}
$this->id = $id;
$this->name = $name;
$this->email = $email;
}
/**
* Behavior: Get customer display name
*/
public function getDisplayName(): string
{
return "{$this->name} ({$this->email})";
}
}
Another example - Coordinates passed as primitives
Before
<?php
declare(strict_types=1);
namespace RefactoringPatterns;
class IntroduceParameterObject
{
public function calculateDistanceBefore(
float $x1,
float $y1,
float $x2,
float $y2
): float {
$dx = $x2 - $x1;
$dy = $y2 - $y1;
return sqrt($dx * $dx + $dy * $dy);
}
public function findNearbyLocationsBefore(
float $centerX,
float $centerY,
float $radius,
array $locations
): array {
$nearby = [];
foreach ($locations as $location) {
$distance = $this->calculateDistanceBefore(
$centerX,
$centerY,
$location['x'],
$location['y']
);
if ($distance <= $radius) {
$nearby[] = $location;
}
}
return $nearby;
}
public function demonstratePattern(): void
{
echo "Example 2: Location Distance Calculation\n";
echo "-----------------------------------------\n";
$locations = [
['name' => 'Store A', 'x' => 10.0, 'y' => 20.0],
['name' => 'Store B', 'x' => 15.0, 'y' => 25.0],
['name' => 'Store C', 'x' => 50.0, 'y' => 50.0],
];
echo "BEFORE (4 coordinate parameters):\n";
$nearbyBefore = $this->findNearbyLocationsBefore(10.0, 20.0, 10.0, $locations);
echo "Found " . count($nearbyBefore) . " nearby locations\n";
echo "Issues: Easy to mix up x1, y1, x2, y2 parameters\n\n";
}
}
After
<?php
declare(strict_types=1);
namespace RefactoringPatterns;
class IntroduceParameterObject
{
public function calculateDistanceAfter(Point $point1, Point $point2): float
{
return $point1->distanceTo($point2);
}
public function findNearbyLocationsAfter(
Point $center,
float $radius,
array $locations
): array {
$nearby = [];
foreach ($locations as $locationData) {
$location = new Point($locationData['x'], $locationData['y']);
if ($center->distanceTo($location) <= $radius) {
$nearby[] = $locationData;
}
}
return $nearby;
}
public function demonstratePattern(): void
{
// Example 2: Coordinate calculations
echo "Example 2: Location Distance Calculation\n";
echo "-----------------------------------------\n";
$locations = [
['name' => 'Store A', 'x' => 10.0, 'y' => 20.0],
['name' => 'Store B', 'x' => 15.0, 'y' => 25.0],
['name' => 'Store C', 'x' => 50.0, 'y' => 50.0],
];
echo "AFTER (Point parameter object):\n";
$center = new Point(10.0, 20.0);
$nearbyAfter = $this->findNearbyLocationsAfter($center, 10.0, $locations);
echo "Found " . count($nearbyAfter) . " nearby locations\n";
echo "Benefits: Point object can now have behavior (distanceTo method)\n\n";
echo "=== Key Benefits ===\n";
echo "1. Function signatures are clearer and more maintainable\n";
echo "2. Related data is explicitly grouped together\n";
echo "3. Behavior can be added to parameter objects\n";
echo "4. Easier to extend without breaking existing code\n";
echo "5. Reduced chance of parameter ordering mistakes\n";
echo "6. Parameter objects can enforce validation rules\n";
}
}
/**
* Parameter Object: Point
* Encapsulates 2D coordinates with geometric operations
*/
class Point
{
public function __construct(
private readonly float $x,
private readonly float $y
) {
}
public function getX(): float
{
return $this->x;
}
public function getY(): float
{
return $this->y;
}
/**
* Behavior: Calculate distance to another point
*/
public function distanceTo(Point $other): float
{
$dx = $other->x - $this->x;
$dy = $other->y - $this->y;
return sqrt($dx * $dx + $dy * $dy);
}
/**
* Behavior: Create a new point offset from this one
*/
public function offset(float $dx, float $dy): Point
{
return new Point($this->x + $dx, $this->y + $dy);
}
}