<?php |
class Person |
{ |
private $prefix; |
private $givenName; |
private $familyName; |
private $suffix; |
public function setPrefix($prefix) |
{ |
$this->prefix = $prefix; |
} |
public function getPrefix() |
{ |
return $this->prefix; |
} |
public function setGivenName($gn) |
{ |
$this->givenName = $gn; |
} |
public function getGivenName() |
{ |
return $this->givenName; |
} |
public function setFamilyName($fn) |
{ |
$this->familyName = $fn; |
} |
public function getFamilyName() |
{ |
return $this->familyName; |
} |
public function setSuffix($suffix) |
{ |
$this->suffix = $suffix; |
} |
public function getSuffix() |
{ |
return $suffix; |
} |
} |
$person = new Person(); |
$person->setPrefix("Mr."); |
$person->setGivenName("John"); |
echo($person->getPrefix()); |
echo($person->getGivenName()); |
?> |
|
乍看之下,这段代码可能会完成大量工作,并且实际上可能更多是在前端的工作。但是,通常,使用优秀的 OO 习惯从长远来看十分划算,因为将极大地巩固未来更改。 |
在清单 3 中所示的代码版本中,我已经更改了内部实现以使用名称部件的关联数组。比较理想的情况是,我希望拥有错误处理并且更仔细地检查元素是否存在,但是本例的目的在于展示使用我的类的代码无需更改的程度 — 代码并没有察觉到类发生更改。记住采用 OO 习惯的原因是要谨慎封装更改,这样代码将更具有可扩展性并且更容易维护。 |
|
清单 3. 使用不同内部实现的另一个示例 |
|
| <?php | class Person | { | private $personName = array(); | public function setPrefix($prefix) | { | $this->personName['prefix'] = $prefix; | } | public function getPrefix() | { | return $this->personName['prefix']; | } | public function setGivenName($gn) | { | $this->personName['givenName'] = $gn; | } | public function getGivenName() | { | return $this->personName['givenName']; | } | /* etc... */ | } | /* | * Even though the internal implementation changed, the code here stays exactly | * the same. The change has been encapsulated only to the Person class. | */ | $person = new Person(); | $person->setPrefix("Mr."); | $person->setGivenName("John"); | echo($person->getPrefix()); | echo($person->getGivenName()); | ?> | |
|
做个好邻居 |
在构建类时,它应当正确地处理自己的错误。如果该类不知道如何处理错误,则应当以其调用者理解的格式封装这些错误。此外,避免返回空对象或者状态无效的对象。许多时候,只需通过检验参数并抛出特定异常说明提供参数无效的原因就可以实现这一点。在您养成这个习惯时,它可以帮您 — 和维护代码或使用对象的人员 — 节省很多时间。 |
坏习惯:不处理错误 |
考虑清单 4 中所示的示例,该示例将接受一些参数并返回填充了一些值的Person对象。但是,在parsePersonName()方法中,没有验证提供的$val变量是否为空、是否是零长度字符串或者字符串是否使用无法解析的格式。parsePersonName()方法不返回Person对象,但是返回 null。使用这种方法的管理员或程序员可能会觉得很麻烦 — 至少他们现在需要开始设置断点并调试 PHP 脚本。 |
|
清单 4. 不抛出或处理错误的坏习惯 |
class PersonUtils |
{ |
public static function parsePersonName($format, $val) |
{ |
if (strpos(",", $val) > 0) { |
$person = new Person(); |
$parts = split(",", $val); // Assume the value is last, first |
$person->setGivenName($parts[1]); |
$person->setFamilyName($parts[0]); |
} |
return $person; |
} |
} |
|
清单 4 中的parsePersonName()方法可以修改为在if条件外部初始化Person对象,确保总是获得有效的Person对象。但是,您得到的是没有 set 属性的Person,这仍然没有很好地改善您的困境。 |
好习惯:每个模块都处理自己的错误 |
不要让调用方凭空猜测,而是对参数进行预先验证。如果未设置的变量无法生成有效的结果,请检查变量并抛出InvalidArgumentException。如果字符串不能为空或者必须为特定格式,请检查格式并抛出异常。清单 5 解释了如何在演示一些基本验证的parsePerson()方法中创建异常以及一些新条件。 |
|
清单 5. 抛出错误的好习惯 |
<?php |
class InvalidPersonNameFormatException extends LogicException {} |
class PersonUtils |
{ |
public static function parsePersonName($format, $val) |
{ |
if (! $format) { |
throw new InvalidPersonNameFormatException("Invalid PersonName format."); |
} |
if ((! isset($val)) | | strlen($val) == 0) { |
throw new InvalidArgumentException("Must supply a non-null value to parse."); |
} |
} |
} |
?> |
|
最终目的是希望人们能够使用您的类,而不必了解其中的工作原理。如果他们使用的方法不正确或者不是按照期望的方法使用,也不需要猜测不能工作的原因。作为一个好邻居,您需要知道对您的类进行重用的人并没有特异功能,因此您需要解决猜测的问题。 |
|
避免看到美杜莎 |
在我最初了解 OO 概念时,我十分怀疑接口是否真正有帮助。我的同事给我打了个比方,说不使用接口就好像看到美杜莎的头。在希腊神话中,美杜莎是长着蛇发的女怪。凡是看了她一眼的人都会变成石头。杀死美杜莎的珀尔休斯通过在盾上观察她的影子,避免了变成石头而得以与她对抗。 |
接口就是对付美杜莎的镜子。当您使用一个特定的具体实现时,代码也必须随着实现代码的更改而更改。直接使用实现将限制您的选择,因为您已经在本质上把类变成了 “石头”。 |
坏习惯:不使用接口 |
清单 6 显示了从数据库中装入Person对象的示例。它将获取人员的姓名并返回数据库中匹配的Person对象。 |
|
清单 6. 不使用接口的坏习惯 |
|
| <?php | class DBPersonProvider | { | public function getPerson($givenName, $familyName) | { | /* go to the database, get the person... */ | $person = new Person(); | $person->setPrefix("Mr."); | $person->setGivenName("John"); | return $person; | } | } | /* I need to get person data... */ | $provider = new DBPersonProvider(); | $person = $provider->getPerson("John", "Doe"); | echo($person->getPrefix()); | echo($person->getGivenName()); | ?> | | 在环境发生更改之前,从数据库中装入Person的代码都可以正常运行。例如,从数据库装入Person可能适用于第一个版本的应用程序,但是对于第二个版本,可能需要添加从 Web 服务装入人员的功能。其实,该类已经变成 “石头”,因为它在直接使用实现类并且现在能做的更改十分有限。 |
好习惯:使用接口 |
清单 7 显示了一个代码示例,在实现了加载用户的新方法后并没有进行更改。该示例显示了一个名为PersonProvider的接口,该接口将声明单个方法。如果任何代码使用PersonProvider,代码都禁止直接使用实现类。相反,它就像是一个实际对象一样使用PersonProvider。 |
|
清单 7. 使用接口的好习惯 |
|
| <?php | interface PersonProvider | { | public function getPerson($givenName, $familyName); | } | class DBPersonProvider implements PersonProvider | { | public function getPerson($givenName, $familyName) | { | /* pretend to go to the database, get the person... */ | $person = new Person(); | $person->setPrefix("Mr."); | $person->setGivenName("John"); | return $person; | } | } | class PersonProviderFactory | { | public static function createProvider($type) | { | if ($type == 'database') | { | return new DBPersonProvider(); | } else { | return new NullProvider(); | } | } | } | $config = 'database'; | /* I need to get person data... */ | $provider = PersonProviderFactory::createProvider($config); | $person = $provider->getPerson("John", "Doe"); | echo($person->getPrefix()); | echo($person->getGivenName()); | ?> | | 在使用接口时,尝试避免直接引用实现类。相反,使用对象外部的内容可以提供正确的实现。如果您的类将装入基于某些逻辑的实现,它仍然需要获取所有实现类的定义,并且那样做也无法取得任何效果。 |
您可以使用 Factory 模式来创建实现接口的实现类的实例。根据约定,factory方法将以create为开头并返回接口。它可以为您的factory获取必要的参数以计算出应当返回哪个实现类。 |
在清单 7 中,createProvider()方法只是获取$type。如果$type被设为database,工厂将返回DBPersonProvider的实例。从数据库中装入人员的任何新实现都不要求在使用工厂和接口的类中进行任何更改。DBPersonProvider将实现PersonProvider接口并且拥有getPerson()方法的实际实现。 |
|
利用最弱的链接 |
将模块松散耦合在一起是件好事情;它是允许您封装更改的属性之一。另外两个习惯 — “保持谨慎” 和 “避免看到美杜莎” — 可帮助您构建松散耦合的模块。要实现松散耦合的类,可通过养成降低类依赖关系的习惯实现。 |
坏习惯:紧密耦合 |
在清单 8 中,降低依赖关系并不是必须降低使用对象的客户机的依赖关系。相反,该示例将演示如何降低与正确类的依赖关系并最小化这种依赖关系。 |
|
清单 8.Address中紧密耦合的坏习惯 |
|
| <?php | require_once "./AddressFormatters.php"; | class Address | { | private $addressLine1; | private $addressLine2; | private $city; | private $state; // or province... | private $postalCode; | private $country; | public function setAddressLine1($line1) | { | $this->addressLine1 = $line1; | } | /* accessors, etc... */ | public function getCountry() | { | return $this->country; | } | public function format($type) | { | if ($type == "inline") { | $formatter = new InlineAddressFormatter(); | } else if ($type == "multiline") { | $formatter = new MultilineAddressFormatter(); | } else { | $formatter = new NullAddressFormatter(); | } | return $formatter->format($this->getAddressLine1(), | $this->getAddressLine2(), | $this->getCity(), $this->getState(), $this->getPostalCode(), | $this->getCountry()); | } | } | $addr = new Address(); | $addr->setAddressLine1("123 Any St."); | $addr->setAddressLine2("Ste 200"); | $addr->setCity("Anytown"); | $addr->setState("AY"); | $addr->setPostalCode("55555-0000"); | $addr->setCountry("US"); | echo($addr->format("multiline")); | echo("/n"); | echo($addr->format("inline")); | echo("/n"); | ?> | | 在Address对象上调用format()方法的代码可能看上去很棒 — 这段代码所做的是使用Address类,调用format()并完成。相反,Address类就没那么幸运。它需要了解用于正确格式化的各种格式化方法,这可能使Address对象无法被其他人很好地重用,尤其是在其他人没有兴趣在format()方法中使用格式化方法类的情况下。虽然使用Address的代码没有许多依赖关系,但是Address类却有大量代码,而它可能只是一个简单的数据对象。 |
Address类与知道如何格式化Address对象的实现类紧密耦合。 |
好习惯:在对象之间松散耦合 |
在构建优秀的 OO 设计时,必须考虑称为关注点分离(Separation of Concerns,SoC)的概念。SoC 指尝试通过真正关注的内容分离对象,从而降低耦合度。在最初的Address类中,它必须关注如何进行格式化。这可能不是优秀的设计。然而,Address类应当考虑Address的各部分,而某种格式化方法应当关注如何正确格式化地址。 |
在清单 9 中,格式化地址的代码被移到接口、实现类和工厂中 — 养成 “使用接口” 的习惯。现在,AddressFormatUtils类负责创建格式化方法并格式化Address。任何其他对象现在都可以使用Address而不必担心要求获得格式化方法的定义。 |
|
清单 9. 在对象之间松散耦合的好习惯 |
|
| <?php | interface AddressFormatter | { | public function format($addressLine1, $addressLine2, $city, $state, | $postalCode, $country); | } | class MultiLineAddressFormatter implements AddressFormatter | { | public function format($addressLine1, $addressLine2, $city, $state, | $postalCode, $country) | { | return sprintf("%s/n%s/n%s, %s %s/n%s", | $addressLine1, $addressLine2, $city, $state, $postalCode, $country); | } | } | class InlineAddressFormatter implements AddressFormatter | { | public function format($addressLine1, $addressLine2, $city, $state, | $postalCode, $country) | { | return sprintf("%s %s, %s, %s %s %s", | $addressLine1, $addressLine2, $city, $state, $postalCode, $country); | } | } | class AddressFormatUtils | { | public static function formatAddress($type, $address) | { | $formatter = AddressFormatUtils::createAddressFormatter($type); | return $formatter->format($address->getAddressLine1(), | $address->getAddressLine2(), | $address->getCity(), $address->getState(), | $address->getPostalCode(), | $address->getCountry()); | } | private static function createAddressFormatter($type) | { | if ($type == "inline") { | $formatter = new InlineAddressFormatter(); | } else if ($type == "multiline") { | $formatter = new MultilineAddressFormatter(); | } else { | $formatter = new NullAddressFormatter(); | } | return $formatter; | } | } | $addr = new Address(); | $addr->setAddressLine1("123 Any St."); | $addr->setAddressLine2("Ste 200"); | $addr->setCity("Anytown"); | $addr->setState("AY"); | $addr->setPostalCode("55555-0000"); | $addr->setCountry("US"); | echo(AddressFormatUtils::formatAddress("multiline", $addr)); | echo("/n"); | echo(AddressFormatUtils::formatAddress("inline", $addr)); | echo("/n"); | ?> | | | 当然,缺点是只要使用模式,通常就意味着工件(类、文件)的数量会增加。但是,通过减少每个类中的维护可以弥补这个缺点,甚至在获得正确的可重用性时反而可以减少工件量。 |
|
您是橡皮;我是胶水 |
具有高度内聚力的 OO 设计被集中并组织到相关模块中。了解 “关注点” 对于决定如何紧密地联系函数和类十分重要。 |
坏习惯:降低内聚力 |
当设计的内聚力较低时,它就不能良好地组织类和方法。意大利面条式代码(spaghetti code)一词通常用于描述捆绑在一起并且具有低内聚力的类和方法。清单 10 提供了意大利面条式代码的示例。相对通用的Utils类将使用许多不同对象并且有许多依赖关系。它执行很多操作,因而很难实现重用。 |
|
清单 10. 降低内聚力的坏习惯 |
|
| <?php | class Utils | { | public static function formatAddress($formatType, $address1, | $address2, $city, $state) | { | return "some address string"; | } | public static function formatPersonName($formatType, $givenName, | $familyName) | { | return "some person name"; | } | public static function parseAddress($formatType, $val) | { | // real implementation would set values, etc... | return new Address(); | } | public static function parseTelephoneNumber($formatType, $val) | { | // real implementation would set values, etc... | return new TelephoneNumber(); | } | } | ?> | | 好习惯:利用高内聚力 |
高内聚力指将相互关联的类和方法分组在一起。如果方法和类都具有高度的内聚力,则可以轻松地分解整个组而不影响设计。具有高内聚力的设计将提供降低耦合的机会。清单 11 显示了被较好组织到类中的两个方法。AddressUtils类将包含用于处理Address类的方法,显示了与地址相关的方法之间的高度内聚力。同样地,PersonUtils将包含专门处理Person对象的方法。这两个拥有高度内聚力方法的新类的耦合性都很低,因为可以完全独立地使用。 |
|
清单 11. 高内聚力的好习惯 |
|
| <?php | class AddressUtils | { | public static function formatAddress($formatType, $address1, | $address2, $city, $state) | { | return "some address string"; | } | public static function parseAddress($formatType, $val) | { | // real implementation would set values, etc... | return new Address(); | } | } | class PersonUtils | { | public static function formatPersonName($formatType, $givenName, | $familyName) | { | return "some person name"; | } | public static function parsePersonName($formatType, $val) | { | // real implementation would set values, etc... | return new PersonName(); | } | } | ?> | | |
|
|
限制传播 |
我经常对我所在的软件团队(我在其中担任技术主管或架构师)的成员提起,OO 语言最大的敌人是复制和粘贴操作。当在缺少预先 OO 设计的情况下使用时,没有任何操作会像在类之间复制代码那样具有破坏性。无论何时,如果想将代码从一个类复制到下一个类中,请停下来并考虑如何使用类层次结构利用类似功能或相同功能。在大多数情况下,使用优秀设计后,您将会发现完全没有必要复制代码。 |
坏习惯:不使用类层次结构 |
清单 12 显示了部分类的简单示例。它们从重复的字段和方法开始 — 从长远来看,不利于应用程序作出更改。如果Person类中有缺陷,则Employee类中也很可能有一个缺陷,因为看上去似乎实现是在两个类之间复制的。 |
|
清单 12. 不使用层次结构的坏习惯 |
<?php |
class Person |
{ |
private $givenName; |
private $familyName; |
} |
class Employee |
{ |
private $givenName; |
private $familyName; |
} |
?> |