最近在搞代码重构,这是一个很好的学习软件设计原则、设计模式、架构设计并实践的机会,本文是以一个iOS开发人员对软件设计原则的一个概括总结。
一、概况
软件设计原则和设计模式是紧密相关的两个概念,但它们有着不同的焦点和目的。软件设计原则主要关注如何设计“好的”软件,强调的是架构设计方面的规范和指导思想;而设计模式则是针对具体的问题和场景,提供精细的解决方案,这些方案包含了具体实现细节和代码结构。
软件设计原则是指导软件开发的通用规范和指导思想,是从更高层面上指导软件设计的。常见的软件设计原则有SOLID
原则、KISS
原则、DRY
原则、YAGNI
原则等等。
下面来对具体的"原则"做个了解.
二、SOLID原则
-
开放封闭原则(Open Close Principle)
对修改封闭,对扩展开放。比如工厂方法模式和策略模式,工厂方法模式通过定义一个抽象的工厂类来管理产品对象的实例化,以便于在不改变原有代码的基础上新增产品对象的类型。
简单工厂、工厂方法、抽象工厂设计模式-iOS -
依赖倒置原则(Dependence Inversion Principle)
依赖于抽象,而不依赖于具体。
依赖倒置原则中的核心思想是:高层模块不应该依赖于低层模块,而双方都应该依赖于抽象。在UITableView中,我们可以看到UITableViewDataSource
和UITableViewDelegate
就是抽象,而UITableView
就是依赖于这两个协议实现列表的数据源和代理。 -
单一职责原则(Simple Responsibility Principle)
设计的类只负责一种类型的职责,只有一个引起它变化的原因。这样可以使代码更加清晰,避免出现过度复杂的继承关系和耦合。比如在UIViewController中,可以将不同类型的任务交给不同的类去实现,保证职责单一,代码也清晰易懂。 -
接口隔离原则(interface Segregation Principle)
用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口。在设计时应该注意:
1)一个类对另一个类的依赖应该建立在最小接口上。
2)建立单一接口,不要建立庞大臃肿的接口。
3)尽量细化接口,接口中的方法尽量少(不是越少越好,而是适度)。 -
里氏替换原则(Liskov Substitution Principle)
要求程序中,使用基类的对象可以被其子类的对象所代替,而不会产生任何错误或异常,使用者不需要关心实现的具体类。在iOS开发中,我们可以利用多态的特性,让不同的子类对象通过父类来使用,从而增加代码的灵活性和扩展性。
三、迪米特法则(Law of Demeter)
也称最少知道原则,Least Knowledge Principle
, LKP。
指一个对象应该对其他对象保持最少的了解,尽量降低类与类之间的耦合度。一个类应依赖于那些最直接合理的类,而不是依赖于很多其他类。主要是:
- 一个对象只应该调用直接朋友(即与其它对象有直接关系的对象)的方法。在对象之间建立一条明确的通信路径可以降低耦合度,使系统更容易维护和修改。
- 谨慎使用外观模式: 外观模式可以帮助我们降低耦合性,但是在使用时需要注意,放置太多的业务逻辑代码到外观模式中会导致对象之间互相依赖,不利于扩展和维护。
- 不暴露任何细节:一个好的设计应该将系统的实现细节封装在类内部,不向外暴露任何不必要的细节信息。这样可以降低类之间的耦合度,使系统更加稳定和灵活。
例如:在iOS开发中,UIView
只知道与其相关联的UIViewController
,而不需要知道UIViewController
背后的一层层业务逻辑和数据存储的实现,以此来实现类间的解耦合。另外一个例子是KVO
.
四、合成复用原则(Composite/Aggregate Reuse Principle)
用组合和聚合关系来代替继承实现代码复用。这里的组合指的是通过将一个或多个对象(组合部分)组合成一个更大的、有着更高层次抽象的整体(组合整体),而聚合则是指在一个类中引用另一个类的实例。
五、KISS原则(Keep It Simple And Stupid)
KISS原则
强调在设计软件时应力求简单,避免复杂和不必要的细节和冗余,以保持代码的简洁、易理解、可维护和可扩展。
六、YAGNI原则(You Ain't Gonna Need It)
YAGNI原则
是一种迭代开发和极限编程中的设计哲学,它告诉程序员不要在软件中添加除了当前需要之外的任何功能,避免浪费时间和精力开发无用功能,或在被证明是需要之前预测未来的需求。
七、DRY原则(Don’t Repeat Yourself)
DRY原则
要求程序员避免代码重复,避免重复造轮子,复用已有的模块和代码。这可以帮助减少错误率,提高代码的可读性、可维护性和可扩展性。
八、GRASP原则(General Responsibility Assignment Software Parren)
职责分配软件模式,GRASP原则
提供了一些模式和约束条件,帮助程序员正确分配和选择各个类和对象所应负责的职责,以提高代码的可读性、可维护性和可扩展性。主要有以下九个
- 建造者(Creator):尽量将对象的创建和初始化工作封装到一个专门的类中。
- 控制器(Controller):控制器模式用于协调和控制系统中的各种活动和任务。
- 信息专家(Infomation Export):问题的解决应该由尽可能具有相关知识的对象来处理。
- 低耦合(Low Coupling): 尽量减少不同对象之间的依赖关系,从而降低耦合。
- 高内聚(High Cohesion): 在单个对象中将相关的属性和方法组织在一起,以提高其内聚性。
- 间接性(Indirection): 使用一个中间对象来封装和管理不同对象之间的通信。
- 多态性(Polymorphism): 通过多态来处理对象可能的状态变化和状态转换,以增强程序的灵活性。
- 受保护的变化(Protected Variations): 在可能发生改变的地方创建保护壳,以防止变化对系统的影响。具体来说,它建议在设计类或模块时,将可能受变化影响的部分封装在一些具有抽象接口的类中,通过抽象层和多态性来限制变化对系统的影响,从而提高系统的可维护性和可扩展性。
- 纯虚构(Pure Fabrication):创建一个虚拟的类或对象来表示某种行为或任务,并将其从任何现有的类中分离出来。
下面是对其中几个不太好理解的原则的举例说明:
1. 接口隔离原则的实例
一个常见的应用场景是网络请求,我们可以通过封装一个网络请求库来方便地对外提供网络请求的功能。在实现网络请求库时,我们可以应用接口隔离原则,将网络请求接口拆分成更加细粒度的接口,如下所示:
protocol NetworkRequestProtocol {
associatedtype ResponseDataType
func get(url: URL, parameters: [String: Any]?,
completion: ((Result<ResponseDataType, NetworkError>) -> Void)?)
func post(url: URL, parameters: [String: Any]?,
completion: ((Result<ResponseDataType, NetworkError>) -> Void)?)
}
protocol NetworkRequestConfigurableProtocol {
func setHTTPHeaderFields(_ headers: [String: String])
func setSerializationType(_ type: NetworkRequestSerializationType)
}
2. 间接性(Indirection)的实例
使用了MusicProvider
这个中间对象作为间接层。通过使用 MusicProvider
对象,我们可以强制对歌曲访问进行验证,而不会直接使用 AudioPlayer
对象,从而实现 Indirection
原则.
protocol MusicProviderProtocol {
func playSong(_ song: Song, completionHandler: @escaping (Error?) -> Void)
}
class MusicProvider: MusicProviderProtocol {
let audioPlayer: AudioPlayerProtocol
let accessManager: AccessManagerProtocol
init(audioPlayer: AudioPlayerProtocol, accessManager: AccessManagerProtocol) {
self.audioPlayer = audioPlayer
self.accessManager = accessManager
}
func playSong(_ song: Song, completionHandler: @escaping (Error?) -> Void) {
guard accessManager.hasAccess(to: song) else {
// 提示用户登录或升级账户以获得足够的访问权限
completionHandler(MyErrors.insufficientAccess)
return
}
audioPlayer.play(song) { error in
completionHandler(error)
}
}
}
3.受保护的变化(Protected Variations)原则的实例
假设我们正在开发一个iOS应用程序,该应用程序包含一个视频播放器功能。我们希望能够将不同的视频播放程序集成到应用程序中,例如AVFoundation或第三方播放器SDK等。为了实现这个目标,我们可以通过下面的方式来应用Protected Variations原则:
我们创建一个名为VideoPlayerProtocol的协议,它定义了播放器的基本行为和接口。在具体的播放器类中,我们将实现视频播放的具体逻辑。协议和具体类的划分允许我们在未来从应用程序中修改具体的播放器实现,而无需改变播放器到应用程序的接口。这可以为应用程序带来极大的灵活性和可维护性。
// VideoPlayerProtocol defines the interface for the video player
protocol VideoPlayerProtocol {
var url: URL { get set }
func play()
}
// We implement the VideoPlayerProtocol for AVFoundation
class AVFoundationVideoPlayer: VideoPlayerProtocol {
var url: URL
init(url: URL) {
self.url = url
}
func play() {
// Play with AVPlayer
}
}
// We can add other video players using the same VideoPlayerProtocol
class ThirdPartyVideoPlayer: VideoPlayerProtocol {
var url: URL
init(url: URL) {
self.url = url
}
func play() {
// Play with third-party player SDK
}
}
4. 纯虚构(Pure Fabrication)
通过PersistenceManager
和AuthService
两个虚构类将业务逻辑和非业务逻辑分离开来并提高代码的复用性和可维护性。
class AuthService {
static let shared = AuthService()
func validate(username: String, password: String, completion: @escaping (Bool) -> Void) {
// 发送网络请求并验证用户名和密码
// 请求完成后调用 completion
}
}
class PersistenceManager {
static let shared = PersistenceManager()
func save(user: User) {
// 保存用户信息
}
func load(completion: @escaping (User?) -> Void) {
// 加载用户信息
// 加载完成后调用 completion
}
}
class User {
var username: String
var email: String
var password: String
func signIn(completion: @escaping (Bool) -> Void) {
AuthService.shared.validate(username: username, password: password) { isValid in
completion(isValid)
}
}
func save() {
PersistenceManager.shared.save(user: self)
}
}
网友评论