今天,我们将讨论每个程序员心中近在咫尺的话题:代码重构。如果您曾经难以理解自己的 C# 代码或花费数小时调试复杂的逻辑,那么可能是时候进行代码改造了。
想象一下,你正在处理一个 C# 项目,当你浏览代码时,你注意到它开始看起来有点混乱。函数太长,变量名称令人困惑,而且到处都是冗余。这就是代码重构概念发挥作用的地方。
重构是在不改变其外部行为的情况下重构现有代码的过程。这不仅仅是让你的代码更漂亮;这是为了让它更容易理解、维护和构建。
让我们介绍一些基本的 C# 代码重构技术,这些技术将帮助你实现更简洁、更易于维护的代码。
重构不仅涉及提高代码的可读性和可维护性,还涉及在必要时优化性能。识别性能瓶颈和低效算法或数据结构,并对其进行重构,以提高执行速度和资源利用率。
对准确反映其用途的变量和方法使用描述性名称。重命名它们可以显著提高代码的可读性和可维护性。
// Before refactoring
public void DisplayUserInfo(string n, int a)
{
Console.WriteLine($"Name: {n}, Age: {a}");
}
// After refactoring
public void DisplayUserInfo(string name, int age)
{
Console.WriteLine($"Name: {name}, Age: {age}");
}
幻数使代码更难理解和维护。将它们替换为命名常量以提高代码的可读性和可维护性。
// Before refactoring
public double CalculateCircleArea(double radius)
{
return 3.14 * radius * radius;
}
// After refactoring
private const double Pi = 3.14;
public double CalculateCircleArea(double radius)
{
return Pi * radius * radius;
}
LINQ(语言集成查询)提供了一种简洁易读的方法来查询 C# 中的数据。它通常可以替换循环和条件语句,从而产生更简洁、更富有表现力的代码。
// Before refactoring
public List<string> FilterNamesStartingWithA(List<string> names)
{
List<string> filteredNames = new List<string>();
foreach (var name in names)
{
if (name.StartsWith("A"))
{
filteredNames.Add(name);
}
}
return filteredNames;
}
// After refactoring
public List<string> FilterNamesStartingWithA(List<string> names)
{
return names.Where(name => name.StartsWith("A")).ToList();
}
复杂的条件语句会使代码难以理解。通过使用一些技术来简化它们,例如将条件逻辑提取到单独的方法中(我们将在后面讨论),使用 switch 语句而不是多个 if-else 块,以及利用三元运算符(例如合并运算符)来简化条件表达式。
// Before refactoring
public string GetWeatherForecast(int temperature)
{
if (temperature > 30)
{
return "Hot";
}
else
{
return "Mild";
}
}
// After refactoring
public string GetWeatherForecast(int temperature)
{
return temperature > 30 ? "Hot" : "Mild";
}
当您找到在较大的方法中执行特定任务的代码块时,请考虑将其提取到其自己的方法中。这不仅提高了可读性,还促进了代码的可重用性。
在 C# 中,可以使用 Visual Studio 或 ReSharper 等工具轻松提取方法。
假设您有一种处理客户订单的方法,该方法涉及验证订单、更新库存、计算总价、生成发货标签和通知客户。该方法变得冗长,并且包含多个职责。ProcessOrder
// Before refactoring
public void ProcessOrder(Order order)
{
// Step 1: Validate order
if (!order.IsValid)
{
throw new ArgumentException("Invalid order.");
}
// Step 2: Update inventory
foreach (var product in order.Products)
{
// Reduce inventory by 1 for each ordered product
int updatedQuantity = product.AvailableQuantity - 1;
// Ensure the updated quantity is non-negative
updatedQuantity = Math.Max(updatedQuantity, 0);
// Update the product's available quantity
product.AvailableQuantity = updatedQuantity;
// Print a message to simulate inventory update
Console.WriteLine($"Inventory updated for product '{product.Name}': Available Quantity = {product.AvailableQuantity}");
}
// Step 3: Calculate total price
double totalPrice = order.Products.Sum(p => p.Price);
double discount = 0;
// Check if the customer is a preferred customer and apply a discount if eligible
if (order.Customer.IsPreferredCustomer)
{
// Apply a 10% discount for preferred customers
discount = totalPrice * 0.1;
}
// Check if the order total exceeds a certain threshold and apply additional discount
if (totalPrice >= 1000)
{
// Apply a $50 discount for orders totaling $1000 or more
discount += 50;
}
else if (totalPrice >= 500)
{
// Apply a $20 discount for orders totaling $500 or more
discount += 20;
}
// Ensure the discount does not exceed the order total
discount = Math.Min(discount, totalPrice);
totalPrice -= discount;
Console.WriteLine($"Order Total: {totalPrice}");
// Step 4: Generate shipping label
string shippingLabel = $"Shipping label for {order.Customer.Name} at {order.Address.Street}, {order.Address.City}, {order.Address.ZipCode}";
// Step 5: Notify customer
Console.WriteLine($"Sending notification to customer {order.Customer.Name}: Your order has been shipped. Shipping label: {shippingLabel}");
}
将订单处理的每个步骤的逻辑提取到具有描述性名称的单独方法中,例如 、 、 和 。这提高了可读性,促进了代码重用,并使方法更简洁、更集中。ValidateOrderUpdateInventoryCalculateTotalPriceNotifyCustomerProcessOrder
// After refactoring
public void ProcessOrder(Order order)
{
ValidateOrder(order);
UpdateInventory(order);
CalculateTotalPrice(order);
NotifyCustomer(order);
}
private void ValidateOrder(Order order)
{
if (!order.IsValid)
{
throw new ArgumentException("Invalid order.");
}
}
private void UpdateInventory(Order order)
{
foreach (var product in order.Products)
{
// Reduce inventory by 1 for each ordered product
int updatedQuantity = product.AvailableQuantity - 1;
// Ensure the updated quantity is non-negative
updatedQuantity = Math.Max(updatedQuantity, 0);
// Update the product's available quantity
product.AvailableQuantity = updatedQuantity;
// Print a message to simulate inventory update
Console.WriteLine($"Inventory updated for product '{product.Name}': Available Quantity = {product.AvailableQuantity}");
}
}
private void CalculateTotalPrice(Order order)
{
double totalPrice = order.Products.Sum(p => p.Price);
totalPrice -= CalculateDiscount(order);
Console.WriteLine($"Order Total: {totalPrice}");
}
private double CalculateDiscount(Order order)
{
double discount = 0;
double totalPrice = order.Products.Sum(p => p.Price);
// Check if the customer is a preferred customer and apply a discount if eligible
if (order.Customer.IsPreferredCustomer)
{
// Apply a 10% discount for preferred customers
discount = totalPrice * 0.1;
}
// Check if the order total exceeds a certain threshold and apply additional discount
if (totalPrice >= 1000)
{
// Apply a $50 discount for orders totaling $1000 or more
discount += 50;
}
else if (totalPrice >= 500)
{
// Apply a $20 discount for orders totaling $500 or more
discount += 20;
}
// Ensure the discount does not exceed the order total
discount = Math.Min(discount, totalPrice);
return discount;
}
private void NotifyCustomer(Order order)
{
string shippingLabel = GenerateShippingLabel(order.Customer, order.Address);
Console.WriteLine($"Sending notification to customer {order.Customer.Name}: Your order has been shipped. Shipping label: {shippingLabel}");
}
private string GenerateShippingLabel(Customer customer, Address address)
{
return $"Shipping label for {customer.Name} at {address.Street}, {address.City}, {address.ZipCode}";
}
在此重构版本中,订单处理的每个步骤都提取到具有描述性名称的单独方法中。这通过将复杂的逻辑分解为更小、更易于管理的单元来提高可读性和可维护性。此外,它还促进了代码重用,因为如果需要,可以独立于代码库的其他部分调用提取的方法。
重复会导致维护噩梦。识别重复的代码并将其重构为可重用的组件,例如方法或类。
假设您有一个程序,您需要计算矩形、圆形和三角形的面积。最初,您可以编写代码来分别计算每个形状的面积,如下所示:
using System;
class Program
{
static void Main(string[] args)
{
double rectangleArea = CalculateRectangleArea(5, 10);
Console.WriteLine("Area of rectangle: " + rectangleArea);
double circleArea = CalculateCircleArea(7);
Console.WriteLine("Area of circle: " + circleArea);
double triangleArea = CalculateTriangleArea(3, 4, 5);
Console.WriteLine("Area of triangle: " + triangleArea);
}
// Calculate the area of a rectangle
static double CalculateRectangleArea(double length, double width)
{
return length * width;
}
// Calculates the area of a circle
static double CalculateCircleArea(double radius)
{
return Math.PI\* radius * radius;
}
// Calculates the area of a triangle
static double CalculateTriangleArea(double baseLength, double height, double side)
{
double s = (baseLength + height + side) / 2;
return Math.Sqrt(s * (s - baseLength) * (s - height) * (s - side));
}
}
在此代码中,我们有计算矩形、圆形和三角形面积的方法。每种方法采用不同的参数并相应地计算面积。
虽然这有效,但有很多重复的代码用于计算面积。如果要添加更多形状,则必须继续添加类似的方法。
更好的方法是创建一个可以计算任何形状面积的单一方法。下面介绍如何重构代码以消除重复:
class EliminateCodeDuplication
{
public static void Run()
{
// Calculate the area of a rectangle
double rectangleArea = CalculateArea(ShapeType.Rectangle, 5, 10);
Console.WriteLine("Area of rectangle: " + rectangleArea);
// Calculate the area of a circle
double circleArea = CalculateArea(ShapeType.Circle, 7);
Console.WriteLine("Area of circle: " + circleArea);
// Calculate the area of a triangle
double triangleArea = CalculateArea(ShapeType.Triangle, 3, 4, 5);
Console.WriteLine("Area of triangle: " + triangleArea);
}
// Enumeration for different shapes
enum ShapeType
{
Rectangle,
Circle,
Triangle
}
// Method to calculate the area of various shapes
static double CalculateArea(ShapeType shape, params double\[\] dimensions)
{
switch (shape)
{
case ShapeType.Rectangle:
return dimensions[0] * dimensions[1];
case ShapeType.Circle:
return Math.PI * dimensions[0] * dimensions[0];
case ShapeType.Triangle:
double s = (dimensions[0] + dimensions[1] + dimensions[2]) / 2;
return Math.Sqrt(s * (s - dimensions[0]) * (s - dimensions[1]) * (s - dimensions[2]));
default:
throw new ArgumentException("Invalid shape specified");
}
}
}
在此重构的代码中:
在许多情况下,代码重复本身并不是坏事,尤其是当重复的代码最少并且用于不同的目的时。如果重复不能显著提高可维护性或可读性,请始终优先考虑清晰度和简单性,而不是消除重复。
为了消除代码重复,一种常见的方法是使用带有可选参数的单个方法,就像我们上面所做的那样。但是,由于每个形状的参数不同,这可能不适合此方案。另一种方法可能涉及为形状创建公共接口或基类,并以多态方式实现面积计算方法。
让我们继续下一节,我们将使用 OOP 核心原则来实现相同的目标。
利用抽象、继承、封装和多态等面向对象的原则来创建模块化、可重用且更易于理解的代码。通过设计具有明确职责和关系的类,可以降低复杂性,并使代码库更加灵活和可扩展。
让我们进一步简化代码,并提供一个更清晰的示例,说明如何使用 OOP 功能来消除代码重复。我们可以通过创建一个基类并从中派生特定的形状,在每个形状类中实现面积计算方法来实现这一点。ShapeBase
方法如下:
class UseOOP
{
public static void Run()
{
Rectangle rectangle = new Rectangle(5, 10);
Console.WriteLine("Area of rectangle: " + rectangle.CalculateArea());
Circle circle = new Circle(7);
Console.WriteLine("Area of circle: " + circle.CalculateArea());
Triangle triangle = new Triangle(3, 4, 5);
Console.WriteLine("Area of triangle: " + triangle.CalculateArea());
}
}
abstract class ShapeBase
{
public abstract double CalculateArea();
}
class Rectangle : ShapeBase
{
private double Length { get; }
private double Width { get; }
public Rectangle(double length, double width)
{
Length = length;
Width = width;
}
public override double CalculateArea()
{
return Length * Width;
}
}
class Circle : ShapeBase
{
private double Radius { get; }
public Circle(double radius)
{
Radius = radius;
}
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
}
class Triangle : ShapeBase
{
private double BaseLength { get; }
private double Height { get; }
private double Side { get; }
public Triangle(double baseLength, double height, double side)
{
BaseLength = baseLength;
Height = height;
Side = side;
}
public override double CalculateArea()
{
double s = (BaseLength + Height + Side) / 2;
return Math.Sqrt(s * (s - BaseLength) * (s - Height) * (s - Side));
}
}
熟悉常见的设计模式,例如单例、工厂、观察者和策略模式等。设计模式为反复出现的设计问题提供了行之有效的解决方案,并可以帮助您以更有条理和可维护的方式构建代码。但是,请明智地使用它们,并且只有当它们真正解决了您遇到的设计挑战时。
让我们使用工厂方法设计模式改进代码示例。当您想要在不指定将要创建的对象的确切类的情况下创建对象时,Factory Method 模式非常有用。我们将创建一个来处理不同形状的创建,以便在添加新形状时轻松扩展。ShapeFactory
class FactoryPattern
{
public static void Run()
{
ShapeFactory factory = new ShapeFactory();
ShapeBase rectangle = factory.CreateShape(ShapeType.Rectangle, 5, 10);
Console.WriteLine("Area of rectangle: " + rectangle.CalculateArea());
ShapeBase circle = factory.CreateShape(ShapeType.Circle, 7);
Console.WriteLine("Area of circle: " + circle.CalculateArea());
ShapeBase triangle = factory.CreateShape(ShapeType.Triangle, 3, 4, 5);
Console.WriteLine("Area of triangle: " + triangle.CalculateArea());
}
class ShapeFactory
{
public ShapeBase CreateShape(ShapeType type, params double[] dimensions)
{
switch (type)
{
case ShapeType.Rectangle:
if (dimensions.Length != 2)
throw new ArgumentException("Invalid dimensions for rectangle.");
return new Rectangle(dimensions[0], dimensions[1]);
case ShapeType.Circle:
if (dimensions.Length != 1)
throw new ArgumentException("Invalid dimensions for circle.");
return new Circle(dimensions[0]);
case ShapeType.Triangle:
if (dimensions.Length != 3)
throw new ArgumentException("Invalid dimensions for triangle.");
return new Triangle(dimensions[0], dimensions[1], dimensions[2]);
default:
throw new ArgumentException("Invalid shape specified.");
}
}
}
}
在此重构的代码中:
此方法遵循工厂方法模式,通过集中对象创建逻辑来促进代码的可扩展性和可维护性。
让我们尝试另一种方法,使用 Strategy Pattern 重构代码。当您想要定义一系列算法、封装每个算法并使它们可互换时,可以使用策略模式。在我们的例子中,我们可以定义一系列算法来计算不同形状的面积,将每个算法封装到它自己的类中,并通过一个通用接口使它们可以互换。
以下是我们如何实现它:
class StrategyPattern
{
public static void Run()
{
Shape rectangle = new Shape(new RectangleAreaCalculator(), 5, 10);
Console.WriteLine("Area of rectangle: " + rectangle.CalculateArea());
Shape circle = new Shape(new CircleAreaCalculator(), 7);
Console.WriteLine("Area of circle: " + circle.CalculateArea());
Shape triangle = new Shape(new TriangleAreaCalculator(), 3, 4, 5);
Console.WriteLine("Area of triangle: " + triangle.CalculateArea());
}
interface IAreaCalculator
{
double CalculateArea(params double[] dimensions);
}
class RectangleAreaCalculator : IAreaCalculator
{
public double CalculateArea(params double[] dimensions)
{
if (dimensions.Length != 2)
throw new ArgumentException("Invalid dimensions for rectangle.");
return dimensions[0] * dimensions[1];
}
}
class CircleAreaCalculator : IAreaCalculator
{
public double CalculateArea(params double[] dimensions)
{
if (dimensions.Length != 1)
throw new ArgumentException("Invalid dimensions for circle.");
return Math.PI * dimensions[0] * dimensions[0];
}
}
class TriangleAreaCalculator : IAreaCalculator
{
public double CalculateArea(params double[] dimensions)
{
if (dimensions.Length != 3)
throw new ArgumentException("Invalid dimensions for triangle.");
double s = (dimensions[0] + dimensions[1] + dimensions[2]) / 2;
return Math.Sqrt(s * (s - dimensions[0]) * (s - dimensions[1]) * (s - dimensions[2]));
}
}
class Shape : ShapeBase
{
private readonly IAreaCalculator _areaCalculator;
private readonly double[] _dimensions;
public Shape(IAreaCalculator areaCalculator, params double[] dimensions)
{
_areaCalculator = areaCalculator;
_dimensions = dimensions;
}
public override double CalculateArea()
{
return _areaCalculator.CalculateArea(_dimensions);
}
}
}
此实现通过将算法封装到单独的类中并为它们提供通用接口来遵循策略模式。它通过启用可互换的算法来计算不同形状的面积,从而提高代码的灵活性和可维护性。
请注意,该类的构造函数方法是一种依赖注入形式。依赖注入是另一种设计模式,在这种模式中,类的依赖关系是从外部提供的,通常通过构造函数参数、setter 方法或接口。Shape
使用依赖注入来解耦组件,提高可测试性、灵活性和可维护性。通过注入依赖项而不是直接在类中创建依赖项,可以使代码更加模块化,更易于更改和维护。
在我们的策略模式示例中,该类需要接口的实现来执行面积计算。我们不是在类中创建特定面积计算器类的实例,而是依靠客户端代码在对象创建期间提供适当的面积计算器实例。ShapeIAreaCalculatorShape
这是通过构造函数注入实现的:
class Shape : ShapeBase
{
private readonly IAreaCalculator _areaCalculator;
private readonly double[] _dimensions;
public Shape(IAreaCalculator areaCalculator, params double[] dimensions)
{
_areaCalculator = areaCalculator;
_dimensions = dimensions;
}
public override double CalculateArea()
{
return _areaCalculator.CalculateArea(_dimensions);
}
}
通过构造函数注入面积计算器依赖性,我们将类与面积计算器的特定实现分离,使其更加灵活且易于维护。该类的客户端可以提供接口的任何实现,使它们能够在不同的计算策略之间切换,而无需修改类本身。这促进了松耦合和依赖反转的原理。ShapeShapeIAreaCalculatorShape
就像重构生产代码一样,重构测试代码以保持其整洁、可读和可维护性至关重要。结构良好且可维护的测试代码有助于更轻松的测试维护,增强测试覆盖率,并提高软件的整体可靠性。将与生产代码库相同的重构技术应用于测试代码。
使用代码分析工具和指标持续评估代码库的质量。监控代码复杂性、代码重复和代码覆盖率等指标,以确定需要重构的区域。通过定期跟踪代码质量指标,您可以主动解决潜在问题,并保持高标准的代码清洁度和可维护性。
重构遗留代码时,请谨慎行事并采用渐进方法。遗留代码库通常缺乏适当的文档和测试覆盖率,这使得它们更难安全地重构。首先确定代码库中可以增量重构的小型独立部分,并确定对应用程序的功能和稳定性至关重要的区域的优先级。
通过将代码重构纳入常规代码审查,使代码重构成为开发过程中不可或缺的一部分。在编写新功能的同时,专门留出时间审查和重构现有代码。定期重新访问和改进代码库有助于防止技术债务累积,并确保代码保持干净、可维护并与不断变化的需求保持一致。
利用 ReSharper、Visual Studio 的内置重构功能和第三方分析器等自动重构工具来识别和应用重构机会。这些工具可以简化重构过程,并帮助确保整个代码库的一致性和最佳实践的遵守性。
在整个重构过程中征求利益相关者、用户和开发团队其他成员的反馈。根据收到的反馈进行迭代,纳入建议并解决提出的任何问题。通过尽早并经常让利益相关者参与进来,可以确保重构工作与业务目标保持一致,并为最终用户带来具体的好处。
在开发团队中培养协作和知识共享的文化。鼓励开发人员对彼此的代码进行同行评审,分享见解,并讨论重构策略和最佳实践。通过利用团队的集体专业知识,您可以更有效地识别改进机会,并确保重构工作与团队的目标和标准保持一致。
记录重构决策背后的基本原理,尤其是在涉及重大更改或权衡的情况下。本文档为将来可能在代码库上工作的其他开发人员提供了宝贵的指导,确保代码更改背后的原因清晰易懂。