【问题标题】:Refactor complex constructor that calls 'base' certain levels deep重构复杂的构造函数,调用“基础”某些级别的深度
【发布时间】:2020-02-29 20:01:27
【问题描述】:

最后,我阅读了很多关于遗留代码以及如何处理它的内容。 我有一个示例项目,其中包含编写得很糟糕的代码,我想对其进行重构。

注意:没有适当的测试,所以我想先添加一些测试,以确保我不会因为重构而破坏任何东西,但我不知道如何测试它。

给定以下 ULM 图:

我想为 Manager 类中的方法编写一个 UT(注意:这些不显示在 ULM 中)。 问题是 Manager 类需要一个继承了几个层次的 User 对象。

为简单起见,我并没有完全绘制完整的图形,但我们假设“用户”对象的构造函数需要多个参数,并且所有这些参数都表示具有这样一个继承链的对象。 我不允许使用 null,因为这些对象在继承级别的某个地方使用,并且当我使用“null”时会引发“Null Exception”。

我想练习 TDD 以向 Manager 类添加更改,但因此,我首先需要能够构造一个“Manager”类。 重构所有其余部分以仅制作 Manager 是不可接受的。

我知道一种技术,就是将“用户”参数更改为接口,但将接口用于“用户”感觉不对。

有什么建议吗?

【问题讨论】:

  • 从代码中,我假设您需要有非空的User 对象,这是您要添加的第一个测试。其余的将按照逻辑流程进行。对于代码的每一部分,您都需要创建一个测试。从if 语句开始,然后执行实际逻辑(过程输入/输出)以确保结果保持不变。
  • 我不清楚,问题出在哪里?
  • 问题是我无法使用“Null”对象创建“UserService”,也无法使用“Null”参数创建“用户”。想象这条链要长得多,我是否需要构建整个依赖对象树才能使这个可测试..
  • 如果你想测试UserService 的行为而不用担心User,你可以用Moq 之类的东西来模拟它
  • 类图可能有助于描述这个问题。

标签: c# refactoring


【解决方案1】:

User 类似乎违反了单一职责原则,从命名上看,它既充当 POCO,又充当服务。

如果是这样的话,创建一个接口确实是正确的排序术语解决方案,直到您也重构 User 并将其分解为多个类。不过 POCO 通常不会转向接口。服务几乎总是转向接口并松散耦合。

如果它只是改变自己的状态,那么你可以很好地使用它。这个想法是单独测试每个类。如果 User 类没有外部依赖项,我会从创建复杂对象开始,不管有多忙。

如果现在太多了,您可以创建一个Fake 类。像这样扩展用户:

public class FakeUser: User {

}

并提供一个伪造的对象来驱动你的UserService的执行

【讨论】:

  • 上面的代码只是一个示例。事实上,我有一个我想测试的对象,它有一个依赖项,但该依赖项的继承性降低了 10 级。我需要做很多设置代码才能调用我的构造函数。那么,救援界面?
  • 还要检查 fake 选项。你可以用一个假的来包装这个类并避免接口。
  • Fake 类不是这里的解决方案,因为 'User' 类有一个构造函数,所以 'Fake' 类的构造函数需要调用 'base' 构造函数。那个“基础”构造函数会做一些逻辑,所以我需要在“假”类中完成整个设置。为了能够编写单元测试而在所有地方添加接口对我来说似乎是错误的......
  • 无论如何你都需要做一些肮脏的事情。您可以在 User 类中创建一个空的或替代的构造函数,它什么都不做,并在您的假中调用该基本版本。幸运的是,单元测试指导更好的代码,因此它不允许变通方法。
  • 请查看我在原始问题中添加的 ULM。它可能会给你一些额外的见解:-)
【解决方案2】:

我认为最简单的解决方案是代理设计模式。您将需要创建一个 ProxyUser ,其中仅包含来自 User 的所需属性和方法(我的意思是只包含 Manager 使用的东西)。然后在你的单元测试中,你可以模拟那个 ProxyUser。

public class Manager
{
    public User User {get;set;}
    public void DoSomething()
    {
        Console.WriteLine(User.Name);
    }
}

public abstract class BaseUser
{
    public virtual string Name {get;}
}

public class ProxyUser : BaseUser
{
    private User _realUser;
    public override string Name => _realUser.Name;
}

public class User : BaseUser
{
    public override string Name {get;set;}
}

然后,在您的单元测试中,您模拟 BaseUser

【讨论】:

    【解决方案3】:

    我建议进行控制反转重构。 这将允许您以某种方式使用一个界面,该界面在其他人尝试过的“模式”中使用它们。 (原谅我,如果这是光顾我不知道你的经验水平)

    所以你基本上:

    [Fact]
        public void ManagerTest()
        {
            // arrange
            MockedUser mockedUser = new MockedUser();
    
            // act
            Manager manager = new Manager(mockedUser);
    
            // assert 
            Assert.Equal("Firstname", manager.GetUser().GetFirstName());
        }
    
    
    public class Manager
        {
            private IUser _user; 
    
            public Manager(IUser user)
            {
                _user = user;
            }
    
            public IUser GetUser()
            {
                return _user;
            }
        }
    
    
    public class MockedUser : IUser
        {
            public string GetFirstName()
            {
                return "Firstname";
            }
    
            public string GetFullName()
            {
                return "Firstname Middlename Lastname";
            }
    
            public string GetLastName()
            {
                return "Lastname";
            }
    
            public string GetMiddleNames()
            {
                return "Middlename";
            }
    
            public void SetFirstName(string firstname)
            {
                return;
            }
    
            public void SetFullName(List<string> fullName)
            {
                return;
            }
    
            public void SetLastName(string lastname)
            {
                return;
            }
    
            public void SetMiddleNames(List<string> middleNames)
            {
                return;
            }
        }
    
    public interface IUser : INames
        {
            void SetFirstName(string firstname);
    
            void SetLastName(string lastname);
    
            void SetMiddleNames(List<string> middleNames);
    
            void SetFullName(List<string> fullName);
    
            string GetFullName();
    
            string GetFirstName();
    
            string GetLastName();
    
            string GetMiddleNames();
        }
    

    从技术上讲,到目前为止,它还没有用户内部的内容。它被一个接口取代。 (你可能会有不同的方法:))

    从技术上讲,“真正的” IUser 对象做什么并不重要,您的模拟可以返回一些硬编码的垃圾。

    然后,当您下线实现 IoC 时,您可以先实现接口的单元测试。然后是重构。

    但是,如果您想测试遗留代码,您会受到限制,即必须按原样测试代码,而这并不容易测试。

    您必须先艰难地完成它,然后再进行重构。 但是根据您的时间限制,这可能无效。

    【讨论】:

      猜你喜欢
      • 2022-12-03
      • 1970-01-01
      • 2017-10-27
      • 1970-01-01
      • 2015-05-28
      • 2015-11-30
      • 2012-01-02
      • 1970-01-01
      • 2017-08-17
      相关资源
      最近更新 更多