You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何正确实现C#继承与接口以解决两类员工的代码异味?

问题描述

我有承包商(contractors)和正式员工(permanent staff)两类员工,需要检索合同到期日期在一年内的承包商,原代码存在代码异味,尝试重构后因继承与接口实现错误导致类型转换问题无法运行,请求正确的C#面向对象实现方案。

原代码(模型)

using System;

namespace Employee.Model
{
    public class IEmployee
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public DateTime ContractEndDate { get; set; }
    }
}

原测试代码

namespace Employee.Tests
{
    public class EmployeeTests
    {
        public static IEmployee SampleData = new[]
        {
            new IEmployee { Id = 1, Name = "permanent staff 1" },
            new IEmployee { Id = 2, Name = "contractor 1", ContractEndDate = new DateTime(2025, 12, 30) },
            new IEmployee { Id = 3, Name = "permanent staff 2" },
            new IEmployee { Id = 4, Name = "contractor 2", ContractEndDate = new DateTime(2027, 12, 30) }
        };

        [Test]
        public void Contract_Matched()
        {
            // Expected Data
            int countEndWithin2Years = 1;

            // Test
            var testCount = ECalculator.GetEndWithinXYears(EmployeeTests.SampleData);

            //Assert
            Assert.That(countEndWithin2Years, Is.EqualTo(testCount));
        }
    }
}

错误的重构尝试

public interface IPermanent
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Permanent : IPermanent
{
    private int id;
    private string name;

    public int Id
    {
        get { return id; }
        set { id = value; }
    }

    public string Name
    {
        get { return name; }
        set { name = value; }
    }
}

public interface IContractor: IPermanent
{
    public DateTime ContractEndDate { get; set; }        
}

public class Contractor : IContractor
{
    private int id;
    private string name;
    private DateTime contractEndDate;

    public int Id
    {
        get { return id; }
        set { id = value; }
    }

    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    public DateTime ContractEndDate
    {
        get { return contractEndDate; }
        set { contractEndDate = value; }
    }
}

public interface IEmployee : IPermanent, IContractor
{
}

修正后的测试用例(仍有问题)

public static IEnumerable<IEmployee> SampleData = new[]
{
    (IEmployee)new Permanent { Id = 1, Name = "permanent staff 1" },
    (IEmployee)new Contractor { Id = 2, Name = "contractor 1", ContractEndDate = new DateTime(2025, 12, 30) },
    (IEmployee)new Permanent { Id = 3, Name = "permanent staff 2" },
    (IEmployee)new Contractor { Id = 4, Name = "contractor 2", ContractEndDate = new DateTime(2027, 12, 30) }
};

[Test]
public void Contract_Matched()
{
    // Expected Data
    int countEndWithin2Years = 1;

    // Test
    var testCount = ECalculator.GetEndWithinXYears(EmployeeTests.SampleData);

    //Assert
    Assert.That(countEndWithin2Years, Is.EqualTo(testCount));
}

正确的C#面向对象实现方案

1. 模型层重构

修正命名规范,拆分不同类型员工的职责,用接口定义通用属性,具体类实现专属逻辑:

using System;

namespace Employee.Model
{
    // 通用员工接口,定义所有员工共有的属性
    public interface IEmployee
    {
        int Id { get; set; }
        string Name { get; set; }
    }

    // 正式员工类,仅实现通用员工属性
    public class PermanentEmployee : IEmployee
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    // 承包商类,继承通用员工接口并添加合同到期属性
    public class ContractorEmployee : IEmployee
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public DateTime ContractEndDate { get; set; }
    }
}

重构说明

  • 修正错误命名:类名不再使用I前缀(I是C#接口的标准命名前缀)
  • 拆分职责:正式员工无需ContractEndDate属性,避免无效字段的冗余
  • 使用自动属性简化代码,无需手动编写私有字段与get/set逻辑

2. 计算器逻辑实现

实现筛选合同到期承包商的核心逻辑,确保类型安全与逻辑正确:

using System;
using System.Collections.Generic;
using System.Linq;
using Employee.Model;

namespace Employee.Calculator
{
    public static class ECalculator
    {
        // 筛选合同到期时间在指定年限内的承包商数量,默认年限为1年
        public static int GetEndWithinXYears(IEnumerable<IEmployee> employees, int years = 1)
        {
            if (employees == null)
                throw new ArgumentNullException(nameof(employees));

            return employees.OfType<ContractorEmployee>()
                .Count(contractor => contractor.ContractEndDate <= DateTime.Now.AddYears(years));
        }
    }
}

逻辑说明

  • 使用OfType<ContractorEmployee>()精准过滤承包商类型,避免类型转换错误
  • 用Count()统计符合到期时间条件的承包商数量
  • 添加参数默认值,满足"一年内"的默认业务需求
  • 增加空值校验,防止空引用异常

3. 修正后的测试代码

修复类型错误,优化测试场景,确保测试逻辑可靠:

using System;
using System.Collections.Generic;
using Employee.Model;
using Employee.Calculator;
using NUnit.Framework;

namespace Employee.Tests
{
    public class EmployeeTests
    {
        public static IEnumerable<IEmployee> SampleData = new List<IEmployee>
        {
            new PermanentEmployee { Id = 1, Name = "permanent staff 1" },
            new ContractorEmployee { Id = 2, Name = "contractor 1", ContractEndDate = DateTime.Now.AddMonths(10) }, // 10个月内到期
            new PermanentEmployee { Id = 3, Name = "permanent staff 2" },
            new ContractorEmployee { Id = 4, Name = "contractor 2", ContractEndDate = DateTime.Now.AddYears(3) } // 3年后到期
        };

        [Test]
        public void Contract_EndsWithinOneYear_CountMatches()
        {
            int expectedCount = 1;
            int actualCount = ECalculator.GetEndWithinXYears(SampleData);
            Assert.That(actualCount, Is.EqualTo(expectedCount));
        }

        [Test]
        public void Contract_EndsWithinTwoYears_CountMatches()
        {
            int expectedCount = 1;
            int actualCount = ECalculator.GetEndWithinXYears(SampleData, 2);
            Assert.That(actualCount, Is.EqualTo(expectedCount));
        }
    }
}

测试修正说明

  • 使用List<IEmployee>替代数组,类型声明更清晰
  • 采用相对时间设置到期日期,避免硬编码日期导致测试失效
  • 重命名测试方法为语义化名称,符合测试规范
  • 增加多场景测试,验证不同年限参数的逻辑正确性

内容的提问来源于stack exchange,提问作者Jeroen Denny

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 18:44:54