一天一点代码坏味道(3)

作为一个后端工程师,想必在职业生涯中都写过一些不好维护的代码。本文是我学习《代码之丑》的学习笔记,今天第三天,品品大类和长参数列表的味道。

上一篇:一天一点代码坏味道(2)

1 大类

对于我们来说,一个人理解的东西是有限的,没有人能够同时面对所有细节

因此,人类选择面对复杂事物的解决方案都是分而治之。

那么,如果一个类里面的内容太多,它就会超过一个人的理解范畴。

问题来了,大类是如何变大的?

职责不单一

单一职责原则是衡量软件设计好坏的一把简单而有效的尺子,通常来说,很多类之所以巨大,大部分情况下都是因为其违反了这个原则。

坏味道代码:

public class User
{
    public long UserId { get; set; }

    public string Name { get; set; }

    public string NickName { get; set; }

    public string Email { get; set; }

    public string PhoneNumber { get; set; }

    public AuthorType AuthorType { get; set; }

    public ReviewStatus AuthorReviewStatus { get; set; }

    public EditorType EditorType { get; set; }
    ...
}

有经验的童鞋应该一眼就发现了其中包含了不同类型的用户的信息,既有用户基本信息,还有作者相关信息,最后还有编辑类型...

三种不同的角色,三种不同诉求的业务方关心的是不同的内容,只是因为她们都是这个系统的用户,就把它们都放在了用户类中。后续需求一变动,这个用户类就会被反复修改。

针对上面这个场景,需要对其中的不同角色进行拆分:

public class User
{
    public long UserId { get; set; }

    public string Name { get; set; }

    public string NickName { get; set; }

    public string Email { get; set; }

    public string PhoneNumber { get; set; }
    ...
}

public class Author
{
    public long UserId { get; set; }

    public AuthorType AuthorType { get; set; }

    public ReviewStatus AuthorReviewStatus { get; set; }
    ...
}

public class Editor
{
    public long UserId { get; set; }

    public EditorType EditorType { get; set; }
    ...
}

字段未分组

拆分之后,User类还是很大,再仔细看,发现其实可以将部分字段进行分组, 比如Email和PhoneNumber都属于用户的联系方式,便可以再次分解。

这里引入一个Contact类,将Email和PhoneNumber放了进去,以后如果还有其他联系方式如QQ、微信之类的需求,也都可以统一放到Contact类中。

public class User
{
    public long UserId { get; set; }

    public string Name { get; set; }

    public string NickName { get; set; }

    public Contact Contact { get; set; }
    ...
}

public class Contact
{
    public string Email { get; set; }

    public string PhoneNumber { get; set; }
    ...
}

由此,我们可以看出,将大类分解成小类,其实也是在做设计工作。欢迎体会软件设计之美

2 长参数列表

方法之间传递参数再常见不过,但是如果不限制参数个数,长参数列表就会出现在你我的项目代码之中,它带来的不可维护度是巨大的。

和大类一样,我们也需要对长参数列表进行拆解。

那么,有哪些拆解方式呢?

将参数列表封装成对象

这是一个熟知的重构方法,记得我在10年前阅读王涛老师《你必须知道的.NET》一书中就了解了这个技巧。

那么,不妨看一个长参数列表的坏味道:

public void CreateBook(string title, string introduction, 
    URL coverUrl, BookType type,
    BookChannel channel, string protagonists,
    string tags, bool completed)
{
    var book = new Book()
    {
        Title = title,
        Introduction = introduction,
        CoverUrl = coverUrl,
        Type = type,
        Channel = channel,
        Protagonists = protagonists,
        Tags = tags,
        Completed = completed
    };
    _repository.Save(book);
}

将其封装为一个类型:

public class NewBookParameters
{
    public string Title { get; set; }

    public string Introduction { get; set; }

    public URL CoverUrl { get; set; }

    public BookType Type { get; set; }

    public BookChannel Channel { get; set; }

    public string Protagonists { get; set; }

    public string Tags { get; set; }

    public bool Completed { get; set; }
}

那么,CreateBook方法就改为这个样子?

public void CreateBook(NewBookParameters parameters)
{
    var book = new Book()
    {
        Title = parameters.Title,
        Introduction = parameters.Introduction,
        CoverUrl = parameters.CoverUrl,
        Type = parameters.Type,
        Channel = parameters.Channel,
        Protagonists = parameters.Protagonists,
        Tags = parameters.Tags,
        Completed = parameters.Completed
    };
    _repository.Save(book);
}

我想,可能我们还是会觉得怪怪的,没有什么大的简化。那么,如果我们给NewBookParameters方法再改改呢?

public class NewBookParameters
{
    public string Title { get; set; }

    public string Introduction { get; set; }

    public URL CoverUrl { get; set; }

    public BookType Type { get; set; }

    public BookChannel Channel { get; set; }

    public string Protagonists { get; set; }

    public string Tags { get; set; }

    public bool Completed { get; set; }

    public Book NewBook()
    {
        return new Book()
        {
            Title = this.Title,
            Introduction = this.Introduction,
            CoverUrl = this.CoverUrl,
            Type = this.Type,
            Channel = this.Channel,
            Protagonists = this.Protagonists,
            Tags = this.Tags,
            Completed = this.Completed
        };
    }
}

这个时候的CreateBook方法就可以极大简化了:

public void CreateBook(NewBookParameters parameters)
{
    var book = parameters.NewBook();
    _repository.Save(book);
}

一般情况下,将长长的参数列表封装为一个类,可以解决大部分场景下的问题。

动与静的分离

还有一些场景,不能简单地将长参数封装为一个类,比如有些原本属于静态结构的部分却以动态参数的方式进行传递,无形之间使得参数列表变长了。

那么,不妨看一个这样的坏味道:

public void GetChapters(
  long bookId, 
  HttpClient httpClient, 
  ChapterProcessor processor)
{
    var requestUri = GenerateRequestUri(bookId);
    var response = httpClient.GetAsync(requestUri).Result;
    var chapters = GenerateChapters(response);
    processor.Process(chapters);
}

在这三个参数中,几乎每次传递的bookId是不一样的,但是httpClient和processor却是一样的。换句话说,bookId是变化的,而httpClient和processor却是不怎么变化的。

用专业术语来讲,这就是动数据与静数据的耦合,需要将其拆开。可以将静态不变的数据作为所在类的一部分,通过依赖注入的方式注入进去即可。

重构代码如下:

public void GetChapters(long bookId)
{
    var requestUri = GenerateRequestUri(bookId);
    var response = _httpClient.GetAsync(requestUri).Result;
    var chapters = GenerateChapters(response);
    _processor.Process(chapters);
}

移除标记参数

有些时候,我们喜欢将flag参数写在参数列表中,各种flag满天飞,一不小心堆积多了,也就会容易产生混乱。

比如,下面这个坏味道:

public void EditChapter(
  long chapterId, 
  string title, 
  string content, 
  bool isApproved)
{
  ...
}

之所以有最后这个flag参数,是因为逻辑代码会根据这个flag参数走不通的处理流程。于是,又到了追问自己的时刻,这个方法的初心(业务)是为了什么?

为了贴近业务,对于flag参数需要适量移除:

// 普通编辑,需要审核
public void EditChapter(long chapterId, string title, string content)
{
  ...
}
// 资深编辑,无须审核
public void EditChapterWithApproval(long chapterId, string title, string content)
{
  ...
}

可以看到,分解成两个方法之后,就消除了flag参数,但需要注意的是不要重复,对于公共部分需要封装尽可能复用以保持两个方法的尽可能独立。

3 小结

本文总结了两类坏味道,一是大类,二是长参数列表。无论是长函数方法、大类 还是 长参数列表,它们的背后都在告诉我们一件事情,即编写“短小”的代码的重要性,而要编写“短小”的代码,需要我们在设计的时候就能“分离关注点”。

最后,感谢郑晔老师的这门《代码之丑》课程,让我受益匪浅!我也诚心把它推荐给关注Edison的各位童鞋!

参考资料

郑晔,《代码之丑》(推荐订阅学习)

Martin Flower著,熊杰译,《重构:改善既有代码的设计》(推荐至少学习第三章)

上一篇:cmd界面退出当前程序的命令、查看端口,更新注册表


下一篇:java 泛型 类型形参(Type Parameters) 通配符(wildcard)边界(Bound)