六大设计原则
开闭原则 Open-Closed Principle
软件实体(例如类、模块、函数等)应该对扩展开放,对修改关闭。
开闭原则要求软件设计应该具有以下特点:
- 可以轻松地扩展系统的功能,而不需要修改现有代码。
- 新功能的添加不应该破坏现有功能的稳定性和正确性。
在iOS开发中 我们更多使用继承和协议(接口)的方式去解决这种问题
🌰: 支付功能
在我们开发中很多要使用支付功能,但是我们应该如何设计这个功能的类呢?
before:一个支付类,里面分别有着微信支付,支付宝支付,等其他支付方式(缺点:每当有新的支付方式就要在类里面添加新的方法和新的属性)
after:声明一个protocol叫Payment,依赖抽象类,通过这种方式,你可以轻松地扩展应用程序以支持不同的支付方式,同时保持支付处理代码的封闭性。这符合开闭原则,因为你可以扩展功能而不修改现有代码。(这个例子也完美的契合后面介绍依赖倒置原则)
swift
protocol Payment {
func pay(amount: Double)
}
class AliPayment: Payment {
func pay(amount: Double) {
}
}
class WeChatPayment: Payment{
func pay(amount: Double) {
}
}
class BankTransferPayment: Payment {
func pay(amount: Double) {
}
}
🌰: 商品功能
当我们设计一个商品的时候 里面应该有名字和价格的属性
swift
class Product {
var name: String
var price: Double
init(name: String, price: Double) {
self.name = name self.price = price
}
func display() {
print("商品名称: \(name)") print("价格: \(price) 元")
}
}
随着业务复杂度提高,你添加了折扣活动的业务的逻辑,这个时候我们应该考虑应该怎么添加新的业务了逻辑了,根据开闭原则,我们不应该直接去修改Product里面的逻辑,这个时候我们可以通过继承的方式解决。
swift
class DiscountedProduct: Product {
var discount: Double
init(name: String, price: Double, discount: Double) {
self.discount = discount
super.init(name: name, price: price)
}
override func display() {
super.display()
let discountedPrice = price * (1.0 - discount)
print("折扣价: \(discountedPrice) 元")
}
}
依赖倒置原则 Dependency Inversion Principle
它强调了高层模块不应该依赖于低层模块,而双方都应该依赖于抽象
特点:
- 高层模块不应依赖于低层模块: 高层模块(应用程序的主要功能)不应该直接依赖于低层模块(具体的实现细节)。相反,它们应该依赖于抽象(接口或协议)。
- 抽象不应依赖于具体: 抽象应该定义一组抽象方法或属性,而不应该依赖于具体的实现。这意味着接口或协议不应该依赖于具体的类。
- 具体实现应该依赖于抽象: 具体的实现细节应该依赖于抽象,而不是相反。这意味着具体的类应该实现抽象定义的接口或协议。
其实第一次看我还是有一点懵,我只理解了要依赖抽象类,但是高层不应该依赖底层模块应该怎么办?接下来我从iOS表达出这个概念。
🌰:支付功能
在上面的例子里面我们介绍了支付功能应该依赖抽象类,这样可以做到支持不同的支付方式,这是底层模块应该处理的逻辑,高层模块我们应该如何设计呢?
swift
// 定义一个抽象协议
protocol Payment {
func pay(amount: Double)
}
<img src="" alt="" width="30%" />
// 创建一个具体的支付处理器类
class AliPayment: Payment {
func pay(amount: Double) {
// 实现支付宝支付的逻辑
print("处理支付宝支付: \(amount) 元")
}
}
// 高层模块依赖于抽象协议
class OrderManager {
let payment: Payment
init(payment: Payment) {
self.payment = payment
}
func checkout(amount: Double) {
// 执行结账逻辑
payment.pay(amount: amount)
}
}
// 使用依赖注入将具体实现传递给高层模块
let AliPayment = AliPayment()
let orderManager = OrderManager(pay: AliPayment)
// 执行结账操作
orderManager.checkout(amount: 100.0)
Payment
协议定义了抽象的支付处理方法,而AliPayment
类提供了具体的实现。OrderManager
高层模块依赖于抽象的Payment
协议,而不是具体的类。通过依赖注入,我们可以轻松地在运行时将具体的支付处理器传递给OrderManager
,实现了依赖倒置原则
总结下在swift中,我们一般怎么做
- 使用协议(Protocol):定义抽象,即接口或协议,以声明模块之间的合同。高层模块依赖于这些抽象,而不是具体的类。
- 使用依赖注入:通过依赖注入将依赖关系从高层模块传递到低层模块,而不是在代码中硬编码依赖关系。
还没学废,我再来一次
🌰:图形绘制功能
我们需要开发一个图形绘制功能,我们应该怎么设计呢? 首先我们需要一个draw方法,但是图形形状不一样的,所以定义一个protocol叫Shape,方法叫draw().
第二步 我们应该创建不同的类,有圆形 有三角形 有 矩形, 这样就做到 他们互相之间不影响。
第三步 我们要做到 高层模块不依赖底层模块,通过依赖倒置方式
swift
protocol Shape {
func draw()
}
class Circle: Shape {
func draw() {
print("绘制圆形")
}
}
class Rectangle: Shape {
func draw() {
print("绘制矩形")
}
}
class Triangle: Shape {
func draw() {
print("绘制三角形")
}
}
class Drawer {
func drawShape(_ shape: Shape) {
shape.draw()
}
}
let circle = Circle()
let rectangle = Rectangle()
let triangle = Triangle()
let drawer = Drawer()
drawer.drawShape(circle) // 输出: 绘制圆形
drawer.drawShape(rectangle) // 输出: 绘制矩形
drawer.drawShape(triangle) // 输出: 绘制三角形
高层模块(Drawer
类)依赖于抽象(Shape
协议),而不依赖于具体的图形类型。这样,你可以轻松地扩展应用程序以支持新的图形类型,而不需要修改现有的代码。
其实6大原则中也有一个和这个原则非常相似叫做迪米特原则,所以平时我们一般都是说5大原则,少说的这个就是迪米特原则。
迪米特原则 Law of Demeter / Least Knowledge
强调的是一个模块不应该直接与太多其他模块进行交互,而应该仅与其密切关联的模块通信。
其实这个原则和依赖倒置原则很相似,目的都是减少耦合,只是依赖倒置原则给了更详细设计方式,比如使用接口的方式和依赖注入降低耦合。
特点:
最小知识原则: 迪米特原则强调模块应该只与其直接朋友(紧密相关的模块)通信,而不应该了解太多关于其他模块的内部细节。直接朋友是指以下几种情况:
- 当前对象本身
- 当前对象的实例变量
- 当前对象的方法参数
- 当前对象调用的方法内部创建的对象
减少依赖关系: 迪米特原则鼓励减少类之间的依赖关系,避免一个类直接依赖于太多其他类。这可以通过引入中间层或接口来实现,以减少直接依赖。
松耦合性: 遵循迪米特原则的系统通常具有更松散的耦合度,因为模块之间的依赖关系更少。这使得系统更容易扩展、维护和测试。
隔离变化: 迪米特原则有助于隔离变化。当一个模块的内部实现发生变化时,只有其直接朋友受到影响,而不会波及到其他模块。
听上去是不是和依赖倒置的思想基本一致,那如何更好的体现这个原则,🌰它又来啦
🌰:商品功能
上面我们说到了商品功能,现在我们丰富的业务逻辑,引入用户和购物车这些内容,那他们关系应该是什么样呢?
根据迪米特原则上面这个图是错误的,我们应该减少依赖关系,让user 持有 shoppingCart,shoppingCart持有商品。
接口隔离原则 Interface Segregation Principle
强调客户端不应该依赖于它们不使用的接口
特点:
接口应该小而专一: 一个接口不应该包含客户端不需要的方法。它应该只包含与特定功能或行为相关的方法。
客户端不应该强制实现不需要的接口: 当一个类实现了一个接口时,它不应该被强制实现接口中的所有方法,尤其是那些它不需要的方法。
接口应该可扩展: 接口的设计应该考虑到未来的扩展。当需要添加新功能时,不应该影响已经存在的客户端代码。
避免"胖接口": "胖接口"是指包含太多方法的接口,这种接口会导致类实现不必要的方法,违反了ISP。
通过细化接口来解耦: 将大接口拆分成多个小接口有助于减少类之间的耦合性,提高系统的灵活性和可维护性。
在iOS中接口的体现就是protocol
,最明显的体现就是UITableview
的两个protocol
(delegate,datasoure),我们可以看到在一个view里面有两个协议组成,为什么不用一个呢?就是因为他们职责不一样,我们打开UITableViewDataSource协议的源码看下就有一行注释特点强调这个事情
swift
// this protocol represents the data model object. as such, it supplies no information about appearance (including the cells)
@MainActor public protocol UITableViewDataSource : NSObjectProtocol {
}
这个我觉得非常浅显易懂,我就不举其他例子了
单一责任原则 Single Responsibility Principle
强调一个类应该只有一个引起它变化的原因,或者说一个类应该只有一个责任
特点:
一个类一个责任: 每个类应该只负责一个明确定义的职责或任务。如果一个类承担了多个不相关的职责,那么当其中一个职责发生变化时,可能会影响到其他职责,导致代码变得脆弱和难以维护。
高内聚低耦合: SRP有助于实现高内聚(High Cohesion)和低耦合(Low Coupling)。高内聚表示类的成员之间关系紧密,执行相同的职责。低耦合表示类之间的依赖关系较弱,一个类的变化不会轻易影响其他类。
划分职责: 如果一个类的职责过于复杂,可以考虑将其分解成多个小类,每个小类负责一部分职责。这有助于提高代码的可读性和维护性。
遵循单一责任原则有助于测试: 当一个类只负责一个职责时,编写单元测试变得更容易,因为你只需测试与该职责相关的代码。
注意代码的变化: 如果你发现一个类的变化频繁,可能是因为它承担了过多的职责。在这种情况下,考虑对类进行重构,将不同的职责分开。
在接口隔离原则篇我们强调了应该接口小而专,避免胖接口 你知道UIView有多少协议吗? 你觉得UIView设计合理吗?
swift
/// 看看UIView源码 遵循了多少个协议
@MainActor open class UIView : UIResponder, NSCoding, UIAppearance, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace, UIFocusItem, UIFocusItemContainer, CALayerDelegate {
/// 属性就更多了
}
你有没有想过,如果一个不是iOS开发,想简单画一个矩形,然后打开UIView看到这么多属性,协议,而且UIView的父类UIResponder
并不属于绘制UI的类,核心负责绘制的是CALayer
会不会很蒙?
这只是我个人拙见,而且iOS历史也很久,历史负担也重,苹果也在尽量去修改,接下来 我要介绍的是SwiftUI
里面绘制一个矩形Rectangle()
swift
Rectangle()
.frame(width: 10,height: 1)
.foregroundColor(Color.black)
/// 下面是他的源码
// A rectangular shape aligned inside the frame of the view containing it.
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen public struct Rectangle : Shape {
/// Describes this shape as a path within a rectangular frame of reference.
/// - Parameter rect: The frame of reference for describing this shape.
/// - Returns: A path that describes this shape.
public func path(in rect: CGRect) -> Path
/// Creates a new rectangle shape.
@inlinable public init()
/// The type defining the data to animate.
public typealias AnimatableData = EmptyAnimatableData
/// The type of view representing the body of this view.
/// When you create a custom view, Swift infers this type from your
/// implementation of the required ``View/body-swift.property`` property.
public typealias Body
}
///下面是 基类 Shape
/// A 2D shape that you can use when drawing a view.
///
/// Shapes without an explicit fill or stroke get a default fill based on the
/// foreground color.
///
/// You can define shapes in relation to an implicit frame of reference, such as
/// the natural size of the view that contains it. Alternatively, you can define
/// shapes in terms of absolute coordinates.
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol Shape : Animatable, View {
/// Describes this shape as a path within a rectangular frame of reference.
/// - Parameter rect: The frame of reference for describing this shape.
/// - Returns: A path that describes this shape.
func path(in rect: CGRect) -> Path
/// An indication of how to style a shape.
/// SwiftUI looks at a shape's role when deciding how to apply a
/// ``ShapeStyle`` at render time. The ``Shape`` protocol provides a
/// default implementation with a value of ``ShapeRole/fill``. If you
/// create a composite shape, you can provide an override of this property
/// to return another value, if appropriate.
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
static var role: ShapeRole { get }
/// Returns the size of the view that will render the shape, given
/// a proposed size.
/// Implement this method to tell the container of the shape how
/// much space the shape needs to render itself, given a size
/// proposal.
/// See ``Layout/sizeThatFits(proposal:subviews:cache:)``
/// for more details about how the layout system chooses the size of
/// views.
/// - Parameters:
/// - proposal: A size proposal for the container.
/// - Returns: A size that indicates how much space the shape needs.
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize
}
在我们上个例子里面可以看到,在SwiftUI
,绘制一个矩形源代码很少,而UIView
里面源码就很多属性和协议,其实很多功能我们并不会用。
里氏替换原则 Liskov Substitution Principle
强调了子类应该能够替代其基类(或父类)而不影响程序的正确性
特点:
子类必须继承父类的所有属性和行为: 子类应该继承父类的属性和方法,确保子类具有与父类相同的接口和行为。
子类可以覆盖(重写)父类的方法: 子类可以重新实现(override)父类的方法,以满足自己的需求,但是不应该改变原有方法的约定和预期行为。
子类不应该引入新的属性或方法: 子类不应该添加新的属性或方法,这可能会导致客户端代码对父类和子类的依赖不一致。
子类的方法参数不应该比父类方法更严格: 如果父类的方法接受某种类型的参数,那么子类的方法可以接受相同类型或者更宽松的参数类型,但不应该接受更严格的参数类型。
子类的返回值类型可以是父类方法返回值类型的子类型: 子类可以扩展父类方法的返回值类型,返回一个子类型,但不应该缩小返回值类型
swift
class Shape {
func area() -> Double {
return 0.0
}
}
class Circle: Shape {
let radius: Double
init(radius: Double) {
self.radius = radius
}
override func area() -> Double {
return Double.pi * radius * radius
}
}
class Square: Shape {
let side: Double
init(side: Double) {
self.side = side
}
override func area() -> Double {
return side * side
}
}
在这个例子中,Circle和Square都是Shape的子类,并且它们都重写了area()方法。这遵循了里氏替换原则,因为客户端代码可以像操作Shape一样操作Circle和Square,而不用担心出现错误。这使得代码更加灵活和可扩展,可以轻松添加新的形状子类而不影响现有的代码。是不是感觉在哪里见过?对就是类似上面介绍 SwiftUI
的Rectangle