• 一天一点代码坏味道(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著,熊杰译,《重构:改善既有代码的设计》(推荐至少学习第三章)

    扫码订阅《代码之丑》

    转载自http://www.cnblogs.com/edisonchou/

  • 相关阅读:
    记Java程序Log4J漏洞的解决过程
    IIS中应用程序池自动停止,重启报错
    如何查看域名所对应的证书有效期?
    查看前端Vue版本命令
    WCF中常见的报错:The content type text
    dotnet 将自动代码格式化机器人带入团队 GitLab 平台
    WPF 引用第三方库的控件在设计器加上设计时数据和属性
    dotnet OpenXML 聊聊文本段落对齐方式
    WPF 布局 在有限空间内让两个元素尽可能撑开的例子
    dotnet 通过 DockerfileContext 解决项目放在里层文件夹导致 VisualStudio 构建失败
  • 原文地址:https://www.cnblogs.com/wangsea/p/15904163.html
Copyright © 2020-2023  润新知