lly's Blog

用心记录点滴


  • 首页

  • 归档

《重构-改善既有代码的设计》系列读书笔记(四、重新组织数据)

发表于 2019-11-09   |  

重新组织数据

Self Encapsulate Field(自封装字段)

你直接访问一个字段,但与字段之间的耦合关系逐渐变得笨拙。

动机
  • 间接访问的好处是子类可以通过覆写一个函数而改变获取数据的途径,支持懒加载。
  • 直接访问的好处是代码更容易阅读。

Replace Data Value with Object(以对象取代数据值)

你有一个数据项,需要与其他数据和行为一起使用才有意义。

动机

当几个相关联的数据同时出现而且经常使用时,将它们组合为一个新的数据类吧。

做法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//before
class Order...
public Order(String customer) {
_customer = customer;
}
public String getCustomer() {
return _customer;
}
public void setCustomer(String arg) {
_customer = arg;
}
private String _customer;
//after
class Customer {
public Customer(String name) {
_name = name;
}
public String getName() {
return _name;
}
private final String _name;
}
class Order...
public Order(String customerName) {
_customer = new Customer(customerName);
}
public String getCustomer() {
return _customer.getName();
}
public void setCustomer(String customerName) {
_customer = new Customer(customerName);
}
private Customer _customer;

Change Value to Reference(将值对象改为引用对象)

  • 值对象不能被修改,引用对象可以被修改
  • 值对象每次获取到的值应该相等,而引用对象每次获取到的值不一定相等
  • 使用单例或者工厂创建引用对象

Change Reference to Value(将引用对象改为值对象)

  • 跟上面的过程刚好相反

Replace Array with Object(以对象取代数组)

动机
  • 一个数组中存放了不同类型的数据,这种骚操作有点要不得啊。。。
做法
  • 这个做法很有步步重构的代表性,所以写详细点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
//before
String[] row = new String[3];
row[0] = "Livepool";
row[1] = "15";
String name = row[0];
int wins = Integer.parseInt(row[1]);
// step 1 新建类
class Performance {
public String[] _data = new String[3];
}
Performance row = new Performance();
row._date[0] = "Livepool";
row._date[1] = "15";
String name = row._date[0];
int wins = Integer.parseInt(row._date[1]);
// step 2 封装替换存取方法
class Performance {
public String[] _data = new String[3];
public String getName() {
return _date[0];
}
public void setName(String arg) {
_date[0] = arg;
}
public int getWins() {
return Integer.parseInt(_data[1]);
}
public void setWins(String arg) {
_data[1] = arg
}
}
Performance row = new Performance();
row.setName = "Livepool";
row.setWins = "15";
String name = row.getName();
int wins = row.getWins();
// step 3 添加属性 删除临时数据
class Performance {
private String _name;
private String _wins;
public String getName() {
return _name;
}
public void setName(String arg) {
_name = arg;
}
public int getWins() {
return Integer.parseInt(_wins);
}
public void setWins(String arg) {
_wins = arg
}
}
Performance row = new Performance();
row.setName = "Livepool";
row.setWins = "15";
String name = row.getName();
int wins = row.getWins();

Duplicate Observed Data(复制被监视数据)

  • mvc模式的应用,数据处理和UI层分离
  • 使用观察者或者通知的方式进行数据的同步

Change Unidirectional Association to Bidirectional(将单向关联改为双向关联)

两个类都需要使用对方的特性,但其间只有一条单向链接。

动机
  • 当两个类需要互相引用时,可以使用反向指针来实现。
  • 如果使用不当,反向指针很容易造成混乱,但只要你习惯了这种手法,其实并不太复杂。
  • 双向依赖容易产生僵尸对象,造成内存泄露。
做法
  • 不建议这么做。。。

Change Bidirectional Association to Unidirectional(将双向关联改为单向关联)

动机
  • 双向依赖导致其中一个类的修改都可能影响另外一个类,形成了高度耦合的代码,并导致重新编译的问题。
  • 将双向依赖改为单向依赖的理由比较充分,打破相互依赖可以避免可能的内存泄露问题。
做法
  • 如果另外一条依赖是完全多余的,则直接去掉。
  • 如果不能去掉,可以采用协议的方式去打破其中一条依赖。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//before
ClassA {
ClassB b = new ClassB();
public void funcA() {
b.funcB();
}
}
ClassB {
ClassA a = new ClassA();
public void funcB() {
a.funcA();
}
}
//after
ClassA_Delegate {
public void delegate_func();
}
ClassA {
ClassA_Delegate delegate;
public void funcA() {
delegate.delegate_func()
}
}
ClassB<ClassA_Delegate> {
ClassA a = new ClassA();
a.delegate = self;
public void funcB() {
a.funcA();
}
public void delegate_func() {
self.funcB()
}
}
// 这个例子比较特殊,两个函数会相互调用产生爆栈,加代理并不能消除互相调用的逻辑,只能消除相互引用。

Replace Magic Number with Symbolic Constant(以字面常量取代魔法数)

  • 道理大家都懂,真正写起来就控制不住自己的手啊。。。

Encapsulate Field(封装字段)

你的类存在一个public字段

动机
  • 限制外界直接修改类的字段
做法
  • 将字段改为private,然后添加存/取方法。

Encapsulate Collection(封装集合)

动机 & 做法
  • 不要直接返回集合本身,这样集合很容易被修改。
  • 应该只返回集合的一个只读副本,并提供集合元素的添加和删除方法。
  • 如果是多线程访问,这种方式还能扩展为线程安全的访问方式。

Replace Type Code with Class(以类取代类型码)

Replace Type Code with Subclasses(以子类取代类型码)

Replace Type Code with State/Strategy(以策略模式取代类型码)

Replace Subclass with Fields(以字段取代子类)

动机
  • 如果子类中只有常数函数,就没有足够的存在价值。
做法
  • 你可以在父类中定义一个相应的字段,在类的构造时进行赋值,然后直接返回该字段的值即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//before
abstract class Person {
abstract boolean isMale();
abstract char getCode();
}
class Male extends Person {
boolean isMale() {
return true;
}
char getCode() {
return 'M';
}
}
class Female extends Person {
boolean isMale() {
return false;
}
char getCode() {
return 'F';
}
}
//after
abstract class Person {
private final boolean _isMale;
private final char _code;
protected Person (boolean isMale, char code) {
_isMale = isMale;
_code = code;
}
static Person createMale() {
return new Person(true,'M');
}
static Person createFemale() {
return new Person(false,'F');
}
boolean isMale() {
return _isMale;
}
char getCode() {
return _code;
}
}

《重构-改善既有代码的设计》系列读书笔记(三、类的重构)

发表于 2019-11-06   |  

在对象之间搬移特性

在对象的设计过程中,决定把责任放在哪里,即使不是最重要的事情,也是最重要的使用之一,虽然不能一开始就完全设计正确,但是我们可以使用重构去完善自己的设计

Move Method(搬移函数)

你的程序中,有个函数与其所在类之外的其他类进行更多的交流,调用后者,或者被后者调用

动机
  • 如果一个类有太多行为,就会承担过多的责任,请为这个类减负
  • 如果一个类与另一个类有太多合作,会形成高度的耦合
  • 如果一个函数使用外部类的次数比使用所在类的次数还多,则应该被移出所在类
做法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// before
class Account...
double overdraftCharge() {
if (_type.isPremium() ){
double result = 10;
if (_daysOverdrawn > 7) result += (_daysOverdrawn - 7) * 0.85;
return result;
}
else {
return _daysOverdrawn * 1.75
}
}
double bankCharge() {
double result = 4.5;
if (_daysOverdrawn > 0) result += overdraftCharge();
return result;
}
private AccountType _type;
private int _daysOverdrawn;
// after
class AccountType...
double overdraftCharge(Account account) {
if (isPremium()) {
double result = 10;
if (account.getDaysOverdrawn() - 7) {
result += (account.getDaysOverdrawn() - 7) * 0.85;
}
else {
return result;
}
}
else {
return account.getDaysOverdrawn() * 1.75;
}
}
class Account...
double bankCharge() {
double result = 4.5;
if (_daysOverdrawn > 0)
result += _type.overdraftCharge(self);
return result;
}
private AccountType _type;
private int _daysOverdrawn;

Move Field(搬移字段)

某个字段被其所在类之外的另一个类更多的用到,搬移它到正确的地方。

动机
做法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// before
class Account...
private AccountType _type;
private double _interestRate;//搬移到AccountType
double interestForAmount_days (double amount,int days) {
return _interestRate * amount * days / 365;
}
//after
class AccountType...
private double _interestRate;
void setInterestRate (double arg) {
_interestRate = arg
}
double getInterestRate () {
return _interestRate;
}
class Account...
private AccountType _type;
double interestForAmount_days (double amount,int days) {
return getInterestRate() * amount * days / 365;
}
private void setInterestRate(double arg){
_type.setInteresetRate(arg);
}
private double getInterestRate() {
return _type.getInteresetRate();
}

Extract Class (提炼类)

某个类做了应该由两个类做的事。

动机
  • 一个类应该是一个清楚的抽象,处理一些明确的责任。
  • 类的不断迭代,会导致体积不断增大,责任越来越多。
  • 如果某些数据和函数总是一起出现,某些数据经常同时变化甚至彼此依赖,这就表示你应该将它们分离出去
  • 如果一个类在子类化时出现分歧,则该类可能需要被分解。
做法
  • 新建类,将需要分离的代码拷贝过来。
  • 建立从旧类访问新类的连接关系。
  • 使用上面的Move Method 和 Move Field方法对新类进行重构。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// before
class Person...
public String getName() {
return _name;
}
public String getTelephoneNumber() {
return ("(" + _officeAreaCode + ")" + _officeNumber);
}
String getOfficeAreaCode() {
return _officeAreaCode;
}
void setOfficeAreaCode(String arg) {
_officeAreaCode = arg;
}
String getOfficeNumber() {
return _officeNumber;
}
void setOfficeNumber(String arg) {
_officeNumber = arg;
}
private String _name;
private String _officeAreaCode;
private String _officeNumber;
//after
class Person...
public String getName() {
return _name;
}
public String getTelephoneNumber() {
return _officeTelephone.getTelephoneNumber();
}
TelephoneNumber getOfficeTelephone() {
return _officeTelephone;
}
private String _name;
private TelephoneNumber _officeTelephone = new TelephoneNumber()
class TelephoneNumber...
public String getTelephoneNumber() {
return ("(" + _officeAreaCode + ")" + _number);
}
String getAreaCode() {
return _areaCode;
}
void setAreaCode(String arg) {
_areaCode = arg;
}
String getNumber() {
return _number;
}
void setNumber(String arg) {
_number = arg;
}
private String _number;
private String _areaCode;

Inline Class (将类内联化)

某个类没有做太多事

动机
  • 如果一个类不再承担足够的责任,不再有单独存在的理由,将它内联到另一个类中
做法
  • Extract Class的逆过程

Hide Delegate(隐藏委托关系)

客户通过一个委托类来调用另外一个对象

动机
  • 封装意味着每个对象应该尽量少的了解系统的其他部分,如果发生变化,需要了解这个变化的对象就会很少,这会使变化更容易
  • 针对上面的例子,如果委托关系发生变化,客户也得相应变化。
做法
  • 在委托类中加一个代理方法,将获取对象的逻辑留在这个类中,不要暴露给客户
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// before
class Person...
Department _department;
public Department getDepartment() {
return _department;
}
public void setDepartment(Department arg) {
_department = arg;
}
class Department {
private String _chargeCode;
private String _manager;
public Department(Person manager) {
_manager = manager;
}
public Person getManager() {
return _manager;
}
}
//after
class Person...
Department _department;
public void setDepartment(Department arg) {
_department = arg;
}
//新加一个代理方法
public Person getManager() {
return _department.getManager();
}
class Department {
private String _chargeCode;
private String _manager;
public Department(Person manager) {
_manager = manager;
}
public Person getManager() {
return _manager;
}
}

Remove Middle Man (移除中间人)

某个类做了过多的简单协议

动机
  • 如果某个类的代理方法越来越多,变成了一个完全的中间人,此时你应该让客户直接调用委托对象本身
  • Hide Delegate 和 Remove Middle Man是一对可逆的过程,代码总是在变化的,之前的修改可能在之后变得并不适用,不用说对不起,继续把这个问题修补好就行了。
做法
  • 和Hide Delegate过程刚好相反
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//before
class Person...
Department _department;
public void setDepartment(Department arg) {
_department = arg;
}
public Person getManager() {
return _department.getManager();
}
//after
class Person...
Department _department;
public Department getDepartment() {
return _department;
}
public void setDepartment(Department arg) {
_department = arg;
}

Introduce Foreign Method(引用外加函数)

你需要为提供服务的类增加一个函数,但是你无法修改这个类

动机
  • 需要为不能修改源代码的类增加函数
  • 如果这个函数只使用一次,可能没有必要,如果需要多次使用,则应该这么做。
做法
1
2
3
4
5
6
7
8
9
//before
Date newStart = new Date (previousEnd.getYear(),previousEnd.getMonth(),previousEnd.getDay() + 1);
//after
Date newStart = nextDay(previousEnd);
private static Date nextDay(Date arg){
return new Date(previousEnd.getYear(),previousEnd.getMonth(),previousEnd.getDay() + 1);
}

Introduce Local Extension(引入本地扩展)

你需要为服务类提供一些额外函数,但是你无法修改这个类。

动机
  • 如果需要加多个函数,则应该对这些函数做一个封装,而不是散落在各处
  • 子类是更好的扩展方式

《重构-改善既有代码的设计》系列读书笔记(二、函数的重构)

发表于 2019-11-04   |  

重新组织函数

过长的函数会带来一系列问题,所以想办法重构吧。

Extract Method(提炼函数)

提炼动机
  • 如果函数内部有一段代码需要使用注释来解释,请把这段代码提炼为一个新的函数
  • 函数粒度越小,被复用的机会就越大
  • 如果一个函数内部调用的都是小颗粒函数,该函数阅读起来会更方便
  • 小颗粒函数的重写会更容易
  • 小颗粒函数需要精准的命名
  • 函数名的长度不是问题,和函数本体之间的语义距离才是,应该做到见字如面
具体做法
  • 创建一个新函数,以它做什么而不是怎么做来命名,如果你想不出一个合适的名称时,先不要提炼
  • 拷贝代码到新函数
  • 检查新函数是否引入作用域限于源函数的变量
  • 检查临时变量(是否引入,是否修改)
  • 替换新函数,测试新函数

Inline Method(内联函数)

提炼动机
  • 将函数调用替换成函数本身,如果函数本身已经清楚易懂的话
  • 间接性可能带来帮助,但非必要间接性总是让人不舒服
  • 如果你需要重构一个函数,该函数内部又调用了一组不太合理的函数,可以将这些函数先内联到该函数中,再进行重构。

Inline Temp(内联临时变量)

提炼动机
  • 某个临时变量被赋予某个函数调用的返回值
具体做法
1
2
3
4
5
6
7
8
9
// before inline
double basePrice = anOrder.basePrice();
return (basePrice > 1000)
// after inline
return (anOrder.basePrice() > 1000)

Replace Temp with Query(以查询取代临时变量)

当函数中的临时变量保存了一组表达式的值,请将这段表达式代码提炼为一个新的函数。

提炼动机
  • 临时变量存储表达式的方式会使你的代码越来越长,因为这段代码是不能被复用的。
具体做法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// before
double getPrice() {
int basePrice = _quantity * _itemPrice;
double discountFactor;
if (basePrice > 1000) discountFactor = 0.95;
else discountFactor = 0.98;
return basePrice * discountFactor;
}
// after
private int basePrice() {
return _quantity * _itemPrice;
}
private double discountFactor() {
if (basePrice() > 1000) return 0.95;
else return 0.98;
}
double getPrice() {
return basePrice() * discountFactor();
}

重构有啥用啊,真。。。真香。。。

Introduce Explaining Variable(引用解释性变量)

将复杂表达式中的某部分用临时变量来解释

提炼动机
  • 复杂的表达式难以阅读,临时变量可以帮助理解,特别是在条件判断表达式上。
具体做法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// before
double price() {
return _quantity * _itemPrice -
Math.max(0,_quantity - 500) * _itemPrice * 0.05 +
Math.min(_quantity * _itemPrice * 0.1, 100.0);
}
// after
double price() {
//可以提炼为方法,看复用情况
final double basePrice = _quantity * _itemPrice;
final double quantityDiscount = Math.max(0,_quantity - 500) * _itemPrice * 0.05;
final double shipping = Math.min(basePrice * 0.1, 100.0);
return basePrice - quantityDiscount + shipping;
}

Split Temporary Variable(分解临时变量)

临时变量被赋值多次,但它既不是循环变量,也不是计算结果收集变量。

提炼动机
  • 临时变量被赋值多次,说明它承担了多个责任,应该被替换为多个临时变量,每个临时变量只承担一个责任。
  • 一个临时变量承担多件事情,会令代码阅读者懵圈。

Remove Assignments to Parameters(移除对参数的赋值)

在引用传递语言中,会改变该参数的值,在值传递语言中,不会影响外部继续使用该参数。

Replace Method with Method Object(以函数对象取代函数)

函数过大时,对其中局部变量的使用使你无法提炼出较小的函数

提炼动机
  • 局部变量太多,无法一一提取。
  • 当使用对象时,所有局部变量就会成为成员变量,方便拆解
具体做法
  • 新建一个类,将需要的局部变量申明为对象的属性
  • 在新类建立一个构造方法,参数为所有需要的局部变量
  • 将源方法的实现拷贝过来,然后使用新建类替换源方法

Substitute Algorithm(替换算法)

《重构-改善既有代码的设计》系列读书笔记(一、从第一个案例说起)

发表于 2019-10-10   |  

读完了martin老师的《代码XX之道》系列后,颇有收获,代码质量也有比较明显的提高,渐渐对代码也有了自己的一些理解。但是学习的脚步不能停,《重构》这本书也是提高代码质量方面的经典书籍,当然不能错过了。

从第一个案例说起–影片租赁系统

发现问题-编写测试用例(测试用例的重构的根本)- 步步为营的修改

重构的本质:尽可能小的修改,这样任何可能出现的错误都会很容易发现。

如何拆分一个臃肿的函数

  • 处理函数内部的参数和变量,不变的变量以参数形式传入,可变的变量可以抽出为一个函数已返回值的形式赋值
  • 先分再治:先将函数拆分为尽可能小的函数,然后再对每个函数进行修改
  • 好的代码应该清晰的表达出自己的功能,变量名称就是代码清晰的关键
  • 任何一个傻瓜都是写出计算机可以理解的代码,唯有写出人类容易理解的代码,才是优秀的程序员
  • 绝大多数情况下,函数应该放在他所使用的数据的所属对象内。
  • 去掉多余的临时变量,临时变量会引发问题,导致大量没有必要的参数传递,并且很容易被跟丢,尤其是在很长的函数中。
  • 尽量使用查询函数替代可能的局部变量。

重构原则

  • 重构的名词解释:对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本

  • 重构的动词解释:使用一些列重构手法,在不改变软件可观察行为的前提下,调整其结构

两顶帽子理论

  • 添加新功能时,你不应该修改既有代码,只管添加新代码。
  • 重构时你不能再添加功能,只管改进程序结构。
  • 编码过程中我们会经常变换帽子。

重构的目的

  • 重构改进软件设计,如果没有重构,程序的设计会逐渐腐败变质。消除重复是重构的重要手段之一。
  • 重构使软件更容易理解,始终提醒自己一点,代码是写给人看的。
  • 重构帮助找到BUG
  • 重构提高编程速度,因为良好的设计是快速开发的根本。

何时重构

不应该专门抽时间来重构,而应该随时重构,只要你发现代码有重构的必要时,就着手重构。重构的原因是你需要做别的事情,而重构能帮助你把事情做得更好。

三次法则:第一次做这件事情时尽管去做,第二次做相似的事情时不情愿的去做,第三次再遇到这种事情时,坚决的重构。

  • 添加功能时重构
  • 修补错误时重构
  • 复审代码时重构

好的代码应该具备以下特性:

  • 容易阅读
  • 所有逻辑都只在唯一地点指定
  • 新的改动不会危及现有行为
  • 尽可能简单表达条件逻辑

当代码不能正常工作时,应该选择重写而不是重构。重构之前的代码,至少大部分情况下是能正常运行的。

项目接近最后期限时,应该避免重构。未完成的重构工作可以比作技术债务,而维护和扩展代码所花费的额外成本就是利息。一定程度的利息是可以接受的,但是如果利息太高,就会被压垮。把债务管理好很重要,应该随时通过重构来偿还一部分债务。

重构的难点

  • 数据库的重构,需要加上中间层和做好数据迁移
  • 接口的重构,添加新接口,让老接口去调用新接口,不要复制函数,并将旧接口标记为deprecated

坏代码的味道

  • 坏代码没有一个统一的评判标准,你必须培养出自己的判断力,学会判断一个类内有多少实例变量算是太大,一个函数内有多少行代码算太长。

坏代码条款

  • 重复代码
  • 过长函数
  • 过大的类
  • 过长的参数
  • 发散式变化(拆分)
  • 霰(xian)弹式修改(合并)
  • 依恋情结(类中方法应该只对该类感兴趣,而不应该依恋其他类)
  • 数据泥团(一些需要同时出现的数据,合并为一个新的对象)
  • 基本类型偏执(比如结合数值和币种的money类等)
  • switch惊悚现身(使用多态替换)
  • 平行继承体系
  • 冗赘类(删除无用类)
  • 夸夸其谈未来性(不要企图使用钩子或者特殊判断处理并未发生的事情)
  • 令人迷惑的暂时字段
  • 过度耦合的消息链
  • 过度滥用的中间人
  • 狎昵关系
  • 异曲同工的类
  • 不完美的库类
  • 纯粹的数据类(需要良好的封装,只读取少修改)
  • 被拒绝的遗赠(使用代理替带方法集成)
  • 过多的注释(不要过度依赖注释,如果一段代码需要注释,则应该将它们提炼为一个新的方法)

iOS性能检测工具总结

发表于 2019-08-22   |  

前言

作为一个资深的iOS开发,除了专业能力要过硬,还应该具有一些产品scene,关注一些技术之外的内容,学会发现问题然后利用技术去解决这些问题。比如用户体验问题,用户在使用app的过程中,可能会遇到哪些体验不佳的问题,我们要做的就是利用技术手段找出这些问题,然后一一解决,尽我们所能为用户提供最后的用户体验。排除交互设计本身外,最可能出现用户体验问题的原因就是手机在性能上出现了一些瓶颈,因此,找出这些性能瓶颈的时间节点和当前实时数据能很大程度上帮助我们定位到最终原因。

基于以上的出发点,我们就需要开发一套自己的性能检测工具和上报机制,在app出现性能问题时拿到一手的数据,帮助我们定位问题和解决问题。

工具介绍

app的性能主要受两部分内容的影响,一个是手机本身的硬件设备,还有一个就是当前网络状况。这里分两部分分别介绍检测工具的功能点。

设备性能

fps

fps主要用来检测当前页面的卡顿情况,如果fps降到50帧以下,页面卡顿就会比较明显,这个时候就需要具体再去分析卡顿原因。

cpu usage

cpu利用率主要有三个可以参考的指标,分别是用户使用率,系统使用率和空闲率。

disk

磁盘空间有已用空间和可用空间。

memory

内存使用情况有总内存,总可用内存和当前app已用内存。这里都是指的物理内存。因为虚拟内存的检测并没有什么意义。

battery

当前电量。

以上这些指标,大部分需要调用mach内核的相关api才能成功获取,而且需要轮询获取才能保证数据的时效性。

网络性能

DNS

包括当前域名对应的ip,解析总耗时和失败情况。

Ping

当前网络延时,一个RTT时长。

Reachability

当前网络状态,包括wifi,4G或者无网。

Metrics(iOS10)

Metrics包括了下面这张图中的相关时长:

iOS10以后,可以直接从URLSession的回调中拿到这些耗时指标。

TraceRoute

一个完整的请求的路径,包括数据经过的每一个节点和耗时。该数据可以完整反映出一个数据报的传输过程。

Traffic(I/O)

网络I/O

上报格式

为了方便上报和数据分析,需要统一各项数据的上报格式,以上面的Metrics为例,将各项数据按统一的格式一同上报,定义格式如下:

上报格式如下:

1
2
NetworkMetrics: 450,50,80,60,100,120,Network Load
NetworkMetrics: 220,0,0,0,100,120,Loacl Cache

操作系统原理--内存管理

发表于 2019-05-29   |  

内存结构

虚拟内存与物理内存

内存的分配与回收

操作系统原理--线程

发表于 2019-05-29   |  

线程的概念

线程与进程的关系

线程的结构

线程的调度

线程的同步

操作系统原理--进程

发表于 2019-05-29   |  

进程的概念

进程的结构

进程的调度

进程间通信

一个网络请求的流程

发表于 2019-04-04   |  

最新在看《计算机网络–自顶向下方法》,把从应用层到链路层的所有网络协议都重新学习了一遍。这里借用这个标题,来梳理一下相关协议的工作原理和流程,以及各层的协议之间是如何协同工作的。

首先 你得连上Internet吧

当我们连接上一个路由器后,会得到一个ip地址,如果此时你的路由器已经连接到了ISP,那么恭喜你可以开始在网上🏄了。那么这个ip地址到底是咋分配到我们的机器的呢?!!!

DHCP(dynamic host configuration protocol)

动态主机配置协议,运行在网卡,路由器和交换机等网络设备上,主要用来分配ip地址。和其他协议类似,也是以报文的形式进行通信。

当我们的主机发起一个网络连接请求时,DHCP协议会生成一个DHCP报文,并使用UDP传输协议进行传输,该UDP报文会包含一个组播ip(255.255.255.255)的目标地址,组播ip意味着该网段内所有设备都会收到这条UDP报文。此时因为本机还没有分配ip,所以源ip会是0.0.0.0。

上面的UDP报文会被包在以太网帧中进行传输,该以太网帧的目的地址也是一个组播mac地址(FF.FF.FF.FF.FF.FF),所以该帧会被网段内所有设备接收。而该帧的原地址即为本机的mac地址(比如 28:cf:e9:14:d1:0f 这是我的设备的mac地址)。

路由器收到该以太网帧,一层层解析出DHCP报文,然后路由器决定分配一个ip地址给主机(10.236.142.22),于是通过路由器上的DHCP服务器包装一个DHCP的响应报文,把包括了DNS服务器ip,路由器ip,子网掩码和分配给主机的ip一起发送给该主机。因为路由器已经知道了主机的mac地址,所以该报文会顺利的到达。

主机网卡在收到DHCP ACK报文后,解析出相关的ip并配置好网络组件。至此,主机连接Internet成功。

只有域名是不可以的

连上Internet后,我们就可以上google了。于是我们在浏览器上输入www.google.com,也就是google的域名,不包括前面的www(www是一个网络应用,中文名叫万维网)。而google的主页应该是放在服务器上的,通过域名是如何访问到服务器上的google主页的呢?!!!

DNS(domain name system)

DNS主要用来查询域名对应的ip地址。和DHCP一样,DNS报文使用UDP进行传输。DNS报文将www.google.com放入报文段,然后组装成UDP报文,在上面的DHCP ACK中,已经拿到了DNS的ip,所以在组装ip报文的时候目的ip直接使用dns的ip,源ip则为主机的ip。

之后,ip报文会被放到以太网帧中进行传输,想要将dns报文发送到dns服务器,首先第一个难题就是如何传送到所在网络的网关路由器,在上面的DHCP ACK报文中,已经拿到了网关路由器的ip了,但是ip只是作用在网络层,如果没有mac地址,链路层上是无法传输的。

ARP(address resolution protocol)

地址解析协议,通过一个ip地址获取mac地址。ARP使用网关路由器的ip地址生成一个ARP查询报文,然后将该报文放到目标地址为(FF.FF.FF.FF.FF.FF)的以太网帧上,因为是广播地址,所以网关路由器顺利收到该帧,并生成一个ARP的回答报文,报文中带上了网关路由器的mac地址。然后将回答报文放到目的地址为主机mac地址的以太网帧中,这样,我们就顺利的拿到了网关路由器的mac地址。

通过ARP,dns查询报文顺利的发送到了网关路由器。之后,又该何去何从?!!!

BGP(broder gateway protocol)

边界网关协议,AS(自治系统)之间的路由选择协议,BGP极其复杂,许多专著致力于研究该主题,它的基本工作流程是:

  • 从相邻AS处获取子网可达性信息
  • 向本AS内部的所有路由器传播这些可达性信息
  • 基于可达性信息和AS策略,决定到达子网的最优路由

我们还得回到最上面的DHCP ACK中去,因为DNS的ip地址是这个报文给返回的,那么它又是如何生成该ip的呢。其实这个ip并不是它生成的,是ISP提供的。当路由器连接上ISP以后,对应的路由转发表会进行更新,所以dns的ip对应的转发接口已经存在于路由转发表中(根据BGP边界网关协议),至此,dns查询报文被发送到对应的dns服务器上。

dns服务器接收到查询报文后,开始根据域名在缓存中查找对应的ip,如果缓存中没有找到,会去到ISP的权威dns服务器上查找,如何还找不到,会去到.com对应顶级dns服务器上查找,如果还是没有找到,会到跟域名服务器上查找,如果根域名上也没有找到,则返回查询失败。如果在其他dns服务器上成功找到对应的ip,该服务器会先缓存到本地,然后生成一个UDP的DNS回答报名,发送到对应的主机上,至此,我们终于拿到了google.com对应的ip地址。接下来我们要开始发送http的get请求了。

顺理成章的http请求

拿到ip后,http客户端会生成一个get请求,然后封装到tcp的报文中。tcp在进行数据传输前,会有一个三次握手的过程,握手成功后,get请求的报文成功被google的服务器接收,然后服务器通过url找到对应的资源,生成一个http的响应报文并封装在tcp的报文中,然后发送给客户端。

因为已经建立了连接,所以响应报文不在需要握手的流程,客户端顺利收到响应报文,并解析出资源数据,将数据丢给浏览器进行显示(浏览器如何解析数据并渲染的流程不在这篇文章的讨论范围之内)。。。

因为http并不是长链接(1.1以后才支持),所以在一个http请求结束后,客户端还是发起一个断开链接的操作,也称四次挥手的过程。至于断开连接为啥会比建立连接多一次握手过程,就留给读者自己去比较和分析吧。

总结

至此 一个完整的网络请求就总结完毕了,当然,这只是一个大概的流程总结,重点介绍和DHCP和链路层相关的几个协议,tcp和ip协议因为之前已经总结过所以这里只是一笔带过。如果想要了解得更为深入的话,当然推荐大家看一下《TCP/IP详解》系列书籍和我正在看的这本《计算机网络–自顶向下方法》书了。

《架构整洁之道》学习笔记

发表于 2019-03-11   |  

第一章 设计与架构究竟是什么

  • 软件架构的终极目标是,用最小的人力成本满足构建和维护该系统的需求.

  • 慢但是稳,是成功的秘诀

  • 程序员们总是用这句话来欺骗自己:我们可以未来再重构代码,产品上线最重要.但是结果大家都知道,产品上线后重构工作就再也没有人提起.所以重构的时机永远不会再有了…

第二章 两个价值维度

架构价值比行为价值更重要

行为价值(业务)

行为价值并不是程序员工作的全部

架构价值

  • software = 灵活的产品
  • 软件变更的难度应该和变更的范畴成等比关系,而与变更的具体形状无关

艾森豪威尔矩阵

  • 我有两种难题:紧急的和重要的,而紧急的难题永远是不重要的,重要的难题永远是不紧急的.
  • 重要比紧急更应该被重视

为好的软件架构而持续斗争

  • 公司内部的抗争本来就是无止境的
  • 如果你是架构师,这项工作就加倍重要

第四章 结构化编程

  • 编码的整个过程应该是可推导的
  • 功能性降解拆分,将复杂的逻辑分解为一个个小的单元,以函数,分支,循环等的方式最终呈现.
  • 结构化编程是对程序控制权的直接转移的限制

第五章 面向对象编程(OOD)

  • 面向对象编程是对程序控制权的间接转移的限制

封装

  • 类的private和public属性和方法

继承

  • 继承的作用是让我们可以在某个作用域内对外部定义的某一组变量与函数进行覆盖.

多态

  • 多态是函数指针的应用
  • 插件化是多态的一种实践
  • 多态是实现依赖反转的一种好的选择

第六章 函数式编程

  • 函数式编程中变量是不可变的
  • 原子操作只能保证当前线程下的线程安全问题
  • 函数式编程是对程序中赋值操作的限制

设计原则(SOLID)

设计目标:

  • 使软件可容忍被改动
  • 使软件更容易被理解
  • 构建可在多个软件系统中复用的组件

第七章 SRP单一职责原则

  • 函数设计也需要遵循SRP原则
  • 任何一个软件模块都应该有且仅有一个被修改的原因
  • 任何一个软件模块都应该只对某一类行为者负责
  • Employee类中各行为应该被分解

第八章 OCP开闭原则

  • 良好的设计软件应该易于扩展,同时抗拒修改
  • 如果A组件不想被B组件上发生的修改所影响,那么就应该让B组件依赖于A组件
  • 利用SRP分解功能,利用DIP反转依赖
  • 软件系统不应该依赖不直接使用的组件

第九章 LSP里氏替换原则

  • 以接口的形式实现函数调用的可替换性

第十章 ISP接口隔离原则

  • 利用接口隔离原则隔离掉不需要的依赖关系,因为不需要的依赖会导致不必要的重新编译和重新部署

第十一章 DIP依赖反转原则

  • 如果想要设计一个灵活的系统,在源代码层面的依赖关系中就应该多引用抽象类型而非具体实现
  • 接口比具体实现更稳定,因为如果修改接口必须修改实现,但是反之则不一定.
  • 不要在具体实现类上创建子类,具体实现应该放到子类去做,除非是比较稳定且通用的逻辑.
  • 不要覆盖包含具体实现的函数,如果必须这样,请在覆盖方法中先调用父类方法.
  • 源代码依赖方向永远是控制流方向的反转

第十二章 组件

  • 组件是软件部署的单元,是整个软件系统在部署过程中可以独立完成部署的最小实体.
  • 重定位技术和链接器是组件的单独部署成为可能
  • 墨菲定律:程序的规模会一直不断地增长下去,直到将有限的编译和链接时间填满为止
  • 摩尔定律:硬件的更新周期为18个月

第十三章 组件聚合

复用/发布等同原则(REP)

  • 软件复用的最小粒度应等同于其发布的最小粒度
  • ERP原则就是指组件中的类与模块必须是彼此紧密相关的,一个组件不能由一组毫无关联的类和模块组成
  • 一个组件中包含的类与模块还应该是可以同时发布的

共同闭包原则(CCP)

  • 我们应该将那些会同时修改,并且为相同目的而修改的类放到同一个组件中,而将不会同时修改,并且不会为了相同目的而修改的那些类放到不同的组件中
  • CCP原则是SRP原则和OCP原则的组件版

共同复用原则(CRP)

  • 不要强迫一个组件的用户依赖他们不需要的东西
  • 我们希望组件中的所有类是不可拆分的,不应该出现别人只需要依赖它的某几个类而不需要其他类的情况
  • CRP的作用不仅是告诉我们应该将哪些类放在一起,更重要的是要告诉我们应该将哪些类分开
  • CRP原则实际上是ISP原则的一个普适版,ISP建议我们不要依赖不需要的类,CRP建议我们不要依赖不需要的组件
  • 优秀的架构师应该在REP,CCP和CRP原则间找到一个好的平衡点,一个项目的组件结构设计的重心是根据该项目的开发时间和成熟度不断变化的

第十四章 组件耦合

无依赖环原则

  • 组件依赖关系图中不应该出现环
  • 依赖环会导致组件之间的发布和运行很难稳定下来
  • 每周构建是指team每一周专门抽出一天来进行组件的构建调试工作,而其他时间则忽略组件的构建问题
  • 消除循环依赖的一个解决办法是将研发项目划分成一些可单独发布的组件,这些组件独立完成构建和发布
  • DIP原则打破循环依赖好的选择

自上而下的设计

  • 组件结构图是不可能自上而下被设计出来的,它必须随着软件系统的变化而变化和扩张.
  • 组件结构图并不是用来描述应用程序功能的,它更像是应用程序在构建性与维护性方面的一张地图
  • 组件结构图的一个重要目的是如何隔离频繁的变更

稳定依赖原则

  • 依赖关系必须要指向更稳定的方向
  • 任何一个我们预期会经常变更的组件都不应该被一个难于修改的组件所依赖
  • 稳定性指标的计算方法:出口依赖/(出口依赖+入口依赖). 0最稳定,1最不稳定.
  • 组件并不是都需要稳定

稳定抽象原则

  • 一个组件的抽象化程度应该与其稳定性保持一致
  • 组件抽象化的计算方法:组件中的抽象类和接口/组件中类的个数 0表现没有抽象类 1表示全是抽象类
  • 好的组件结构应该在抽象和稳定之间找到一个好的平衡点

第十五章 什么是软件架构

  • 软件架构师自身需要是程序员,并且必须坚持一直做一线程序员,绝对不要听从那些说应该让软件架构师从代码中解放出来以专心解决高阶问题的伪建议
  • 软件架构的实质就是规划如何将系统切分成组件,并安排好组件之间的排序关系,以及组件之间的通信方式
  • 如果想设计一个便于推进各项工作的系统,其策略就是要在设计中尽可能长时间的保留尽可能多的可选项
  • 开发,部署,运行和维护是软件架构的主要目标
  • 哪些可选项应该保留:它们就是那些无关紧要的细节设计
  • 策略是软件中所有的业务规则与操作过程,是系统真正的价值所在,而细节则是程序员们与策略交互的方式.
  • 软件架构师的目标是创建一种系统形态,该形态会以策略为最基本的元素,并让细节与策略脱离关系,并允许在具体决策过程中推迟或延迟与细节相关的内容

第十六章 独立性

所谓独立性是指一个良好的软件架构必须支持一下几点:

  • 支持系统所有用例的能力
  • 独立的开发能力
  • 独立的部署能力

再谈解耦模式

  • 源码层次解耦,也叫单体结构
  • 部署层次解耦
  • 服务层次解耦(微服务)

第十七章 划分边界

软件设计本身就是一门划分边界的艺术。架构师追求的目标就是最大限度降低构建和维护一个系统所需要的人力,而一个系统最消耗人力资源的地方,就是系统中存在的耦合,尤其是那些过早做出的,不成熟的决策所导致的耦合(深以为然)!!!

边界线应该画在何处?

边界线应该画在那些不相关的事情之间。比如UI与业务逻辑,UI与数据库,业务逻辑与数据库…

插件式架构

  • 组件应该可以用插件的方式集成到其他系统中。
  • 插件式的架构保证了组件的变更不会影响系统整体的业务逻辑。
  • 插件式架构是SRP原则的具体实现。

第十九章 策略与层次

软件就是策略语句的集合,软件设计的工作重点之一就是将这些策略彼此分离,然后将它们按照变更的方式进行重新分组。其中变更原因、时间和层次相同的策略应该被分到同一个组件中(OCP)。反之,变更原因、时间和层次不同策略则应该分属不用的组件。

架构设计的工作需要将组件重排组合成为一个有向无环图。图中的每一个节点代表的是一个拥有相同层次策略的组件,每一条单向链接都代表了一种组件之间的依赖关系,他们将不同级别的组件链接起来。

在一个设计良好的架构中,依赖关系的方向通常取决于他们所关联的组件层次。依赖关系应该与数据流向脱钩,而与组件所在的层次挂钩。

第二十章 业务逻辑

  • 关键业务逻辑,是指系统中真正用来赚钱或者省钱的部分。
  • 关键业务数据,是指关键业务逻辑需要处理的数据。
  • 物业实体(Entity):关键业务逻辑+关键业务数据。

第二十二章 整洁架构

  • 六边形架构(端口与适配器架构)
  • DCI架构
  • BCE架构

共同的设计目标:按照不同关注点对软件进行切割。即这些架构都会将软件切割成不同的层,至少有一层是只包含该软件的业务逻辑的,而用户接口,系统接口则属于其他层。

整洁架构图:

整洁架构

由内向外分别代表:

  • 业务实体
  • 用例
  • 接口适配器
  • 框架和驱动程序

依赖关系规则

  • 越靠近中心,其所在的软件层级就越高。外层代表机制,内层代表策略。
  • 依赖关系应该是外层依赖内层,低层依赖高层。
  • 使用DIP原则控制依赖。
  • 夸边界的数据处理,不直接使用业务实体数据对象,也不要违反依赖规则。

第二十三章 展示器和谦卑对象

  • 谦卑对象是指系统中难以测试的部分,比如UI。
  • 展示器则是可测试的对象,展示器的工作是将需要展示的数据按照规则进行处理,然后给UI层去展示。

第二十四章 不完全边界

构建不完全边界的一种方法就是在将系统分割成一系列可以独立编译、独立部署的组件之后,再把这些组件构建成一个新的组件。

  • 策略模式:不直接依赖实例,而是依赖一个由实例提供的抽象策略。
  • 门户模式:类似抽象工厂的方式提供可替换的抽象方法。
1234…8
lly

lly

耿直的一Boy

76 日志
© 2021 lly
由 Hexo 强力驱动
主题 - NexT.Mist