在过去的时间里,我一直在使用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);
}
}
我发现通过先写测试用例,我的代码自然而然地变得更模块化、更易于维护了。
但除此之外,它还让我有信心确保后续的修改不会破坏现有的功能。
我最终没得到那份工作,但那次面试彻底改变了我对编码的看法。
从那以后,我开始编写清晰、可维护、可扩展的代码。更重要的是,我开始考虑谁会来阅读和维护我的代码了。
我更加刻意地去运用设计模式,妥善地处理错误,并且确保我的代码有足够的扩展性,能够满足未来的需求。我开始先写测试用例,并且更深入地思考边界情况以及意外的输入情况了。
如果你喜欢我的文章,请给我一个赞!谢谢