3.5 消除重复代码:提取公共逻辑
重复代码是重构中最常见的目标。当多个函数中出现相似的代码片段时,应将其提取为独立的工具方法或服务类。
重构前:
<?php
public function exportUsersToCsv(array $users): string
{
$csv = "ID,Name,Emailn";
foreach ($users as $user) {
$csv .= $user['id'] . ',' . $user['name'] . ',' . $user['email'] . "n";
}
return $csv;
}
public function exportOrdersToCsv(array $orders): string
{
$csv = "OrderID,Total,Statusn";
foreach ($orders as $order) {
$csv .= $order['id'] . ',' . $order['total'] . ',' . $order['status'] . "n";
}
return $csv;
}
?>重构后:
<?php
private function arrayToCsv(array $data, array $headers): string
{
$csv = implode(',', $headers) . "n";
foreach ($data as $row) {
$csv .= implode(',', array_values($row)) . "n";
}
return $csv;
}
public function exportUsersToCsv(array $users): string
{
return $this->arrayToCsv($users, ['ID', 'Name', 'Email']);
}
public function exportOrdersToCsv(array $orders): string
{
return $this->arrayToCsv($orders, ['OrderID', 'Total', 'Status']);
}
?>3.6 分离命令与查询
一个函数要么修改状态(命令),要么返回数据(查询),不要同时做两件事。分离命令与查询能显著降低函数的复杂度。
重构前:
<?php
public function processPayment(Payment $payment): bool
{
// 执行支付
$result = $this->gateway->charge($payment);
// 更新数据库
if ($result->isSuccess()) {
$this->db->update('payments', ['status' => 'paid'], ['id' => $payment->getId()]);
return true;
}
return false;
}
?>重构后:
<?php
public function processPayment(Payment $payment): PaymentResult
{
// 命令:执行支付
return $this->gateway->charge($payment);
}
public function updatePaymentStatus(Payment $payment, PaymentResult $result): void
{
// 命令:更新数据库
if ($result->isSuccess()) {
$this->db->update('payments', ['status' => 'paid'], ['id' => $payment->getId()]);
}
}
?>重构后,调用方可以更清晰地控制流程,也更容易进行单元测试。
四、实战案例:重构一个复杂的订单处理函数
下面是一个综合案例,展示如何将多个重构技巧结合起来,将一个臃肿的函数变得优雅。
重构前的混乱函数:
<?php
public function handleOrder(array $data): array
{
$order = [];
if (isset($data['user_id']) && isset($data['items'])) {
$user = $this->userRepo->find($data['user_id']);
if ($user !== null && $user->isActive()) {
$total = 0;
foreach ($data['items'] as $item) {
$product = $this->productRepo->find($item['product_id']);
if ($product !== null && $product->isInStock()) {
$total += $product->getPrice() * $item['quantity'];
} else {
$this->logger->warning('Product not found or out of stock: ' . $item['product_id']);
}
}
if ($total > 0) {
if (isset($data['coupon'])) {
$coupon = $this->couponRepo->findByCode($data['coupon']);
if ($coupon !== null && $coupon->isValid()) {
$total *= (1 - $coupon->getDiscountPercent() / 100);
}
}
$order['id'] = uniqid('ord_');
$order['total'] = round($total, 2);
$order['status'] = 'pending';
$this->db->insert('orders', $order);
$this->eventDispatcher->dispatch(new OrderCreatedEvent($order));
return $order;
} else {
throw new RuntimeException('Order total is zero');
}
} else {
throw new RuntimeException('User not found or inactive');
}
} else {
throw new InvalidArgumentException('Missing required fields');
}
}
?>重构后的优雅版本:
<?php
public function handleOrder(array $data): array
{
$this->validateOrderData($data);
$user = $this->getActiveUser($data['user_id']);
$items = $this->resolveOrderItems($data['items']);
$total = $this->calculateTotal($items);
$total = $this->applyCouponIfPresent($total, $data['coupon'] ?? null);
$order = $this->createOrder($data['user_id'], $total, $items);
$this->persistOrder($order);
$this->dispatchOrderEvent($order);
return $order;
}
private function validateOrderData(array $data): void
{
if (empty($data['user_id']) || empty($data['items'])) {
throw new InvalidArgumentException('Missing required fields: user_id and items');
}
}
private function getActiveUser(int $userId): User
{
$user = $this->userRepo->find($userId);
if ($user === null || !$user->isActive()) {
throw new RuntimeException('User not found or inactive');
}
return $user;
}
private function resolveOrderItems(array $itemsData): array
{
$resolved = [];
foreach ($itemsData as $itemData) {
$product = $this->productRepo->find($itemData['product_id']);
if ($product === null || !$product->isInStock()) {
$this->logger->warning('Product not found or out of stock: ' . $itemData['product_id']);
continue;
}
$resolved[] = [
'product' => $product,
'quantity' => $itemData['quantity']
];
}
if (empty($resolved)) {
throw new RuntimeException('No valid items in order');
}
return $resolved;
}
private function calculateTotal(array $items): float
{
$total = 0;
foreach ($items as $item) {
$total += $item['product']->getPrice() * $item['quantity'];
}
return $total;
}
private function applyCouponIfPresent(float $total, ?string $couponCode): float
{
if ($couponCode === null) {
return $total;
}
$coupon = $this->couponRepo->findByCode($couponCode);
if ($coupon !== null && $coupon->isValid()) {
return $total * (1 - $coupon->getDiscountPercent() / 100);
}
return $total;
}
private function createOrder(int $userId, float $total, array $items): array
{
return [
'id' => uniqid('ord_'),
'user_id' => $userId,
'total' => round($total, 2),
'status' => 'pending',
'items' => $items
];
}
private function persistOrder(array $order): void
{
$this->db->insert('orders', $order);
}
private function dispatchOrderEvent(array $order): void
{
$this->eventDispatcher->dispatch(new OrderCreatedEvent($order));
}
?>重构后的版本遵循单一职责原则,每个函数都只完成一个明确的任务,代码行数虽然增加了,但可读性、可测试性和可维护性都得到了质的飞跃。
五、重构的最佳实践与注意事项
始终在测试保护下进行重构:确保有足够的单元测试覆盖,否则重构可能引入回归缺陷。
一次只做一种重构:不要同时提取方法、改变参数、调整逻辑。每次只改变一个方面,然后运行测试。
使用IDE的重构工具:现代PHP IDE(如PhpStorm)提供了安全的提取方法、重命名、移动等重构操作,可以显著降低出错概率。
保持提交粒度适中:每次重构完成后,提交一个清晰的版本,并写上明确的提交信息,方便后续回溯。
不要过度重构:如果一个函数只有几行代码,并且已经足够清晰,就没有必要强行拆分成多个函数。过度抽象同样会增加理解成本。
六、总结
函数重构是提升PHP代码质量最直接、最有效的手段之一。通过提取方法、减少参数、使用哨兵语句、引入设计模式等技巧,可以将混乱的函数变得结构清晰、职责单一、易于测试和扩展。重构不是一蹴而就的工作,而是需要融入日常开发习惯中的持续改进过程。希望本文的技巧和案例能够帮助你在实际项目中写出更高质量的PHP代码。