PHP代碼簡(jiǎn)潔之道——SOLID原則
SOLID 是Michael Feathers推薦的便于記憶的首字母簡(jiǎn)寫(xiě),它代表了Robert Martin命名的最重要的五個(gè)面對(duì)對(duì)象編碼設(shè)計(jì)原則:
- S: 單一職責(zé)原則 (SRP)
- O: 開(kāi)閉原則 (OCP)
- L: 里氏替換原則 (LSP)
- I: 接口隔離原則 (ISP)
- D: 依賴(lài)反轉(zhuǎn)原則 (DIP)
單一職責(zé)原則 Single Responsibility Principle (SRP)
"修改一個(gè)類(lèi)應(yīng)該只為一個(gè)理由"。人們總是易于用一堆方法塞滿(mǎn)一個(gè)類(lèi),如同我們?cè)陲w機(jī)上只能攜帶一個(gè)行李箱(把所有的東西都塞到箱子里)。這樣做的問(wèn)題是:從概念上這樣的類(lèi)不是高內(nèi)聚的,并且留下了很多理由去修改它。將你需要修改類(lèi)的次數(shù)降低到最小很重要。這是因?yàn)?,?dāng)有很多方法在類(lèi)中時(shí),修改其中一處,你很難知曉在代碼庫(kù)中哪些依賴(lài)的模塊會(huì)被影響到。
Bad:
- class UserSettings{
- private $user;
- public function __construct($user)
- {
- $this->user = $user;
- }
- public function changeSettings($settings)
- {
- if ($this->verifyCredentials()) {
- // ...
- }
- }
- private function verifyCredentials()
- {
- // ...
- }
- }
Good:
- class UserAuth {
- private $user;
- public function __construct($user){
- $this->user = $user;
- }
- public function verifyCredentials(){
- // ...
- }
- }
- class UserSettings {
- private $user;
- private $auth;
- public function __construct($user) {
- $this->user = $user;
- $this->auth = new UserAuth($user);
- }
- public function changeSettings($settings){
- if ($this->auth->verifyCredentials()) {
- // ...
- }
- }
- }
開(kāi)閉原則 Open/Closed Principle (OCP)
正如Bertrand Meyer所述,"軟件的實(shí)體(類(lèi), 模塊, 函數(shù),等)應(yīng)該對(duì)擴(kuò)展開(kāi)放,對(duì)修改關(guān)閉。"這個(gè)原則是在說(shuō)明應(yīng)該允許用戶(hù)在不改變已有代碼的情況下增加新的功能。
Bad:
- abstract class Adapter{
- protected $name;
- public function getName(){
- return $this->name;
- }
- }
- class AjaxAdapter extends Adapter{
- public function __construct(){
- parent::__construct();
- $this->name = 'ajaxAdapter';
- }
- }
- class NodeAdapter extends Adapter{
- public function __construct(){
- parent::__construct();
- $this->name = 'nodeAdapter';
- }
- }
- class HttpRequester{
- private $adapter;
- public function __construct($adapter)
- {
- $this->adapter = $adapter;
- }
- public function fetch($url)
- {
- $adapterName = $this->adapter->getName();
- if ($adapterName === 'ajaxAdapter') {
- return $this->makeAjaxCall($url);
- }
- elseif ($adapterName === 'httpNodeAdapter') {
- return $this->makeHttpCall($url);
- }
- }
- private function makeAjaxCall($url)
- { // request and return promise
- }
- private function makeHttpCall($url)
- { // request and return promise
- }
- }
在上面的代碼中,對(duì)于HttpRequester類(lèi)中的fetch方法,如果我新增了一個(gè)新的xxxAdapter類(lèi)并且要在fetch方法中用到的話(huà),就需要在HttpRequester類(lèi)中去修改類(lèi)(如加上一個(gè)elseif 判斷),而通過(guò)下面的代碼,就可很好的解決這個(gè)問(wèn)題。下面代碼很好的說(shuō)明了如何在不改變?cè)写a的情況下增加新功能。
Good:
- interface Adapter{
- public function request($url);
- }
- class AjaxAdapter implements Adapter{
- public function request($url)
- { // request and return promise
- }
- }
- class NodeAdapter implements Adapter{
- public function request($url)
- { // request and return promise
- }
- }
- class HttpRequester{
- private $adapter;
- public function __construct(Adapter $adapter)
- { $this->adapter = $adapter;
- }
- public function fetch($url)
- { return $this->adapter->request($url);
- }
- }
里氏替換原則 Liskov Substitution Principle (LSP)
對(duì)這個(gè)概念***的解釋是:如果你有一個(gè)父類(lèi)和一個(gè)子類(lèi),在不改變?cè)薪Y(jié)果正確性的前提下父類(lèi)和子類(lèi)可以互換。這個(gè)聽(tīng)起來(lái)讓人有些迷惑,所以讓我們來(lái)看一個(gè)經(jīng)典的正方形-長(zhǎng)方形的例子。從數(shù)學(xué)上講,正方形是一種長(zhǎng)方形,但是當(dāng)你的模型通過(guò)繼承使用了"is-a"的關(guān)系時(shí),就不對(duì)了。
Bad:
- class Rectangle{
- protected $width = 0;
- protected $height = 0;
- public function render($area)
- { // ...
- }
- public function setWidth($width)
- { $this->width = $width;
- }
- public function setHeight($height)
- { $this->height = $height;
- }
- public function getArea()
- { return $this->width * $this->height;
- }
- }
- class Square extends Rectangle{
- public function setWidth($width)
- {
- $this->width = $this->height = $width;
- }
- public function setHeight(height)
- { $this->width = $this->height = $height;
- }
- }
- function renderLargeRectangles($rectangles){
- foreach ($rectangles as $rectangle) {
- $rectangle->setWidth(4);
- $rectangle->setHeight(5);
- $area = $rectangle->getArea(); // BAD: Will return 25 for Square. Should be 20.
- $rectangle->render($area);
- }
- }
- $rectangles =
- [new Rectangle(), new Rectangle(), new Square()];
- renderLargeRectangles($rectangles);
Good:
- abstract class Shape{
- protected $width = 0;
- protected $height = 0;
- abstract public function getArea();
- public function render($area) { // ...
- }
- }
- class Rectangle extends Shape{
- public function setWidth($width)
- { $this->width = $width;
- }
- public function setHeight($height)
- { $this->height = $height;
- }
- public function getArea()
- { return $this->width * $this->height;
- }
- }
- class Square extends Shape{
- private $length = 0;
- public function setLength($length)
- { $this->length = $length;
- }
- public function getArea()
- { return pow($this->length, 2);
- }
- }
- function renderLargeRectangles($rectangles){
- foreach ($rectangles as $rectangle) {
- if ($rectangle instanceof Square) {
- $rectangle->setLength(5);
- } elseif ($rectangle instanceof Rectangle) {
- $rectangle->setWidth(4);
- $rectangle->setHeight(5);
- }
- $area = $rectangle->getArea();
- $rectangle->render($area);
- }
- }
- $shapes = [new Rectangle(), new Rectangle(), new Square()];
- renderLargeRectangles($shapes);
接口隔離原則
接口隔離原則:"客戶(hù)端不應(yīng)該被強(qiáng)制去實(shí)現(xiàn)于它不需要的接口"。
有一個(gè)清晰的例子來(lái)說(shuō)明示范這條原則。當(dāng)一個(gè)類(lèi)需要一個(gè)大量的設(shè)置項(xiàng),為了方便不會(huì)要求客戶(hù)端去設(shè)置大量的選項(xiàng),因?yàn)樵谕ǔK麄儾恍枰械脑O(shè)置項(xiàng)。使設(shè)置項(xiàng)可選有助于我們避免產(chǎn)生"胖接口"
Bad:
- interface Employee{
- public function work();
- public function eat();
- }
- class Human implements Employee{
- public function work()
- { // ....working
- }
- public function eat()
- { // ...... eating in lunch break
- }
- }class Robot implements Employee{
- public function work()
- { //.... working much more
- }
- public function eat()
- { //.... robot can't eat, but it must implement this method
- }
- }
上面的代碼中,Robot類(lèi)并不需要eat()這個(gè)方法,但是實(shí)現(xiàn)了Emplyee接口,于是只能實(shí)現(xiàn)所有的方法了,這使得Robot實(shí)現(xiàn)了它并不需要的方法。所以在這里應(yīng)該對(duì)Emplyee接口進(jìn)行拆分,正確的代碼如下:
Good:
- interface Workable{
- public function work();
- }
- interface Feedable{
- public function eat();
- }
- interface Employee extends Feedable, Workable{
- }
- class Human implements Employee{
- public function work()
- { // ....working
- }
- public function eat()
- { //.... eating in lunch break
- }
- }// robot can only work
- class Robot implements Workable{
- public function work()
- { // ....working
- }
- }
依賴(lài)反轉(zhuǎn)原則 Dependency Inversion Principle (DIP)
這條原則說(shuō)明兩個(gè)基本的要點(diǎn):
- 高階的模塊不應(yīng)該依賴(lài)低階的模塊,它們都應(yīng)該依賴(lài)于抽象
- 抽象不應(yīng)該依賴(lài)于實(shí)現(xiàn),實(shí)現(xiàn)應(yīng)該依賴(lài)于抽象
這條起初看起來(lái)有點(diǎn)晦澀難懂,但是如果你使用過(guò)php框架(例如 Symfony),你應(yīng)該見(jiàn)過(guò)依賴(lài)注入(DI)對(duì)這個(gè)概念的實(shí)現(xiàn)。雖然它們不是完全相通的概念,依賴(lài)倒置原則使高階模塊與低階模塊的實(shí)現(xiàn)細(xì)節(jié)和創(chuàng)建分離??梢允褂靡蕾?lài)注入(DI)這種方式來(lái)實(shí)現(xiàn)它。更多的好處是它使模塊之間解耦。耦合會(huì)導(dǎo)致你難于重構(gòu),它是一種非常糟糕的的開(kāi)發(fā)模式。
Bad:
- class Employee{
- public function work()
- { // ....working
- }
- }
- class Robot extends Employee{
- public function work() { //.... working much more
- }
- }
- class Manager{
- private $employee;
- public function __construct(Employee $employee)
- { $this->employee = $employee;
- } public function manage()
- { $this->employee->work();
- }
- }
Good:
- interface Employee{
- public function work();
- }
- class Human implements Employee{
- public function work()
- { // ....working
- }
- }
- class Robot implements Employee{
- public function work()
- { //.... working much more
- }
- }
- class Manager{
- private $employee;
- public function __construct(Employee $employee)
- { $this->employee = $employee;
- } public function manage()
- { $this->employee->work();
- }
- }
別寫(xiě)重復(fù)代碼 (DRY)
這條原則大家應(yīng)該都是比較熟悉了。
盡你***的努力去避免復(fù)制代碼,它是一種非常糟糕的行為,復(fù)制代碼通常意味著當(dāng)你需要變更一些邏輯時(shí),你需要修改不止一處。
Bad:
- function showDeveloperList($developers){
- foreach ($developers as $developer) {
- $expectedSalary =
- $developer->calculateExpectedSalary();
- $experience = $developer->getExperience();
- $githubLink = $developer->getGithubLink();
- $data = [
- $expectedSalary,
- $experience,
- $githubLink
- ];
- render($data);
- }
- }
- function showManagerList($managers){
- foreach ($managers as $manager) {
- $expectedSalary =
- $manager->calculateExpectedSalary();
- $experience = $manager->getExperience();
- $githubLink = $manager->getGithubLink();
- $data = [
- $expectedSalary,
- $experience,
- $githubLink
- ];
- render($data);
- }
- }
Good:
- function showList($employees){
- foreach ($employees as $employee) {
- $expectedSalary =
- $employee->calculateExpectedSalary();
- $experience = $employee->getExperience();
- $githubLink = $employee->getGithubLink();
- $data = [
- $expectedSalary,
- $experience,
- $githubLink
- ];
- render($data);
- }
- }
Very good:
- function showList($employees){ foreach ($employees as $employee) {
- render([
- $employee->calculateExpectedSalary(),
- $employee->getExperience(),
- $employee->getGithubLink()
- ]);
- }
- }
后記:雖然OOP設(shè)計(jì)需要遵守如上原則,不過(guò)實(shí)際的代碼設(shè)計(jì)一定要簡(jiǎn)單、簡(jiǎn)單、簡(jiǎn)單。在實(shí)際編碼中要根據(jù)情況進(jìn)行取舍,一味遵守原則,而不注重實(shí)際情況的話(huà),可能會(huì)讓你的代碼變的難以理解!