代码之丑(第二期)

337 阅读4分钟

在日常编程中,优雅的代码总是千篇一律,而“丑陋”的代码确是千奇百怪,“代码之丑”第二期,一起来看看吧

一、大类

如果一个类,代码杂糅在一起,你一眼看上去就不想读,这大概率是个“大类“(这里的类特指实体类)。

大类的产生

  • 职责不单一

    public class User {
      private long userId;
      private String name;
      private String nickname;
      private String email;
      private String phoneNumber;
      private AuthorType authorType;
      private ReviewStatus authorReviewStatus;
      private EditorType editorType;
      ...
    }
    

    上述代码,将用户字段、作者字段、编辑字段揉在一起,但是他们并不是并行字段,一个人不能是普通用户并同时拥有作者字段和编辑字段,这样后续再有不同职责的“用户”也会加入到这个类里面,造成大类的产生。可以把 UserAuthorEditor 三个类拆开来,这样就会避免大类的产生,并且使用 userId 做关联

    public class User {
      private long userId;
      private String name;
      private String nickname;
      private String email;
      private String phoneNumber;
      ...
    }
    
    public class Author {
      private long userId;
      private AuthorType authorType;
      private ReviewStatus authorReviewStatus;
      ...
    }
    
    public class Editor {
      private long userId;
      private EditorType editorType;
      ...
    }
    
  • 字段未分组

    public class User {
      private long userId;
      private String name;
      private String nickname;
      private String email;
      private String phoneNumber;
      ...
    }
    

    修改后的 User 类还可以继续拆分,根据类的不同信息做分组,例如将用户主体信息与联系方式信息拆分

    public class User {
      private long userId;
      private String name;
      private String nickname;
      private Contact contact;
      ...
    }
    
    public class Contact {
      private String email;
      private String phoneNumber;
      ...
    }
    

二、长参数

我们应该写短的代码,逻辑单一明确,看上去不冗余。

聚沙成塔

public void createBook(final String title, 
                       final String introduction,
                       final URL coverUrl,
                       final BookType type,
                       final BookChannel channel,
                       final String protagonists,
                       final String tags,
                       final boolean completed) {
  ...
  Book book = Book.builder
    .title(title) 
    .introduction(introduction)
    .coverUrl(coverUrl)
    .type(type)
    .channel(channel)
    .protagonists(protagonists)
    .tags(tags)
    .completed(completed)
    .build();
    
  this.repository.save(book);
}

上述代码将一堆作品相关的参数揉在一起,那么我之后增加一个作品相关的参数时必然会直接在方法后面加,这样会导致参数列表非常庞大且不好维护。解决的方法是封装,这里参数都是跟作品相关的,所以封装一个作品类,然后使用该类作为参数即可

public void createBook(final NewBookParamters parameters) {
  ...
}

public class NewBookParamters {
  private String title;
  private String introduction;
  private URL coverUrl;
  private BookType type;
  private BookChannel channel;
  private String protagonists;
  private String tags;
  private boolean completed;
  ...
}

但是这样的话使用 parameters 时需要调用一堆 get 方法,需要把他们一个个取出来,就像这样

public void createBook(final NewBookParamters parameters) {
  ...
  Book book = Book.builder
    .title(parameters.getTitle()) 
    .introduction(parameters.getIntroduction())
    .coverUrl(parameters.getCoverUrl())
    .type(parameters.getType())
    .channel(parameters.getChannel())
    .protagonists(parameters.getProtagonists())
    .tags(parameters.getTags())
    .completed(parameters.isCompleted())
    .build();
    
  this.repository.save(book);
}

站在设计的角度,我们这里引入的是一个新的模型,一个模型的封装应该是以行为为基础的。现在模型产生了,我们需要给他配套的行为,这个封装类的行为应该是构建一个作品出来,所以:

public class NewBookParamters {
  private String title;
  private String introduction;
  private URL coverUrl;
  private BookType type;
  private BookChannel channel;
  private String protagonists;
  private String tags;
  private boolean completed;
  
  public Book newBook() {
    return Book.builder
      .title(title) 
      .introduction(introduction)
      .coverUrl(coverUrl)
      .type(type)
      .channel(channel)
      .protagonists(protagonists)
      .tags(tags)
      .completed(completed)
      .build();
  }
}

这样创建作品的业务逻辑完成了简化,而作品本身作为一个模型也具备了其行为

public void createBook(final NewBookParamters parameters) {
  ...
  Book book = parameters.newBook();
    
  this.repository.save(book);
}

动静分离

考虑参数间的变化频率是否一致

并不是所有参数都可以关联到同个模型,下方代码中 bookId 是频繁变动的书籍 id,每次请求都不一样,而 httpClient 和 processor 很有可能是静态的,每次请求都携带相同的参数,这时候就可以将静态参数提取成所在类的变量,注意要考虑具体业务来判断该参数是否静态。

public void getChapters(final long bookId, 
                        final HttpClient httpClient,
                        final ChapterProcessor processor) {
  HttpUriRequest request = createChapterRequest(bookId);
  HttpResponse response = httpClient.execute(request);
  List<Chapter> chapters = toChapters(response);
  processor.process(chapters);
}

改变之后:

public void getChapters(final long bookId) {
  HttpUriRequest request = createChapterRequest(bookId);
  HttpResponse response = this.httpClient.execute(request);
  List<Chapter> chapters = toChapters(response);
  this.processor.process(chapters);
}

告别标记

apporved 参数标记了是否审核通过,根据这个标记会有不同的处理逻辑。在实际代码中,你是愿意看到分类明确、业务逻辑单一的代码,还是愿意看到一堆标记需要你排列组合、甚至标记注释都没有的代码?

注意:标记不只 boolean,还有 int、枚举、String 的魔法值等等,如果一个标记代表了情况 A、情况 B,那当情况 C 发生的时候是否还需要继续耦合在这一个方法里?

public void editChapter(final long chapterId, 
                        final String title, 
                        final String content, 
                        final boolean apporved) {
  ...
}

解决标记,最简单的就是将标记代表的不同路径拆分,或者你可以提取他们的公共模块,所以下述代码有可能可以分成三块:公共逻辑、普通编辑、直接通过的编辑

// 普通的编辑,需要审核
public void editChapter(final long chapterId, 
                        final String title, 
                        final String content) {
  ...
}

// 直接审核通过的编辑
public void editChapterWithApproval(final long chapterId,
                                    final String title,
                                    final String content) {
 ...
}

三、总结

在业务开发中,我们经常会为了业务便利而定义“大类”、使用长参数、使用标记,这看起来并没什么不好的,或者你会说“业务开发就是这样的,功能没问题就可以了,谁会管写法呢?”,但我仍然认为遵守一定的代码规范并养成习惯,这是一个程序员的职业