【Code】《代码整洁之道》笔记-Chapter16-重构SerialDate
第16章 重构SerialDate
如果你找到JCommon类库,深入该类库,其中有个名为org.jfree.date
的程序包。在该程序包中,有个名为SerialDate
的类,我们即将剖析这个类。
SerialDate
的作者是David Gilbert。David显然是一位经验丰富、能力很强的程序员。如我们将看到的,他在代码中展示了极高的专业性和原则性。无论怎么说,SerialDate
都是“好代码”,而我将把它撕成碎片。
这并非恶意的行为,我也不认为自己比David强许多,有权对他的代码说三道四。其实,如果你看过我的代码,我敢说你也会发现好些该埋怨的东西。
不,这也并非傲慢无礼的行为。我所要做的,只是一种专业眼光的检视,不多也不少,那是我们都该坦然接受的做法。那是我们应该欢迎别人对自己做的事。只有通过这样的批评,我们才能学到东西。医生就是这样做的,飞行员就是这样做的,律师就是这样做的,我们程序员也需要学习如何这样做。
多说一句关于David Gilbert的事:David不仅是一位优秀的程序员,他还有着将代码免费呈献给社区的勇气和好心。他公开代码,让所有人都能看到,邀请大众使用并审查。做得真好!
SerialDate
(见代码清单B-1)是一个用Java呈现日期的类。为什么在Java已经有java.util.Date
和java.util.Calendar
的情况下,还需要一个呈现日期的类呢?作者编写这个类,是为了解除我自己也常感到的痛苦。在开放的Javadoc(第67行)中,他很好地解释了原因。我们可以质疑他的初衷,但我的确有处理这个问题的需要,而且我也欢迎有一个关乎日期甚于关乎时间的类存在。
16.1 首先,让它能工作
在一个名为SerialDateTests
的类(见代码清单B-2)中,有一些单元测试。测试都通过了,但不幸的是,快览一遍测试,发现它们并没有测试所有东西[T1]。例如,用“查找使用”搜索方法MonthCodeToQuarter
(第356行),会发现没有被用过[F4]。因此,单元测试并没有测试这个方法。
所以,我用Clover来检查单元测试覆盖了哪些代码。Clover报告说,在SerialDate
的185个可执行语句中,单元测试只执行了91个(约50%)[T2]。覆盖图看起来像是一床满是补丁的棉被,整个类上布满大块的未执行代码。
我的目标是完整地理解和重构这个类,如果没有好得多的测试覆盖率,就达不到目标。所以,我完全重起炉灶编写了自己的单元测试(见代码清单B-4)。
在阅读这些测试时,你可以看到,其中许多注释掉了。这些测试不能通过。它们代表了我以为SerialDate
应该有的行为。在我重构SerialDate
时,也将让这些测试通过。
即便有些测试被注释掉,Clover也还是会报告新的单元测试执行了185个可执行语句中的170个(92%)。这样就好多了,而且我想我们可以把这个覆盖率提高些。
前几个注释掉的测试(第23~63行)是我一厢情愿。程序并没有设计为通过这些测试,但对我来说它们代表的行为显而易见[G2]。我不太确定StringToWeekdayCode
方法为何要写成那样,不过既然它已经在那儿,显然不该是区分大小写的。编写这些测试是区区小事[T3],通过测试更加容易。我只修改了第259行和第263行,就能使用equalsIgnoreCase
了。
我注释掉了第32行和第45行的测试,因为我不太明确是否应该支持tues和thurs缩写。
第153行和第154行的测试不能通过。显然,它们本该通过[G2]。我们可以轻易地修正,只要对stringToMonthCode
作出以下修改就行,对于第163行和第213行的测试也一样。
457 if ((result < 1) || (result > 12)) {result = -1;
458 for (int i = 0; i < monthNames.length; i++) {
459 if (s.equalsIgnoreCase(shortMonthNames[i])) {
460 result = i + 1;
461 break;
462 }
463 if (s.equalsIgnoreCase(monthNames[i])) {
464 result = i + 1;
465 break;
466 }
467 }
468 }
第318行注释掉的测试暴露了getFollowingDayOfWeek
方法中的一个缺陷(第672行)。2004年12月25日是周六。下一个周六是2005年1月1日。然而,运行测试时,会看到getFollowingDayOfWeek
返回12月25日之后的周六还是12月25日。显然这不对[G3] [T1]。我们看到问题在第685行。那是个典型的边界条件错误[T5]。应该是这样:
685 if (baseDOW >= targetWeekday) {
很有意思,这个函数是之前一次修改的结果。修改记录(第43行)显示,getPrevious- DayOfWeek
、getFollowingDayOfWeek
和getNearestDayOfWeek
中的“缺陷”已被修正[T6]。
测试getNearestDayOfWeek
(第705行)的单元测试testGetNearestDayOfWeek
(第329行)之前的版本不像现在一样没有遗漏。我添加了大量测试用例,因为初始的测试用例并没有全部通过[T6]。查看哪些测试用例被注释掉,你可以看到失败的模式,这很有启发。如果最近的日期是在未来,算法就会失败。显然存在某种边界条件错误[T5]。
Clover汇报的测试覆盖模式也很有趣[T8]。第719行根本没有执行!这意味着第718行的if
语句总是得到false
的结果。没错,看一眼代码就知道是这样。变量adjust
总是为负,所以不会大于或等于4。所以,算法错了。
正确的算法如下所示:
int delta = targetDOW - base.getDayOfWeek();
int positiveDelta = delta + 7;
int adjust = positiveDelta % 7;
if (adjust > 3)adjust -= 7;return SerialDate.addDays(adjust, base);
最后,只要简单地抛出IllegalArgumentException
异常而不是从weekInMonthToString
和relativeToString
返回错误字符串,第417行和第429行的测试就能通过。
做出这些修改后,所有的单元测试都通过了,我确信SerialDate
现在可以工作。是时候让它“做对”了。
16.2 让它做对
我们将从头到尾遍历SerialDate
,同时加以改进。尽管在本章的讨论中你看不到这个过程,在每次做修改后,我还是要运行全部JCommon
单元测试,包括我为SerialDate
改进的那些单元测试。所以,后面你看到的所有修改,对于JCommon
都是可工作的。
从第1行开始,我看到大量有关许可、版权、作者和修改历史的注释。我明白,的确有些法律事宜要说明,所以版权和许可信息应该保留。另外,修改历史是产生于19世纪60年代的古董,现今源代码控制工具可以帮我们做到这个。应该删掉修改历史[C1]。
从第61行开始的导入列表应该通过使用java.text.*
和java.util.*
来缩短。[J1]
Javadoc的HTML格式化工作(第67行)令我畏惧。一个源文件里面有多种语言,我有点发怵。这条注释有4种语言:Java、英文、Javadoc和html[G1]。有那么多语言,注释就很难直截了当。例如,生成Javadoc后,第71行和第72行原本很好的位置就丢失了,而且谁想在源代码中看到<ul>
和<li>
这样的东西呢?更好的策略可能是用<pre>
标签把整个注释部分包围起来,这样,对于源代码的格式化只会限于Javadoc之内(更好的解决方案是让Javadoc不对注释做格式化,这样注释在代码和文档中就会是同一种样式)。
第86行是类声明。这个类为何要命名为SerialDate
呢?Serial一词有什么妙处吗?是不是因为该类派生自Serializable
?看来不是这样的。
别猜了,我知道为什么(或者我认为自己知道)要用Serial一词。线索就在第98行和第101行的常量SERIAL_LOWER_BOUND
和SERIAL_UPPER_BOUND
。更好的线索在从第830行开始的注释中。该类被命名为SerialDate
,是因为它用“序列数”(serial number)来实现,该序列数恰好是从1899年12月30日后的天数。
对此我有两个问题。首先,术语“序列数”并不真对。可能有点诡辩,但其呈现方式却更接近相对偏移。术语“序列数”更多地用于产品版本标识,而非日期标识。我没发现这个名称特别有描述力[N1]。更有描述力的术语大概是“顺序”(ordinal)。
第二个问题更突出。名称SerialDate
暗示了一种实现。该类是一个抽象类,没必要暗示任何有关实现的事。实际上,没理由隐藏实现!我发现这个名称放在了不正确的抽象层级上[N2]。以我之见,该类的名称应该就是简单的Date
。
不幸的是,Java类库里面有太多名为Date
的类了,所以这大概也不是最好的名称。因为这个类关于日期而非时间,所以我想将其命名为Day
,但Day这个名字也在多处被滥用。最后,我选了DayDate
作为最佳折中方案。
从现在起,我将使用术语DayDate
。请记住,你读到的代码清单,还是用的SerialDate
。
我理解为何DayDate
继承自Comparable
和Serializable
。不过,为什么它要继承自MonthConstants
呢?类MonthConstants
(见代码清单B-3)只是一大堆定义了月份的静态常量。从常量类继承是Java程序员用的一种老花招,这样他们就能避免形如MonthConstants.January
的表达式,不过这是一个坏主意[J2]。MonthConstants
其实应该是一个枚举。
public abstract class DayDate implements Comparable,Serializable {public static enum Month {JANUARY(1),FEBRUARY(2),MARCH(3),APRIL(4),MAY(5),JUNE(6),JULY(7),AUGUST(8),SEPTEMBER(9),OCTOBER(10),NOVEMBER(11),DECEMBER(12);Month(int index) {this.index = index;}public static Month make(int monthIndex) {for (Month m : Month.values()) {if (m.index == monthIndex)return m;}throw new IllegalArgumentException("Invalid month index " + monthIndex);}public final int index;}
把MonthConstants
改成枚举,导致对DayDate
类和所有用到这个类的代码的一些修改。我花了一小时来改代码。不过,原来以int
为月份类型的函数,现在都用上Month
枚举元素了。这意味着我们可以去除isValidMonthCode
方法(第326行),以及monthCodeToQuarter
等位置的月份代码错误检查(第356行)了[G5]。
下一步,我们看到第91行的serialVersionUID
。该变量用于控制序列号。如果我们修改了它,那么用这个软件编写的旧版本DayDate
都将不再可用,而是返回一个InvalidClassException
异常。如果你没有声明serialVersionUID
变量,则编译器会自动生成一个,每次修改模块时都会得到不一样的值。我知道,所有的文档都建议手工控制这个变量,但对我来说自动控制序列号安全得多[G4]。我宁可调试InvalidClassException
,也不愿意面对因忘记修改serialVersionUID
引起的后续工作。所以,我要删除这个变量——至少暂时这么做[2]。
我发现第93行的注释是多余的。这正是谎言和误导信息所在之地[C2]。所以我要删掉它和它的同类。
第97行和第100行的注释有关序列数,我之前已经讨论过这个问题[C1],它们描述的变量是DayDate
能够描述的最早和最晚的日期。这可以搞得更清楚些[N1]。
public static final int EARLIEST_DATE_ORDINAL = 2; // 1/1/1900
public static final int LATEST_DATE_ORDINAL = 2958465; // 12/31/9999
我不太清楚为什么EARLIEST_DATE_ORDINAL
的值是2而不是0。在第829行的注释中有提示,说明这与用Microsoft Excel展示日期的方式有关。在DayDate
的派生类SpreadsheetDate
中能看得更深入(见代码清单B-5)。第71行的注释很好地描述了这个问题。
我的问题是,这看来应该与SpreadsheetDate
有关,而与DayDate
无关才对。所以,EARLIEST_DATE_ORDINAL
和LATEST_DATE_ORDINAL
实在不该属于DayDate
,而应该移到SpreadsheetDate
中[G6]。
的确,搜索一下代码就知道,这些变量值仅在SpreadsheetDate
中用到,在DayDate
中没用到,在JCommon
框架的其他类中也没用到。所以,我将把这些变量值向下移到SpreadsheetDate
中。
下面的两个变量,MINIMUN_YEAR_SUPPORTED
和MAXIMUM_YEAR_SUPPORTED
(第104行和第107行)地位尴尬。显然,如果DayDate
是一个没有提供实现铺垫的抽象类,它就不该告知我们有关最小年份和最大年份的信息。同样,我很想把这些变量向下移到SpreadsheetDate
中[G6]。然而,如果快速查找这些变量的使用情况,会发现另一个类也在用:RelativeDayOfWeekRule
(见代码清单B-6)。在第177行和第178行的getDate
函数中,它们被用来检查getDate
的年份参数是否有效,而抽象类的用户需要得知其实现信息,这是一个矛盾。
我们要做的是既提供信息,又不污染DayDate
。通常,我们会从派生类实体中获取实现信息。不过,我们并未向getDate
函数传入DayDate
的实体,反而返回了一个DayDate
的实体,这意味着必须在某处创建这个实体。第187~205行提供了线索。DayDate
实体是在getPreviousDayOfWeek
、getNearestDayOfWeek
或getFollowingDayOfWeek
这3个函数其中之一里面创建的。看一下DayDate
代码清单,我们看到,这些函数(第638~724行)全都返回了由addDays
(第571行)创建的日期实体,addDays
调用CreateInstance
(第808行),创建出一个SpreadsheetDate!
[G7]。
通常来说,基类不宜了解其派生类的情况。为了修正这个毛病,我们应该利用抽象工厂模式(ABSTRACT FACTORY),创建一个DayDateFactory
,该工厂类将创建我们所需要的DayDate
的实体,并回答有关实现的问题,例如最大和最小日期之类。
public abstract class DayDateFactory {private static DayDateFactory factory = new SpreadsheetDateFactory();public static void setInstance(DayDateFactory factory) {DayDateFactory.factory = factory;}protected abstract DayDate _makeDate(int ordinal);protected abstract DayDate _makeDate(int day, DayDate.Month month, int year);protected abstract DayDate _makeDate(int day, int month, int year);protected abstract DayDate _makeDate(java.util.Date date);protected abstract int _getMinimumYear();protected abstract int _getMaximumYear();public static DayDate makeDate(int ordinal) {return factory._makeDate(ordinal);}public static DayDate makeDate(int day, DayDate.Month month, int year) {return factory._makeDate(day, month, year);}public static DayDate makeDate(int day, int month, int year) {return factory._makeDate(day, month, year);}public static DayDate makeDate(java.util.Date date) {return factory._makeDate(date);}public static int getMinimumYear() {return factory._getMinimumYear();}public static int getMaximumYear() {return factory._getMaximumYear();}
}
该工厂类用makeDate
方法替代了createInstance
方法,前者的名称稍好一些[N1]。在初始状态下,它使用SpreadsheetDateFactory
,但随时可以使用其他工厂。委托到抽象方法的静态方法混合采用了单件模式(SINGLETON)、油漆工模式和抽象工厂模式,我发现这种手段很有用。
SpreadsheetDateFactory
看起来像这个样子:
public class SpreadsheetDateFactory extends DayDateFactory {public DayDate _makeDate(int ordinal) {return new SpreadsheetDate(ordinal);}public DayDate _makeDate(int day, DayDate.Month month, int year) {return new SpreadsheetDate(day, month, year);}public DayDate _makeDate(int day, int month, int year) {return new SpreadsheetDate(day, month, year);}public DayDate _makeDate(Date date) {final GregorianCalendar calendar = new GregorianCalendar();calendar.setTime(date);return new SpreadsheetDate(calendar.get(Calendar.DATE),DayDate.Month.make(calendar.get(Calendar.MONTH) + 1),calendar.get(Calendar.YEAR));}protected int _getMinimumYear() {return SpreadsheetDate.MINIMUM_YEAR_SUPPORTED;}protected int _getMaximumYear() {return SpreadsheetDate.MAXIMUM_YEAR_SUPPORTED;}
}
如你所见,我已经把变量MINIMUM_YEAR_SUPPORTED
和MAXIMUM_YEAR_SUPPORTED
移到了它们该在的SpreadsheetDate
中[G6]。
DayDate
的下一个问题是第109行的日期常量。这些常量其实应该是枚举类型[J3]。我们之前见过这种模式,不再赘述。你可以在最终的代码清单中看到。
接着,我们看到第140行中一系列以LAST_DAY_OF_MONTH
开头的数组。首先,描述这些数组的注释全属多余[C3],光看名称就足够了,所以我要删除这些注释。
这个数组没理由不是私有的[G8],因为有一个静态函数lastDayOfMonth
提供同样的数据。
下一个数组AGGREGATE_DAYS_TO_END_OF_MONTH
更神秘一些,在JCommon
框架中根本没用到它[G9]。所以我直接删除了。
对LEAP_YEAR_AGGREGATE_DAYS_TO_END_OF_MONTH
也一样。
AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH
只在SpreadsheetDate
中用到(第434行和第473行)。是否把它移到SpreadsheetDate
中是一个问题。不转移的理由是,该数组并不专属于任何特定的实现[G6],另外,实际上并不存在SpreadsheetDate
之外的实现,所以,数组应该移到靠近其使用位置的地方[G10]。
说服我的理由是保持一致[G11],数组应该私有,并通过类似于julianDateOfLastDayOfMonth
这样的函数来暴露。看来没人需要那样的函数。而且,如果有新的DayDate
实现需要该数组,可以轻易地把它移回到DayDate
中去。所以我就把它移到SpreadsheetDate
里面了。
对LEAP_YEAR_AGGREGATE_DAYS_TO_END_OF_MONTH
也一样。
接着,我们看到3组可以转换为枚举的常量(第162~205行)。第一个用来选择月份中的一周,我将其转换为名为WeekInMonth
的枚举。
public enum WeekInMonth {FIRST(1), SECOND(2), THIRD(3), FOURTH(4), LAST(0);public final int index;WeekInMonth(int index) {this.index = index;}
}
第二组常量(第177~187行)有点麻烦。常量INCLUDE_NONE
、INCLUDE_FIRST
、INCLUDE_SECOND
和INCLUDE_BOTH
用于描述某个范围的终止日是否包含在该范围之内。数学上,用术语“开放区间”“半开放区间”和“闭合区间”来表示。我想,用数学术语来命名会更清晰[N3],所以就将其转换为枚举DateInterval
,其中包括CLOSED
、CLOSED_LEFT
、CLOSED_RIGHT
和OPEN
枚举元素。
第三组常量(第189~205行)描述了是否该在最后、下一个或最近的日期实体中呈现对某个星期中的特定一天的查找结果。怎么命名是一个难题。最终,我给WeekdayRange
设定了LAST
、NEXT
和NEAREST
枚举元素。
你也许不会同意我起的名字。对我而言这些名字有意义,但对你可能不然。要点是它们眼下变成了易于修改的形式[J3],不再以整数形式传递,而是作为符号传递。我可以用IDE的“修改名称”功能来改动名称或类型,无须担忧漏掉代码中某处−1
或2
之类的数字,也不必担忧某些int参数声明处于描述不佳的状态。
第208行的描述字段看来没有任何地方用到,我把它及其取值器和赋值器都删掉了。
我还删除了第213行的默认构造器[G12],编译器会为我们自动生成的。
略过isValidWeekdayCode
方法(第216~238行),在创建Day
枚举时已经把它删掉了。
于是来到stringToWeekdayCode
方法(第242~270行)。没有方法签名增添价值的Javadoc都是废话[C3]、[G12],唯一的价值是对返回值−1的描述。然而,我们改用了Day
枚举,所以这条注释完全错误了[C2]。该方法现在抛出一个IllegalArgumentException
异常,所以我删除了Javadoc。
我还删除了参数和变量声明中的全部final
关键字,我敢说,它们毫无价值,只会混淆视听[G12]。删除这些final
,不合乎某些成例。例如,Robert Simmons就强烈建议我们“……在代码中遍布final
。”我不能苟同。我认为,final
有少数的好用法,例如,偶尔使用的final
常量,但除此之外该关键字利小于弊。我之所以这么认为,或许是因为final
可能捕获到的那些错误类型,早已被我编写的单元测试捕获了。
我不喜欢for
循环(第259行和第263行)中的那些if
语句[G5],所以我利用“||
”操作符把它们连接为单个if
语句。我还使用Day
枚举整理for
循环,做了一些装饰性的修改。
我认为,这个方法并不真属于DayDate
类,它其实是Day
的一个解析函数。所以,我将它移到Day
枚举中。不过,那样Day
枚举就会变得太大。因为Day
的概念并不依赖DayDate
,所以我把Day
枚举移到DayDate
类之外,放到它自己的源代码文件中。
我还把下一个函数weekdayCodeToString
(第272~286行),移植到Day
枚举中,称其为toString
。
public enum Day {MONDAY(Calendar.MONDAY),TUESDAY(Calendar.TUESDAY),WEDNESDAY(Calendar.WEDNESDAY),THURSDAY(Calendar.THURSDAY),FRIDAY(Calendar.FRIDAY),SATURDAY(Calendar.SATURDAY),SUNDAY(Calendar.SUNDAY);public final int index;private static DateFormatSymbols dateSymbols = new DateFormatSymbols();Day(int day) {index = day;}public static Day make(int index) throws IllegalArgumentException {for (Day d : Day.values())if (d.index == index)return d;throw new IllegalArgumentException(String.format("Illegal day index: %d.", index));}public static Day parse(String s) throws IllegalArgumentException {String[] shortWeekdayNames =dateSymbols.getShortWeekdays();String[] weekDayNames =dateSymbols.getWeekdays();s = s.trim();for (Day day : Day.values()) {if (s.equalsIgnoreCase(shortWeekdayNames[day.index]) ||s.equalsIgnoreCase(weekDayNames[day.index])) {return day;}}throw new IllegalArgumentException(String.format("%s is not a valid weekday string", s));}public String toString() {return dateSymbols.getWeekdays()[index];}
}
有两个getMonth
函数(第288~316行)。第一个函数调用第二个函数。第二个函数只被第一个函数调用。所以,我把这两个函数合二为一,而且极大地简化之[G9][G12][F4]。最后,我把名称修改得更具描述力[N1]。
public static String[] getMonthNames() {return dateFormatSymbols.getMonths();
}
由于有了Month
枚举,函数isValidMonthCode
(第326~346行)就变得没什么用,因此我把它删除了[G9]。
函数monthCodeToQuarter
(第356~375行)有特性依恋(FEATURE ENVY)的味道,可以是Month
枚举中的一个名为quarter
的方法,我就这么办了。
public int quarter() {return 1 + (index-1)/3;
}
这样一来,Month
枚举就大到需要放到自己的类中了。我把它从DayDate
中移出来,与Day
枚举保持一致[G11][G13]。
后面两个方法被命名为monthCodeToString
(第377~426行)。我们再次看到其中一个方法使用标识调用其兄弟方法的模式。将标识作为参数传递给函数的做法通常不太好,尤其是当该标识只是有关其输出格式时[G15]。我重命名、简化、重新构架了这些函数,并把它们移到Month
枚举中[N1][N3][G14]。
public String toString() {return dateFormatSymbols.getMonths()[index - 1];
}public String toShortString() {return dateFormatSymbols.getShortMonths()[index - 1];
}
下一个方法是stringToMonthCode
(第428~472行)。我重新为它命名,转移到Month
枚举中,并且简化之[N1][N3][C3][G14][G12]。
public static Month parse(String s) {s = s.trim();for (Month m : Month.values())if (m.matches(s))return m;try {return make(Integer.parseInt(s));}catch (NumberFormatException e) {}throw new IllegalArgumentException("Invalid month " + s);
}private boolean matches(String s) {return s.equalsIgnoreCase(toString()) ||s.equalsIgnoreCase(toShortString());
}
方法isLeapYear
(第495~517行)可以写得更具表达力一些[G16]。
public static boolean isLeapYear(int year) {boolean fourth = year % 4 == 0;boolean hundredth = year % 100 == 0;boolean fourHundredth = year % 400 == 0;return fourth && (!hundredth || fourHundredth);
}
下一个函数leapYearCount
(第519~536行)并不真属于DayDate
。除了SpreadsheetDate
中的两个方法,没有其他调用者,所以我将它往下放。
函数lastDayOfMonth
(第538~560行)使用了LAST_DAY_OF_MONTH
数组,该数组应该隶属于Month
枚举[G17],所以我就把它移到那儿去了。我还简化了这个函数,使其更具表达力[G16]。
public static int lastDayOfMonth(Month month, int year) {if (month == Month.FEBRUARY && isLeapYear(year))return month.lastDay() + 1;elsereturn month.lastDay();
}
现在,事情变得比较有趣了。下一个函数是addDays
(第562~576行)。首先,由于该函数对DayDate
的变量进行操作,它就不该是静态的[G18],因此,我把它修改为实体方法。其次,它调用了函数toSerial
,这个函数应该重新命名为toOrdinal
[N1]。最后,该方法可以简化。
public DayDate addDays(int days) {return DayDateFactory.makeDate(toOrdinal() + days);
}
对于addMonth
(第578~602行)也一样。它应该是一个实体方法[G18],算法过于复杂,所以我利用解释临时变量模式(EXPLAINING TEMPORARY VARIABLES)来使其更为透明。我还将方法getYYY
重命名为getYear
[N1]。
public DayDate addMonths(int months) {int thisMonthAsOrdinal = 12 * getYear() + getMonth().index - 1;int resultMonthAsOrdinal = thisMonthAsOrdinal + months;int resultYear = resultMonthAsOrdinal / 12;Month resultMonth = Month.make(resultMonthAsOrdinal % 12 + 1);int lastDayOfResultMonth = lastDayOfMonth(resultMonth, resultYear);int resultDay = Math.min(getDayOfMonth(), lastDayOfResultMonth);return DayDateFactory.makeDate(resultDay, resultMonth, resultYear);
}
对函数addYear
(第604~626行)也照方办理。
public DayDate plusYears(int years) {int resultYear = getYear() + years;int lastDayOfMonthInResultYear = lastDayOfMonth(getMonth(), resultYear);int resultDay = Math.min(getDayOfMonth(), lastDayOfMonthInResultYear);return DayDateFactory.makeDate(resultDay, getMonth(), resultYear);
}
把这些方法从静态方法变为实体方法,让我有点心头发痒。用date.addDays(5)
这样的表达方法,是不是明确地表示date
对象并没变动,以及返回了一个DayDate
的新实体呢?或者,它只是错误地暗示我们往date
对象添加了5天呢?你可能不会认为这是一个大问题,但下列代码可能会有欺骗性。
DayDate date = DateFactory.makeDate(5, Month.DECEMBER, 1952);
date.addDays(7); // bump date by one week
有些读到这段代码的人会认为addDays
在修改date
对象。所以,我们需要消除这种歧义[N4]。我把名称改为plusDays
和plusMonths
。我认为,方法的初衷很清楚地被
DayDate date = oldDate.plusDays(5);
所体现,不过下列代码对认为date
对象被修改的读者来说,看起来并不那么顺畅:
date.plusDays(5);
算法越来越有趣,getPreviousDayOfWeek
(第628~660行)可以工作,不过有点复杂了。经过一番思考,了解到它的功能后[G21],我就能够使用解释临时变量模式来简化它[G19],使其更为清晰。我还将它从静态方法改为实体方法[G18],并删除了重复的实体方法[G5](第997~1008行)。
public DayDate getPreviousDayOfWeek(Day targetDayOfWeek) {int offsetToTarget = targetDayOfWeek.index - getDayOfWeek().index;if (offsetToTarget >= 0)offsetToTarget -= 7;return plusDays(offsetToTarget);
}
对getFollowingDayOfWeek
(第662~693行)也如法炮制:
public DayDate getFollowingDayOfWeek(Day targetDayOfWeek) {int offsetToTarget = targetDayOfWeek.index - getDayOfWeek().index;if (offsetToTarget <= 0)offsetToTarget += 7;return plusDays(offsetToTarget);
}
下一个函数是我们之前修改过的getNearestDayOfWeek
(第695~726行)。我之前所做的修改和前两个函数没有保持一致[G11],所以我将它改为和这两个函数保持一致,并且使用解释临时变量模式[G19]来阐明算法。
public DayDate getNearestDayOfWeek(final Day targetDay) {int offsetToThisWeeksTarget = targetDay.index - getDayOfWeek().index;int offsetToFutureTarget = (offsetToThisWeeksTarget + 7) % 7;int offsetToPreviousTarget = offsetToFutureTarget - 7;if (offsetToFutureTarget > 3)return plusDays(offsetToPreviousTarget);elsereturn plusDays(offsetToFutureTarget);
}
方法getEndOfCurrentMonth
(第728~740行)有点奇怪,因为它获取了DayDate
参数,从而成为一个依恋[G14]其自身类的实体方法。我将其改为真正的实体方法,并修改了几个名称。
public DayDate getEndOfMonth() {Month month = getMonth();int year = getYear();int lastDay = lastDayOfMonth(month, year);return DayDateFactory.makeDate(lastDay, month, year);
}
重构weekInMonthToString
(第742~761行)的过程非常有趣。利用IDE的重构工具,我先将其移到我之前创建的WeekInMonth
枚举中,再将其重命名为toString
。接着,我把它从静态方法改为实体方法。所有的测试都通过了。(你能猜出来我打算做什么吗?)
接下来,我删掉了整个方法!有5个断言失败了(第411~415行,见代码清单B-4)。我改动了这些代码行,让它们使用枚举元素的名称(FIRST
、SECOND
……)。全部测试都通过了。你知道为什么吗?你是否知道为什么这些步骤都是必要的吗?重构工具确保之前对weekInMonthToString
方法的调用现在都调用weekInMonth
枚举元素的toString
方法,全部枚举元素都以返回其名称的形式实现了toString
方法……
我不幸有点聪明过头了。经过这一套美妙的重构,我终于意识到,这个函数的唯一调用者,就是我刚修改的测试,所以我删除了这些测试。
愚我一次,是你之耻。愚我两次,是我之耻!所以,在判定除测试之外没有人调用过relativeToString
(第765~781行)后,我就删除了该函数及其测试。
我最后将其改为这个抽象类的抽象方法。第一个函数保持了原样:toSerial
(第838~844行),前文我曾把其名称改为toOrdinal
,以现在的情形看,我决定把名称改为getOrdinalDay
。
下一个抽象方法是toDate
(第838~844行)。它将DayDate
转换为java.util.Date
。这个方法为何是抽象的?查看其在SpreadsheetDate
中的实现(第198~207行,见代码清单B-5),可以看到它并不依赖该类的实现[G6],所以,我把它往上推了。
方法getYYYY
、getMonth
和getDayOfMonth
已经是抽象方法。不过,getDayOfWeek
方法是另一个应该从SpreadsheetDate
中提出来的方法,因为它不依赖DayDate
之外的东西[G6]。是这样吗?
仔细阅读代码清单B-5第247行,可以发现该算法暗中依赖顺序日期的起点(换言之,第0天的星期日数)。所以,即便该方法没有物理上的依赖,也不能移到DayDate中,因为它的确有逻辑上的依赖。
这样的逻辑依赖困扰了我[G22]。如果有什么东西在逻辑上依赖实现的话,也该有物理上的依赖存在。我也认为,算法本身也该有一小部分依赖实现。
所以我在DayDate
中创建了一个名为getDayOfWekForOrdinalZero
的抽象方法,并在SpreadsheetDate
中实现它,返回Day.SATURDAY
。然后我把getDayOfWeek
上移到DayDate
中,并调用getOrdinalDay
和getDayOfWeekForOrdinalZero
。
public Day getDayOfWeek() {Day startingDay = getDayOfWeekForOrdinalZero();int startingOffset = startingDay.index - Day.SUNDAY.index;return Day.make((getOrdinalDay() + startingOffset) % 7 + 1);
}
顺便说一句,请仔细阅读第895~899行的注释。这样的重复有必要吗?通常,我会删除这类注释。
下一个方法是compare
(第902~913行)。同样,该抽象方法是不恰当的[G6],我将其实现上移到DayDate
,其名称也不足够有沟通意义[N1]。方法实际上返回的是自参数日期以来的天数,所以我把名称改为daysSince
。我还注意到该方法没有测试,就为它编写了测试。
下面6个函数(第915~980行)全都是应该在DayDate
中实现的抽象方法。我把它们全都从SpreadsheetDate
中抽出来了。
最后一个函数isInRange
(第982~995行)也需要推到上一层并重构之。那个switch
语句有点儿丑陋[G23],可以把那些条件判断移到DateInterval
枚举中去。
public enum DateInterval {OPEN {public boolean isIn(int d, int left, int right) {return d > left && d < right;}},CLOSED_LEFT {public boolean isIn(int d, int left, int right) {return d >= left && d < right;}},CLOSED_RIGHT {public boolean isIn(int d, int left, int right) {return d > left && d <= right;}},CLOSED {public boolean isIn(int d, int left, int right) {return d >= left && d <= right;}};public abstract boolean isIn(int d, int left, int right);
}public boolean isInRange(DayDate d1, DayDate d2, DateInterval interval) {int left = Math.min(d1.getOrdinalDay(), d2.getOrdinalDay());int right = Math.max(d1.getOrdinalDay(), d2.getOrdinalDay());return interval.isIn(getOrdinalDay(), left, right);
}
我们来到了DayDate
的末尾。现在我们要从头到尾再过一次,看看整个重构过程是怎样良好执行的。
首先,开端注释过时已久,我缩短并改进了它[C2]。
然后,我把全部枚举移到它们自己的文件中[G12]。
接着,我把静态变量(dateFormatSymbols
)和3个静态方法(getMonthNames
、isLeapYear
和lastDayOfMonth
)移到名为DateUtil
的新类中[G6]。
我把那些抽象方法上移到它们该在的顶层类中[G24]。
我把Month.make
改为Month.fromInt
[N1],并如法炮制所有其他枚举。我还为全部枚举创建了toInt()
访问器,把index
字段改为私有。
在plusYears
和plusMonths
中存在一些有趣的重复[G5],我通过抽离出名为correctLastDayOfMonth
的新方法消解了重复,使这3个方法清晰多了。
我消除了魔术数1 [G25],用Month.JANUARY.toInt()
或Day.SUNDAY.toInt()
做了恰当的替换。我在SpreadsheetDate
上花了点儿时间,清理了一下算法。最终结果在代码清单B-7~代码清单B-16中。
有趣的是,DayDate
的代码覆盖率降低到了84.9%!这并不是因为测试到的功能减少了,而是因为该类缩减得太多,导致少量未覆盖到的代码行拥有了更大权重。DayDate
的53个可执行语句中有45个得到测试覆盖。未覆盖的代码行微细到不值得测试。
16.3 小结
我们再一次遵从了童子军军规。我们签入的代码,要比签出时整洁了一点。虽然花了点儿时间,不过很值得。测试覆盖率提升了,修改了一些缺陷,代码清晰并缩短了。后来者有望比我们更容易地应对这些代码,他们也有可能把代码整理得更干净些。
相关文章:
【Code】《代码整洁之道》笔记-Chapter16-重构SerialDate
第16章 重构SerialDate 如果你找到JCommon类库,深入该类库,其中有个名为org.jfree.date的程序包。在该程序包中,有个名为SerialDate的类,我们即将剖析这个类。 SerialDate的作者是David Gilbert。David显然是一位经验丰富、能力…...
redis 内存中放哪些数据?
在 Java 开发中,Redis 作为高性能内存数据库,通常用于存储高频访问、低延迟要求、短期有效或需要原子操作的数据。以下是 Redis 内存中常见的数据类型及对应的使用场景,适合面试回答: 1. 缓存数据(高频访问,降低数据库压力) 用户会话(Session):存储用户登录状态、临时…...
【Python使用】嘿马云课堂web完整实战项目第4篇:封装异常处理,封装JSON返回值【附代码文档】
教程总体简介:项目概述 项目背景 项目的功能构架 项目的技术架构 CMS 什么是CMS CMS需求分析与工程搭建 静态门户工程搭建 SSI服务端包含技术 页面预览开发 4 添加“页面预览”链接 页面发布 需求分析 技术方案 测试 环境搭建 数据字典 服务端 前端 数据模型 页面原…...
「数据可视化 D3系列」入门第三章:深入理解 Update-Enter-Exit 模式
深入理解 Update-Enter-Exit 模式 一、数据绑定三态:Update、Enter、Exit三种状态的直观理解 二、基础概念1. Update 选区 - 处理已有元素2. Enter 选区 - 处理新增数据3. Exit 选区 - 处理多余元素 三、完整工作流程四、三种状态的底层原理数据绑定过程解析键函数&…...
中间件--ClickHouse-5--架构设计(分布式架构,列式压缩存储、并行计算)
1、整体架构设计 ClickHouse 采用MPP(大规模并行处理)架构,支持分布式计算和存储,其核心设计目标是高性能列式分析。 (1)、存储层 列式存储: 数据按列存储(而非传统行式存储&#…...
AgentGPT 在浏览器中组装、配置和部署自主 AI 代理 入门介绍
AI MCP 系列 AgentGPT-01-入门介绍 Browser-use 是连接你的AI代理与浏览器的最简单方式 AI MCP(大模型上下文)-01-入门介绍 AI MCP(大模型上下文)-02-awesome-mcp-servers 精选的 MCP 服务器 AI MCP(大模型上下文)-03-open webui 介绍 是一个可扩展、功能丰富且用户友好的…...
【开源项目】Excel手撕AI算法深入理解(三):Backpropagation、mamba、RNN
项目源码地址:https://github.com/ImagineAILab/ai-by-hand-excel.git 一、Backpropagation 1. 反向传播的本质 反向传播是通过链式法则计算损失函数对网络参数的梯度的高效算法,目的是用梯度下降优化参数。其核心思想是: 前向传播…...
uniapp的通用页面及组件基本封装
1.基本布局页面 适用于自定义Navbar头部 <template><view class"bar" :style"{height : systemInfo.statusBarHeight px, background: param.barBgColor }"></view><view class"headBox" :style"{ height: param.h…...
Ubuntu和Debian 操作系统的同与异
首先需要说明:Ubuntu 是基于 Debian 操作系统开发的。它们之间的关系如下 起源与发展:Debian 是一个社区驱动的开源 Linux 发行版,始于 1993 年,是最早的 Linux 发行版之一,以其稳定性和自由软件政策著称。Ubuntu 是基…...
【android bluetooth 协议分析 21】【ble 介绍 1】【什么是RPA】
通俗易懂地讲解一下 BLE(低功耗蓝牙)中的 Resolvable Private Address(RPA,可解析私有地址)。 1. 一句话理解 RPA 是一种“临时的、隐私保护的蓝牙设备地址”,别人无法随便追踪你,但“授权的设…...
狂神SQL学习笔记九:MyISAM 和 lnnoDB 区别
show create database school –查看创建数据库的语句 show create table student – 查看student数据表的定义语句 desc student –显示表的结构 MYISAMINNODB事务支持不支持支持数据行锁定不支持支持行锁定外键不支持支持全文索引支持不支持表空间的大小较小较大&#x…...
深度学习--神经网络的构造
在当今数字化时代,深度学习已然成为人工智能领域中最为耀眼的明星。而神经网络作为深度学习的核心架构,其构造方式决定了模型的性能与应用效果。本文将深入探讨深度学习神经网络的构造,带您领略这一前沿技术的奥秘。 一、神经网络基础概念…...
Jenkins 代理自动化-dotnet程序
两种方式 容器部署 本地部署 容器部署 可自动实现,服务器重启,容器自动运行 主要将dockerfile 写好 本地部署 1.服务器重启自动运行代理 参考下面的链接,只是把程序换成 java程序,提前确认好需要的jdk版本 Ubuntu20.04 设置开机…...
【区块链+ 人才服务】“CERX Network”——基于 FISCO BCOS 的研学资源交换网络 | FISCO BCOS 应用案例
CERX Network (Consortium-based Education Resource Exchanging Network) 是定位于面向高校科学研究与教学 的分布式研学资产交换网络, 构建一个用于数据、 算法模型、 论文和课程的研学资源价值流转平台。项目以 FISCO BCOS 联盟链为底层平…...
中间件--ClickHouse-6--SQL基础(类似Mysql,存在差异)
ClickHouse语言类似Mysql,如果熟悉Mysql,那么学习ClickHouse的语言还是比较容易上手的。 1、建表语法(CREATE TABLE) (1)、表引擎(Engine) MySQL: 默认使用 InnoDB 引…...
[MSPM0开发]MSPM0G3507番外一:关于使用外部高速晶振HFXT后程序可能不运行的问题
一、问题描述 如下图所示,MSPM0G3507时钟树配置为使用外部HFXT(外部高速晶振)作为HSCLK时钟源。 配置结果MCLK 40MHz。 另外配置PB22为输出模式,控制外部LED亮灭。 在main.c中主要代码如下: 主要完成延时并翻转LED控…...
2025年计算机领域重大技术突破与行业动态综述
——前沿技术重塑未来,开发者如何把握机遇? 2025年第一季度,全球计算机领域迎来多项里程碑式进展,从量子计算到人工智能,从芯片设计到网络安全,技术革新与产业融合持续加速。本文梳理近三个月内最具影响力…...
我的机器学习之路(初稿)
文章目录 一、机器学习定义二、核心三要素三、算法类型详解1. 监督学习(带标签数据)2. 无监督学习(无标签数据)3. 强化学习(决策优化)(我之后主攻的方向) 四、典型应用场景五、学习路线图六、常见误区警示七…...
交易模式革新:Eagle Trader APP上线,助力自营交易考试效率提升
近年来,金融行业随着投资者需求的日益多样化,衍生出了众多不同的交易方式。例如,为了帮助新手小白建立交易基础,诞生了各类跟单社区;而与此同时,一种备受瞩目的交易方式 —— 自营交易模式,正吸…...
emotn ui桌面tv版官网-emotn ui桌面使用教程
在智能电视和盒子的使用中,出色的桌面系统能大幅提升体验,Emotn UI桌面TV版便是其中的佼佼者。 访问Emotn UI桌面TV版官网,首页简洁清晰,“产品介绍”“下载中心”等板块一目了然。官网对其功能优势详细阐述,在“下载中…...
Django之modelform使用
Django新增修改数据功能优化 目录 1.新增数据功能优化 2.修改数据功能优化 在我们做数据优化处理之前, 我们先回顾下传统的写法, 是如何实现增加修改的。 我们需要在templates里面新建前端的页面, 需要有新增还要删除, 比如说员工数据的新增, 那需要有很多个输入框, 那html…...
Hadoop:大数据时代的基石
在当今数字化浪潮中,数据量呈爆炸式增长,企业和组织面临着前所未有的数据处理挑战。从社交媒体的海量信息到物联网设备的实时数据,如何高效地存储、管理和分析这些数据成为了一个关键问题。Apache Hadoop 作为大数据处理领域的核心框架&#…...
定制开发还是源码搭建?如何快速上线同城外卖跑腿APP?
在“万物皆可同城配送”的时代,同城外卖跑腿APP成为众多创业者和本地服务商的热门选择。无论是打造本地生活服务平台,还是拓展快送业务,拥有一款功能完善、体验流畅的外卖跑腿APP,已经成为进入市场的标配。 然而,对于…...
How AI could empower any business - Andrew Ng
How AI could empower any business - Andrew Ng References 人工智能如何为任何业务提供支持 empower /ɪmˈpaʊə(r)/ vt. 授权;给 (某人) ...的权力;使控制局势;增加 (某人的) 自主权When I think about the rise of AI, I’m reminded …...
SpringBoot-基础特性
1.SpringApplication 1.1.自定义banner 类路径添加banner.txt或设置spring.banner.location就可以定制 banner 1.2.自定义 SpringApplication import org.springframework.boot.Banner; import org.springframework.boot.SpringApplication; import org.springframework.bo…...
系统环境变量有什么实际作用,为什么要配置它
系统环境变量有什么实际作用,为什么要配置它 系统环境变量具有以下重要实际作用: 指定程序路径:操作系统通过环境变量来知晓可执行文件、库文件等的存储位置例如,当你在命令提示符或终端中输入一个命令时,系统会根据环境变量PATH中指定的路径去查找对应的可执行文件。如果…...
C++ | STL之list详解:双向链表的灵活操作与高效实践
引言 std::list 是C STL中基于双向链表实现的顺序容器,擅长高效插入和删除操作,尤其适用于频繁修改中间元素的场景。与std::vector不同,std::list的内存非连续,但提供了稳定的迭代器和灵活的元素管理。本文将全面解析std::list的…...
Spring Cloud 服务间调用深度解析
前言 在构建微服务架构时,服务间的高效通信是至关重要的。Spring Cloud 提供了一套完整的解决方案来实现服务间的调用、负载均衡、服务发现等功能。本文将深入探讨 Spring Cloud 中服务之间的调用机制,并通过源码片段和 Mermaid 图表帮助读者更好地理解…...
什么是时间复杂度和空间复杂度?
什么是时间复杂度和空间复杂度? 时间复杂度:衡量代码运行时间随输入规模增大而增长的速度。简单来说,就是“代码跑多快”。 空间复杂度:衡量代码运行时额外占用的内存空间随输入规模增大而增长的速度。简单来说,就是“代码用多少内存”。 我们通常用 大 O 表示法(Big O N…...
算法思想之分治-快排
欢迎拜访:雾里看山-CSDN博客 本篇主题:算法思想之分治-快排 发布时间:2025.4.15 隶属专栏:算法 目录 算法介绍核心步骤优化策略 例题颜色分类题目链接题目描述算法思路代码实现 排序数组题目链接题目描述算法思路代码实现 数组中的…...
25.4.15学习总结
问题: 邮箱验证码通过公钥加密后发到前端,在前端用私钥解密验证可行吗? 结论: 在前端使用私钥解密通过公钥加密的邮箱验证码在技术上是可行的,但存在严重的安全风险,不建议采用。 问题分析 非对称加密的…...
小程序获取用户总结(全)
获取方式 目前小程序获取用户一共有3中(自己接触到的),但由于这个API一直在改,所以不确定后期是否有变动,还是要多关注官方公告。 方式一 使用wx.getUserInfo 实例: wxml 文件<button open-type="getUserInfo" bindgetuserinfo="onGetUserInfo&quo…...
如何成为一名嵌入式软件工程师?
如何成为一名嵌入式软件工程师? 01明确岗位的角色与定位 嵌入式软件工程师主要负责开发运行在特定硬件平台上的软件,这些软件通常与硬件紧密集成,以实现特定的功能。 不仅需要精通编程语言(如C/C、Java等)和软件开发工…...
机器人发展未来两年会有突破吗?
未来两年,机器人技术将在芯片、编码器、材料、加工工艺和AI等核心领域迎来系统性突破,推动行业从专用化向通用化转型。以下从技术路径、产业动态和商业化前景三个维度展开分析,结合权威数据与技术趋势,构建机器人技术演进的全景框架。 一、芯片技术:3nm制程与存算一体架构…...
【grafana原生告警中心配置飞书机器人告警】
在grafana中的connect point中使用webhook的方式推送到飞书,始终无法触发告警,原因是grafana推送的格式飞书不识别,现有两种方式 1.使用中转服务 使用flask搭建一个服务,grafana告警先通过webhook发送到web服务中,格…...
HTTP HTTPS RSA
推荐阅读 小林coding HTTP篇 文章目录 HTTP 80HTTP 响应码1xx:信息性状态码(Informational)2xx:成功状态码(Success)3xx:重定向状态码(Redirection)4xx:客户端…...
【机器学习】如何正确下载sklearn包
TOC 直接pip install sklearn时,报错 sklearn的包,实际上叫scikit-learn pip install scikit-learn发现成功了: 总结 下载sklearn包的语句:pip install scikit-learn 完成。...
【Python进阶】断言(assert)的十大核心应用场景解析
目录 前言:技术背景与价值当前技术痛点解决方案概述目标读者说明 一、技术原理剖析核心概念图解核心作用讲解关键技术模块技术选型对比 二、实战演示环境配置要求核心代码实现(10个案例)案例1:参数合法性检查案例2:不变…...
关于汽车辅助驾驶不同等级、技术对比、传感器差异及未来发展方向的详细分析
以下是关于汽车辅助驾驶不同等级、技术对比、传感器差异及未来发展方向的详细分析: 一、汽车辅助驾驶等级详解 根据SAE(国际自动机工程师学会)的标准,自动驾驶分为 L0到L5 六个等级: 1. L0(无自动化&…...
STM32 HAL库之WDG示例代码
独立看门狗(IWDG) 初始化独立看门狗,在main.c中的 MX_IWDG_Init();,也就是iwdg.c中的初始化代码 void MX_IWDG_Init(void) {/* USER CODE BEGIN IWDG_Init 0 *//* USER CODE END IWDG_Init 0 *//* USER CODE BEGIN IWDG_Init 1 …...
【差分隐私相关概念】瑞丽差分隐私(RDP)命题10
命题10证明中的最后一个不等号成立,关键在于将事件 A A A上的积分与Rnyi散度 D α ( P ∥ Q ) D_\alpha(P \parallel Q) Dα(P∥Q)的定义联系起来,并通过积分放缩得到上界。具体推导如下: Rnyi散度的定义: D α ( P ∥ Q ) 1 …...
Android 开发 如何生成系统签名
在源码中拿到安全文件 文件路径 lagvm/LINUX/android/build/target/product/security如下两个文件 platform.pk8 platform.x509.pem 使用Android studio生成一个jks Android studio 顶部 buildGenerate Signed Bundle or APKapkcrate new记住 记住alias 和password linux下…...
(EtherCAT 转 EtherNet/IP)EtherCAT/Ethernet/IP/Profinet/ModbusTCP协议互转工业串口网关
型号 协议转换通信网关 EtherCAT 转 EtherNet/IP MS-GW12 概述 MS-GW12 是 EtherCAT 和 EtherNet/IP 协议转换网关,为用户提供两种不同通讯协议的 PLC 进行数据交互的解决方案,可以轻松容易将 EtherNet/IP 网络接入 EtherCAT 网络中,方便…...
适合stm32 前端adc使用的放大器芯片
在 STM32 前端 ADC 应用中,合适的放大器芯片需具备低噪声、高精度、低失调电压等特性。以下为你推荐几款常用的放大器芯片: 低功耗、高精度型 OPA2333 特点:这是一款微功耗、零漂移运算放大器,失调电压极低,仅为 2.5…...
《Ethical Implications of ChatGPT in Higher Education: A Scoping Review》全文翻译
《Ethical Implications of ChatGPT in Higher Education: A Scoping Review》 ChatGPT在高等教育中的伦理影响:一项范围界定性综述 摘要 本范围界定性综述探讨了在高等教育中使用ChatGPT所引发的伦理挑战。通过回顾近期发表的英文、中文和日文的学术文章&#x…...
day26 学习笔记
文章目录 前言一、边缘填充1.边界复制2.边界反射3.边界常数4.边界包裹5.代码示例 二、透视变换三、颜色加法 前言 通过今天的学习,我掌握了OpenCV中有关边缘填充,透视变换以及颜色加法的相关概念和操作 一、边缘填充 当我们对图像进行仿射变换后往往会发…...
LVGL Animation Image(Animimg)控件详解
一、Animation Image(Animimg)控件详解 1. 概述 功能:Animimg 是 LVGL 中用于显示动画图像的控件。特点:支持从多个静态图像创建动画效果。 2. 创建和初始化 创建方法:lv_obj_t * lv_animimg_create(lv_obj_t * pa…...
【unity游戏开发入门到精通——UGUI】GraphicRaycaster图形射线投射器组件
注意:考虑到UGUI的内容比较多,我将UGUI的内容分开,并全部整合放在【unity游戏开发——UGUI】专栏里,感兴趣的小伙伴可以前往逐一查看学习。 文章目录 前言Graphic Raycaster参数1、Ignore Reversed Graphics:是否忽略反…...
WPF GDI 画 晶圆Mapping图
效果图 UI代码 <Window x:Class="WpfWaferMapping.Window3"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft.com/expre…...
AI核心概念之“提示(Prompt)” - 来自DeepSeek
1. 表层理解:Prompt 是用户输入的文本指令 直观表现: 对于普通用户,Prompt 是输入到对话框的文本(例如 ChatGPT 中的问题:“写一首关于秋天的诗”),点击发送后,模型返回结果。 常见…...