# iOS开发之重构笔记-Extra Method

语言：zh-CN
原文：[iOS开发之重构笔记-Extra Method](<https://allluckly.cn/%E6%8A%95%E7%A8%BF/tuogao26>)
中文：[中文 HTML](<https://allluckly.cn/%E6%8A%95%E7%A8%BF/tuogao26>)
English: [English HTML](<https://allluckly.cn/en/journal/2016-03-01-ios-refactoring-notes-extract-method/>)
发布于: 2016-03-01T00:00:00+08:00
内容更新于: 2016-03-01T00:00:00+08:00
署名：SindriLin

结合《重构》的提炼函数方法，把冗长代码按意图拆成独立函数，逐步处理参数、局部变量和返回值，并用实际示例讨论如何降低重复逻辑和提升可读性。

原文署名SindriLin，Bison 编辑收录。

[原文所列来源](<http://www.jianshu.com/p/f8c363dded91>)

**阅读前** 这是 2015–2017 年间的技术记录，本次整理未重新运行验证示例。涉及的 API、依赖和工具版本请结合发表日期阅读。

编辑：[Bison](<https://allluckly.cn/>)  
 投稿：[SindriLin](<http://www.jianshu.com/p/f8c363dded91>)

### 前言

这算是我的读书笔记吧，最近在看[Martin Fowler](<http://baike.baidu.com/link?url=kUPRU72mr6gghQt-wVEdPYB2ySblcwg_QS0Et4BUVQo64ZE2fLnMfR--c1uRBhoLywABBDLv4480RJ69WkyNQq>)大神的《重构 改善既有代码的设计》，决定将书中的代码换成熟悉的代码来实现。对于重构的概念，我直接引用书中的原话来科普下：

> 所谓重构是这样一个过程：在不改变代码外在行为的前提下，对代码做出修改，以改进程序的内部结构。重构是一种经千锤百炼形成的有条不紊的程序整理方法，可以最大限度地减少整理过程中引入错误的几率。本质上说，重构就是在代码写好之后改进它的设计。

学习重构的意义何在呢？更多的是为了提高自己对于软件工程的见解，提高自己的技术。

### 提炼函数

Extra Method（提炼函数）是一种常见的重构手法，当你觉得你的代码需要添加一段注释才能让人理解用途的时候，不要犹豫，直接将这些代码提炼出来，放在独立的函数中。提炼函数的做法步骤如下：

创建一个新函数，根据这个函数的意图来对它命名（以“做什么”而不是“怎么做”来命名，比如重写一个数组的访问，使得不发生越界。那么我会命名为func safeObjectAtIndex(index: Int)而不是func MakeIndexLessThanCountToGetObject(index: Int)，这个例子有点蠢）

- 将提炼出来的代码从原函数中复制到新建的目标函数里
- 检查提炼出的代码，看看其中是否需要使用到在原函数中的局部变量或者函数参数
- 检查是否有任何局部变量的值被新提炼的函数改变，如果有，看看能否将提炼代码处理为查询操作，并返回改变的值。如果很难这么做，需要使用其它方式进行修改
- 将被提炼的代码段的局部变量或者函数参数传入给新提炼的函数
- 处理完所有的局部变量，进行编译、测试

1、初始代码

```
func printOwing() {
    var outstanding: CGFloat = 0.0

    // print banner
    print("********************************")
    print("******* Customer Owes ********")
    print("********************************")

    // calculate outstanding
    for order in _orders {
        outstanding += order.amount
    }

    // print details
    print("name: \(name)")
    print("amount: \(outstanding)")
}
```

2、提炼功能单一的代码块

这一步骤包括把功能相同单一的代码提炼出来（比如日志输出代码），以及简单的接收原函数局部变量作为函数参数的提炼函数（修改局部变量在提炼函数中的命名使其更符合要求）

```
func printOwing() {
    var outstanding: CGFloat = 0.0

    printBanner()
    // calculate outstanding
    for order in _orders {
        outstanding += order.amount
    }
    printDetails(outstanding)
}

func printBanner() {
    print("********************************")
    print("******* Customer Owes ********")
    print("********************************")
}

func printDetails(amount: CGFloat) {
    print("name: \(name)")
    print("amount: \(amount)")
}
```

3、对局部变量进行再赋值  
 对需要进行计算的局部变量进行提炼，首先检查局部变量是否会在原函数跟提炼代码中一并发生改变，如果不会，那么将局部变量作为提炼函数的返回值使用

```
func printOwing() {
    printBanner()
    var outstanding: CGFloat = getOutstanding()
    printDetails(outstanding)
}

func printBanner() {
    print("********************************")
    print("******* Customer Owes ********")
    print("********************************")
}

func printDetails(amount: CGFloat) {
    print("name: \(name)")
    print("amount: \(amount)")
}

func getOutstanding() -> CGFloat {
    var result: CGFloat = 0.0
    for order in _orders {
        result+= order.amount
    }
    return result
}
```

ps：在swift中存在元组类型，但是建议不要让函数返回多种不同值的元组，这会导致函数的复用性降低

### 实战例子

今天在项目中自定义的一个tableView表尾视图需要在数据源变化的时候改变自身的高度，且高度有一个最低值。已知表尾视图有个belongView的对tableView的弱引用，屏幕高度为kScreenHeigth，导航栏总体高度为kNavigationBarHeight（在打开个人热点时这个值会发生改变），其中最低高度值取决于最下方按钮addButton的位置。我的重构步骤如下：

1、初始代码

```
func updateViewHeight() {
    let tableView: UITableView! = belongView!
    var headerHeight = tableView.tableViewHeaderView?.frame.height
    headerHeight = (headerHeight == nil ? 0 : headerHeight)
    let cellCount = tableView.dataSource.numberOfRowsInSection(tableView: tableView, section: 0)
    var viewHeight: CGFloat = kScreenHeight - kNavigationBarHeight - CGFloat(cellCount) * tableView.rowHeight - headerHeight
    let buttonBottomDistance: CGFloat = 15.0
    let minHeight: CGFloat = CGRectGetMaxY(addButton) + buttonBottomDistance
    self.frame.height = max(minHeight, viewHeight)
}
```

2、将局部变量提炼成函数获取  
 我先把单元格个数、表头高度以及最低高度提炼出来

```
func updateViewHeight() {
    let headerHeight: CGFloat = getTableViewHeaderViewHeight()
    let cellCount: Int= getTableViewCellsCount()
    var viewHeight: CGFloat = kScreenHeight - kNavigationBarHeight - CGFloat(cellCount) * belongView!.rowHeight - headerHeight
    let minHeight: CGFloat = getViewMinHeight()
    self.frame.height = max(minHeight, viewHeight)
}

func getTableViewHeaderViewHeight() -> CGFloat {
    var headerHeight = 0.0
    if belongView!.tableViewHeaderView {
        headerHeight = (belongView!.tableViewHeaderView?.height)!
    }
    return headerHeight
}

func getTableViewCellsCount() -> Int {
    return (belongView!.dataSource.tableView(tableView: belongView!, numberOfRowsInSection: 0))!
}

func getViewMinHeight() -> CGFloat {
    let buttonBottomDistance: CGFloat = 15.0
    return CGRectGetMaxY(addButton) + buttonBottomDistance
}
```

3、将相同功能的代码再次提炼  
 由于代码中获取单元格数量的目的是为了获取所有单元格高度，因此可以提炼一个获取单元格总高度的方法getCellsTotalHeight

```
func updateViewHeight() {
    let headerHeight: CGFloat = getTableViewHeaderViewHeight()
    var viewHeight: CGFloat = kScreenHeight - kNavigationBarHeight - getCellsTotalHeight() - headerHeight
    let minHeight: CGFloat = getViewMinHeight()
    self.frame.height = max(minHeight, viewHeight)
}

func getTableViewHeaderViewHeight() -> CGFloat {
    var headerHeight = 0.0
    if belongView!.tableViewHeaderView {
        headerHeight = (belongView!.tableViewHeaderView?.height)!
    }
    return headerHeight
}

func getCellsTotalHeight() -> CGFloat {
    let cellCount: Int= getTableViewCellsCount()
    return CGFloat(cellCount) * belongView!.rowHeight
}

func getTableViewCellsCount() -> Int {
    return (belongView!.dataSource.tableView(tableView: belongView!, numberOfRowsInSection: 0))!
}

func getViewMinHeight() -> CGFloat {
    let buttonBottomDistance: CGFloat = 15.0
    return CGRectGetMaxY(addButton) + buttonBottomDistance
}
```

4、提炼功能相近的函数  
 可以看到updateViewHeight的目的是为了更新表尾视图的高度，那么里面还存在一些计算高度的代码，将这些代码做最后的提炼成函数getAdjustHeight

```
func updateViewHeight() {
    self.frame.height = getAdjustHeight()
}

func getAdjustHeight() -> CGFloat {
    let headerHeight: CGFloat = getTableViewHeaderViewHeight()
    var viewHeight: CGFloat = kScreenHeight - kNavigationBarHeight - getCellsTotalHeight() - headerHeight
    let minHeight: CGFloat = getViewMinHeight()
    return max(viewHeight, minHeight)
}

func getTableViewHeaderViewHeight() -> CGFloat {
    var headerHeight = 0.0
    if belongView!.tableViewHeaderView {
        headerHeight = (belongView!.tableViewHeaderView?.height)!
    }
    return headerHeight
}

func getCellsTotalHeight() -> CGFloat {
    let cellCount: Int= getTableViewCellsCount()
    return CGFloat(cellCount) * belongView!.rowHeight
}

func getTableViewCellsCount() -> Int {
    return (belongView!.dataSource.tableView(tableView: belongView!, numberOfRowsInSection: 0))!
}

func getViewMinHeight() -> CGFloat {
    let buttonBottomDistance: CGFloat = 15.0
    return CGRectGetMaxY(addButton) + buttonBottomDistance
}
```

5、将临时变量替换成提炼的函数  
 最后，我们将必要的局部变量变成函数，并且将计算高度的临时变量变成函数调用可以看到updateViewHeight的目的是为了更新表尾视图的高度，那么里面还存在一些计算高度的代码，将这些代码做最后的提炼成函数getAdjustHeight

```
func updateViewHeight() {
    self.frame.height = getAdjustHeight()
}

func getAdjustHeight() -> CGFloat {
    var viewHeight: CGFloat = kScreenHeight - kNavigationBarHeight - getCellsTotalHeight() - getTableViewHeaderViewHeight()
    return max(viewHeight, getViewMinHeight())
}

func getTableViewHeaderViewHeight() -> CGFloat {
    var headerHeight = 0.0
    if belongView!.tableViewHeaderView {
        headerHeight = (belongView!.tableViewHeaderView?.height)!
    }
    return headerHeight
}

func getCellsTotalHeight() -> CGFloat {
    return CGFloat(getTableViewCellsCount()) * belongView!.rowHeight
}

func getTableViewCellsCount() -> Int {
    return (belongView!.dataSource.tableView(tableView: belongView!, numberOfRowsInSection: 0))!
}

func getViewMinHeight() -> CGFloat {
    return CGRectGetMaxY(addButton) + getButtonBottomDistance()
}

func getButtonBottomDistance() -> CGFloat {
    return 15.0
}
```

大功告成

---

好文推荐：[Bison眼中的iOS开发多线程是这样的（三）](<https://allluckly.cn/%E5%A4%9A%E7%BA%BF%E7%A8%8B/duoxiancheng03>)

