原文链接:http://letsswift.com/2015/01/ios-design-pattern-in-swift-1/
OS设计模式 - 大伙应该都听说过,但是有多少人真正的了解他们呢?虽然大多数开发者都认同设计模式的重要性,但是在实际开发中却并不怎么注意使用设计模式,而且关于设计模式的文章也是凤毛麟角,这更使得开发者无从下手去学习设计模式。
设计模式是一个处理软件设计中常见问题的解决方法,并可以重复使用。它向开发者提供了设计模板,使开发者更容易写出逻辑清晰、具有可复用性的代码。它还可以使代码具有松耦合性,能让开发者轻松的更新或替换项目中使用的组件。
在本教程中,大伙要开发一个音乐仓库应用,能显示你们收藏的专辑以及相关信息。
在开发过程中,大伙会逐渐掌握大多数通用的Cocoa设计模式:
- 构建设计模式:Singleton。
- 架构设计模式:MVC,Decorator,Adapter,Facade。
- 行为设计模式:Observer,Memento。
千万不要认为该文章只是对设计模式理论上的讲解,大伙需要在你们的音乐仓库应用中运用到这些设计模式。你们的应用最终看起来大概是这个模样:
准备开始
从这里下载初始项目,解压Zip文件,然后在Xcode中打开BlueLibrarySwift.xcodeproj工程。
在开始之前,有三件事需要大伙注意:
- 在
ViewController
中有两个IBOutlet
连接着Storyboard中的TableView和Toolbar。 - 在Storyboard中的
ViewController
里含有三个组件,并且设置了AutoLayout布局约束。最上面的组件用来显示音乐专辑的封面。中间是一个TableView,用来显示与该专辑相关的信息。最下面是两个Toolbar按钮,一个是撤销操作按钮,另一个是删除选中专辑的按钮。你们的Storyboard看起来应该是下面这个样子:
- 在工程里还有一个HTTP Client类(
HTTPClient
),这个类目前是空的,但是你们在之后会充实它。
注意:你们知道吗,当你们创建了一个新的Xcode项目后,你们所编写的代码其实就已经在遵循一定的设计模式了,Model-View-Controller, Delegate, Protocol, Singleton这些设计模式统统可以免费使用哦。
在带你们深入了解第一个设计模式之前,你们需要创建两个类,用于存储和展示音乐专辑的数据。
点击菜单 File\New\File… (或者按下 Command+N 快捷键)。选择 iOS > Cocoa Touch Class ,点击 Next。设置该类的名称为 Album ,并让它继承 NSObject。最后选择语言为Swift 然后点击 Next,最后点击 Create。
打开Album.swift文件,在Album类中定义如下属性:
var title : String!
var artist : String!
var genre : String!
var coverUrl : String!
var year : String!
然后添加一个初始化方法:
init(title: String, artist: String, genre: String, coverUrl: String, year: String) {
super.init()
self.title = title
self.artist = artist
self.genre = genre
self.coverUrl = coverUrl
self.year = year
}
这段代码为Album类创建了一个初始化方法,当你要创建一个新的专辑时你要通过这个初始化方法,传入专辑名称、演唱者、风格、专辑封面图片的URL以及年份这些属性。
接下来需要再添加一个方法:
func description() -> String {
return "title: \(title)" +
"artist: \(artist)" +
"genre: \(genre)" +
"coverUrl: \(coverUrl)" +
"year: \(year)"
}
description()
方法将专辑的这些属性拼成一个字符串,并返回。
我们再次通过刚才创建类的步骤创建一个名为AlibumView的类,注意该类要继承UIView。
打开AlbumView.swift文件,在该类中添加两个属性:
private let coverImage: UIImageView!
private let indicator: UIActivityIndicatorView!
coverImage
属性用来展示专辑封面,indicator
是当正在下载封面图片时转动的菊花。
大伙注意,这里的两个属性分配了private
访问级别,也就是说这两个属性只能在AlbumView.swift文件中使用,因为其他的类压根没有必要知道这两个属性的存在。(译者:其实这里用不用private都无所谓,因为咱们写的又不是library或者framework。)
接下来为AlbumView
类添加初始化方法:
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
init(frame: CGRect, albumCover: String) {
super.init(frame: frame)
backgroundColor = UIColor.blackColor()
coverImage = UIImageView(frame: CGRectMake(5, 5, frame.size.width - 10, frame.size.height - 10))
addSubview(coverImage)
indicator = UIActivityIndicatorView()
indicator.center = center
indicator.activityIndicatorViewStyle = .WhiteLarge
indicator.startAnimating()
addSubview(indicator)
}
由于UIView
遵循了NSCoding
,而AlbumView
又继承了UIView
,所以这里需要写一个NSCoder
的初始化方法,但我们不会在这个方法中处理什么逻辑,所以调用super.init
即可。
AlbumView
真正的初始化方法是另外一个init
方法,在这个方法中设置了一些默认的属性,比如将背景色设置为黑色、实例化了coverImage
属性,并让它与父容器有5px的四周间距、实例化indicator
并设置位置及风格。
最后再添加一个方法:
func highlightAlbum(#didHighlightView: Bool) {
if didHighlightView == true {
backgroundColor = UIColor.whiteColor()
} else {
backgroundColor = UIColor.blackColor()
}
}
这个方法是专辑封面高亮开关,高亮时专辑封面背景色为白色,否则为黑色。
现在编译你们的工程,确保一切都没有问题,然后准备开始第一个设计模式的学习。
设计模式的王者 – MVC模式
Model-View-Controller (MVC)设计模式是Cocoa框架的基石,毋庸置疑它是开发者们最常用的设计模式没有之一。它把应用中的对象按它们的角色进行分类,并鼓励开发者按这种角色分类创建项目目录,将代码放置在合适的位置,保证项目结构清晰明确。
顾名思义,MVC中有三种角色:
- Model:这种对象保存应用程序的数据,并定义如何操作处理这些数据。比如之前你们创建的
Album
类就是一个Model对象。 - View:这种对象主要负责Model对象的呈现,以及用户交互。基本上,由
UIView
衍生出的类都是View对象。在你们的音乐仓库应用中AlbumView
就是一个View对象。 - Controller:这种对象充当着应用程序的协调者,由它来协调所有的事情。它会访问Model对象的数据,然后展示在相应的View对象中;它也会监听用户在View对象上的交互,从而通知Model对象进行相应的数据操作等等。你们应用中的
ViewController
就是一个Controller对象。
下图可以很好的说明Model对象和View对象是如何通过Controller对象进行通信的:
当Model对象的数据发生改变时,它会通知Controller对象,然后Controller对象更新对应的View对象上展示的数据。当用户在View对象进行了交互操作时,View对象会通知Controller对象,然后Controller对象会更新对应的Model对象中的数据。
你们可能会有这样的疑惑,为什么不把这些操作处理都写在Controller对象中呢,这样就不用通知来通知去的,不是更简单吗?
我告诉你们两个概念,你们就明白这样做的目的了,那就是低耦合性和高复用性。我举个例子,在一个应用中每个界面中的数据大多数是来自于多个Model对象,如果把View对象和Model对象绑定死了,那么就没法处理这种情况了。
拿咱们这个音乐仓库的应用来说,如果你们以后想做一个电影仓库或者图书仓库,你仍然可以使用AlbumView
这个View对象来展示你的电影或者图书Model对象。假如你的电影仓库应用需要展示电影主题曲的一些信息,那么或许你就可以直接复用Album
对象,因为Album
对象不依赖于任何View对象。这就是MVC设计模式的强大之处。
如何使用MVC设计模式
首先,你要确保项目中每个类的功能,要么是Controller,要么是View,要么是Model。千万不要将两种角色混合于一个类,每个类只有单一的职责。不过,到目前为止,你们已经创建了标准的Model类Album
和View类AlbumView
。
其次,为了使项目目录结构清晰明了,以及能够更感官的实行MVC模式,你们需要在工程中创建三个文件组,来区分开这三种角色的文件。
通过File\New\Group(或同时按下Command+Option+N)在工程中创建一个名为 Model 的文件组,然后以同样的方式创建 View 文件组和 Model 文件组。
最后将 Album.swift 文件拖进Model文件组,将 AlbumView.swift文件拖进View文件组,将ViewController.swift 文件拖进Model文件组。
此时,你们的项目目录结构应该是这样的:
现在的项目结构看起来已然井然有序,当然你们还可以创建其他的文件组和类文件,但是要记住的是,Model、View、Controller这三个文件组是整个程序的核心所在。
现在项目结构已经理清楚了,接下来的工作就需要从某个地方获取到专辑的相关数据。你们可以创建一个名为API的类,用于负责整个应用的数据管理工作。并且将拉开你们要了解的下一个设计模式 — Singleton。
Singleton设计模式
单例模式使一个类在整个应用生命周期内只存在一个实例,并且有一个全局的方法来访问这个实例。在单例模式下,当第一次访问某个类的实例时,该类通常使用延迟加载的方式创建该类的单例。
注意: Apple在iOS和OSX中大量使用了单例模式,比如:NSUserDefaults.standardUserDefaults(),UIApplication.sharedApplication(), UIScreen.mainScreen(),NSFileManager.defaultManager()。
你们可能会有疑问,为什么我们要这么在意一个类有一个或多个实例?代码和内存现在是如此的廉价,不是么?
其实不然,有些情况下,确实只需要类实例化一次,且仅有一次。比如有这么一种情况,在一个应用的生命周期里,应用(Application)设备的主屏幕是只存在一份的,那么你当然希望应用和设备屏幕的实例有且只有一个。或者你需要一个全局的处理配置的类,这样能线程安全的访问配置文件,避免多个配置类同时访问一个配置文件。这些就是单例模式的好处所在。
如何使用Singleton设计模式
先看看下面这张图:
上图是Logger的类图,从图中可以看出,Logger有一个instance
属性以及sharedInstance
和init
两个方法。
当第一次调用sharedInstance
方法时,instance
属性还没有初始化,所以你会创建一个新的Logger类实例,并返回一个该实例的引用。
当再次调用sharedInstance
方法时,instance
属性会立即返回,并且不需要再进行任何实例化操作。这个逻辑就保证了Logger类的实例有且仅有一份。
你们将要通过Singleton设计模式,创建一个单例的类用于管理所有专辑的数据。
你们应该注意到了,在项目的目录结构中,有一个文件组叫做API,这个文件组里存放的类基本都是为应用提供服务的类。我们在这个文件组中创建一个名为 LibraryAPI 的类。
打开 LibraryAPI.swift 文件,添加如下代码:
//1
class var sharedInstance: LibraryAPI {
//2
struct Singleton {
//3
static let instance = LibraryAPI()
}
//4
return Singleton.instance
}
- 创建一个类变量的计算属性。类变量类似Objective-C中的类方法,也就是说在任何时候你访问
sharedInstance
属性时,都不需要对LibraryAPI
进行实例化,关于属性类型更多的知识请参阅Swift文档 – properties。 - 在类变量中内嵌一个结构体,名为
Singleton
。 -
Singleton
中包含一个名为instance
的静态常量属性。用static
申明属性意味着该属性只能存在一份。这里要注意的是Swift中的静态属性都会延迟加载,也就是说只有instance
被使用时,才会初始化它。还要注意的一点是,一旦instance
被初始化了,那么它就是一个常量属性,不会有第二次初始化的机会了。这就是Singleton模式的精髓所在。 - 返回该计算属性的值。
注意:如果想了解Swift中创建单例的其他方法请参阅这里:Github page。
你们现在已经有一个单例模式的对象,作为管理专辑数据的入口。接下来要更进一步,创建一个类,用于处理你们数据的持久性。
继续在 API 这个文件组中创建一个类,名为PersistencyManager
,并让它继承NSObject
类。
打开 PersistencyManager.swift 文件,申明一个属性:
private var albums = [Album]()
这里申明了一个private访问权限的变量属性,用于储存专辑数据。这个数组是可变数组,所以你们可以很轻松的增删专辑。
接下来我们在该类中添加如下初始化方法:
override init() {
//Dummy list of albums
let album1 = Album(title: "Best of Bowie",
artist: "David Bowie",
genre: "Pop",
coverUrl: "http://www.coversproject.com/static/thumbs/album/album_david%20bowie_best%20of%20bowie.png",
year: "1992")
let album2 = Album(title: "It's My Life",
artist: "No Doubt",
genre: "Pop",
coverUrl: "http://www.coversproject.com/static/thumbs/album/album_no%20doubt_its%20my%20life%20%20bathwater.png",
year: "2003")
let album3 = Album(title: "Nothing Like The Sun",
artist: "Sting",
genre: "Pop",
coverUrl: "http://www.coversproject.com/static/thumbs/album/album_sting_nothing%20like%20the%20sun.png",
year: "1999")
let album4 = Album(title: "Staring at the Sun",
artist: "U2",
genre: "Pop",
coverUrl: "http://www.coversproject.com/static/thumbs/album/album_u2_staring%20at%20the%20sun.png",
year: "2000")
let album5 = Album(title: "American Pie",
artist: "Madonna",
genre: "Pop",
coverUrl: "http://www.coversproject.com/static/thumbs/album/album_madonna_american%20pie.png",
year: "2000")
albums = [album1, album2, album3, album4, album5]
}
在初始化方法中,你们可以构建一些专辑添加到albums
数组中,这里我构建了5个专辑。
然后再添加几个方法:
func getAlbums() -> [Album] {
return albums
}
func addAlbum(album: Album, index: Int) {
if (albums.count >= index) {
albums.insert(album, atIndex: index)
} else {
albums.append(album)
}
}
func deleteAlbumAtIndex(index: Int) {
albums.removeAtIndex(index)
}
这些方法可以让你们方便的存、取、删除album
数组中的专辑。
然后编译你们的项目,确保编译通过。此时大伙也许又有了疑问,我们应该如何使用PersistencyManager
类呢?别着急,在下一节里,会向大家介绍 Facade 设计模式,届时你们就会明白LibraryAPI
与PersistencyManager
之间的关系,以及如何使用PersistencyManager
了。