改变我编写代码方式

作者:微信公众号:【架构师老卢】
11-15 9:45
103

在过去的时间里,我一直在使用C#进行开发工作,并且已经准备好迎接下一个挑战了。当时我面对两位资深开发人员,其中一位是西门子的首席架构师。

面试问题一开始都是最常规的那种,比如在C#方面的经验、对.NET框架的熟悉程度,或是应对特定编码挑战的方法。

我自信满满地作答,毕竟我花了数年时间开发应用程序、构建解决方案以及解决复杂问题。至少,我当时认为自己是这样做的。

意外转折

在问了几个简单的问题之后,首席架构师给我出了一个编码挑战,这个挑战乍一看挺简单的。

开发一个基础程序,用于从CSV文件中读取数据,按照类别进行筛选,然后以一种清晰、结构化的格式输出结果。

不算太复杂,我之前都写过上百个类似的脚本了。

于是我直接上手,迅速开始用自己最熟悉的C#特性来写代码。用一个简单的StreamReader来读取文件,用List<Product>来存储产品数据,然后通过循环按照类别对它们进行筛选和展示。

以下是我当时写的代码的简化版本:

public class Product
{
    public string Name { get; set; }
    public string Category { get; set; }
    public decimal Price { get; set; }
}

public void ProcessProducts(string filePath, string categoryFilter)
{
    var products = new List<Product>();

    using (var reader = new StreamReader(filePath))
    {
        while (!reader.EndOfStream)
        {
            var line = reader.ReadLine();
            var values = line.Split(',');

            var product = new Product
            {
                Name = values[0],
                Category = values[1],
                Price = decimal.Parse(values[2])
            };
            products.Add(product);
        }
    }

    var filteredProducts = products.Where(p => p.Category == categoryFilter);

    foreach (var product in filteredProducts)
    {
        Console.WriteLine($"{product.Name}, {product.Price:C}");
    }
}

代码能运行,而且我觉得效率还挺高的。

然后他抛给我一个重磅问题:

“这是编写这段代码的最佳方式吗?”

我愣住了。这代码能运行啊,而且也满足要求了呀,还能怎样呢?

就在这时,事情变得有意思起来了。

他开始跟我说,虽然代码是正确的,但可读性很差。解决方案必须具备可维护性、可扩展性,并且要清晰明了,这样其他开发人员才能看得懂。

这时我才意识到,我之前写代码几乎完全是面向机器的,而没有考虑到最终要维护我代码的那些人。

看看我上面那个ProcessProducts方法,乍一看好像还行,但如果不逐行查看的话,不一定能明白它到底在做什么。

架构师建议为了提高可读性和可维护性对代码进行重构。

第一步,我们把职责进行了分离——将解析、筛选和输出分别放到不同的函数中。

我是这样做的:

public IEnumerable<Product> LoadProducts(string filePath)
{
    var products = new List<Product>();

    using (var reader = new StreamReader(filePath))
    {
        while (!reader.EndOfStream)
        {
            var line = reader.ReadLine();
            var values = line.Split(',');

            var product = new Product
            {
                Name = values[0],
                Category = values[1],
                Price = decimal.Parse(values[2])
            };
            products.Add(product);
        }
    }

    return products;
}

public IEnumerable<Product> FilterProductsByCategory(IEnumerable<Product> products, string category)
{
    return products.Where(p => p.Category == category);
}

public void DisplayProducts(IEnumerable<Product> products)
{
    foreach (var product in products)
    {
        Console.WriteLine($"{product.Name}, {product.Price:C}");
    }
}

这样代码就更易于阅读和维护了。

现在,如果有人想要更改产品的筛选方式或者输出方式,他们只需要修改相应的部分就行,不用在整个方法里到处查找了。

考虑异常情况

接着,这位资深架构师又提出了一些问题,让我开始思考自己的编码方式以及如何应对那些预料之外的情况。

“要是CSV文件格式有误怎么办?”他问道,“要是某个产品有缺失字段或者价格无效怎么办?”

他说得对。我之前编码的时候都是基于一种假设——假设一切都会正常,代码会在所谓的“正常流程”下运行。

但实际上,数据往往是杂乱的。正如他所解释的那样,防御性编码就是要预见到可能出现的故障并为之做好准备。就算是那些很少发生的边界情况,你也得有所规划。

基于这个经验教训,我们添加了错误处理来应对文件格式或解析方面的意外问题:

public IEnumerable<Product> LoadProducts(string filePath)
{
    var products = new List<Product>();

    using (var reader = new StreamReader(filePath))
    {
        while (!reader.EndOfStream)
        {
            var line = reader.ReadLine();
            var values = line.Split(',');

            try
            {
                var product = new Product
                {
                    Name = values[0],
                    Category = values[1],
                    Price = decimal.Parse(values[2])
                };
                products.Add(product);
            }
            catch (FormatException ex)
            {
                Console.WriteLine($"Error parsing product data: {ex.Message}");
            }
        }
    }

    return products;
}

好的代码不仅要有功能,还要具备健壮性。

设计模式

最后,他们要求我用策略模式(Strategy Pattern)来实现同样的代码,策略模式允许在运行时确定行为。我第一次用到它的时候就是在产品筛选这块。

以下是我们实现产品筛选的策略模式的方式:

public interface IProductFilterStrategy
{
    IEnumerable<Product> Filter(IEnumerable<Product> products);
}

public class CategoryFilter : IProductFilterStrategy
{
    private readonly string _category;

    public CategoryFilter(string category)
    {
        _category = category;
    }

    public IEnumerable<Product> Filter(IEnumerable<Product> products)
    {
        return products.Where(p => p.Category == _category);
    }
}

public class PriceFilter : IProductFilterStrategy
{
    private readonly decimal _minPrice;
    private readonly decimal _maxPrice;

    public PriceFilter(decimal minPrice, decimal maxPrice)
    {
        _minPrice = minPrice;
        _maxPrice = maxPrice;
    }

    public IEnumerable<Product> Filter(IEnumerable<Product> products)
    {
        return products.Where(p => p.Price >= _minPrice && p.Price <= _maxPrice);
    }
}

测试驱动开发

在面试快结束的时候,架构师提到了测试用例这个话题,这差不多是最后一个问题了。

先进行测试能够确保你发现边界情况,并且能确切知道代码是否准确实现了你想要的功能。

以下是使用NUnit对我们的产品筛选功能进行的一个简单测试:

[TestFixture]
public class ProductFilterTests
{
    [Test]
    public void CategoryFilter_ShouldReturnOnlyMatchingProducts()
    {
        var products = new List<Product>
        {
            new Product { Name = "Laptop", Category = "Electronics", Price = 1000 },
            new Product { Name = "Book", Category = "Books", Price = 20 }
        };

        var categoryFilter = new CategoryFilter("Electronics");
        var filteredProducts = categoryFilter.Filter(products);

        Assert.AreEqual(1, filteredProducts.Count());
        Assert.AreEqual("Laptop", filteredProducts.First().Name);
    }

    [Test]
    public void PriceFilter_ShouldReturnProductsWithinPriceRange()
    {
        var products = new List<Product>
        {
            new Product { Name = "Laptop", Category = "Electronics", Price = 1000 },
            new Product { Name = "Book", Category = "Books", Price = 20 }
        };

        var priceFilter = new PriceFilter(50, 2000);
        var filteredProducts = priceFilter.Filter(products);

        Assert.AreEqual(1, filteredProducts.Count());
        Assert.AreEqual("Laptop", filteredProducts.First().Name);
    }
}

我发现通过先写测试用例,我的代码自然而然地变得更模块化、更易于维护了。

但除此之外,它还让我有信心确保后续的修改不会破坏现有的功能。

我最终没得到那份工作,但那次面试彻底改变了我对编码的看法。

从那以后,我开始编写清晰、可维护、可扩展的代码。更重要的是,我开始考虑谁会来阅读和维护我的代码了。

我更加刻意地去运用设计模式,妥善地处理错误,并且确保我的代码有足够的扩展性,能够满足未来的需求。我开始先写测试用例,并且更深入地思考边界情况以及意外的输入情况了。

相关留言评论
昵称:
邮箱:
阅读排行