iOS-设计原则篇

六大设计原则

开闭原则 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

强调的是一个模块不应该直接与太多其他模块进行交互,而应该仅与其密切关联的模块通信。

其实这个原则和依赖倒置原则很相似,目的都是减少耦合,只是依赖倒置原则给了更详细设计方式,比如使用接口的方式和依赖注入降低耦合。

特点:
最小知识原则: 迪米特原则强调模块应该只与其直接朋友(紧密相关的模块)通信,而不应该了解太多关于其他模块的内部细节。直接朋友是指以下几种情况:

  • 当前对象本身
  • 当前对象的实例变量
  • 当前对象的方法参数
  • 当前对象调用的方法内部创建的对象

减少依赖关系: 迪米特原则鼓励减少类之间的依赖关系,避免一个类直接依赖于太多其他类。这可以通过引入中间层或接口来实现,以减少直接依赖。

松耦合性: 遵循迪米特原则的系统通常具有更松散的耦合度,因为模块之间的依赖关系更少。这使得系统更容易扩展、维护和测试。

隔离变化: 迪米特原则有助于隔离变化。当一个模块的内部实现发生变化时,只有其直接朋友受到影响,而不会波及到其他模块。

听上去是不是和依赖倒置的思想基本一致,那如何更好的体现这个原则,🌰它又来啦

🌰:商品功能

上面我们说到了商品功能,现在我们丰富的业务逻辑,引入用户和购物车这些内容,那他们关系应该是什么样呢?

graph TD User --> ShoppingCart User --> Product

根据迪米特原则上面这个图是错误的,我们应该减少依赖关系,让user 持有 shoppingCart,shoppingCart持有商品。

graph TD User --> ShoppingCart --> Product

接口隔离原则 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,而不用担心出现错误。这使得代码更加灵活和可扩展,可以轻松添加新的形状子类而不影响现有的代码。是不是感觉在哪里见过?对就是类似上面介绍 SwiftUIRectangle

相关推荐
伯牙碎琴3 小时前
十一、SOA(SOA的具体设计模式)
架构
Karoku06610 小时前
【网站架构部署与优化】web服务与http协议
linux·运维·服务器·数据库·http·架构
Lill_bin13 小时前
深入理解ElasticSearch集群:架构、高可用性与数据一致性
大数据·分布式·elasticsearch·搜索引擎·zookeeper·架构·全文检索
zyhJhon13 小时前
软考架构-面向服务的架构风格
架构
nbsaas-boot13 小时前
微服务之间的安全通信
安全·微服务·架构
数据运营新视界17 小时前
你知道企业架构中核心的4大架构联系和不同吗?
大数据·架构
Xinan_____17 小时前
Linux——高流量 高并发(访问场景) 高可用(架构要求)
linux·运维·架构
杨诚实17 小时前
20240912软考架构-------软考161-165答案解析
数据库·架构
achirandliu19 小时前
汽车电子电气架构从12V提升至48V,带来那些好处? 包括那些改变?
架构·汽车·汽车电子电气架构·从12v升到48v
huaqianzkh19 小时前
了解MySQL 高可用架构:主从备份
数据库·mysql·架构