SOLID 准则简介

SOLID 准则是五个面向目标规划的根本准则,旨在协助开发者构建易于办理和扩展的系统。详细包含:

  1. 单一责任准则(SRP) :一个类,一个责任。
  2. 开放关闭准则(OCP) :对扩展开放,对修正关闭。
  3. 里氏替换准则(LSP) :子类可替代基类。
  4. 接口隔离准则(ISP) :最小接口,防止不必要依靠。
  5. 依靠倒置准则(DIP) :依靠抽象,不依靠详细。

Swift 编程语言中也适用这些准则,遵从这些准则,Swift 开发者能够规划出愈加灵敏、易于维护和扩展的应用程序。

里氏替换准则

氏替换准则强调子类目标应该能够替换其超类目标被运用,而不损坏程序的正确性。换句话说,程序中的目标应该能够在不改动程序希望行为的情况下,被它们的子类所替换。

示例1: 恪守LSP的规划

错误代码

有一个Shape父类,Shape类是抽象类,两个子类SquareShapeCircleShape

class Shape { }
​
class SquareShape: Shape {
 func drawSquare() { }
}
​
class CircleShape: Shape {
 func drawCircle() { }
}
​
func draw(shape: Shape) {
 if let square = shape as? SquareShape {
   square.drawSquare()
 } else if let circle = shape as? CircleShape {
   circle.drawCircle()
 }
}

另外,还有一个draw(shape:)办法,参数为Shape类型。在该办法中,尝试将行参转变为子类:

func draw(shape: Shape) {
 if let square = shape as? SquareShape {
   square.drawSquare()
 } else if let circle = shape as? CircleShape {
   circle.drawCircle()
 }
}
​
let square: Shape = SquareShape()
draw(shape: square)
  • 入参为子类,则制作图形。
  • 入参为父类,则不做处理。

这个示例不只违反了 里氏替换 准则,也违反了 开闭准则。 假如要添加 Triangle ,就需要添加 if-case 句子,以便能够制作。

优化后代码

创建一个协议,协议声明一个公共办法draw(),让SquareShapeCircleShape类恪守协议,这样子类与父类就不会有不同的行为,从而恪守了里氏替换准则。

protocol Shape {
 func draw()
}
​
class SquareShape: Shape {
 func draw() {
   // draw the square
 }
}
​
class CircleShape: Shape {
 func draw() {
   // draw the circle
 }
}
​
func draw(shape: Shape) {
 shape.draw()
}
​

这样也能够让draw(shape:)办法对于修正关闭。当添加了新图案类型,其有必要恪守Shape协议,从而完成draw()办法。

public class TriangleShape: Shape {
 public func draw() {
   // draw the triangle
 }
}

示例2:防止违反LSP的规划

Rectangle类有两个特点 widthheight,一个计算面积的办法 area()Square子类重写了特点set办法,以便设置一个边长的时分另一个边也同样的长度,即满足正方形四边等长。

错误代码

class Rectangle {
 var width: Int
 var height: Int

 init(width: Int, height: Int) {
   self.width = width
   self.height = height
 }

 func area() -> Int {
   return width * height
 }
}
​
class Square: Rectangle {
 override var width: Int {
   didSet {
     super.height = width
   }
 }

 override var height: Int {
   didSet {
     super.width = height
   }
 }
}

运用上述类的场景如下,值应该是25 还是 35?

let square = Square(width: 10, height: 10)
let rectangle: Rectangle = square
​
rectangle.height = 7
rectangle.width = 5
​
print(rectangle.area())

设置rectangle目标高为7、宽为5,由于咱们不知道其真实类型为Square,这儿预期面积为7*5 = 35。但运行得到面积为25。这儿就违反了里氏替换准则,由于Square子类的行为与Rectangle父类不共同。

另一个损坏里氏替换准则会来带问题的场景是开发、运用framework。当运用framework时,咱们无需、也不想了解其私有结构。当运用其公开结构时,应有共同的行为表现,而不依靠对其私有结构的了解。

优化后代码

为了恪守LSP,咱们能够运用组合而非承继,创建一个协议Geometrics,让不同的实体有相同的行为。

protocol Geometrics {
 func area() -> Int
}
​
public class Rectangle {
 public var width: Int
 public var height: Int
 
 public init(width: Int, height: Int) {
   self.width = width
   self.height = height
 }
}
​
extension Rectangle: Geometrics {
 public func area() -> Int {
   return width * height
 }
}
​
public class Square {
 public var edge: Int
 
 public init(edge: Int) {
   self.edge = edge
 }
}
​
extension Square: Geometrics {
 public func area() -> Int {
   return edge * edge
 }
}

通常情况下,优先运用组合(composition)而非承继(inheritance),能够处理违反里氏替换准则问题。创建一个协议,让不同的实体有相同的行为,不会调用到不该运用的特点或办法。

let rectangle: Geometrics = Rectangle(width: 10, height: 10)
print(rectangle.area())
​
let rectangle2: Geometrics = Square(edge: 5)
print(rectangle2.area())

示例3:反常处理的补全

在这个比如中,EncryptedFileReader承继自FileReader偏重写了read办法。

enum FileError: Error {
    case fileNotFound
    case unauthorizedAccess
    case corruptedData
}
class FileReader {
    func read(from path: String) throws -> String {
        // 模仿读取文件,假定文件总是存在
        return "File content"
    }
}
class EncryptedFileReader: FileReader {
    override func read(from path: String) throws -> String {
        // 在读取之前进行解密处理
        // 假定在某些情况下,数据可能被识别为损坏
        let decrypted = "Decrypted file content"
        let dataCorrupted = false // 经过某种逻辑确定
        if dataCorrupted {
            throw FileError.corruptedData
        }
        return decrypted
    }
}

引进新反常类型,如EncryptedFileReader中的FileError.corruptedData,契合里氏替换准则(LSP)的精神,但这需要在完成和文档化方面进行仔细的办理。里氏替换准则要求子类目标能够替换父类目标,而不改动程序的正确性。这不只仅涉及到接口的共同性,也包含行为的兼容性——即子类在承继和扩展父类功用时,不该该损坏原有的契约和预期行为。

反常类型的引进与里氏替换准则

引进EncryptedFileReader的新反常类型是否契合里氏替换准则(LSP)取决于三个关键方面:

  1. 反常处理的兼容性:新反常应与父类FileReader的反常处理逻辑兼容,即使它是特殊的反常类型。假如它不迫使调用者改动错误处理战略,这种规划恪守了LSP。例如,corruptedData反常假如能够在现有错误处理框架内被处理,便是兼容的。
  2. 预期行为的坚持:新反常不该改动办法的预期行为。EncryptedFileReader运用者应能透明处理新反常,如同处理FileReader的其他反常相同,不改动根本的反常处理结构。
  3. 透明性和文档:透明性是LSP的核心,引进新反常时,应经过文档让运用者了解新反常的类型和处理办法。假如EncryptedFileReader文档明晰说明了这些,协助运用者正确处理反常,则遵从了LSP。

假如EncryptedFileReader在这些方面做到了兼容性、坚持预期行为和透明性,其规划就契合LSP。这样的规划增强了代码的可维护性、可扩展性和健壮性,保证了软件组件间依靠联系的稳定性,防止了因扩展或修正导致的问题。

总结

完成里氏替换,开发者应当遵从以下辅导准则:

  • 坚持接口共同性:子类应坚持与基类相同的办法签名,保证在替换时,接口的运用办法坚持不变。
  • 保证成果的共同:子类办法履行的成果应与基类办法兼容,防止修正了基类预期的行为或输出格局。
  • 防止子类中的强制类型转换:子类办法中防止运用对基类行为的强制类型转换,这可能会导致运行时错误。

遵从LSP能够带来以下优点:

  • 提高代码的可复用性:经过承继和多态,能够运用通用的超类编写算法,然后经过不同的子类来扩展算法的行为,无需修正原有代码。
  • 添加代码的可维护性:当子类替换超类时,不需修正代码,减少了因扩展和修正引进bug的危险。
  • 提高代码的健壮性:保证了基类和子类之间的行为共同性,有助于防止运行时的错误。