【Code】《代码整洁之道》笔记-Chapter10-类

RK3588上rknn_toolkit_lite2报错解决:手把手教你更新librknnrt.so的正确姿势 本文针对在RK3588平台上使用rknn_toolkit_lite2时遇到的“Invalid RKNN model version”等常见错误,提供了完整的诊断与解决方案。文章深入剖析了RKNN生态系统各组件(如librknnrt.so、RKNN Server)版本不兼容的根源,并手把手指导如何安全、正确地更新librknnrt.so运行时库,确保AI模型在RK3588 NPU上实现高效推理。 阅读详情

第10章 类

到目前为止,本书一直在讨论如何编写良好的代码行和代码块。我们深入研究了函数的恰当构成,以及函数之间如何互相关联。尽管讨论了这么多关于代码语句及由代码语句形成的函数的表达力,但是,除非我们将注意力放到代码组织的更高层面,否则始终不能得到整洁的代码。

10.1 类的组织

遵循标准的Java约定,类应该从一组变量列表开始。如果有公共静态常量,应该先出现。然后是私有静态变量,以及私有实体变量。很少会有公共变量。

公共函数应跟在变量列表之后。我们喜欢把由某个公共函数调用的私有工具函数紧随在该公共函数后面。这符合自上向下原则,让程序读起来就像一篇报纸文章。

封装

我们喜欢保持变量和工具函数的私有性,但并不执着于此。有时,我们也需要用到受保护的(protected)变量或工具函数,好让测试可以访问到。对我们来说,测试说了算。若同一程序包内的某个测试需要调用一个函数或变量,我们就会将该函数或变量置为受保护或在整个程序包内可访问。然而,我们首先会想办法使之保有隐私。放松封装总是下策。

10.2 类应该短小

关于类的第一条规则是类应该短小。第二条规则是还要更短小。不,我们并不是要重谈“函数”一章的论调。就像函数一样,在设计类时,首要规则就是要更短小。和函数一样,马上有个问题出现,那就是“多小合适呢?”

对于函数,我们通过计算代码行数衡量其大小。对于类,我们采用不同的衡量方法,即计算其权责(responsibility)。

代码清单10-1给出了某个类的轮廓。SuperDashboard类曝露大概70个公共方法,其中大多数开发者都会同意,这实在是太长了。有些开发者或许会将SuperDashboard类称为“神类”。

代码清单10-1 权责太多

public class SuperDashboard extends JFrame implements MetaDataUser 
   public String getCustomizerLanguagePath() 
   public void setSystemConfigPath(String systemConfigPath) 
   public String getSystemConfigDocument() 
   public void setSystemConfigDocument(String systemConfigDocument) 
   public boolean getGuruState() 
   public boolean getNoviceState() 
   public boolean getOpenSourceState() 
  public void showObject(MetaObject object) 
  public void showProgress(String s)
  public boolean isMetadataDirty() 
  public void setIsMetadataDirty(boolean isMetadataDirty) 
  public Component getLastFocusedComponent() 
  public void setLastFocused(Component lastFocused) 
  public void setMouseSelectState(boolean isMouseSelected)
  public boolean isMouseSelected() 
  public LanguageManager getLanguageManager() 
  public Project getProject() 
  public Project getFirstProject() 
  public Project getLastProject() 
  public String getNewProjectName()
  public void setComponentSizes(Dimension dim)
  public String getCurrentDir() 
  public void setCurrentDir(String newDir) 
  public void updateStatus(int dotPos, int markPos) 
  public Class[] getDataBaseClasses() 
  public MetadataFeeder getMetadataFeeder() 
  public void addProject(Project project)
  public boolean setCurrentProject(Project project)
  public boolean removeProject(Project project) 
  public MetaProjectHeader getProgramMetadata()
  public void resetDashboard()
  public Project loadProject(String fileName, String projectName)
  public void setCanSaveMetadata(boolean canSave)
  public MetaObject getSelectedObject() 
  public void deselectObjects() 
  public void setProject(Project project) 
  public void editorAction(String actionName, ActionEvent event)
  public void setMode(int mode) 
  public FileManager getFileManager() 
  public void setFileManager(FileManager fileManager) 
  public ConfigManager getConfigManager() 
  public void setConfigManager(ConfigManager configManager) 
  public ClassLoader getClassLoader() 
  public void setClassLoader(ClassLoader classLoader) 
  public Properties getProps() 
  public String getUserHome() 
  public String getBaseDir() 
  public int getMajorVersionNumber() 
  public int getMinorVersionNumber() 
  public int getBuildNumber() 
  public MetaObject pasting(
    MetaObject target, MetaObject pasted, MetaProject project)
  public void processMenuItems(MetaObject metaObject)
  public void processMenuSeparators(MetaObject metaObject)
  public void processTabPages(MetaObject metaObject)
  public void processPlacement(MetaObject object) 
  public void processCreateLayout(MetaObject object) 
  public void updateDisplayLayer(MetaObject object, int layerIndex)
  public void propertyEditedRepaint(MetaObject object) 
  public void processDeleteObject(MetaObject object) 
  public boolean getAttachedToDesigner() 
  public void processProjectChangedState(boolean hasProjectChang)
  public void processObjectNameChanged(MetaObject object) 
  public void runProject() 
  public void setAllowDragging(boolean allowDragging) 
  public boolean allowDragging() 
  public boolean isCustomizing() 
  public void setTitle(String title) 
  public IdeMenuBar getIdeMenuBar() 
  public void showHelper(MetaObject metaObject, String propertyName)
  // ... many non-public methods follow ...
}

如果SuperDashboard类只包括代码清单10-2中的方法呢?

代码清单10-2 足够短小了吗?

public class SuperDashboard extends JFrame implements MetaDataUser{
  public Component getLastFocusedComponent() 
  public void setLastFocused(Component lastFocused) 
  public int getMajorVersionNumber() 
  public int getMinorVersionNumber() 
  public int getBuildNumber() 
}

5个方法不算多,在这里,虽然方法数量较少,但是SuperDashboard还是拥有太多权责。

类的名称应当描述其权责。实际上,命名正是帮助判断类的长度的第一个手段。如果无法为某个类命以精确的名称,那么这个类大概就太长了。类名越含糊,该类越有可能拥有过多权责。例如,如果类名中包含含义模糊的词,如Processor、Manager或Super,那么这种现象往往说明有不恰当的权责聚集情况存在。

我们也应该能够用大概25个单词简要描述一个类,且不用“若”(if)“与”(and)“或”(or)或者“但”(but)等词汇。我们该如何描述SuperDashboard类呢?“SuperDashboard类提供了对最后拥有焦点的组件的访问能力,我们还能通过它跟踪版本号和构建序列号。”“还能”二字恰好提示了SuperDashboard类有太多权责。

10.2.1 单一权责原则

单一权责原则(SRP)认为,类或模块应该有且只有一条加以修改的理由。该原则既给出了权责的定义,又是关于类的长度的指导方针。类只应有一个权责——只有一条修改的理由。

代码清单10-2中貌似很小的SuperDashboard类有两条加以修改的理由。第一,它跟踪大概会随软件每次发布而更新的版本信息。第二,它管理Java Swing组件(派生自JFrame,顶层GUI窗口的Swing表现形态)。每次修改Swing代码时,无疑都要更新版本号,但反之未必可行:也可能依据系统中其他代码的修改而更新版本信息。

鉴别权责(修改的理由)常常帮助我们在代码中认识到并创建出更好的抽象。可以轻易地将全部3个处理版本信息的SuperDashboard方法拆解到名为Version的类中(如代码清单10-3所示)。Version类是个极有可能在其他应用程序中得到复用的构造!

代码清单10-3 单一权责类

public class Version {
  public int getMajorVersionNumber() 
  public int getMinorVersionNumber() 
  public int getBuildNumber() 
}

SRP是面向对象设计中最为重要的概念之一,也是较为容易理解和遵循的概念之一。奇怪的是SRP往往也是最容易被破坏的类设计原则。经常会遇到做太多事的类,为什么呢?

让软件能工作和让软件保持整洁,是两种截然不同的工作。我们中的大多数人脑力有限,只能更多地把精力放在让代码能工作上,而不是放在保持代码有组织和整洁上,这全然正确。分而治之在编程行为中的重要程度,等同于其在程序中的重要程度。

问题是太多人在程序能工作时就以为万事大吉了。我们没能把思维转向有关代码组织和整洁的部分。我们直接转向了下一个问题,而没能回过头将臃肿的类切分为只有单一权责的去耦式单元。

与此同时,许多开发者害怕数量巨大的短小单一目的类会导致难以一目了然抓住全局。他们认为,要搞清楚一件较大工作如何完成,就得在类与类之间找来找去。

然而,有大量短小类的系统并不比有少量庞大类的系统拥有更多移动部件,其数量大致相等。问题是:你是想把工具归置到有许多抽屉、每个抽屉中装有定义和标记良好的组件的工具箱中呢?还是想要少数几个能随便把所有东西扔进去的抽屉?

每个达到一定规模的系统都会包括大量逻辑和复杂性。管理这种复杂性的首要目标就是加以组织,以便开发者知道到哪儿能找到东西,并且在某个特定时间只需要理解直接相关的复杂性。反之,拥有巨大、多目的类的系统,总是让我们在目前并不需要了解的一大堆东西中艰难跋涉。

再强调一下:系统应该由许多短小的类而不是少量巨大的类组成,每个小类封装一个权责,只有一个修改的原因,并与少数其他类一起协同达成期望的系统行为。

10.2.2 内聚

类应该只有少数实体变量。类中的每个方法都应该操作一个或多个这种变量。通常而言,方法操作的变量越多,就越黏聚到类上。如果一个类中的每个变量都被每个方法所使用,则该类具有最大的内聚性。

一方面,一般来说,创建这种极大化内聚类是既不可取也不可能的;另一方面,我们希望内聚性保持在较高位置。内聚性高,意味着类中的方法和变量互相依赖、互相结合成一个逻辑整体。

看看代码清单10-4中一个Stack类的实现方式。这个类内聚性非常高。在3个方法中,只有size()方法没有使用所有两个变量。

代码清单10-4 Stack.java(一个内聚类)

public class Stack {
  private int topOfStack = 0;
  List<Integer> elements = new LinkedList<Integer>();

  public int size() {
    return topOfStack;
  }

  public void push(int element) {
    topOfStack++;
    elements.add(element);
  }

  public int pop() throws PoppedWhenEmpty {
    if (topOfStack == 0)
      throw new PoppedWhenEmpty();
    int element = elements.get(--topOfStack);
    elements.remove(topOfStack);
    return element;
  }
}

保持函数和参数列表短小的策略,有时会导致为一组子集方法所用的实体变量数量增加。出现这种情况时,往往意味着至少有一个类要从大类中挣扎出来。你应当尝试将这些变量和方法分拆到两个或多个类中,让新的类更为内聚。

10.2.3 保持内聚性就会得到许多短小的类

仅仅是将较大的函数切割为小函数,就将导致更多的类出现。想想看一个有许多变量的大函数。你想把该函数中某一小部分拆解成单独的函数。不过,你想要拆出来的代码使用了该函数中声明的4个变量。是否必须将这4个变量都作为参数传递到新函数中去呢?

完全没必要!只要将4个变量提升为类的实体变量,完全无须传递任何变量就能拆解代码了。应该很容易将函数拆分为小块。

可惜这也意味着类丧失了内聚性,因为堆积了越来越多只为允许少量函数共享而存在的实体变量。等一下!如果有些函数想要共享某些变量,为什么不让它们拥有自己的类呢?当类丧失了内聚性,就拆分它!

所以,将大函数拆分为许多小函数时,往往也是将类拆分为多个小类的时机。程序会更加有组织,也会拥有更为透明的结构。

为了说明我的意思,不如从Knuth的名著Literate Programming中摘取一个经过时间考验的例子。代码清单10-5展示了Knuth的PrintPrimes程序的Java版本。为示公平,以下程序并非Knuth原版,而是用他的Web工具输出的版本。采用它作为例子的目的,是因为它是展示如何将较大的函数分解为多个较小的函数和类的极好入手点。

代码清单10-5 PrintPrimes.java

package literatePrimes;

public class PrintPrimes {
  public static void main(String[] args) {
    final int M = 1000;
    final int RR = 50;
    final int CC = 4;
    final int WW = 10;
    final int ORDMAX = 30;
    int P[] = new int[M + 1];
    int PAGENUMBER;
    int PAGEOFFSET;
    int ROWOFFSET;
    int C;
    int J;
    int K;
    boolean JPRIME;
    int ORD;
    int SQUARE;
    int N;
    int MULT[] = new int[ORDMAX + 1];

    J = 1;
    K = 1;
    P[1] = 2;
    ORD = 2;
    SQUARE = 9;

    while (K < M) {
      do {
        J = J + 2;
        if (J == SQUARE) {
          ORD = ORD + 1;
          SQUARE = P[ORD] * P[ORD];
          MULT[ORD - 1] = J;
        }
        N = 2;
        JPRIME = true;
        while (N < ORD && JPRIME) {
          while (MULT[N] < J)
            MULT[N] = MULT[N] + P[N] + P[N];
          if (MULT[N] == J)
            JPRIME = false;
          N = N + 1;
        }
      } while (!JPRIME);
      K = K + 1;
      P[K] = J;
    }
    {
      PAGENUMBER = 1;
      PAGEOFFSET = 1;
      while (PAGEOFFSET <= M) {
        System.out.println("The First " + M + 
                           " Prime Numbers --- Page " + PAGENUMBER);
        System.out.println("");
        for (ROWOFFSET = PAGEOFFSET; ROWOFFSET < PAGEOFFSET + RR; ROWOFFSET++){
          for (C = 0; C < CC;C++)
            if (ROWOFFSET + C * RR <= M)
              System.out.format("%10d", P[ROWOFFSET + C * RR]);
          System.out.println("");
        }
        System.out.println("\f");
        PAGENUMBER = PAGENUMBER + 1;
        PAGEOFFSET = PAGEOFFSET + RR * CC;
      }
    }
  }
}

该程序只有一个大函数,简直一团糟,它拥有很深的缩进结构,冗余的变量和紧密耦合的结构。至少应该将其拆分为数个较小的函数。

从代码清单10-6到代码清单10-8,展示了将代码清单10-5中的代码拆分为较小的类和函数,并为这些类、函数和变量起个好名字后的结果。

代码清单10-6 PrimePrinter.java(重构后)

package literatePrimes;

public class PrimePrinter {
  public static void main(String[] args) {
    final int NUMBER_OF_PRIMES = 1000;
    int[] primes = PrimeGenerator.generate(NUMBER_OF_PRIMES);

    final int ROWS_PER_PAGE = 50;
    final int COLUMNS_PER_PAGE = 4;
    RowColumnPagePrinter tablePrinter =
      new RowColumnPagePrinter(ROWS_PER_PAGE,
                               COLUMNS_PER_PAGE,
                               "The First " + NUMBER_OF_PRIMES + 
                               " Prime Numbers");
    tablePrinter.print(primes);
  }
}

代码清单10-7 RowColumnPagePrinter.java

package literatePrimes;

import java.io.PrintStream;

public class RowColumnPagePrinter {
  private int rowsPerPage;
  private int columnsPerPage;
  private int numbersPerPage;
  private String pageHeader;
  private PrintStream printStream;

  public RowColumnPagePrinter(int rowsPerPage,
                              int columnsPerPage,
                              String pageHeader) {
    this.rowsPerPage = rowsPerPage;
    this.columnsPerPage = columnsPerPage;
    this.pageHeader = pageHeader;
    numbersPerPage = rowsPerPage * columnsPerPage;
    printStream = System.out;
  }

  public void print(int data[]) {
    int pageNumber = 1;
    for (int firstIndexOnPage = 0;
         firstIndexOnPage < data.length;
         firstIndexOnPage += numbersPerPage) {
      int lastIndexOnPage = 
        Math.min(firstIndexOnPage + numbersPerPage - 1, 
                 data.length - 1);
      printPageHeader(pageHeader, pageNumber);
      printPage(firstIndexOnPage, lastIndexOnPage, data);
      printStream.println("\f");
      pageNumber++;
    }
  }

  private void printPage(int firstIndexOnPage,
                         int lastIndexOnPage,
                         int[] data) {
    int firstIndexOfLastRowOnPage = 
      firstIndexOnPage + rowsPerPage - 1;
    for (int firstIndexInRow = firstIndexOnPage; 
         firstIndexInRow <= firstIndexOfLastRowOnPage; 
         firstIndexInRow++) {
      printRow(firstIndexInRow, lastIndexOnPage, data);
      printStream.println("");
    }
  }

  private void printRow(int firstIndexInRow, 
                        int lastIndexOnPage, 
                        int[] data) {
    for (int column = 0; column < columnsPerPage; column++) {
      int index = firstIndexInRow + column * rowsPerPage;
      if (index <= lastIndexOnPage)
        printStream.format("%10d", data[index]);
    }
  }

  private void printPageHeader(String pageHeader, 
                               int pageNumber) {
    printStream.println(pageHeader + " --- Page " + pageNumber);
    printStream.println("");
  }

  public void setOutput(PrintStream printStream) {
    this.printStream = printStream;
  }
}

代码清单10-8 PrimeGenerator.java

package literatePrimes;

import java.util.ArrayList;

public class PrimeGenerator {
  private static int[] primes;
  private static ArrayList<Integer> multiplesOfPrimeFactors;

  protected static int[] generate(int n) {
    primes = new int[n];
    multiplesOfPrimeFactors = new ArrayList<Integer>();
    set2AsFirstPrime();
    checkOddNumbersForSubsequentPrimes();
    return primes;
  }

  private static void set2AsFirstPrime() {
    primes[0] = 2;
    multiplesOfPrimeFactors.add(2);
  }

  private static void checkOddNumbersForSubsequentPrimes() {
    int primeIndex = 1;
    for (int candidate = 3;
         primeIndex < primes.length;
         candidate += 2) {
      if (isPrime(candidate))
        primes[primeIndex++] = candidate;
    }
  }

  private static boolean isPrime(int candidate) {
    if (isLeastRelevantMultipleOfNextLargerPrimeFactor(candidate)) {
      multiplesOfPrimeFactors.add(candidate);
      return false;
    }
    return isNotMultipleOfAnyPreviousPrimeFactor(candidate);
  }

  private static boolean
  isLeastRelevantMultipleOfNextLargerPrimeFactor(int candidate) {
    int nextLargerPrimeFactor = primes[multiplesOfPrimeFactors.size()];
    int leastRelevantMultiple = nextLargerPrimeFactor * nextLargerPrimeFactor;
    return candidate == leastRelevantMultiple;
  }

  private static boolean 
  isNotMultipleOfAnyPreviousPrimeFactor(int candidate) {
    for (int n = 1; n < multiplesOfPrimeFactors.size(); n++) {
      if (isMultipleOfNthPrimeFactor(candidate, n))
        return false;
    }
    return true;
  }

  private static boolean 
  isMultipleOfNthPrimeFactor(int candidate, int n) {
    return 
      candidate == smallestOddNthMultipleNotLessThanCandidate(candidate, n);
  }

  private static int 
  smallestOddNthMultipleNotLessThanCandidate(int candidate, int n) {
    int multiple = multiplesOfPrimeFactors.get(n);
    while (multiple < candidate)
      multiple += 2 * primes[n];
    multiplesOfPrimeFactors.set(n, multiple);
    return multiple;
  }
}

你可能注意到的第一件事就是程序比原来长了许多,从1页多增加到了将近3页。这有几个原因:其一,重构后的程序采用了更长、更有描述性的变量名;其二,重构后的程序将函数和类声明当作是给代码添加注释的一种手段;其三,我们采用了空格和格式技巧让程序更可读。

注意程序是如何被拆分为3个主要权责的。PrimePrinter类中只有主程序。主程序的权责是处理执行环境。如果调用方式改变,它也会随之改变。例如,如果程序被转换为SOAP服务,则该类也会受影响。

RowColumnPagePrinter类懂得如何将数字列表格式化到有着固定行数、列数的页面上。若输出格式需要改动,则该类也会受影响。

PrimeGenerator类懂得如何生成素数列表。注意,这并不意味着要实体化为对象。该类就是一个有用的作用域,在其中声明并隐藏变量。如果计算素数的算法发生改动,则该类也会改动。

这并不算是重写!因为我们没有从头开始写一遍程序。实际上,如果你仔细看上述两个不同的程序,就会发现它们采用了同样的算法和机制来完成工作。

我们通过编写验证第一个程序的精确行为的用例来实现修改。然后,我们做了许多小改动,每次改动一处。每改动一次,就执行一次,确保程序的行为没有变化。一小步接着一小步,第一个程序就这样被逐渐清理和转换为第二个程序。

10.3 为了修改而组织

对于多数系统,修改将一直持续。每处修改都让我们冒着系统其他部分不能如期望般工作的风险。在整洁的系统中,我们对类加以组织,以降低修改的风险。

代码清单10-9中的Sql类用来生成提供恰当元数据的SQL格式化字符串。这个类还没写完,所以暂时不支持update语句等SQL功能。当需要Sql类支持update语句时,我们就得“打开”这个类进行修改。打开类带来的问题是风险随之而来。对类的任何修改都有可能破坏类中的其他代码。因此必须全面重新测试。

代码清单10-9 一个必须打开修改的类

public class Sql {
  public Sql(String table, Column[] columns)
  public String create()
  public String insert(Object[] fields)
  public String selectAll()
  public String findByKey(String keyColumn, String keyValue)
  public String select(Column column, String pattern)
  public String select(Criteria criteria)
  public String preparedInsert()
  private String columnList(Column[] columns)
  private String valuesList(Object[] fields, final Column[] columns)
  private String selectWithCriteria(String criteria)
  private String placeholderList(Column[] columns)
}

当增加一种新类型语句时,就要修改Sql类。改动单个语句类型时,也要进行修改,比如,打算让select功能支持子查询。如果存在两个修改的理由,就说明Sql违反了SRP原则。

可以从简单的组织性观点发现对SRP的违反。Sql的方法概述显示,存在类似于selectWithCriteria等只与select语句有关的私有方法。

出现了只与类的一小部分有关的私有方法行为,就意味着存在改进空间。然而,展开行动的基本动因却应该是系统的变动。若我们认为Sql类在逻辑上已具足,则无须担心对权责的拆分。如果在可预见的未来无须增加update功能,就不该去修改Sql类。不过,一旦打开了类,就应当修正设计方案。

代码清单10-10中的解决方式如何呢?代码清单10-9中Sql类的每个接口方法都重构到从Sql类派生出来的类中了。注意那些私有方法,如valuesList,直接移到了需要用它们的地方。公共私有行为被划分到独立的两个工具类Where和ColumnList中。

代码清单10-10 一组封闭类

abstract public class Sql {
  public Sql(String table, Column[] columns)
  abstract public String generate();
}

public class CreateSql extends Sql {
  public CreateSql(String table, Column[] columns)
  @Override public String generate()
}

public class SelectSql extends Sql {
  public SelectSql(String table, Column[] columns)
  @Override public String generate()
}

public class InsertSql extends Sql {
  public InsertSql(String table, Column[] columns, Object[] fields)
  @Override public String generate()
  private String valuesList(Object[] fields, final Column[] columns)
}

public class SelectWithCriteriaSql extends Sql {
  public SelectWithCriteriaSql(
    String table, Column[] columns, Criteria criteria)
  @Override public String generate()
}

public class SelectWithMatchSql extends Sql {
  public SelectWithMatchSql(
    String table, Column[] columns, Column column, String pattern)
  @Override public String generate()
}

public class FindByKeySql extends Sql{
  public FindByKeySql(
    String table, Column[] columns, String keyColumn, String keyValue)
  @Override public String generate()
}

public class PreparedInsertSql extends Sql {
  public PreparedInsertSql(String table, Column[] columns)
  @Override public String generate(){
  private String placeholderList(Column[] columns)
}

public class Where {
  public Where(String criteria)
  public String generate()
}
public class ColumnList {
  public ColumnList(Column[] columns)
  public String generate()
}

每个类中的代码都变得极为简单。理解每个类花费的时间缩减到近乎为零。函数对其他函数造成毁坏的风险也变得几近于无。从测试的角度看,验证方案中每一处逻辑都成了极为简单的任务,因为类与类之间相互隔离了。

当需要增加update语句时,现存类无须做任何修改,这也同样重要!我们在Sql类的新子类UpdateSql中构建update语句的逻辑,系统中的其他代码都不会因为这个修改而被破坏。

重新架构的Sql逻辑百利而无一弊。它符合SRP,也符合其他面向对象设计的关键原则,如开放闭合原则(OCP):类应当对扩展开放,对修改封闭。通过子类化手段,重新架构的Sql类对添加新功能是开放的,而且可以同时不触及其他类,只要将UpdateSql类放置到位就行了。

我们希望精心组织系统,从而在添加或修改特性时尽可能少惹麻烦。在理想系统中,我们通过扩展系统而非修改现有代码来添加新特性。

隔离修改

需求会改变,所以代码也会改变。在OO 101中,我们学习到,具体类包含实现细节(代码),而抽象类则只呈现概念。依赖具体细节的客户类,当细节改变时,就会有风险。我们可以借助接口和抽象类来隔离这些细节带来的影响。

对具体细节的依赖给对系统的测试带来了挑战。如果我们构建一个依赖外部TokyoStockExchangeAPI的Portfolio类以代表投资组合的价值,则测试用例就会受到价值查询的连带影响。如果每隔5分钟就有新说法,就很难写出测试来。

与其设计直接依赖TokyoStockExchange的Portfolio类,不如创建StockExchange接口,其中只声明一个方法:

public interface StockExchange {
  Money currentPrice(String symbol);
}

我们设计TokyoStockExchange类来实现这个接口。我们还要确保Portfolio的构造器接受作为参数的StockExchange引用:

public  Portfolio  {
  private  StockExchange  exchange;
  public  Portfolio(StockExchange  exchange)  {
    this.exchange  =  exchange;
  }
  //  ...
}

现在就可以为StockExchange接口创建可测试的尝试性实现了,该尝试性实现将返回固定的现值。如果测试中购买了5股微软股票,则尝试性实现总是返回每股100美元的现值。对于StockExchange接口的尝试性实现简化为简单的表格查找。然后再编写一个总投资价值为500美元的测试。

public class PortfolioTest {
  private FixedStockExchangeStub exchange;
  private Portfolio portfolio;

  @Before
  protected void setUp() throws Exception {
    exchange = new FixedStockExchangeStub();
    exchange.fix("MSFT", 100);
    portfolio = new Portfolio(exchange);
  }

  @Test
  public void GivenFiveMSFTTotalShouldBe500() throws Exception {
    portfolio.add(5, "MSFT");
    Assert.assertEquals(500, portfolio.value());
  }
}

如果系统解耦到这样测试的程度,也就更加灵活,更加可复用。部件之间的解耦代表着系统中的元素互相隔离得很好。隔离也让对系统每个元素的理解变得更加容易。

通过降低连接度,我们的类就遵循了另一条类设计原则,即依赖倒置原则(Dependency Inversion Principle,DIP)。本质而言,DIP认为类应当依赖抽象而不是依赖具体细节。

我们的Portfolio类不再依赖TokyoStockExchange类的实现细节,而是依赖StockExchange接口。StockExchange接口呈现的是有关询问某只股票价格的抽象概念,这种抽象隔离了所有询价的特定细节,其中包括价格数据来自何处的类。

uniapp | vue2 | vue3 - 解决调用天地图功能服务时 http 网络请求报错 net::ERR ABORTED 503(Service Temporarily Unavailabl) vue,uni-app,天地图,请求报错,503,请求失败,map.tianditu,天地图503,天地图请求503,接口503错误怎么办,vue天地图,uniAPP天地图,获取定位接口服务器报错503,获取当前定位,定位功能,IP属地定位,天地图服务器报错,如何解决报错,net::ERRABORTED503(ServiceTemporarilyUnavailabl),GET,POST,服务暂时不可用,react天地图,一直等待吗,报错怎么办,定位报错,轨迹,经纬度,天地图服务器503,在vue2 阅读详情

相关推荐

逆向CarPlay协议时,除了USB主从切换,你还需要注意这3个关键点

本文深入探讨逆向CarPlay协议时的3个关键点,包括MFi协议背景、多通道协同工作机制和协议分析工具的高级过滤技巧。特别解析了USB主从切换后的复杂交互过程,帮助安全研究员和逆向工程师更好地理解CarPlay协议的核心机制与潜在安全研究切入点。

weixin_29164497的博客 385

Fastjson反序列化漏洞深度剖析:从原理到实战攻防

JSON反序列化是现代Web应用数据交换的核心技术,其原理是将JSON字符串转换为内存中的对象实例。然而,这一过程若未对类型信息进行严格校验,攻击者可能通过精心构造的恶意JSON数据,触发目标应用加载并执行任意类,进而实现远程代码执行(RCE)。这种漏洞在Java生态中具有极高的技术价值和广泛影响,尤其在高性能JSON处理库的应用场景下,安全风险尤为突出。Fastjson作为广泛使用的库,其默认的autoType特性曾成为经典攻击入口,而围绕JNDI注入等手法的利用链,更是安全攻防研究的重点。本文聚焦于Fa

weixin_34323858的博客 400

【PVCodeNet】《Palm Vein Recognition Network Combining Transformer and CNN》

1]吴凯,沈文忠,贾丁丁,等.融合Transformer和CNN的手掌静脉识别网络[J].计算机工程与应用,2023,59(24):98-109.

bryant_meng 1283

Clean Code(代码整洁之道)-类

1.类的组织:遵循标准Java约定,类应从一组变量开始。如果有公共静态常量,应先出现,然后是私有静态变量、私有实体变量。公共函数应在变量列表后面,私有函数紧跟其调用者函数。 2.类应该短小:对于函数,我们通过行数计算大小,而对于类,则通过权责来计算其大小。类的名称应当描述其权责,类名越模糊,该类越有可能拥有太多权责。   1).单一权责(SRP):类或模块有且仅有一条加以修改的理由。鉴别权责(...

ldc9001的专栏 669

游戏开发实战:如何用旋转矩阵实现2D角色平滑转向(附Unity代码)

本文深入探讨了在Unity游戏开发中,如何运用旋转矩阵原理实现2D角色的平滑转向。文章从核心理论出发,提供了计算目标旋转、实现平滑插值(Lerp与SmoothDamp)的完整C#代码,并解决了与动画系统、物理引擎协同的常见难题,帮助开发者提升游戏操作手感与视觉流畅度。

weixin_29057237的博客 290

Ubuntu docker镜像中下载安装 DPABI(DPARSF)

在Ubuntu服务器的docker镜像中用MATLAB运行DPABI(以及使用DPARSF的功能)。

Pandako's Blog 1723

软件测试(四)之 PrintPrimes

HOMEWORK 3 题目所给代码如下: 1 /******************************************************* 2 * Finds and prints n prime integers 3 * Jeff Offutt, Spring 2003 4 **********************...

weixin_34050005的博客 184

STM32 无线充电接收端硬件设计:从 LC 谐振到 PWM 控制的 5 大关键电路解析

本文深入解析了基于STM32的无线充电接收端硬件设计,重点探讨了LC谐振倍压整流、INA282电流采样、IR2104S半桥驱动、超级电容保护及STM32最小系统等5大关键电路。针对超级电容的恒功率充电需求,详细介绍了PID控制算法优化及PCB设计实战要点,帮助工程师提升无线充电系统的效率和可靠性。

weixin_31774841的博客 386

Code练习笔记6:用两个栈实现队列

用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )

单车的博客 326

解锁RK3588 HDMI输出:分辨率与EDID的深度探索

本文详细探讨了瑞芯微RK3588处理器的HDMI输出分辨率配置与EDID解析技术。文章首先介绍了RK3588处理器的性能特点及其HDMI功能在多媒体和工业领域的应用价值,随后分别阐述了HDMI输出分辨率的硬件连接要求和软件配置方法,包括4K/8K等不同分辨率的配置实例。在EDID部分,深入解析了EDID的数据结构、交互机制,以及数据获取与修改的具体方法。最后总结了配置过程中常见问题的解决方案,并展望了RK3588在高分辨率显示领域的发展前景。全文为开发者提供了RK3588 HDMI显示配置的完整技术指南。

远程部署调试 运行安装 项目调试 二次开发 项目技术新 持续迭代 部分源码免费分享 593

滑模观测器与锁相环结合应用:让电机“无感”也能精准旋转的奥秘

本文介绍了永磁同步电机无传感器控制中的滑模观测器(SMO)结合锁相环(PLL)技术。SMO通过创建虚拟电机模型,利用电流误差产生控制信号来估计反电动势;PLL则从反电动势中解码转子位置和速度信息。该组合方案兼具SMO的鲁棒性和PLL的高精度,计算量小且硬件要求低。文章详细阐述了原理、数学模型、参数整定方法及实际实现技巧,包括离散化处理、符号函数替代等优化手段,并分析了低速性能限制、抖振问题等常见挑战及解决方案。最后指出该技术未来将与人工智能、多观测器融合等方向结合发展。

汽车软件开发 196

使用Visual Studio Code记笔记

使用VSCode记笔记

beyond谚语的博客 5464

基于Simulink实现电动汽车电机驱动系统的建模与仿真

通过上述步骤,我们成功实现了基于Simulink的电动汽车电机驱动系统的建模与仿真。该系统能够全面评估电机驱动系统的性能,验证控制策略的有效性,并通过优化设计提高车辆的动力性和效率。引入智能预测:结合人工智能技术,实现更智能的控制策略。扩展功能:增加对多种电机类型的支持,提升系统通用性。实验验证:将仿真模型应用于实际硬件,进行实验验证,评估其在实际工况下的表现。

xiaoheshang_123的博客 318

【Code】《代码整洁之道》笔记-Chapter16-重构SerialDate

如果你找到JCommon类库,深入该类库,其中有个名为的程序包。在该程序包中,有个名为SerialDate的类,我们即将剖析这个类。SerialDate的作者是David Gilbert。David显然是一位经验丰富、能力很强的程序员。如我们将看到的,他在代码中展示了极高的专业性和原则性。无论怎么说,SerialDate都是“好代码”,而我将把它撕成碎片。这并非恶意的行为,我也不认为自己比David强许多,有权对他的代码说三道四。其实,如果你看过我的代码,我敢说你也会发现好些该埋怨的东西。

江湖人称菠萝包 1518

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

【Code】《代码整洁之道》笔记-Chapter9-单元测试

过去十年以来,编程专业领域进步很大。1997年时,没人听说过测试驱动开发。对于我们之中的大多数人来说,单元测试是那种用来确保程序“可运行”的用过即扔的短代码。我们辛勤地编写类和方法,再弄出一些特殊代码来测试它们。通常这些代码会是一种简单的驱动式程序,让我们能够手工与自己编写的程序交互。我记得在20世纪90年代曾为一套嵌入式实时系统编写过C++程序。想法很简单;到达指定毫秒数时,在一个新线程中执行Command的execute方法。问题在于如何测试它。我随便写了个简单的驱动式程序,聆听来自键盘的动作。

江湖人称菠萝包 1415

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

【Code】《代码整洁之道》笔记-Chapter14-逐步改进

本章研究一个逐步改进的案例。你将看到一个开始还不错,但规模扩大后即出问题的模块。你还将看到这个模块是如何被重构得整洁起来的。我们中的大多数人都会遇到解析命令行参数的情况。如果没有就手的工具,就得遍历传入main函数的字符串数组。有一些不同来源的好工具,但没有一个是最符合要求的。所以,我当然要自己写一个,我把它叫作Args。Args非常易于使用。你只要简单地用输入参数和格式化字符串构造Args类,再向Args实体询问参数值即可。看看代码清单14-1中给出的简单例子。代码清单14-1 Args的简单用法。

江湖人称菠萝包 1223

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

【Code】《代码整洁之道》笔记-Chapter11-系统

复杂要人命。它消磨开发者的生命,让产品难以规划、构建和测试。

江湖人称菠萝包 1207

【Code】《代码整洁之道》笔记-Chapter8-边界

我们很少能控制系统中的全部软件。有时我们购买第三方程序包或使用开放源代码,有时我们依靠公司中其他团队打造组件或子系统。不管是哪种情况,我们都得将外来代码干净利落地整合进自己的代码中。本章将介绍一些保持软件边界整洁的实践手段和技巧。

江湖人称菠萝包 1211

【Code】《代码整洁之道》笔记-Chapter12-迭进

假使有4条简单的规则,跟着做就能帮助你创建优良的设计,会如何?假使遵循这些规则,你就能洞见代码的结构和设计,更能轻易地应用SRP和DIP之类的原则,便会如何?假使这4条规则有利于良好的设计“浮现”出来,又会如何?我们中的许多人认为,Kent Beck关于简单设计的4条规则,对于创建具有良好设计的软件有着莫大的帮助。据Kent所述,只要遵循以下规则,设计就能变得“简单”:运行所有测试;不可重复;表达了程序员的意图;尽可能减少类和方法的数量。以上规则按其重要程度排列。

江湖人称菠萝包 920

【Code】《代码整洁之道》笔记-Chapter7-错误处理

对异常分类有很多方式。可以依其来源分类:是来自组件还是其他地方?也可以依其类型分类:是设备错误、网络错误还是编程错误?不过,当我们在应用程序中定义异常类时,最重要的考虑应该是它们如何被捕获。我们来看一个不太好的异常分类的例子。下面的语句是对某个第三方代码库的调用。try {语句包含了一大堆重复代码,这并不出奇。在大多数异常处理中,不管真实原因如何,我们总是做相对标准的处理。我们得记录错误,确保能继续工作。

江湖人称菠萝包 1312

【Code】《代码整洁之道》笔记-Chapter6-对象和数据结构

我们将变量设置为私有(private)有一个理由:不想让其他人依赖这些变量。我们还想在心血来潮时能自由修改其类型或实现。那么,为什么还是有那么多程序员不假思索就给对象添加赋值器(setter)和取值器(getter),将私有变量公之于众,如同它们是公共变量一般呢?

江湖人称菠萝包 1617

【Code】《代码整洁之道》笔记-Chapter4-注释

什么也比不上放置良好的注释来得有用。什么也不会比乱七八糟的注释更有本事搞乱一个模块。什么也不会比陈旧、提供错误信息的注释更有破坏性。注释并不像辛德勒的名单。它们并不“纯然地好”。实际上,注释最多也就是一种必需的恶。若编程语言足够有表达力,或者我们长于用这些语言来表达意图,就不那么需要注释——也许根本不需要。注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败。注意,我用了“ 失败”一词,我是说真的,注释总是一种失败。我们总无法找到不用注释就能表达自我的方法,所以总要有注释,这并不值得庆贺。

江湖人称菠萝包 1055

【Code】《代码整洁之道》笔记-Chapter17-味道与启发

Martin Fowler在其大作《重构:改善既有代码的设计》(Refactoring: Improving the Design of Existing Code)中指出了许多不同的“代码的坏味道”。下面的清单包括很多Martin指出的“坏味道”,还添加了更多我自己提出的“坏味道”,也包括我借以历练本业的其他珍宝与启发。我借由遍览和重构几个不同的程序总结出这个清单。每次修改,我都问自己为什么要这样改,把修改的原因写下来,结果就得到相当长的清单,该清单列出了在读代码时让我闻起来不舒服的味道。

江湖人称菠萝包 1366

【Code】《代码整洁之道》笔记-Chapter5-格式

当有人查看底层代码实现时,我们希望他们为代码的整洁、一致及所感知到的对细节的关注而震惊。我们希望他们高高扬起眉毛,一路看下去。我们希望他们感受到专业人士们的劳作。但若他们看到的只是一堆像是由酒醉的水手写出的鬼画符,那他们多半会得出结论,认为项目其他任何部分也同样对细节漠不关心。你应该保持良好的代码格式。你应该选用一套管理代码格式的简单规则,然后贯彻这些规则。如果你在团队中工作,则团队应该一致同意采用一套简单的格式规则,所有成员都要遵从这套规则。使用能帮你应用这套格式规则的自动化工具会很有帮助。

江湖人称菠萝包 1041

【Code】《代码整洁之道》笔记-Chapter2-有意义的命名

起好名字最难的地方在于需要良好的描述技巧和共有文化背景。与其说这是一种技术、商业或管理问题,还不如说是一种教学问题。其结果是,这个领域内的许多人都没能学会做得很好。我们有时会担心其他开发者反对重命名。讨论一下就会知道,如果名称改得更好,那大家真的会感激你。多数时候我们并不记忆类名和方法名,而使用现代工具应对这些细节,好让自己集中精力把代码写得像词句篇章,至少像是表和数据结构(词句并非总是呈现数据的最佳手段)。改名可能会让某些人吃惊,就像你做到其他代码改善工作一样。别让这种事阻碍你前进的步伐。

江湖人称菠萝包 1128

【Code】《代码整洁之道》笔记-Chapter13-并发编程

对象是过程的抽象。线程是调度的抽象。编写整洁的并发程序很难——非常难,而编写在单线程中执行的代码却简单得多。编写表面上看似不错、深入进去却支离破碎的多线程代码也简单,但是系统一旦遭受压力,这种代码就扛不住了。本章将讨论并发编程的需求及其困难之处,并给出一些应对这些难点、编写整洁的并发代码的建议。最后,我们将讨论与测试并发代码有关的问题。整洁的并发编程是个复杂话题,值得用一整本书来讨论。本书只做概览,并在附录A中提供更详细的指引。如果你只是对并发好奇,阅读本章就足够了。

江湖人称菠萝包 1377
上一篇: 【Code】《代码整洁之道》笔记-Chapter9-单元测试
下一篇: 【Code】《代码整洁之道》笔记-Chapter11-系统
江湖人称菠萝包
博客等级 码龄10年 762粉丝 314原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值