本節講解的內容
- 封裝
- 繼承
- 多態
- 重載
- 重寫
前言
PHP的面向對象和JAVA的面向對象一樣,都分爲三大特徵,封裝,繼承,多態。這三個特徵把面向對象進行了很多方面的優化。這三大特徵也是在開發面向對象的時候需要考慮的問題。
封裝
在面向對象中什麼是封裝呢?
封裝:把抽象出來的數據和對數據的操作封裝在一起,數據被保護在內部,程序的其他部分只有通過被授權的操作(成員方法),才能對數據進行操作。
上面有提到抽象,也就是把一類事物共有屬性和行爲(方法)提取出來,形成一個模板(類), 這種研究問題的方法,我們稱爲抽象。
就像我們的銀行賬號,不管是誰的賬號,都包括賬號,密碼,同時也有一些共同的方法,取款,存款,查詢餘額。我們用封裝的思想就是:
<?php
class Account{
public $account_no;
private $pwd;
private $balance;
public function __construct($account_no, $pwd = '123456', $balance = 0.0){
$this->account_no = $account_no;
$this->pwd = $pwd;
$this->balance = $balance;
}
//存款
public function deposit($amount, $pwd){
if($pwd == $this->pwd){
echo '<br> 存款成功';
$this->balance += $amount;
}else{
echo '<br> 密碼不正確';
}
}
//取款
public function withdraw($amount, $pwd){
if($pwd == $this->pwd){
if($this->balance >= $amount){
echo '<br> 取款成功';
$this->balance -= $amount;
}else{
echo '<br> 餘額不足..';
}
}else{
echo '<br>密碼錯誤';
}
}
//查詢
public function query($pwd){
if($pwd == $this->pwd){
echo '<br> 賬號' . $this->account_no . ' 餘額=' . $this->balance;
}else{
echo '<br> 查詢密碼錯誤';
}
}
}
$account = new Account(123456789, 'hsp123', 2000000);
$account->deposit(30000, 'hsp123');
$account->query('hsp123');
$account->withdraw(99999900, 'hsp123');
$account->query('hsp123');
上面的代碼就是銀行業務的封裝,通過封裝代碼。我們操作,只需要調用裏面提供的方法就可以了,而不用管理裏面的業務是怎麼處理的。這是一個重要的思想。而我們實現封裝的方法是使用訪問修飾符,利用訪問修飾符的訪問特性,把不讓外部使用的屬性或方法封裝起來。而在上節中我們講過三種訪問修飾符。
那麼我們怎麼訪問protected 和 private 成員屬性呢?這裏有三種方法。
第一種
使用魔術方法__get() 和 __set() 來訪問
<?php
class Cat{
private $name;
private $age;
public function __construct($name,$age){
$this -> name = $name;
$this -> age = $age;
}
public function __set($name,$value){
//判斷類中有沒有這個屬性
if(property_exists($this,$name)){
$this -> $name = $value;
}else{
echo '沒有這個屬性';
}
}
public function __get($name){
return $this -> $name;
}
}
$cat = new Cat('小白',2);
$cat -> name = '小花';
echo $cat -> name;
......結果......
小花
我們通過魔術方法set和get的特性來改變受保護的屬性的值,但是這種方法並不推薦使用。因爲不夠靈活, 不可以對傳入的數據進行校驗。而下面的方法就可以對傳入的數據進行校驗。
第二種
爲每個private或者protected 成員變量提供一對getXxx() 和 setXxx() 的方法。
<?php
class Cat{
private $name;
private $age;
public function __construct($name,$age){
$this -> name = $name;
$this -> age = $age;
}
public function setName($value){
if(is_string($value)){
$this -> name = $value;
}
}
public function getName($password){
if('12345' == $password){
return $this -> name;
}else{
echo '密碼不對';
}
}
}
$cat = new Cat('小白',2);
$cat -> setName('小花');
echo $cat -> getName('12345');
......結果......
小花
使用這種方法可以對傳進去的數據進行判斷。我們推薦使用這種方法。
第三種
使用一個統一的方法,對屬性進行操作。
<?php
class Cat{
private $name;
private $age;
public function __construct($name,$age){
$this -> name = $name;
$this -> age = $age;
}
//顯示對象的信息
public function showInfo(){
echo $this -> name . '的年齡是:' . $this-> age;
}
}
$cat = new Cat('小白',2);
$cat -> showInfo();
......結果......
小白的年齡是:2
這三種方法,我們在開發中第二種和第三種用的比較多,當對單一的屬性操作的時候,可以使用第二種,當對多種屬性進行操作的時候,可以使用第三種。
繼承
在開發中使用繼承的情況太多了,繼承的出現,減少了代碼的冗餘性。是代碼看起來更加的清晰。那麼什麼是繼承呢?
<?php
//定義一個小學生,它有考試的方法,設置成績的方法
class Pupil {
public $name;
public $age;
private $grade;
public function __construct($name, $age){
$this->name = $name;
$this->age = $age;
}
public function showInfo(){
echo '<br> 學生的信息如下:';
echo '<br> 學生的名字是:' . $this->name;
echo '<br> 學生的成績是:' . $this->grade;
}
//設置成績
public function setGrade($grade){
$this->grade = $grade;
}
public function testing(){
echo '<br> 小學生在考試.........';
}
}
//定義一個大學生,它有考試的方法,設置成績的方法
class Graduate {
public $name;
public $age;
private $grade;
public function __construct($name, $age){
$this->name = $name;
$this->age = $age;
}
public function showInfo(){
echo '<br> 學生的信息如下:';
echo '<br> 學生的名字是:' . $this->name;
echo '<br> 學生的成績是:' . $this->grade;
}
//設置成績
public function setGrade($grade){
$this->grade = $grade;
}
public function testing(){
echo '<br> 大學生在考試.....';
}
}
//使用一下
$pupil1 = new Pupil('小明', 40);
$pupil1->testing();
$pupil1->setGrade(100);
$pupil1->showInfo();
echo '<br>';
//使用
$graduate1 = new Graduate('小華', 20);
$graduate1->testing();
$graduate1->setGrade(60);
$graduate1->showInfo();
在上面的代碼中我們可以看到兩個類中都有相同的屬性和方法,如果我們的代碼中有很多這樣的代碼,就會造成代碼的冗餘,更不利於我們的維護。而解決的最好的方法就是使用繼承,從而提高代碼的複用性。
繼承的語法:
class 方法名 extends 父類的方法名{
}
繼承使用關鍵字extends進行繼承的。可以理解爲代碼的複用,讓我們的編程更加靠近人的思維,當多個類存在相同的屬性(變量)和方法時,可以從這些類中抽取出父類,在父類中定義這些相同的屬性和方法,所有的子類不需要重新定義這些屬性和方法,只需要繼承父類就行了
<?php
//使用繼承來完成
class Student{
public $name;
public $age;
private $grade;
public function __construct($name, $age){
$this->name = $name;
$this->age = $age;
}
public function showInfo(){
echo '<br> 學生的信息如下:';
echo '<br> 學生的名字是:' . $this->name;
echo '<br> 學生的成績是:' . $this->grade;
}
//設置成績
public function setGrade($grade){
$this->grade = $grade;
}
}
//這裏 extends 關鍵字就是表示 Pupil 類 繼承了 Student類
class Pupil extends Student{
public function testing(){
echo '<br> 小學生在考試.........';
}
}
class Graduate extends Student{
public function testing(){
echo '<br> 大學生在考試.........';
}
}
$pupil1 = new Pupil('小明', 40);
$pupil1->testing();
$pupil1->setGrade(100);
$pupil1->showInfo();
echo '<br>';
$graduate1 = new Graduate('小華', 20);
$graduate1->testing();
$graduate1->setGrade(60);
$graduate1->showInfo();
可以看到上面的代碼中把一樣的屬性和方法都出去到Student類(父類)中,我們再寫兩個類來通過extends這個關鍵字來繼承父類。
用來繼承的類稱爲父類,而繼承這個類的類稱之爲子類。
只要通過繼承,我們就能使用父類裏面的屬性和方法。但是如果父類的屬性或者方法被private修飾,則子類不能夠繼承,這就是protected和private的區別。
子類通過繼承得到父類的屬性和方法,那麼是不是就以爲着把父類的代碼賦值了一份拷貝到子類呢?其實並不是的。繼承不是簡單把父類的屬性和方法定義拷貝一份到子類,而是建立了一個查找的關係。而我們再訪問時,這種查找關係是:
- 當某個對象去操作屬性和方法時,首先在本類去看有沒有對應的屬性和方法, 如果有,就判斷是否有訪問權限,如果可以訪問則訪問,如果不可以訪問就報錯
- 當某個對象去操作屬性和方法時,首先在本類去看有沒有對應的屬性和方法, 如果沒有, 就會去查找自己的父類, 父類有,再判斷是否可以訪問,可以訪問就訪問,不可以訪問就報錯。
- 這個查找的邏輯到頂級類。。。。。
在繼承中還是有很多需要注意的地方:
- 子類最多隻能繼承一個父類(指直接繼承),這和c++是不一樣的。
- 子類可以繼承其父類(或者基類)的 public ,protected修飾的變量(屬性) 和 函數(方法)
在創建某個子類對象時,默認情況下會自動調用其父類的構造函數(指在子類沒有自定義構造函數情況時)
<?php class A{ public function __construct(){ echo '父類的構造函數'; } } class B extends A{ } //子類沒有定義構造函數,就會執行父類的構造函數。 $b = new B(); .....結果...... 父類的構造函數
如果在子類中需要訪問其父類的方法(構造方法/成員方法 方法的訪問修飾符是public/protected),可以使用父類::方法名(或者 parent::方法名 ) 來完成
<?php class A{ public function __construct(){ echo '父類的構造函數<br>'; } } class B extends A{ //子類定義了自己的構造函數,不會調用父類的構造函數。 public function __construct(){ parent::__construct(); echo '子類的構造函數<br>'; } } //子類沒有定義構造函數,就會執行父類的構造函數。 $b = new B(); .....結果...... 父類的構造函數 子類的構造函數
- 如果子類中的方法和父類方法相同,我們稱爲方法重寫。
多態
多態通俗的講就是多種形態,就是指在面向對象中,對象在不同情況下的多種狀態(根據使用的上下文)PHP天生就是多態語言,同時PHP可以根據傳入的對象類型不同,調用對應對象的方法
在PHP中變量的定義不像其他語言在變量名前面定義類型,而是直接用$符號來定義,可以接受任意類型的值。這就爲多態創造了條件。根據傳入的對象類型不同,調用對應對象的方法,我們也可以使用類型約束來對傳入的值進行約束。
類型約束
類型約束的基本概念是 PHP 5 可以使用類型約束。函數的參數可以指定必須爲對象(在函數原型裏面指定類的名字),接口,數組(PHP 5.1 起)或者 callable(PHP 5.4 起)。如前PHP只支持這幾種。
<?php
class MyClass
{
//傳進的參數約束爲cat或者cat的子類
public function test (Cat $cat ){
echo '對象<br>';
}
//參數是數組
public function test_array (array $arr ) {
print_r($arr);
}
}
class Cat {
}
$cat = new Cat();
$myClass = new MyClass();
$arr = array(1,2,4,5);
$myClass -> test($cat);
$myClass -> test_array($arr);
.....結果......
對象
Array ( [0] => 1 [1] => 2 [2] => 4 [3] => 5 )
如果要進行類型約束,在參數前面寫上類型就行了,可以試一下傳入的不是約束的類型,會報一個致命的錯誤。
多態的示例:
<?php
//多態的案例
//定義動物的父類
class Anmial{
public $name;
public function __construct($name){
$this->name = $name;
}
}
//貓類
class Cat extends Anmial{
public function showInfo(){
echo '<br> 貓貓名字是' . $this->name;
}
}
//狗類
class Dog extends Anmial{
public function showInfo(){
echo '<br> 狗名字是' . $this->name;
}
}
//猴子類
class Monkey extends Anmial{
public function showInfo(){
echo '<br> 猴子名字是' . $this->name;
}
}
//定義事物
class Food{
public $name;
public function __construct($name){
$this->name = $name;
}
}
class Fish extends Food{
public function showInfo(){
echo '<br> ' . $this->name;
}
}
class Bone extends Food{
public function showInfo(){
echo '<br> ' . $this->name;
}
}
class Peach extends Food{
public function showInfo(){
echo '<br>' . $this->name;
}
}
//主人類
class Master{
public $name;
public function __construct($name){
$this->name = $name;
}
//主人可以餵食
//當我們的類型約束是父類時,可以接受 他的子類的對象實例
public function feed(Anmial $aniaml, Food $food){
echo '<br> 主人 ' . $this->name;
$aniaml->showInfo(); //使用多態,根據傳入的值不同,調用不同的方法。
echo '<br> 喂得食物是';
$food->showInfo();
}
}
$dog = new Dog('黃狗');
$cat = new Cat('花貓');
$monkey = new Monkey('猴');
$fish = new Fish('鯊魚');
$bone = new Bone('骨頭');
$peach = new Peach('桃子');
$master = new Master('小明');
$master->feed($dog, $bone);
echo '<br><br>';
$master->feed($cat, $fish);
echo '<br><br>';
$master->feed($monkey, $peach);
上面的代碼在主人的餵食方法中,使用類型約束,根據傳進去的對象不同,調用不同對象的方法。
重載
方法的重載
重載:簡單說,就是函數或者方法有相同的名稱,但是參數列表不相同的情形,這樣的同名不同參數的函數或者方法之間,互相稱之爲重載函數或者方法。
上面的關於方法的重載可以理解爲在一個類中,有兩個方法名一樣的函數,但是函數的參數是不一樣的,我們在調用的時候,系統會根據我們傳入的參數的不同,而自動的調用不同的函數,這就是重載,但是在PHP中一個類中不能有兩個方法名相同的函數,儘管你的參數不同,它會報一個Cannot redeclare的錯誤。那麼在PHP中就不能重載了嗎?其實可以的,利用魔術方法。
在上節中介紹魔術方法中有一個方法是當我們訪問不可訪問或不存在的方法時,系統自動調用的,也就是__call()方法。PHP中可以利用這個魔術方法進行重載
<?php
class Calculate{
//定義兩個方法,計算加法,注意兩個方法的方法名是不一樣的
private function add($a,$b,$c){
return $a + $b + $c;
}
private function add1($a,$b){
return $a + $b;
}
public function __call($name,$val_arr){
if($name == 'add'){
//得到數組裏面的參數,確定幾個參數
$num = count($val_arr);
if($num == 2){
return $this -> add1($val_arr[0],$val_arr[1]);
}else if($num == 3){
return $this -> add($val_arr[0],$val_arr[1],$val_arr[2]);
}
}
}
}
$calculate = new Calculate();
echo $calculate -> add(1,2);
echo '<br>';
echo $calculate -> add(1,2,3);
.....結果......
3
6
看到代碼有沒有被欺騙的感覺-_-,先把類中的兩個方法設置成private,這樣在類外就訪問不到,當我們在類外訪問add方法的時候,會調用魔術方法,然後通過傳入的數組的個數,確定參數的個數,從而在魔術方法中去掉用合適的方法。這就是PHP的方法重載。
屬性的重載
在PHP面向對象編程中,當你去給一個不存在的屬性賦值時,PHP默認會’動態的’, 給你創建一個對應的屬性,這個稱爲屬性重載。
<?php
class A{
//在類中只定義一個變量
public $name = '小明';
}
$a = new A();
echo '<pre>';
var_dump($a);
$a -> age = 12;
var_dump($a);
.....結果......
object(A)#1 (1) {
["name"]=>
string(6) "小明"
}
object(A)#1 (2) {
["name"]=>
string(6) "小明"
["age"]=>
int(12)
}
從結果中可以看到,當我們給一個不存在屬性age賦值後,在輸出的類結構中出現了age這個屬性,這就是屬性的重載。
如果不想讓類的屬性自動增加,可以使用魔術方法__set()和__get()方法進行控制。
重寫
方法的重寫
在上面我們介紹了面向對象的繼承機制。而重寫是基於繼承才引發出來的概念。當一個類繼承了另外一個類後,在父類中有一個方法,子類繼承,但是在子類中覺得父類的方法不能滿足要求,需要子類在重寫定義一個和父類的方法一樣的方法進行重新的定義,稱爲重寫。說白了就子類有一個方法,和父類(基類)的某個方法的名稱、參數個數一樣。
<?php
class Animal{
//動物都有吃這個行爲,具體的要看是什麼動物
public function eat(){
echo '吃飯<br>';
}
}
class Cat extends Animal{
//對父類的方法進行重寫
public function eat(){
echo '貓在吃飯<br>';
}
}
$cat = new Cat();
$cat -> eat();
.....結果......
貓在吃飯
如果父類的方法中使用了類型約束,那麼子類的類型約束也必須一樣。
在方法的重寫中:
- 子類的方法的參數個數 ,方法名稱,要和父類方法的參數個數,方法名稱一樣
- 子類方法不能縮小父類方法的訪問權限(可以大於可以等於)
注意:如果父類的方法名是private,則子類並不會進行重寫。
屬性的重寫
對於屬性的重寫也是,public 和 protected 可以被重寫,private 的屬性不能被重寫
<?php
class Animal{
public $name = '小花';
protected $age = 12;
private $sex = '雄';
}
class Cat extends Animal{
public $name = '小白';
protected $age = 4;
private $sex = '雄';
}
$cat = new Cat();
echo '<pre>';
var_dump($cat);
......結果......
object(Cat)#1 (4) {
["name"]=>
string(6) "小白"
["age":protected]=>
int(4)
["sex":"Cat":private]=>
string(3) "雄"
["sex":"Animal":private]=>
string(3) "雄"
}
可以看到name和age被重寫了,private不能被重寫。
總結
面向對象中,封裝是一個很重要的思想,封裝的實現,可以降低代碼的耦合度,同時繼承的時候減少代碼的冗餘度,php的獨特語言結構讓多態也散發着光芒,而重寫的掌握,讓我們對繼承有了更深層次的瞭解。(ps:今天10.1號,祖國的生日,我要去給祖國母親慶生去了)