标签归档:思考PHP语言

思考PHP之五:访问控制

面向对象三大特性:封装性、继承性和多态性。 封装隐藏了对象内部的细节和实现, 使对象能够集中而完整的描述并对应一个具体的事物。 它使对象只提供对外的访问接口,这样可以在不改变接口的前提下改变实现细节,而且能使对象自我完备。 除此之外,封装还可以增强安全性和简化编程。 继承的目的是为了实现代码复用,它是一种一般化与特殊化的关系,其中子类是父类的细化。 在实现继承时最需要考虑的问题是子类和父类是不是”IS-A”的关系。

PHP(其它面向对象的语言也类似)对于封装和继承的一些特性是通过访问控制实现。访问控制的作用是控制成员变量,成员方法和基类。 曾经一直以为访问控制的作用仅仅是控制一个类的成员方法和成员变量,这把自己的思维局限于一类一对象了, 这两个方面的控制是PHP对面向对象中封装特性的支持。 把思维拉升到面向对象的体系之上,访问控制也控制了基类(或父类)的行为,或者说控制了继承特性的某些方面。

PHP中关于访问控制的关键字和Java等其它面向对象语言一样,如下:

  • public 所定义的类成员可以在任何地方被访问
  • protected 所定义的类成员则可以被其所在类、其所在类的子类和父类访问
  • private 定义的类成员则只能被其所在类访问。

以上的类成员包括成员变量和成员函数。不管是成员变量还是成员方法,PHP默认都是public。 在Java中访问控制默认为包可见,在C++中访问控制默认为私有(private),而PHP则是公有的(public),这比Java还要open。 笔者认为这是PHP的一个历史遗留问题。如果可以重新设计PHP,可能是另一个结果,并且这也是语言的对于访问的态度问题。

前面介绍的各个访问控制是针对封装性,对于继承性,如下:

  • public/protected 可以被继承
  • private 没有被继承

实际上,在PHP中,私有方法也会被继承下来,只是其上下文没有改变(还是父类),从而在调用的时候出错。

一般来说,private定义的成员只能被内部调用,仅供当前类使用,这在PHP的源码中检查访问权限控制时, 以private的成员会检查是否属于当前类体现。public定义的成员则属于类或对象的外部接口, 声明的public成员最好是定义好后就不要再变更,这会影响到调用了类的这些方法的相关客户。 好的public和private的设计对于对象本身的自我完备的实际有重大的意义。

但是public关键字有一些二义性。对于封装性,它是公有的,任何地方都可以访问的成员;对于继承性, 它允许子类继承此成员。同时兼顾这两个特性,当我们把它作为一个接口提供给外部使用时就会有一些歧义: 子类可以覆盖该成员方法,同时也可以调用访方法,如果子类覆盖了该成员方法并调用了该方法, 则它的实现就和你当初作为接口提供给外部时的含义有一些不同了。和public一样,protected也有类似的问题。 可以思考一下:各语言这样实现的目的是什么?是否有更好的方案?

思考PHP语言四:接口和抽象类

思考PHP语言四:接口和抽象类
【概述】
在写PHP的日子里,我们多是按照需求完成相关功能,对于一些设计的工作较少的接触,也许是PHP的历史遗留问题或者其它,对于PHP的接口与抽象类使用得较少,但是对于一个支持面向对象的语言,接口与抽象是两个非常重要的内容。这里我们介绍接口与抽象类的一些基础知识,思考一些关于面向对象的东西。
【接口】
接口是一些方法特征的集合,这里的方法没有实现,只有声明。如果一个类继承了某个接口,则需要实现这个接口的所有方法。接口除了声明方法外,还可以定义常量。如下所示:

1
2
3
4
interface IFoo {
	const CONST_VAR = 'martin';
	public function method($str);
}

接口中方法的声明需要包含方法的名称,参数。不用包含参数类型和返还类型(PHP的函数本来就不用定义参数类型和返还类型)。
接口的方法只能是public,这说明接口本身包含的对外开放的意义在访问控制中体现出来了,并且当我们实现接口的方法时,也不能修改方法的访问控制权限。

接口是可以继承的,接口可以继承接口(和类继承一样,使用extends关键字),类可以继承接口(我们称之为接口继承,用implements关键字)

实现同一个接口的两个类可能功能完全不同,但是他们有相同的方法以及方法参数,并且都是公开的,从而他们可以提供相类似的服务,从而具有相同的接口。

接口的可插入性
在一个拥有多个层次结构的类组织中,如果要给某个处于中间位置的类添加父类,如果我们没有接口,则我们需要修改这个类的所有父类,打乱整个类的层次结构。当有了接口后,任何一个类都可以实现一个接口,此时,接口不会影响父类,但是会影响所有的子类,此类将必须实现这个接口的所有方法(抽象类可以不用实现所有方法,只是将实现下移到子类),子类则可以自动从此类继承实现后的方法。这就是接口的可插入性。

接口通常被用来声明一个新的类型,并且会作为一个类组织结构的起点,特别是当某个实体属于多个类型时,此时接口的作用就体现出来了。另外,理想情况下(现实中没发现过)一个类只应该实现接口或抽象类所声明的方法,纯理想呵。

【抽象类】
抽象类是类的一种,通过在类定义前添加abstract关键字实现。如下示例:

1
2
3
abstract class AbstarctFoo {
	abstract public function method();
}

抽象类不能实例化,一般作为类组织结构中枝节点或根节点存在。
抽象类提供一个类型的部分实现,抽象类可以有实例变量,可以有构造方法,可以有抽象方法,同时也可以有具体的方法实现。抽象类的构造方法可以被子类调用,只是需要显式调用。
我们在做设计时需要注意的是一般不要从具体类继承,具体类是用来实例化的。
在类的组织结构中,一般抽象类是枝节点,具体类是叶节点,在设计结构时应该尽量将公共代码放到作为父类的抽象类中,这样可以提高代码的复用性,与公共代码的移动相反,数据应该尽量放到具体类。
在继承过程中,子类的责任是扩展父类,而不是置换或取消掉父类的职责,当有取消或置换父类的职责的情况发生时,此时可能你的设计有问题了,需要考虑他们是否是is-a的关系?
抽象类和接口有相同的方法时
在PHP中,如果一个类继承了一个抽象类并且实现了一个接口,如果此时这个抽象类和接口中有相同名称的方法,则此时对于接口的实现会报错。与此相同,当一个抽象类实现一个接口时,如果接口已经声明了方法A,则在抽象方法中将不能再次声明此方法。

思考PHP语言三:异常处理

思考PHP语言三:异常处理
【概述】
异常处理是指在语言中能够使程序按照一种标准的方法对于某些运行时错误和其他程序所检测到的异常事件做出反应。异常发生的时间是不可以确定的,如果一种语言不包括异常处理机制,这就会给语言带来额外的复杂性。
一般来说,对于异常的处理有三种方案:
1、将一个额外的参数作为状态变量,通过标记这个状态值判断是否发生了异常。在C的标准库中许多函数使用返回值作为出错的状态变量。
2、将一个标号参数传给子程序,当异常发生时,通过标号将程序调用跳转到程序中的不同位置
3、将一个异常处理独立出来,作为专门的子程序或类存在。

在基于C的语言中,将异常出现称之为thrown,而不是raise,是因为在标准的C程序库中已经存在一个叫做raise的函数。
在PHP5中使用第3种方式。
【PHP中的异常处理】
在PHP5以后,PHP添加了异常处理模块。PHP的异常处理模块与java有一些类似,异常可被 throw 语句抛出并被 catch 语句捕获。需要进行异常处理的代码都必须放入 try 代码块内,以便捕获可能存在的异常。每一个 try 至少要有一个与之对应的 catch。使用多个 catch 可以捕获不同的类所产生的异常(嗯,这里没有throws子句)。当 try 代码块不再抛出异常或者找不到 catch 能匹配所抛出的异常时,PHP 代码就会在跳转到最后一个 catch 的后面继续执行。所以在安排catch的顺序时,需要将详细或子类的异常类放到前面。在有一些情况下,不管try子句是否抛出异常,也不管是否捕获了异常,程序都要执行一个过程,此时我们就需要finally子句,只是在PHP中并没有finally子句(HOHO)。
一个异常的简单示例:

1
2
3
4
5
try {
	throw new Exception("error msg");
}catch(Exception $e) {
	 echo 'Caught exception: ',  $e->getMessage(), "\n";
}

PHP提供了Exception类,以及在SPL中提供了一些内置的异常类,我们可以通过继承Exception类而实现属于自己的异常类。在一个面向对象系统的设计中我们需要考虑关于错误的处理情况,是以PHP的内部错误报告,还是以异常的方式给出,此时我们需要规划一套专属于我们系统的异常子系统。并且对于PHP的内部错误报告我们可以使用ErrorException进行转换,如下所示帮助文档中的代码:

1
2
3
4
5
6
7
function exception_error_handler($errno, $errstr, $errfile, $errline ) {
    throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
}
set_error_handler("exception_error_handler");
 
/* Trigger exception */
strpos();

【断言】
断言常常被用于防错性的程序设计。在一个程序中可以有多条断言,以便确保程序计算的正确性。
PHP提供了assert函数,当执行到此函数时,如果条件为真,则什么都不执行,如果条件为假,则警告并中断程序: Warning: assert() [function.assert]: Assertion failed
经常我们会用echo或die来调试我们的程序,在调试完后我们需要删除相关调试代码。使用assert语句可以在出现问题时将程序调用,而不需要在后面将这些断言删除,这样可以节省删除操作并且在以后的维护过程中也可以重用这些语句,这里就有点测试的感觉了。
当然,我们也可以将断言和异常一起使用,如下所示代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 
/**
 * 断言回调函数,抛出异常
 */
function assert_callcack() {
    throw new Exception("msg");
}
 
// Set our assert options
assert_options(ASSERT_ACTIVE,   true);
assert_options(ASSERT_BAIL,     true);
assert_options(ASSERT_WARNING,  true); // 必须为true
assert_options(ASSERT_CALLBACK, 'assert_callcack');
 
/* 触发一个断言*/
assert(0);
 
echo 'Never reached';

断言和异常的结合处在于断言的回调函数,只是需要注意的是:assert_options(ASSERT_WARNING, true);
因为断言的错误级别为warning。