【译】设计模式引导--OOP的能力

原文链接 作者:Hitendra Solanki

导读—本博客系列要求具有面向对象编程的中级专业知识。您应该对类、对象、构造函数、继承、值和引用类型有基本的了解。通过仔细地从头到尾阅读本系列文章,不管是中级还是高级开发,您都将有所收获。

设计模式用于表示经验丰富的面向对象软件开发人员社区采用的最佳实践。

建造者模式帮助我们更简单更易读地创建一个类,它遵守着以下两条规则: 1、分割原始类和它的构造方法 2、在最后一个返回类的实例

建造者模式最佳的例子就是SwiftUI,是的你没有看错。SwiftUI中大部分类像是Text,Image都是使用的建造者模式。

问题:

想一下,一个Person类拥有不少于十个属性,当你要使用它时,你需要为它创建一个构造方法。它的构造者将拥有不少于十个参数,去管理这么一个带有很多参数的单一函数或构造方式将是非常困难的,最终你也会让这端代码失去可读性。看下面的例子:

class Person {
  //personal details
  var name: String = ""
  var gender: String = ""
  var birthDate: String = ""
  var birthPlace: String = ""
  var height: String = ""
  var weight: String = ""
  
  //contact details
  var phone: String = ""
  var email: String = ""
  
  //address details
  var streeAddress: String = ""
  var zipCode: String = ""
  var city: String = ""
  
  //work details
  var companyName: String = ""
  var designation: String = ""
  var annualIncome: String = ""
  
  //constructor
  init(name: String,
       gender: String,
       birthDate: String,
       birthPlace: String,
       height: String,
       weight: String,
       phone: String,
       email: String,
       streeAddress: String,
       zipCode: String,
       city: String,
       companyName: String,
       designation: String,
       annualIncome: String) {
    self.name = name
    self.gender = gender
    self.birthDate = birthDate
    self.birthPlace = birthPlace
    self.height = height
    self.weight = weight
    self.phone = phone
    self.email = email
    self.streeAddress = streeAddress
    self.zipCode = zipCode
    self.height = height
    self.city = city
    self.companyName = companyName
    self.designation = designation
    self.annualIncome = annualIncome
  }
}

//This is function in Xcode-Playground which executes our test code
func main() {
  let hitendra = Person(name: "Hitendra Solanki",
                        gender: "Male",
                        birthDate: "2nd Oct 1991",
                        birthPlace: "Gujarat, India",
                        height: "5.9 ft",
                        weight: "85kg",
                        phone: "+91 90333-71772",
                        email: "hitendra.developer@gmail.com",
                        streeAddress: "52nd Godrej Street",
                        zipCode: "380015",
                        city: "Ahmedabad",
                        companyName: "Fortune 500",
                        designation: "Software architect",
                        annualIncome: "45,000 USD")
  
  //use of Person object
  print("\(hitendra.name) works in \(hitendra.companyName) compay as a \(hitendra.designation).")
}

//call main to execute our test code in Xcode-Playground
main()

/* Console output:
Hitendra Solanki works in Fortune 500 compay as a Software architect.
*/

将上面的例子在playground中运行一下,你会得到预期结果。逻辑上这也是对的。

我们可以尝试优化上面的代码,从解决这两个问题入手。 1、我们必须按照既定的顺序传参数,而不能通过重新排列参数提高可读性。 2、即使创建对象时我们不知道一些属性值,我们也不得不传入所有参数。

例如你需要创建一个Person类,但是这个人还在找工作。只有当他进入某一公司我们才能得到他的工作信息。

解决方案:

1、创建相关属性的逻辑分组。 2、为不同分组的属性创建不同的建造者类。 3、在建造者类中最后一步返回实例。

让我们从上面的例子开始,我们已经拥有一个Person类,它含有14个属性。我们仔细观察这14个属性,可以将它分为四组。 1、个人信息 2、联系方式 3、地址信息 4、公司信息

通过强大的设计模式我们可以解决上面两个问题,具体代码如下:

//This is function in playground which executes our test code
func main() {
  
  var hitendra = Person() //person with empty details
  let personBuilder = PersonBuilder(person: hitendra)
  
  hitendra = personBuilder
    .personalInfo
      .nameIs("Hitendra Solanki")
      .genderIs("Male")
      .bornOn("2nd Oct 1991")
      .bornAt("Gujarat, India")
      .havingHeight("5.9 ft")
      .havingWeight("85 kg")
    .contacts
      .hasPhone("+91 90333-71772")
      .hasEmail("hitendra.developer@gmail.com")
    .lives
      .at("52nd Godrej Street")
      .inCity("Ahmedabad")
      .withZipCode("380015")
    .build()
  
  //use of Person object
  print("\(hitendra.name) has contact number \(hitendra.phone) and email \(hitendra.email)")
  
  //later on when we have company details ready for the person
  hitendra = personBuilder
    .works
      .asA("Software architect")
      .inCompany("Fortune 500")
      .hasAnnualEarning("45,000 USD")
    .build()
  
  //use of Person object with update info
  print("\(hitendra.name) works in \(hitendra.companyName) compay as a \(hitendra.designation).")
}

//call main to execute our test code
main()

//Person class which only contains the details
class Person {
  //personal details
  var name: String = ""
  var gender: String = ""
  var birthDate: String = ""
  var birthPlace: String = ""
  var height: String = ""
  var weight: String = ""
  
  //contact details
  var phone: String = ""
  var email: String = ""
  
  //address details
  var streeAddress: String = ""
  var zipCode: String = ""
  var city: String = ""
  
  //work details
  var companyName: String = ""
  var designation: String = ""
  var annualIncome: String = ""
  
  //empty constructor
  init() { }
}

//PersonBuilder class helps to construct the person class instance
class PersonBuilder {
  var person: Person
  init(person: Person){
    self.person = person
  }
  
  //personal details builder switching
  var personalInfo: PersonPersonalDetailsBuilder {
    return PersonPersonalDetailsBuilder(person: self.person)
  }
  
  //contact details builder switching
  var contacts: PersonContactDetailsBuilder {
    return PersonContactDetailsBuilder(person: self.person)
  }
  
  //address details builder switching
  var lives: PersonAddressDetailsBuilder {
    return PersonAddressDetailsBuilder(person: self.person)
  }
  
  //work details builder switching
  var works: PersonCompanyDetailsBuilder {
    return PersonCompanyDetailsBuilder(person: self.person)
  }
  
  func build() -> Person {
    return self.person
  }
}

//PersonPersonalDetailsBuilder: update personal details
class PersonPersonalDetailsBuilder: PersonBuilder {
  func nameIs(_ name: String) -> Self {
    self.person.name = name
    return self
  }
  func genderIs(_ gender: String) -> Self {
    self.person.gender = gender
    return self
  }
  func bornOn(_ birthDate: String) -> Self {
    self.person.birthDate = birthDate
    return self
  }
  func bornAt(_ birthPlace: String) -> Self {
    self.person.birthPlace = birthPlace
    return self
  }
  func havingHeight(_ height: String) -> Self {
    self.person.height = height
    return self
  }
  func havingWeight(_ weight: String) -> Self {
    self.person.weight = weight
    return self
  }
}

//PersonContactDetailsBuilder: update contact details
class PersonContactDetailsBuilder: PersonBuilder {
  func hasPhone(_ phone: String) -> Self {
    self.person.phone = phone
    return self
  }
  func hasEmail(_ email: String) -> Self {
    self.person.email = email
    return self
  }
}

//PersonAddressDetailsBuilder: update address details
class PersonAddressDetailsBuilder: PersonBuilder {
  func at(_ streeAddress: String) -> Self {
    self.person.streeAddress = streeAddress
    return self
  }
  func withZipCode(_ zipCode: String) -> Self {
    self.person.zipCode = zipCode
    return self
  }
  func inCity(_ city: String) -> Self {
    self.person.city = city
    return self
  }
}

//PersonCompanyDetailsBuilder: update company details
class PersonCompanyDetailsBuilder: PersonBuilder {
  func inCompany(_ companyName: String) -> Self {
    self.person.companyName = companyName
    return self
  }
  func asA(_ designation: String) -> Self {
    self.person.designation = designation
    return self
  }
  func hasAnnualEarning(_ annualIncome: String) -> Self {
    self.person.annualIncome = annualIncome
    return self
  }
}

/* Console output:
 
 Hitendra Solanki has contact number +91 90333-71772 and email hitendra.developer@gmail.com
 Hitendra Solanki works in Fortune 500 compay as a Software architect.
 
 */

在上面的例子中,我们把Person类根据职责分割成了几个不同的类。我们创建了多个建造者,他们分别管理相关分组内的属性,而Person只持有这些建造者。

我们拥有一个建造者基类PersonBuilder和四个衍生的建造者类,PersonPersonalDetailsBuilder, PersonContactDetailsBuilder, PersonAddressDetailsBuilderPersonCompanyDetailsBuilder

当其他四个从Personbuilder衍生出来的建造者需要更新相关属性时,Personbuilder这个基类可以帮助我们在它们之间进行转换。

在上面的例子中我们可以看到新的构造方法变得更加易读了,我们可以用一种更加优雅的方式更新一组或者某一个属性。

需要注意一下,上面的例子中我们再每个建造者更新方法之后返回了它自己。这让我们能够在相同的建造者中写出链式方法,而不是分开的多行。这个概念称为流程模式。

优点

1、用一种优雅的方式很容易地初始化一个含很多参数的类。 2、遵从单一职责原则。 3、根据你的情况,以任意的顺序初始化对象和更新属性。