C#接口:理解奖品表单接线(Tim Corey,第09课)
在 Tim Corey 的《C# 应用程序从头到尾》系列中,第 09 课重点是连接奖品窗体。 表面上看,这个窗体似乎简单——只需收集用户输入,验证,创建模型并保存。 但 Tim 解释真正的复杂性在于决定在哪里保存数据:数据库、文本文件还是两者兼有。 Tim 的视频通过介绍 C# 编程中的核心概念:接口,带我们走过解决方案。
在本文中,我们将通过 Tim 的讲解深入研究接口,以便您更好地理解它们如何帮助构建可扩展的、可维护的应用程序。
问题:我们在哪里保存数据?
Tim 开始陈述奖品窗体的目的:它接受输入,验证并将其保存到存储中。 但他警告说,棘手的部分是决定在哪里存储数据。 他强调教程往往跳过这一点,因为这并不容易,但他希望学习者正面解决这一问题。
他解释说,最初您可能会尝试一个简单的解决方案:检查您是使用 SQL 还是文本文件,然后执行正确的保存过程。 但 Tim 很快展示这样做如何变得丑陋且难以维护。 如果每个窗体都必须检查使用哪种存储类型,代码会变得重复、杂乱且难以更改。
丑陋的方式:硬编码条件
Tim 勾画了一个伪代码示例。 他解释可能会开始检查一个布尔值,如 usingSQL == true,然后打开数据库连接,保存模型,并返回带有 ID。 然后您可能对文本文件做相同的操作,手动生成一个 ID,因为文本文件无法自动生成。
他说这很快变得重复。多个窗体需要此逻辑,每次您添加新数据源如 MySQL 时,都必须更新每个窗体。 Tim 称这"不可扩展"并强调它违反了"DRY"原则(不要重复自己)。 他明确表示:"一定有更好的方法。"
拉动线程:更好的方法
Tim介绍了他的策略:拉动线程。 他从询问代码需要哪些信息以及信息来自哪里开始。 他识别出了两个关键问题:
我们如何知道使用哪个数据源?
我们如何连接两个不同的数据源以完成相同的任务?
Tim解释说,唯一不同的是保存的实际操作。 从表单的角度来看,它只需要说:"这是模型。 保存它。"表单不应该关心是保存到SQL还是文本文件。
解决方案:全局配置 + 接口
Tim建议使用全局配置系统。 他说,为了知道要使用哪个数据源,应用程序需要全球可访问的数据,并建议使用静态类来存储此信息。 他承认通常避免使用全局变量,但在这种情况下,确实需要全局数据。
接下来,Tim解释了关键概念:接口。 他将接口定义为合约——任何实现它的类将包含某些方法或属性的承诺。 Tim强调,这使应用程序能够调用相同的方法,而不论数据源。 表单不在乎是SQL还是文本文件;它只关心调用该方法。
Tim说:"如果您需要执行相同的任务,但在幕后是通过两种不同方式完成,您需要一个接口。"
创建接口
Tim通过在Tracker库中创建一个接口来进入实际实现。 他将其命名为IDataConnection,并解释了以"I"开头的接口命名惯例。 他强调这对于清晰地将其识别为接口很重要。
Tim在接口中添加了一个方法:
PrizeModel CreatePrize(PrizeModel model);他解释说,这个方法是一个合同:它必须存在于任何实现IDataConnection的类中。 表单将调用此方法,并期望返回一个带有ID的PrizeModel。 Tim解释这是表单如何对存储类型保持无偏见的方法。
创建全局配置静态类
接下来,Tim创建了一个名为GlobalConfig的静态类。他解释说,静态类不能被实例化,并且是全球可访问的。 这是应用程序将存储可用数据连接列表的地方。
他定义了一个属性:
public static List<IDataConnection> Connections { get; private set; }Tim解释使用私有设置,以便只有类本身可以修改列表,而应用程序的其他部分只能读取。
然后他创建了方法:
public static void InitializeConnections(bool database, bool textFiles)此方法设置可用的数据连接。 Tim强调列表允许多个连接,这意味着应用程序可以保存到SQL、文本文件或两者。
理解接口:一个现实世界的例子
Tim停下来安抚学习者,这是复杂的材料,但可以实现。 他建议先看一次视频,然后边看边编码。
他解释说,接口是一种合同,任何实现它的类都必须遵循合同。 他通过创建一个实现IDataConnection的SQLConnector类来演示这一点。
创建类时,Visual Studio会警告合同未履行。 Tim演示如何使用"Implement Interface"自动生成CreatePrize方法。他还解释了NotImplementedException框架及其存在的原因——它允许代码编译,而不假装方法有效。
创建SQL和文本连接器
Tim添加了SQLConnector类和TextConnector类,均实现IDataConnection。 他解释尽管保存到SQL数据库和保存到文本文件是非常不同的过程,但它们都满足相同的接口合同。
他现在添加了一个简单的示例返回值,并放置了TODO注释以提醒自己在稍后实现真正的保存逻辑。 这使应用程序在继续课程的同时仍能正常运行。
最终设置:连接全局配置
Tim回到GlobalConfig类并连接实际的连接。 他展示了如何初始化Connections列表并添加SQLConnector和TextConnector的实例。
他解释为什么需要两个独立的if语句而不是if-else——因为用户可能希望同时保存到两个数据源。
在哪里调用InitializeConnections?
Tim解释说,InitializeConnections必须在应用程序启动时调用。 他修改Program.cs并调用:
GlobalConfig.InitializeConnections(true, true);在启动表单之前。 这确保了连接列表准备就绪,并可在整个应用程序中访问。
然后,他将启动表单更改为CreatePrizeForm,以便可以立即测试功能。
验证奖项表单
Tim打开表单并解释第一个任务:验证四个字段。 他更喜欢保持事件处理程序简洁,因此创建了一个名为ValidateForm()的私有方法。
Tim解释此方法可以从任何地方调用,不仅仅是按钮点击。 它返回一个布尔值,指示表单是否有效。 他展示了他使用输出变量的模式:
bool output = true; return output;他说他喜欢以true开始,因为在出现问题时将其更改为false比在每次检查后设置为true更容易。
检查名次编号
Tim解释第一个验证:名次编号必须大于零的整数。
他使用int.TryParse将PlaceNumberValue.Text(字符串)转换为整数。 Tim分解了TryParse的工作原理:
它接受一个字符串,并尝试将其转换为数字。
它返回一个指示成功或失败的布尔值。
- 它使用输出参数输出转换后的值。
Tim强调TryParse比Parse更安全,因为它不会因错误的输入而崩溃——而是返回false并将输出设置为零。
然后他解释了逻辑:
如果placeNumberValidNumber为false,则设置output = false。
- 如果placeNumber < 1,则设置output = false。
Tim警告在此处使用else语句是不合适的,因为此方法具有多个检查。 如果一个检查失败,方法仍应评估其他检查以收集所有错误。
验证名次名称
Tim转向下一个验证:名次名称不能为空。
他检查:
if (placeNameValue.Text.Length == 0) { output = false; }Tim解释在真实应用中,您将为每个失败的验证显示错误消息。 但现在,他保持简单,只返回true/false。
验证奖金额与奖项比例
Tim解释表单必须包含奖金额或奖项比例(其中之一必须大于零)。 他指出重要的不同:
奖项比例是一个整数(int)
- 奖金额是一个小数(decimal),因为金钱可以包含分。
他创建了变量:
decimal prizeAmount = 0; int prizePercentage = 0;然后他对两者使用TryParse:
bool prizeAmountValid = decimal.TryParse(prizeAmountValue.Text, out prizeAmount); bool prizePercentageValid = int.TryParse(prizePercentageValue.Text, out prizePercentage);Tim解释两者都必须是有效数字。 如果任一无效,表单则无效。
接下来,他检查至少一个大于零:
if (prizeAmount <= 0 && prizePercentage <= 0) { output = false; }Tim还添加了一个检查以确保百分比在0和100之间:
if (prizePercentage < 0 ||prizePercentage > 100) { output = false; }他解释原因:150%意味着您将发放超过奖品池的东西,这是不可能的。
使用验证结果
所有检查完成后,Tim解释如何使用结果:
if (ValidateForm()) { // create model and save } else { MessageBox.Show("此表单包含无效信息。 请检查并重试。"); }Tim指出您可以在第一次失败时提早返回,但他选择运行所有检查,以便用户可以一次看到所有验证错误。 这样可以减少挫折,因为他们可以一次性修复一切问题。
创建PrizeModel
Tim解释表单一旦有效,下一步是创建PrizeModel。
他演示如何实例化一个模型:
PrizeModel model = new PrizeModel(); model.PlaceName = placeNameValue.Text; model.PlaceNumber = placeNumberValue.Text; // 问题:这是一个字符串Tim强调问题:PlaceNumber是一个int,但表单值是一个字符串。 为了解决这个问题,他解释了两种选择:
在表单中再次解析每个值(重复)。
- 在PrizeModel中添加一个构造函数重载。
Tim选择了选项2。
在PrizeModel中重载构造函数
Tim添加了一个重载的构造函数,它接受四个字符串:
public PrizeModel(string placeName, string placeNumber, string prizeAmount, string prizePercentage)
{
PlaceName = placeName;
PlaceNumber = int.TryParse(placeNumber, out int placeNumberValue) ? placeNumberValue : 0;
PrizeAmount = decimal.TryParse(prizeAmount, out decimal prizeAmountValue) ? prizeAmountValue : 0;
PrizePercentage = double.TryParse(prizePercentage, out double prizePercentageValue) ? prizePercentageValue : 0;
}Tim解释不在乎解析是否失败,因为它将默认为零,这本来就是数字的默认值。
这个构造函数允许表单直接使用字符串输入创建PrizeModel,并让模型处理解析。
使用IDataConnection保存模型
模型存在后,Tim解释如何使用全局连接列表保存它。
他使用foreach循环:
foreach (IDataConnection db in GlobalConfig.Connections) { db.CreatePrize(model); }
Tim解释这段循环调用每个连接(SQL和文本文件)上的CreatePrize()。 即使方法尚未实现,表单也能正常工作并假装保存数据。 这证明了接口和全局配置模式是有效的。
测试表单
Tim强调尽早测试的重要性。 他添加断点并运行应用程序。
他首先测试一个空表单。
他逐步执行ValidateForm()。
他发现输出为false,验证失败,如预期一样。
然后他填入有效的数据,确认构造函数正确初始化模型。
- 他还确认循环遍历了两个连接。
Tim演示了表单是功能正常的,并且模式验证成功。
最终清理:清空表单
Tim做了一些最后的调整:
成功创建奖品后,清空表单字段。
- 为奖金额和百分比设置默认值为0,以便用户每次不必输入零。
然后他确认表单在有效提交后正确清空。
接下来是什么?
Tim以视频结束,并表示下一步是连接SQL和文本连接类,以便实际保存数据。
他提醒观众关注下一课,在那里他将实现SQL连接器并实际连接到数据库。

