六大规划准则

开闭准则 Open-Closed Principle

软件实体(例如类、模块、函数等)应该对扩展敞开,对修正封闭。

开闭准则要求软件规划应该具有以下特色:

  • 能够轻松地扩展体系的功用,而不需求修正现有代码。
  • 新功用的增加不该该损坏现有功用的稳定性和正确性。

iOS开发中 咱们更多运用承继和协议(接口)的办法去解决这种问题

iOS-设计原则篇

: 付出功用

在咱们开发中许多要运用付出功用,可是咱们应该怎样规划这个功用的类呢?

before:一个付出类,里边分别有着微信付出,付出宝付出,等其他付出办法(缺陷:每当有新的付出办法就要在类里边增加新的办法和新的特色)

after:声明一个protocol叫Payment,依靠笼统类,经过这种办法,你能够轻松地扩展应用程序以支撑不同的付出办法,一起坚持付出处理代码的封闭性。这符合开闭准则,由于你能够扩展功用而不修正现有代码。(这个比如也完美的符合后边介绍依靠倒置准则)


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) {
    }
}

: 产品功用

当咱们规划一个产品的时分 里边应该有姓名和价格的特色


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里边的逻辑,这个时分咱们能够经过承继的办法解决。


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表达出这个概念。

iOS-设计原则篇

:付出功用

在上面的比如里边咱们介绍了付出功用应该依靠笼统类,这样能够做到支撑不同的付出办法,这是底层模块应该处理的逻辑,高层模块咱们应该怎样规划呢?

// 界说一个笼统协议
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):界说笼统,即接口或协议,以声明模块之间的合同。高层模块依靠于这些笼统,而不是具体的类。
  • 运用依靠注入:经过依靠注入将依靠联系从高层模块传递到低层模块,而不是在代码中硬编码依靠联系。
iOS-设计原则篇

还没学废,我再来一次

:图形制作功用

咱们需求开发一个图形制作功用,咱们应该怎样规划呢? 首先咱们需求一个draw办法,可是图形形状不相同的,所以界说一个protocol叫Shape,办法叫draw().
第二步 咱们应该创立不同的类,有圆形 有三角形 有 矩形, 这样就做到 他们相互之间不影响。
第三步 咱们要做到 高层模块不依靠底层模块,经过依靠倒置办法

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

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

其实这个准则和依靠倒置准则很相似,目的都是削减耦合,只是依靠倒置准则给了更具体规划办法,比如运用接口的办法和依靠注入降低耦合。

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

  • 当时目标本身
  • 当时目标的实例变量
  • 当时目标的办法参数
  • 当时目标调用的办法内部创立的目标

削减依靠联系: 迪米特准则鼓励削减类之间的依靠联系,防止一个类直接依靠于太多其他类。这能够经过引进中间层或接口来完成,以削减直接依靠。

松耦合性: 遵从迪米特准则的体系一般具有更松散的耦合度,由于模块之间的依靠联系更少。这使得体系更简单扩展、保护和测验。

阻隔改动: 迪米特准则有助于阻隔改动。当一个模块的内部完成发生改动时,只要其直接朋友受到影响,而不会涉及到其他模块。

听上去是不是和依靠倒置的思想根本共同,那怎样更好的表现这个准则,它又来啦

iOS-设计原则篇

:产品功用

上面咱们说到了产品功用,现在咱们丰富的事务逻辑,引进用户和购物车这些内容,那他们联系应该是什么样呢?

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协议的源码看下就有一行注释特色强调这个事情

// 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规划合理吗?


/// 看看UIView源码 遵从了多少个协议
@MainActor open class UIView : UIResponder, NSCoding, UIAppearance, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace, UIFocusItem, UIFocusItemContainer, CALayerDelegate {
/// 特色就更多了 
}

你有没有想过,假如一个不是iOS开发,想简单画一个矩形,然后翻开UIView看到这么多特色,协议,并且UIView的父类UIResponder并不归于制作UI的类,核心担任制作的是CALayer 会不会很蒙?

这只是我个人拙见,并且iOS前史也好久,前史担负也重,苹果也在尽量去修正,接下来 我要介绍的是SwiftUI里边制作一个矩形Rectangle()

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)父类的办法,以满足自己的需求,可是不该该改动原有办法的约好和预期行为。
子类不该该引进新的特色或办法: 子类不该该增加新的特色或办法,这可能会导致客户端代码对父类和子类的依靠不共同。
子类的办法参数不该该比父类办法更严厉: 假如父类的办法承受某种类型的参数,那么子类的办法能够承受相同类型或许更宽松的参数类型,但不该该承受更严厉的参数类型。
子类的返回值类型能够是父类办法返回值类型的子类型: 子类能够扩展父类办法的返回值类型,返回一个子类型,但不该该缩小返回值类型

iOS-设计原则篇
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