使用DB2的Docker镜像建立本地开发用的数据库

这个是2019-07-23的笔记,DB2的社区免费版授权和镜像的使用方法随时可能改变。

从哪儿找DB2

DB2的免费版本在 2019年的社区版DB2 中可以找到,提供了三种免费版的使用方法:

  1. 使用docker镜像
  2. 下载DB2程序
  3. IBM Cloud的DB2服务(免费套餐)

这里记一下使用第一种方法Docker镜像的构建过程。

开工

第一步,打开冰箱….啊不,下载镜像,可以在docker hub搜DB2,这篇笔记使用的是 ibmcom/db2

第二步,创建容器,在控制台执行:

1
docker run --name db2 --privileged -p 50000:50000 -e LICENSE=accept -e DB2INST1_PASSWORD=password -e DBNAME=MYDATABASE ibmcom/db2

简单解释一下

  1. --name 容器的名字,以后启动关闭的时候用
  2. --privileged 不知道(弄懂了再写)
  3. -p 端口映射,这里的意思是本地的50000端口映射到容器的50000,这样我们就可以通过localhost:50000来访问容器里的数据库了
  4. -e 给容器里设置一些环境变量
    1. LICENSE 嗯。。。
    2. DB2INST1_PASSWORD 为默认的用户名db2inst1设置的密码。
    3. DBNAME 直接使用变量作为名字建立一个database
  5. 最后那个ibmcom/db2是镜像名

第三步,连接:

1
2
3
4
url: jdbc:db2://localhost:50000/MYDATABASE
username: db2inst1
password: password
driver: db2jcc4.jar

驱动文件可以在IBM的服务网页找到 db2jcc4.jar

结束

有时候会提示port无法使用,容器不能启动。先确定port50000没有被占用,如果可以使用,就重启docker。

关于枚举型的一些理解

最近工作中写了一个枚举类,在code-review的时候被佐藤老师指摘了好几处,特别总结一下加深理解。

什么是枚举型 Enum

Enum是Java 5开始引入的一个接口,关键字 enum 创建的类会自动使用Enum接口。

1
2
3
4
enum WeekDay {
MONDAY, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

像这样定义了WeekDay之后,我们就不必为周一到周日创建整形或者字符串的静态变量了,可以直接把WeekDay中定义的七个名字当做基本型来使用: WeekDay.MONDAY , WeekDay.TUESDAY , WeekDay.WEDNESDAY , WeekDay.THURSDAY , WeekDay.FRIDAY , WeekDay.SATURDAY , WeekDay.SUNDAY

枚举型的构造体是私有的,不可以使用构造体来构建枚举类的实体,但是枚举类中的元素是公开的,可以像基本型一样直接取来使用。

1
WorkDay day = WorkDat.SUNDAY; 

用枚举类的思维方式来使用枚举类

引入枚举类的一个主要目的,是可以有一种类型安全,不可变的方法来定义常量。在引入枚举类之前,一般通过定义基本型的常量来表达一些固定的值。
比如 static final int MONDAY = 1; 这种写法看似没什么问题,可没有什么实际意义的1在这里承担了很重要的职责,不免让人产生疑问,改成0可不可以呢?
而在判断是不是MONDAY时,又必须和1作比较,这就让程序逐渐变得复杂而又危险了。

引入枚举类之后,MONDAY本身就可以起到表达数值,作为判断条件的作用,从而减少了产生bug的可能性。
所以,像下面这种强行使用枚举类的name toString来判断对象是否为枚举类中的一个元素的写法,是很不符合枚举类的思维方式的,浪费了使用枚举类的好处。

1
2
3
4
5
String a = System.getEnv(“TODAY”);
String today = "Undefined";
if(WeekDay.MONDAY.name.equals(a)){
today = WeekDay.MONDAY.name;
}

我们可以给WeekDay添加一个方法来返回元素:

1
2
3
4
5
6
7
8
public WeekDay as(String candidate){
if(MONDAY.name.equals(candidate)){
return MONDAY;
}else if(...){
...
}
return UNDEFINED; //添加一个未定义的元素
}

这样我们在外面就可以告别那个中间的String,直接获取WeekDay的对象:

1
2
3
4
5
6
7
8
WeekDay today = WeekDay.as(System.getEnv("TODAY"));
If(today == WeekDay.UNDEFINED){
// 对于未定义,空的设定值,或者默认值,可以在这里自由的设置
}

if(today == WeekDay.MONDAY){
// 想干啥干啥咯
}

关联 [使用私有构造体或者枚举类型来强化单例性]

区分 Authentication(AuthN) 和 Authorization(AuthZ)

最近写的一段代码里,关于关键字Auth的使用在review的时候被佐藤老师批评了,一直以来对于认证和授权都没有好好的理解,在这里整理一下。

别直接用Auth来命名

之前对于Auth这个关键字的理解,一直都处于大概知道什么意思的模糊状态。其实使用Auth作为关键字来命名方法和变量,并不是很合适,因为AuthenticationAuthorization 的缩写都可以是Auth

Authentication 的中文翻译是认证,日语翻译是認証,可以理解为判断你是不是谁的操作,比如输入用户名和密码,系统判断这个用户名存在,且密码匹配,就是认证。

Authorization 的中文翻译是授权,日语翻译是認可, 我的理解是你可不可以的意思,例如用户名密码通过认证之后,系统判断该用户是否有访问权限,给与权限的操作,就是授权。

401 和 403

HTTP的返回值401和403是一对很容易弄混的数字。

  • 401 Unauthorized
  • 403 Forbidden

401是发生用户认证错误时的返回值,而403是拒绝访问。那么结合上面对AuthenticationAuthorization的理解不难发现:

  • 401 虽然写着Unauthorized的名字,返回的却是Authentication的错误
  • 403 则承担着返回Unauthorized错误的职责

代码重构 - 改善方法的结构

代码重构做的最多的就是改善方法的结构。去掉方法中不需要的元素,修改不明确的名称,将复杂而冗长的方法改成精简而明确的小方法群组,是重构方法的主要思路。

方法重构用的最多的是【提取方法】,有种将一地零落的玩具分类整理装箱的感觉;而【方法内联】一般在方法提取的过于详细以至于产生了反作用,或者需要重新整理方法间关系时使用。

【提取方法】的最大问题,是如何处理局部变量,这就用到了对局部变量的重构技巧。

提取方法 Extract Method

如果发现方法内有一个代码块在很具体的做一件事儿,或者有一句漂亮的注释解释了接下来好几行的操作,那就试试把它提取成一个方法吧,然后取个足以说明其作用的方法名。比如:

1
2
3
4
5
6
7
void printOwing(double amount) {
printBanner();

//print details
System.out.println ("name:" + _name);
System.out.println ("amount" + amount);
}

重构后是这个样子:

1
2
3
4
5
6
7
8
9
void printOwing(double amount) {
printBanner();
printDetails(amount);
}

void printDetails (double amount) {
System.out.println ("name:" + _name);
System.out.println ("amount" + amount);
}

漂亮的代码往往方法都很简短,有着意思明确的方法名。方法越简洁,被重复利用的机会就越大;方法名如果足以传达信息,又可以省去不少的注释。
方法的简洁程度和方法名的好坏是联系在一起的,提取方法可以看做是用方法名来取代原位置的代码块,如果不能通过方法名理解原本代码块要执行的操作,重构也就失去了意义。
重构的时候不用刻意去思考方法或者方法名的长度,重构的关键在于方法名和代码块之间的语义距离(semantic distance)。

局部变量的处理

如果要提取的代码块使用了局部变量,提取方法的操作就要变得稍微复杂一些了。
首先我们需要观察一下局部变量定义、赋值以及被调用的位置,如果局部变量相关的代码可以移动位置,不妨先优化一下,因为优化前后,接下来的重构操作可能会有很大不同。

  1. 如果局部变量足够“局部”,可以直接放到新方法内部,当然是最简单的情况。
  2. 如果局部变量的赋值是在新方法的处理范围外,新方法只是调用了变量值而没有进行修改,我们可以把局部变量当做参数传达给新的方法。
  3. 如果需要新方法的处理来给变量赋值,而变量值的使用又在新方法的范围外,则需要为新方法添加返回值,返回变量值。

想返回多个变量值咋整嘞?这里涉及到了[单一返回值]的问题。编程语言普遍使用的是单一返回值的方法结构,这样可以保证代码的可读性,避免混乱。如果遇到了需要返回多个值的情况,不妨试着细化方法的划分,使用多个单一返回值的方法来实现。

栗子,重构下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void printOwing() {

Enumeration e = _orders.elements();
double outstanding = 0.0;

printBanner();

// calculate outstanding
while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}

printDetails(outstanding);
}

书中直接进行了如下的重构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void printOwing() {
printBanner();
double outstanding = getOutstanding();
printDetails(outstanding);
}

double getOutstanding() {
Enumeration e = _orders.elements();
double outstanding = 0.0;
while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}
return outstanding;
}

书中说将outstanding的计算提取成了一个独立的方法,而前面的局部变量因为只在计算中被使用到,所以一并提取了出来。这里有一个看似理所当然却值得做笔记的操作:作者并没有将printBanner()或者printDetails()放在新方法内。
如果将pringDetails()放到getOutstanding()里,不就不需要设返回值了? 这是因为重构的对象是计算outstanding的代码块,如果将不相关的方法一并放到新方法内,就偏离了重构的目的。
假设将printDetails()放在了getOutstanding()内,再看printOwing()会发现,可以获取的信息变少了,printDetails()这个操作被隐藏在了getOutStanding里,而这并不在我们重构的计划内。
提取方法的重构目的,是用简明的方法调用来代替具体的代码块,提高原位置代码的可读性。如果将代码的方法调用看做是一个树状结构,提取方法就是在增加分支的深度。我们在读代码的时候,并不能看到下层方法的内容,所以将原本需要在上层直接读到的内容放在了很深的位置,反而会降低代码的可读性。

方法内联 Inline Method

重构的一个重要目的是用简洁明了的方法名来替代代码原本所在的位置,以提高可读性。但有时会发现一些方法的内容已经简洁的跟方法名不相上下,这时就可以考虑舍弃方法了。

比如下面代码,moreThanFiveLateDeliveries_numberOfLateDeliveries > 5几乎没什么区别,也就没有必要留着方法了。

1
2
3
4
5
6
int getRating() {
return (moreThanFiveLateDeliveries()) ? 2 : 1;
}
boolean moreThanFiveLateDeliveries(){
return _numberOfLateDeliveries > 5;
}
1
2
3
int getRating() {
return (_numberOfLateDeliveries > 5)? 2 : 1;
}

内联局部变量

如果有一个局部变量像下面的basePrice一样只做了一次很简单的赋值操作,又没有被多个位置引用,那就没必要留着它了。

1
2
double basePrice = anOrder.basePrice();
return (basePrice > 1000)
1
return (anOrder.basePrice() > 1000)

使用查找方法替代局部变量

有时我们为了重复使用某一个表达式的结果,会将其保存在局部变量中,但是局部变量的访问有范围限制,想要使用该变量,就需要在同一个方法内,结果导致方法过长,过长的方法往往有着复杂的结构而又不好重构。如果使用查找方法来替代局部变量,便可以摆脱局部变量的范围限制,在类的各个地方都能使用表达式的结果,重构的时候也就少了很多顾虑。

1
2
3
4
5
double basePrice = _quantity * _itemPrice;
if (basePrice > 1000)
return basePrice * 0.95;
else
return basePrice * 0.98;

basePrice提取成为一个方法:

1
2
3
4
5
6
7
8
if (basePrice() > 1000)
return basePrice() * 0.95;
else
return basePrice() * 0.98;
...
double basePrice() {
return _quantity * _itemPrice;
}

引入解释用变量

比如下面代码

1
2
3
4
5
6
if ( (platform.toUpperCase().indexOf("MAC") > -1)&&
(browser.toUpperCase().indexOf("IE") > -1)&&
wasInitialized() && resize > 0 )
{
// do something
}

条件语句的表达式非常复杂,难以理解,可以引入解释变量,提高条件语句的可读性:

1
2
3
4
5
6
7
final boolean isMacOs     = platform.toUpperCase().indexOf("MAC") > -1;
final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1;
final boolean wasResized = resize > 0;

if (isMacOs && isIEBrowser && wasInitialized() && wasResized) {
// do something
}

不难发现,这个重构操作对条件语句的重构有很不错的效果,但是会引入新的局部变量。我们完全可以通过提取方法来简化表达式,所以作者也有提到:一般在无法提取方法时,才会想起来引入解释变量。
有些方法由于使用了大量局部变量,使用提取方法来重构会很麻烦,这时引入解释变量可能有助于梳理算法结构,对进一步的重构有很大帮助。

分割局部变量

我们可以这样理解:局部变量在方法中的作用大致有两种,

  1. 一种是在循环的处理中,用来当做index或者flag。
  2. 另一种是用来保存值或参照,方便多次的使用,这种局部变量应该只能被赋值一次。
    如果一个局部变量承担了超出上述范围的责任,就该考虑分割它了。让一个局部变量承担多种责任,会降低代码的可读性,造成混乱。所以遇到这种情况,尽可能一个变量一个责任的进行分割。分割之后,再考虑其他的重构操作。比如:
1
2
3
4
double temp = 2 * (_height + _width);
System.out.println (temp);
temp = _height * _width;
System.out.println (temp);

temp两次每调用时,保存的内容不一样,所以应该分割为两个变量:

1
2
3
4
final double perimeter = 2 * (_height + _width);
System.out.println (perimeter);
final double area = _height * _width;
System.out.println (area);

移除对参数的赋值操作

**参数可以引用,修改,但不可以直接使用 = 赋值 **

1
2
3
void aMethod(Object foo) {
foo.modifyInSomeWay(); // √ 这个可以
foo = anotherObject; // × 这个不行

首先要弄懂赋值的概念,使用=的操作,其作用不是修改变量的值,而是改变了该变量名所参照的对象。在方法中改变参数的参照(进行赋值操作),我的理解是有两个问题:

  1. 如果想通过方法的调用来改变参数的参照对象,可以通过返回值来进行赋值,直接在方法中使用=,调用方法的地方看不到该赋值操作,会降低代码的可读性。
  2. 如果是Pass By Value的编程语言,改变参数的参照(Reference),并不会影响到函数外的原变量。这就造成了混乱。

Pass By Value && Pass By Reference

复习一下值传递和引用传递

  • 值传递
    • 方法的参数是变量值的拷贝,在方法内修改参数值不会影响方法外变量的值
  • 引用传递
    • 方法的参数时变量的地址,在方法内修改参数会直接影响该内存地址的内容,方法外变量的值也会改变。

Java是值传递的编程语言,对于基本数据类型的值传递很好理解,对于引用类型的参数,java基本数据类型传递与引用传递区别 这篇文章里有个图很有助于理解

方法的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.zejian.test;
/**
* java中的按值调用
* @author zejian
*/
public class CallByValue {
private static User user=null;
public static void updateUser(User student){
student.setName("Lishen");
student.setAge(18);
}


public static void main(String[] args) {
user = new User("zhangsan",26);
System.out.println("调用前user的值:"+user.toString());
updateUser(user);
System.out.println("调用后user的值:"+user.toString());
}
}

我们可以把引用类型的对象看作是一个值,引用类型的参数所传递的,是这个对象(图中的user),这就是引用类型的值传递。
通过student调用对象的方法是可以修改user内容的,但如果使用=来对student进行赋值,只是让student指向了一个新的对象,并不会影响到user。为了防止参数赋值造成混乱,Java其实可以将参数设置为final,只不过好像没怎么见谁用过…

把方法换成对象

如果一个方法中有太多的局部变量,以至于无法通过提取方法进行重构(可是不进行重构方法又太丑陋了),不妨把方法整体提取为一个独立的对象,局部变量变为该对象的属性后,就可以在对象内轻松地提取方法进行下一步的重构了。

书中例子:

1
2
3
4
5
6
7
8
9
10
Class Account
int gamma (int inputVal, int quantity, int yearToDate) {
int importantValue1 = (inputVal * quantity) + delta();
int importantValue2 = (inputVal * yearToDate) + 100;
if ((yearToDate - importantValue1) > 100)
importantValue2 -= 20;
int importantValue3 = importantValue2 * 7;
// and so on.
return importantValue3 - 2 * importantValue1;
}

上面代码中,如果想要将计算返回值的部分提取出来,就需要将好多局部变量作为参数传递,非常复杂。我们可以使用对象来替代方法,首先写一个Gamma类:

1
2
3
4
5
6
7
8
class Gamma...
private final Account _account;
private int inputVal;
private int quantity;
private int yearToDate;
private int importantValue1;
private int importantValue2;
private int importantValue3;

再为Gamma类添加一个构造体:

1
2
3
4
5
6
Gamma (Account source, int inputValArg, int quantityArg, int yearToDateArg) {
_account = source; // 为了使用方法delta()
inputVal = inputValArg;
quantity = quantityArg;
yearToDate = yearToDateArg;
}

然后将原方法的处理内容转移到Gamma中,作为一个待重构的方法:

1
2
3
4
5
6
7
8
9
int compute () {
importantValue1 = (inputVal * quantity) + _account.delta();
importantValue2 = (inputVal * yearToDate) + 100;
if ((yearToDate - importantValue1) > 100)
importantValue2 -= 20;
int importantValue3 = importantValue2 * 7;
// and so on.
return importantValue3 - 2 * importantValue1;
}

这时,由于局部变量都已经变成了Gamma类的属性,我们提取方法时不需要再担心局部变量了:

1
2
3
4
5
6
7
8
9
10
11
12
13
int compute () {
importantValue1 = (inputVal * quantity) + _account.delta();
importantValue2 = (inputVal * yearToDate) + 100;
importantThing();
int importantValue3 = importantValue2 * 7;
// and so on.
return importantValue3 - 2 * importantValue1;
}

void importantThing() {
if ((yearToDate - importantValue1) > 100)
importantValue2 -= 20;
}

优化算法

书中用的是substitue algorithm, 意思是用简明易懂的写法来替代旧算法,并没有追求提高算法效率或者降低消耗,我在理解的时候把这个重构操作也归类为优化

意思很简单:即使做的还是那些事儿,如果可以让你的算法看起来更容易理解,Just do it.

代码重构 - 简化条件语句

原文 publisher logo Refactoring: Improving the Design of Existing Code Chapter 9 Simplifying Conditianal Expressions

分离条件 - Decompose Conditional

很多程序之所以“复杂难懂”,是因为出现了“复杂难懂”的条件语句。当人们看到条件语句时,最想获取的信息是:if XXX 的时候,do YYY; else do ZZZ。所以简化条件语句最简单直接的方法就是把长长的判断语句或者大段大段的处理分离出来,留下最基本的if else结构,一目了然,至于具体的判断方法和处理内容,则去具体的方法中查看。

遇到有嵌套结构的条件语句时,不妨先试试先整理成卫语句(Guard Clauses),行不通再进行分解处理。

比如我们遇到下面这段代码:

1
2
3
4
5
if (date.before (SUMMER_START) || date.after(SUMMER_END)){
charge = quantity * winterRate + winterServiceCharge;
}else{
charge = quantity * summerRate;
}

对条件和判断后的代码分别施放Extract Method法术:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (notSummer(date)){
charge = winterCharge(quantity);
} else {
charge = summerCharge (quantity);
}

private boolean notSummer(Date date) {
return date.before (SUMMER_START) || date.after(SUMMER_END);
}

private double summerCharge(int quantity) {
return quantity * summerRate;
}

private double winterCharge(int quantity) {
return quantity * winterRate + winterServiceCharge;
}

当然,每次抽离出方法的时候,都不要忘记进行测试,保证重构不会影响原本的处理结果,很多时候条件语句并不是十分复杂,看起来好像没有抽离成为方法的必要,这就要看条件语句读起来是否顺畅易懂了,如果可以把原本机器感十足的语句变得更像一段流畅直白的描述,条件语句的重构就是有意义的。

整合条件 - Consolidate Conditional Expression

有些条件语句的虽然有着不同的判断条件,却执行着相同的动作。如果可以确定这些判断不需要完全的独立,不妨整合到一起,再抽离出一个方法,这样,条件语句就更有“一个判断”的感觉了。

比如下面的代码:

1
2
3
4
5
double disabilityAmount() {
if (_seniority < 2) return 0;
if (_monthsDisabled > 12) return 0;
if (_isPartTime) return 0;
// compute the disability amount

可以整合成:

1
2
3
double disabilityAmount() {
if (isNotEligableForDisability()) return 0;
// compute the disability amount

整合的对象不仅仅是同等级的条件语句(使用ors),有一些嵌套的条件语句更有整合的必要(使用ands):

1
2
3
4
if (onVacation())
if (lengthOfService() > 10)
return 1;
return 0.5;

如果内层的if只是起到筛选的作用,而没有产生分支,就可以将内层条件语句跟外层整合到一起:

1
2
if (onVacation() && lengthOfService() > 10) return 1;
else return 0.5;

如果条件语句的处理只是返回不同的值,不妨使用三元运算符(ternary operator) 。不过要注意,三元运算符的使用因人而异,有些项目并不喜欢代码里出现三元运算符。

1
return (onVacation() && lengthOfService() > 10) ? 1 : 0.5;

整合重复的条件语句块 Consolidate Duplicate Conditional Fragments

当条件语句的ifelse里出现了相同的处理时,不妨试试将其移动到条件语句之前或者之后,使条件语句中的处理更有“分歧”的感觉。

1
2
3
4
5
6
7
8
if (isSpecialDeal()) {
total = price * 0.95;
send();
}
else {
total = price * 0.98;
send();
}

send()出现在具有分歧的处理之后,可以移动到条件语句的后面;如果出现在具有分歧的处理之前,则移动到条件语句的前面。

1
2
3
4
5
if (isSpecialDeal())
total = price * 0.95;
else
total = price * 0.98;
send();

如果无法确定整合的内容在前还是应该在后,不妨先看一看在条件语句块内,该内容的位置变化是否影响处理结果,
在不影响处理结果的前提下将要整合的内容前移或者后撤之后,再进行重构就可以了。

移除控制标记 Remove Control Flag

如果你使用一个变量的值控制真假的判断,不妨考虑在变量值的修改处用break或者return来替代。

比如下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void checkSecurity(String[] people) {
boolean found = false;
for (int i = 0; i < people.length; i++) {
if (! found) {
if (people[i].equals ("Don")){
sendAlert();
found = true;
}
if (people[i].equals ("John")){
sendAlert();
found = true;
}
}
}
}

这段代码使用变量found来判断来逃离for循环,显然可以使用break来代替:

1
2
3
4
5
6
7
8
9
10
11
12
void checkSecurity(String[] people) {
for (int i = 0; i < people.length; i++) {
if (people[i].equals ("Don")){
sendAlert();
break;
}
if (people[i].equals ("John")){
sendAlert();
break;
}
}
}

这样不仅是代码看起来简洁了许多,也避免了后续得无意义的判断。当然,这里的条件语句还可以整合一下,就不细说了。

有些时候控制标记不仅仅是用来充当真假的判断条件,也用来保存返回值,这种情况可以考虑在适当的时候使用return直接结束方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void checkSecurity(String[] people) {
String found = "";
for (int i = 0; i < people.length; i++) {
if (found.equals("")) {
if (people[i].equals ("Don")){
sendAlert();
found = "Don";
}
if (people[i].equals ("John")){
sendAlert();
found = "John";
}
}
}
someLaterCode(found);
}

我们可以先把for循环的内容抽离出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void checkSecurity(String[] people) {
String found = foundMiscreant(people);
someLaterCode(found);
}
String foundMiscreant(String[] people){
String found = "";
for (int i = 0; i < people.length; i++) {
if (found.equals("")) {
if (people[i].equals ("Don")){
sendAlert();
found = "Don";
}
if (people[i].equals ("John")){
sendAlert();
found = "John";
}
}
}
return found;
}

可以看到标记found成为了返回值,程序的意思很明显是:[如果found不是空的,就返回其内容], 所以我们可以直接返回要赋给found的值,取消found这个变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
String foundMiscreant(String[] people){
for (int i = 0; i < people.length; i++) {
if (people[i].equals ("Don")){
sendAlert();
return "Don";
}
if (people[i].equals ("John")){
sendAlert();
return "John";
}
}
return "";
}

细心观察 return 的位置, 你会有意想不到的收获

使用卫语句替代嵌套的条件语句 Replace Nested Conditional with Guard Clauses

条件语句在程序中的使用可以有两种思路:

  1. 用来处理明确的分支,程序往哪个方向走都有可能,完全取决于条件的真假。这种情况使用基本的if else来处理。
  2. 用来处理特殊情况,程序有着一个基本的前进方向,出现一些特殊情况时,需要立即返回特定值或者转为进行特定的处理。这种情况的条件语句,我们可以称之为 卫语句(guard clause)

当我们遇到层层嵌套的if else语句时,可以梳理一下程序的语义,如果发现条件语句的作用不是用来产生“平等”的分支,而是一种“出现了这种情况就赶快退出去吧”的感觉的话,不妨将if else简化成guard clause.

这里作者介绍的是 嵌套的条件语句 ,实际上使用卫语句的思路在单一if-else时也适用,每当我们写出一个完全体的if-else时,都应该思考一下条件语句属于那种类型,很多时候if-else的分歧处理都可以用if的卫语句解决。

比如下面的这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
double getPayAmount() {
double result;
if (_isDead) result = deadAmount();
else {
if (_isSeparated) result = separatedAmount();
else {
if (_isRetired) result = retiredAmount();
else result = normalPayAmount();
};
}
return result;
};

这种梯田式的条件语句,而且处理的对象都是返回值,很明显可以使用卫语句来简化,我们从外向里,一层层的将result的赋值改为return,最后可以得到:

1
2
3
4
5
double getPayAmount() {  
if (_isDead) return deadAmount();
if (_isSeparated) return separatedAmount();
if (_isRetired) return retiredAmount();
return normalPayAmount();};

使用卫语句进行重构并不局限于这一种情况,有时候我们可以发现条件语句筛选了部分情况来进行特定的处理,剩下的情况则返回默认值。这时我们可以考虑反转判断的条件式,来得到一个卫语句,今儿简化代码:

1
2
3
4
5
6
7
8
9
public double getAdjustedCapital() {  
double result = 0.0;
if (_capital > 0.0) {
if (_intRate > 0.0 && _duration > 0.0) {
result = (_income / _duration) * ADJ_FACTOR;
}
}
return result;
}

书中作者给出了详细的重构过程,这里就不细说了。

1
2
3
4
5
public double getAdjustedCapital() {    
if (_capital <= 0.0) return 0.0;
if (_intRate <= 0.0 || _duration <= 0.0) return 0.0;
return (_income / _duration) * ADJ_FACTOR;
}

工作中很常用到的一个思路就是,如果有一个大大的if框住了几乎整个方法的内容,一定要看看可不可以反转if的条件在最前面直接return,从{}中解放这段代码。

利用多态来重构条件语句 Replace Conditional with Polymorphism

比如我们有下面这个结构, Employeetype为抽象类EmployeeType,被三个类实现,每个类返回的typeCode不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@startuml
abstract class EmployeeType{
{abstract} int getTypeCode()
}

class Employee{
int payAmount()
int getType()
private EmployeeType type
}

class Engineer extends EmployeeType{
int getTypeCode()
}

class Manager extends EmployeeType{
int getTypeCode()
}
class Salesman extends EmployeeType{
int getTypeCode()
}

Employee --> "1" EmployeeType : type
@enduml

Employee的方法payAmount()的内容为:

1
2
3
4
5
6
7
8
9
10
11
12
int payAmount() {
switch (getType()) {
case EmployeeType.ENGINEER:
return _monthlySalary;
case EmployeeType.SALESMAN:
return _monthlySalary + _commission;
case EmployeeType.MANAGER:
return _monthlySalary + _bonus;
default:
throw new RuntimeException("Incorrect Employee");
}
}

因为type的其实是EmployeeType子类的属性,所以我们可以将这个方法移动到EmployeeType中,利用子类来实现不同的处理。首先,移动方法到EmployeeType里,由于使用了Employee才有的属性,所以我们需要将employee作为参数传递进去:

1
2
3
4
5
6
7
8
9
10
11
12
int payAmount(Employee emp) {
switch (getTypeCode()) {
case ENGINEER:
return emp.getMonthlySalary();
case SALESMAN:
return emp.getMonthlySalary() + emp.getCommission();
case MANAGER:
return emp.getMonthlySalary() + emp.getBonus();
default:
throw new RuntimeException("Incorrect Employee");
}
}

EmployeeType是抽象类,所以我们要继续将方法payAmout()在具体的子类中进行实现,不过子类中就用不到switch了。在重构的过程中,我们可以在EmployeeType中添加例外处理,比如当我们在Engineer中实现了payAmount()后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Engineer ...
int payAmount(Employee emp) {
return emp.getMonthlySalary();
}


class EmployeeType...
int payAmount(Employee emp) {
switch (getTypeCode()) {
case ENGINEER:
throw new RuntimeException ("Should be being overridden");
case SALESMAN:
return emp.getMonthlySalary() + emp.getCommission();
case MANAGER:
return emp.getMonthlySalary() + emp.getBonus();
default:
throw new RuntimeException("Incorrect Employee");
}
}

如此下去,直到在所有的子类中都实现了payAmount之后,将EmployeeType的方法改为abstract

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class EmployeeType...  
abstract int payAmount(Employee emp);

class Engineer ...
int payAmount(Employee emp) {
return emp.getMonthlySalary();
}

class Salesman...
int payAmount(Employee emp) {
return emp.getMonthlySalary() + emp.getCommission();
}
class Manager...
int payAmount(Employee emp) {
return emp.getMonthlySalary() + emp.getBonus();
}

使用空对象 Introduce Null Object

空对象模式是一种需要小心使用的设计模式,而且在JavaSE8中引入Optional<>后,几乎用不到空对象。

插入断言 Introduce Assertion

善用 Assert.isTrue(), 确保一些必须成立的前提条件,从而让后面的条件判断达到最简化。

通过命令行从本地推送代码到GitHub

首先,Git和GitHub不是一个东西。Git是一个版本管理系统,而GitHub是一个用来保存软件代码的平台。我们不妨把git看作是通讯技术,而github只是一个通讯服务商。也就是说除了github之外,还有很多的代码托管平台,大家都是用的git来上传下载代码,只不过github名气最大。

  • 为了不受各种不同的软件影响,这里说的都是通过命令行操作.
  • 这里只介绍一些最基本的命令和用法,在命令后面加 --help可以查看详细的文档.

环境搭建

首先要做的事安装git,下载地址在这里.

git安装完之后,打开命令行执行git --version看看是否可以查看git的版本,如果返回版本信息,就OK了. git会自己安装一个git Bash, 不过git的命令行并不局限于bash,windows下直接在cmd或者powershell都可以使用.

本地的代码推送到github时,github需要验证用户的信息. 就好比你可以每次进门都填一次我叫什么,我从哪儿来,我是干什么的,也可以提前登陆好身份信息,“哔”的一声刷脸进门,这个登录的身份信息就是ssh key.

接下来的操作分为三步:

  1. 确定本地是否已经有建立好的ssh key
    • 打开git Bash(这里需要用到Bash, windows的cmd是没有ls命令的) 输入ls -al ~/.ssh 这里一个搜索命令,返回名称为.ssh的目录信息。如果有id_rsa.pub文件,就说明已经有ssh key了.
  2. 如果没有,建立一组新的ssh key
    • 执行命令ssh-keygen -t rsa -b 4096 -C "your_email@example.com"来生成一组ssh key,接下来直接按回车就可以了(文件位置使用默认位置不需要输入,passphrase默认空白,不设置,设置的话以后每次推送代码都要输密码)。
    • 将生成的key登录到ssh-agent. 执行命令启动ssh-agent,windows:eval $(ssh-agent -s) Mac或者Linux:eval "$(ssh-agent -s)".然后执行命令添加key,Mac:ssh-add -K ~/.ssh/id_rsa windows或者Linux:ssh-add ~/.ssh/id_rsa
  3. 将建立好的ssh key登录到github
    • 将ssh key的内容复制到剪贴板. window可以使用命令:clip < ~/.ssh/id_rsa.pub Mac可以使用:pbcopy < ~/.ssh/id_rsa.pub Linux可以使用:sudo apt-get install xclip xclip -sel clip < ~/.ssh/id_rsa.pub

添加到github这一步我们单独说一下,以防弄错。打开github页面,点击用户头像,进入setting,在菜单中间的位置找到SSH and GPG keys,点击右上角的绿色按钮New SSH key,在Title里填写要添加的ssh key的介绍,比如”公司那台贼慢的电脑/家里的老爷机/学校的大屁股显示器”,然后在将复制的SSH key粘贴到Key里面,点击Add SSH key就可以了。

克隆项目代码

我们来到GitHub的代码页,可以在代码的右上角看到一个绿色按钮Clone or Download,点开之后可以看到克隆代码用的链接。

链接有两种,httpsssh, 简单来讲,使用https的链接克隆的项目需要用github的用户名密码来推送,使用ssh链接克隆的项目则使用ssh-key验证

由于我们已经设置好了ssh-key,所以使用格式为git@github.com:用户名/项目名.github.io.git的ssh链接.

1
git clone git@github.com:用户名/项目名.github.io.git

新建的repository

如果我们要将本地的工程文件推送到一个新建的GitHub库,首先确定两件事儿:

  1. GitHub上建立了一个空的库,README都没有的那种,可以看到这个页面
  2. 本地的工程文件初始化了git,并且没有关联其他的库. git的初始化可以使用git init命令,移除其他的关联,可以使用git remote remove name 这里的name一般是origin.

然后执行下面的命令就可以了:

1
2
3
git commit -m "first commit"
git remote add origin ssh链接
git push -u origin master

remote 和 local

git管理的代码有两个地方:

  1. 一个是remote 可以理解为远端,也就是指托管代码的地方,使用github时,值得也就是github服务器端
  2. 一个是local 直译本地,意译本地,他也就是本地的意思

从远端向本地同步代码,叫做pull 也就是拉,将本地代码同步到远端,叫做push 推送,简单而形象。

local值不需要,当然也不能修改,因为代码就在本地. remote的值可以随时添加,删除或者修改,remote是name和url成对设置的,一般默认的remote名是origin

  • git remote 确认remote,会返回remote的名称
  • git remote get-url remoteName 获得remote的url
  • git remote set-url remoteName url 设置remote的url

branch

为了实现并行的开发,代码通过branch(分支)来进行管理,不妨branch理解成时间线、河流,可分成几个分支并行前进,也可以随时汇集到一起(只要内容没有冲突)。
可以通过git branch来查当前本地的分支。
切换分支的命令为 git checkout "branch_name"
常用的生成分支命令为 git checkout -b "new_branch_name" "base_branch_name"
使用git branch "new_branch_name也可以生成新的分支,不过该命令并不切换到新生成的分支.

  • 分支的名称不可以有空格,所以一般通过下划线连接单词,当分支名含有特殊符号比如 # 时,需要在分支名前后加 “ 来防止控制台执行时的歧义。

commit

如果说branch是时间线,那么commit就是时间线上的存档点。刚刚创建或者修改完的文件,在git中并不算保存了,如果强行修改分支,这些内容就会丢失。将修改的内容保存到branch的记录中的操作,叫做commit(提交).

执行commit之前,首先需要把准备提交的文件添加到提交名单,这个操作使用 git add 命令。
添加之前,可以通过git status命令来查看有哪些文件被修改了.

这里简单的从bash颜色上区分一下,
绿色的内容(Changes to be committed) 是已经准备好可以提交的内容,
红色的内容(Changes not staged for commit) 是还没有添加到提交列表的修改.

下面介绍几种简单常用的命令

1
2
3
4
5
6
7
8
9
10
11
# 将所有的修改添加到提交列表
git add --all

# 将指定文件添加到提交列表,多个文件的时候使用空格区分
git add aaa.md
git add bbb.md ccc.md path/ddd.md

# 可以使用正规表达来指定多个文件
git add a*.md
git add path/\*.md

文件添加完,就可以执行commit来提交了:

1
2
3
4
5
# 提交的时候,commit信息是必须的,可以直接使用-m 来指定commit信息
git commit -m "在这里输入一条commit信息"

# 使用github的时候,可以在commit信息里通过 #+编号 来关联Issue,这样提交的commit可以在Issue中直接找到。
git commit -m "#001 commmit信息"

push

将remote的最新内容同步到本地的命令是git pull,将本地的修改推送到remote的命令是git push.

  • 出现冲突的时候可以加上--force来强制推送,不过这个操作不推荐,推送代码尽可能在解决冲突之后,使用最简单流畅的git push

推送命令的对象是分支,所以当我们推送本地创建的新分支时,需要指定一下这个分支在remote的信息:

1
2
3
4
5
# git push 推送命令
# --set-upstream 告诉git 我要设置推送目的地
# remotename 要推送到的remote名 一般是 origin
# branch_name 推送的branch名 含有特殊字符的话要用"branch_name"
git push --set-upstream remotename branch_name
  • 无论是切换分支,还是推送的时候,如果前面命令输入正确,git bash可以补全分支名。所以多用tab不仅方便,还能帮忙检查一下分支名前面的命令行有没有错。

python 中的字符串格式化

原文: python-f-string

3.6版本开始,python引入了一种叫做f-strings的写法,使用改写发可以极大地增强字符串的可读性(当然好处不止这些).
不过在学习f-strings之前,先看看以往的写法都有哪些。

旧方法

3.6之前,我们常用的字符串格式化写法有两种:

  1. 使用转移符 %
  2. 使用字符串的格式化方法 str.format()

旧选项1 %

跟其他编程语言一样,python也可以用%来把变量值插入到字符串中的对应位置。写法如下:

1
2
3
name = 'Tom'
print('Hello %s.' % name)
# 输出结果: 'Hello Tom.'

输出多个变量时,在%后使用括号将所有变量放到一起:

1
2
3
4
5
6
name1 = 'Tom'
name2 = 'Jerry'
type1 = 'cat'
type2 = 'god'
print('Hello %s, you are a %s.this is %s, a %s' % (name1, type1, name2, type2))
# 输出结果: 'Hello Tom, you are a cat. this is Jerry, a god'

但是转移符这种东西,一个不小心就容易写错。

旧选项2 str.format()

2.6开始,python为字符串添加了一个format()方法,可以将引入的参数按顺序替换到字符串中的{}处。例如:

1
2
3
4
5
6
name1 = 'Tom'
name2 = 'Jerry'
type1 = 'cat'
type2 = 'god'
print('Hello {}, you are a {}.this is {}, a {}'.format(name1, type1, name2, type2))
# 输出结果: 'Hello Tom, you are a cat. this is Jerry, a god'

使用format()你还可以自定义参数的插入顺序,例如:

1
2
3
4
5
6
name1 = 'Tom'
name2 = 'Jerry'
type1 = 'cat'
type2 = 'god'
print('Hello {0}, you are a {2}.this is {1}, a {3}'.format(name1, name2, type1, type2))
# 输出结果: 'Hello Tom, you are a cat. this is Jerry, a god'

format()方法的功能还不止于此,既然index可以用来出入参数,那可不可以用参数名称呢?毕竟0,1,2,3的可读性有限。答案是可以:

1
2
3
4
5
6
name1 = 'Tom'
name2 = 'Jerry'
type1 = 'cat'
type2 = 'god'
print('Hello {name_a}, you are a {type_a}.this is {name_b}, a {type_b}'.format(name_a = name1, name_b = name2, type_a = type1, type_b = type2))
# 输出结果: 'Hello Tom, you are a cat. this is Jerry, a god'

既然可以这样写了,那干脆直接引入dict得了.这个也是可以的:

1
2
3
4
5
6
character = {
'name' : 'Tom',
'type' : 'cat'
}
print('Hello {name}, you are a {type}.'.format(**character))
# 输出结果: 'Hello Tom, you are a cat.'

这样可读性就非常好了,不过在3.6版本,更好的写法出现了。

新方法

关于f-string的引入,可以参看PEP498, 或者Python-doc.

写法非常简单,在字符串前加f或者F,在字符串中直接插入{变量名},就可以了:

1
2
3
4
name = 'Tom'
type = 'cat'
print(f'Hello {name}, you are a {type}.')
# 输出结果: 'Hello Tom, you are a cat.'

{}中不仅可以使用字符串,还可以使用运算公式,甚至调用方法:

1
2
3
4
5
def change_name(input):
return 'Jerry'
name = 'Tom'
print(f"Hello {change_name(name)}.")
# 输出结果: 'Hello Jerry.'

当使用object时,f-string会默认调用__str__()方法,如果想调用__repr__,需要在对象名后面加!r

1
2
3
4
f"{object}"
# 输出 object.__str__()的内容
f"{object!r}"
# 输出 object.__repr__()的内容

最后一个考点:换行。在python中可以使用'''来输出带有换行的字符串,不过直接在前面加上f的结果并不怎么好看:

1
2
3
4
5
6
message = f'''
Hi,
you are a
Cat
'''
# message : '\nHi, \nyou are a \nCat\n'

f-string的换行方法是,在每行的最后添加\

1
2
3
4
5
message = f''\
f'Hi '\
f'you are a '\
f'Cat '
# message : 'Hi you are a Cat'

注意 一般情况下f-string使用单引号和双引号没有区别,但比如引用字典,需要单引号来标记key时,f-stirng就需要使用双引号了,否则字符串中的单引号会被错误理解。

Hexo 说明文档笔记

theme文件夹结构

  • _config.yml
  • languages
  • layout
  • scripts
  • source

_config.yml

主题的配置文件,修改主题的配置文件不需要重启服务器(修改网站根目录的那个_config.yml则需要重启才能生效)

languages

国际化设置,使用yml文件设置网站元素的翻译,具体参考i18n

layout

网页、模板的设置。hexo默认使用Swig,不过也可以手动添加EJS,Haml,Jade或者Pug的支持。模板的写法参考 Templates

scripts

存放JavaScript代码的文件夹,Hexo会自动加载这里的javaScript文件。扩展功能参考 plugins

source

顾名思义,这里是存放资源的。不属于页面模板的资源理论上都应该在此文件夹下面。

模板

Layout 布局

在主题文件夹下的layout中定义不同模板的页面的呈现方式,具体的模板文件定义了页面的body内容,而layout则定义了如何显示body
比如在布局中这样写:

1
2
3
4
<!DOCTYPE html>
<html>
<body><%- body %></body>
</html>

系统默认的布局为layout布局,不过在post的头部可以通过定义来使用具体的布局。

Partial 组件

不同页面之间共享的内容可以写成组件来管理,比如Header,Footer,Sidebar
引用组件时:

1
<%- partial('partial/header') %>

变量

资源文件夹

Hexo 3 开始,在_config.yml中启用post_asset_folder=true, 使用hexo的资源文件夹功能。
启用后,_posts里每生成一个post,会在相同路径下生成一个同名的文件夹作用该post的资源管理文件夹。

引用资源文件夹中的资源

启用了asset_folder后,可以使用以下几种命令,在文章中引用资源:

1
2
3
{% asset_path name %}
{% asset_img name title %}
{% asset_link name title %}

name的地方填写资源文件夹内的相对路径。

比如以下的文件结构:

1
2
3
4
- sample.md
- sample
- part1
- img.jpg

在sample.md中引用img.jpg时的写法是:

1
{% asset_img part1\img.jpg this is title %}

优点

使用资源文件夹与使用markdown的引用语法的最大不同就是:在网站首页或者归档处显示时,使用资源文件夹功能的引用可以被显示,而markdown语法引用的内容则无法被正确显示。

样板

国际化 i18n

i18n (Internationalization) 是指针对不同的国家、地域、语言环境,改变网站通用部分的显示语言。

设置

使用YAML或者JSON编写不同的语言文件,并统一放在主题文件夹languages中。

使用

使用__或者_p来取得对应语言的字符串。比如:

1
2
3
4
menu:
titile: HOME
archive: ARCHIVE
search: SEARCH
1
2
3
4
menu:
titile: 首页
archive: 归档
search: 搜索

在模板中使用目录时(例如语言是英文):

1
2
3
<%= __('menu.title') %> // HOME
<%= __('menu.archive') %> // 归档
<%= __('menu.search) %> // 搜索

Embadded-JavaScript

Ejs 文件是什么

嵌入式JavaScript代码:Embadded JavaScript 是一种从JavaScript快速生成html的简易模板,详细说明可以阅读说明文档 Ejs 中文文档
Hexo网页使用ejs的时候,使用命令 npm install hexo-renderer-ejs --save, 添加ejs的支持就可以了。这里作为备忘录,简单记录一下标签的写法。

标签

  • <% ‘脚本’ 标签,用于流程控制,无输出。
  • <%_ 删除其前面的空格符
  • <%= 输出数据到模板(输出是转义 HTML 标签)
  • <%- 输出非转义的数据到模板
  • <%# 注释标签,不执行、不输出内容
  • <%% 输出字符串 ‘<%’
  • %> 一般结束标签
  • -%> 删除紧随其后的换行符
  • _%> 将结束标签后面的空格符删除

整理一下就是说,ejs代码就是写在html中的javascript,标签是<% %>,标签的开头和结尾有几中命令符可以使用,分别是

  1. _ 删除空格;
  2. =输出数据;
  3. - 在前:输出非转义数据(比如插入处理),在后:删除换行;
  4. # 注释;
  5. % 用于输出 <%

插入

自定义“转义符” (官方文档写的是分隔符-delimiters,不是很理解)

jQuery 基本知识点备忘录

$(document).ready()

页面载入需要首先准备DOM(Document Object Model),jQuery可以检测到页面的DOM是否准备完毕,$(document).ready(function)中的function,会等到页面的DOM准备完毕再运行。
类似的还有$(window).on("load",funciton),这种写法下的function会在整个页面(并不仅仅是DOM)载入完毕时再执行。

1
2
3
4
5
6
7
$( document ).ready(function() {
console.log( "document loaded" );
});

$( window ).on( "load", function() {
console.log( "window loaded" );
});

将上面的代码插入页面的某个部件里执行,可以看到document很快就载入完成,等页面的所有其他元素都载入完成,window loaded才会被执行输出。