这是php中正确的面向对象编程吗?[关闭]

Is this correct object oriented programming in php? [closed]

提问人:Aaron 提问时间:3/17/2011 最后编辑:RolandoMySQLDBAAaron 更新时间:4/18/2015 访问量:2310

问:

这可以归类为正确的OOP编程吗?

class Greeting {

    public $greet = array('Hi','Hello', 'Howzit', 'Ola', 'Whats up');

    function __construct($name) {
        $this->name = $name;
        shuffle($this->greet);
    }
}

$hi = new Greeting('INSERTNAMEHERE'); /*NAME OF PERSON GOES HERE*/
echo $hi->greet[1] .' '. $hi->name;
PHP 哎呀

评论

1赞 mario 3/17/2011
良好的 OOP 使代码更具可读性。要掌握 OOP,您唯一需要学习的是:消息传递。
2赞 R. Martinho Fernandes 3/17/2011
@mario:我觉得这种方法太简单了。比如说,多态性呢?封装?
1赞 Tanner Ottinger 3/23/2011
据我所知,只有一件事:当您创建实例时,您需要一个大写的 g。
0赞 István Ujj-Mészáros 11/12/2013
是的,+1 关闭最佳问题/答案之一。恭喜。

答:

1赞 Naftali 3/17/2011 #1

它不完全正确的 OOP,因为变量没有被封装。$greet

为了使它“更正确”(无论这意味着什么),你必须将参数设为私有,并为它创建一些 get 和 set 方法(变量也是如此——也应该在使用它之前声明)。$greet$name

在 PHP 中,get 方法如下所示:

如果我想得到 VAR:

$hi = new greeting('INSERTNAMEHERE');/*NAME OF PERSON GOES HERE*/
echo $hi->__get('greet') .' '. $hi->__get('name');

Get 是在 Greeting 类中创建的:

function __get($var){
    return $this->$var;
}

和 set 是一样的:

function __set($var, $val){
    $this->$var = $val;
}

评论

0赞 mario 3/17/2011
这不是一个好主意,如果它被__get/__set破坏,那就有点毫无意义了。
0赞 Naftali 3/17/2011
我说要把问候婴儿车设为私人,这样它就不会被外界操纵
7赞 Pekka 3/17/2011 #2

嗯,有两件事:

  • 洗牌该阵列并从外部访问它,并让外部调用依赖于被洗牌的阵列,感觉不对。如果有一个方法可以返回数组洗牌的结果,并且可能还会立即返回名称,那就更好了。

  • 最好事先声明所有对象属性,以便以后可以记录。因此,如果您使用 ,则应声明它。$this->name

实现的两点可能如下所示:

class greeting 
 {
 protected $greet = array('Hi','Hello', 'Howzit', 'Ola', 'Whats up');
 protected $name = null;

 public function __construct($name) 
 {
  $this->name = $name;
 }

 public function greet()
 { 
  shuffle($this->greet);
  return $this->greet[1]." ".$this->name;
 }
}

评论

0赞 ifaour 3/17/2011
我在这里遗漏了什么还是错过了方法?greet
0赞 ifaour 3/17/2011
和 ?谁想要喝咖啡休息一下?:-)shuffle
0赞 Pekka 3/17/2011
@ifaour哈哈哈!我。我现在要拿一个。(悄悄地仔细检查代码)
0赞 Ryre 3/17/2011 #3

我可能会将 getters/setter 用于$greet,而不是将其公开。我不明白为什么它不会被视为 OOP。

http://en.wikipedia.org/wiki/Object-oriented_programming

评论

4赞 mario 3/17/2011
getter 和 setter 不是一个好主意在PHP中也不行。
0赞 Ryre 3/17/2011
有趣的读物。谢谢你。我太习惯了 .NET,它只是强迫他们。莫尔读书做!
4赞 dnagirl 3/17/2011 #4

有一种方法可能会更好。这个想法是用户不需要知道 $this->greet 是一个数组。greet

所以:

class Greeting { 
  protected $greet = array('Hi','Hello', 'Howzit', 'Ola', 'Whats up'); 
  protected $name='You';    

  function __construct($name) {
         $this->name = $name;         
  }

  function greet(){
    shuffle($this->greet);
    echo "{$this->greet[0]} {$this->name}!";
  }

}  
$hi = new Greeting('INSERTNAMEHERE');/*NAME OF PERSON GOES HERE*/ 
$hi->greet(); 

评论

0赞 dqhendricks 3/17/2011
忘了声明名字 var 虽然正如 Pekka 提到的那样。
0赞 Aaron 3/17/2011
在 echo 旁边的 shuffle 下的 greet 方法中,我看到 {$this->greet[0]} {$this->name}!{} 之间的这些变量有什么胆量?
1赞 dnagirl 3/17/2011
@Matthew:当你使用复杂的变量(如双引号)或在双引号内时,你会在它们周围加上大括号,这样 PHP 就知道你指的是一个变量。另一种选择是像这样连接字符串: 就我自己而言,我发现连接时比使用大括号和双引号时犯的语法错误更多。$this->name$arr[0]{}echo $this->greet[0] . ' ' . $this->name . '!';
6赞 RobertPitt 3/17/2011 #5

从初学者的角度来看,这是肯定的,但由于这是一个单独的对象,因此它不能像使用多个对象那样定向。

真正的 OOP 是指将应用程序的各个实体分离为对象/依赖项。

例如,一个简单的网站将包括以下内容:

  • 数据库
  • 错误处理
  • 会话
  • 安全

这些被称为实体,不应该直接相互交互,这就是为什么有单独的。

因此,如果我们想交互会话和安全性以保证会话是安全的,我们将向会话对象添加一个方法以返回 PHP 标准结果,例如数组或字符串,这样许多对象可以相互交互,而无需过多依赖实际对象。

看看你在问候课上的尝试,我会这样看:

class User
{
    protected $data;

    public function __construct(array $user_data)
    {
        $this->data = $user_data;
    }

    public function getUsername()
    {
        return $this->data['username'];
    }
}

class Greeting
{
    private $message = "welcome to mysite %s";

    public function __construct(string $to_greet)
    {
        $this->message = sprintf($this->message,$to_greet);
    }

    public function getGreeting()
    {
        return $this->message;
    }
}

以下内容将按如下方式使用:

$User = new User(array(
    'id' => 22,
    'username' => 'Robert Pitt'
));

$Greeting = new Greeting($User->getUsername());

echo $Greeting->getGreeting();

现在我已经提到对象确实直接相互交互,但如果它们这样做,它们应该被封装,以便所有数据库对象(如 Database、Result DB_Error)只会相互交互。

这允许代码可以传输到其他项目,而不必解决太多问题,也称为库。

如果这些对象与克洛斯利相关,并且所有对象都捆绑在一起,则可以执行以下操作:

$User = new User(/*..From DB ..*/);
$Greeting = new Greeting($User);

echo $Greeting->getGreeting();

评论

1赞 Pekka 3/17/2011
好吧,这让事情更上一层楼。在学习IMO的要点时,这有点多,但仍然很好。+1
0赞 Pekka 3/17/2011
次要观察:为什么在第一个区块是公开的?不应该做?我个人更愿意将原始消息存储在一个单独的变量中,而不是覆盖旧的变量 - 可以查看类定义并假设变量包含模式。$messageprotected$rawMessage
0赞 RobertPitt 3/17/2011
是的,@pekka,我完全明白你的意思,一般来说,这就是你如何处理它,但这完全取决于类的使用方式,以及程序员如何感到舒适,严格在当今的健壮框架中是必不可少的,但作为示例代码,我认为它足以实现此目的。
0赞 Gordon 3/17/2011
(吹毛求疵)标量类型提示位于 PHP.next 的主干中,但它们不是强制执行的。它们在 PHP 5.3.5 及更早版本中根本不可用。
2赞 Riimu 3/17/2011 #6

实际上,这有点离谱。首先,公共变量通常是坏的。您不应该直接从代码中访问类的变量。应尝试将类提供的功能封装到类本身中。

首先,让我根据你的示例向你展示一个好的 OOP 代码是什么样子的:

<?php

class greeting
{
    private static $greets = array('Hi','Hello', 'Howzit', 'Ola', 'Whats up');

    private $name;

    function __construct ($name)
    {
        $this->name = $name;
    }

    function doGreeting ()
    {
        $i = array_rand(self::$greets);
        return self::$greets[$i] . ' ' . $this->name;
    }
}

$hi = new greeting('INSERTNAMEHERE');/*NAME OF PERSON GOES HERE*/
echo $hi->doGreeting();

现在,让我们谈谈这里的差异。

首先,问候语存储在静态变量中。由于这些文本可能不会因实例而异,因此将它们存储在整个类共享的静态变量中会更有效,因此每个实例都没有自己的数组副本。您还会注意到它是私有的。这就是封装。外部代码不需要了解类的内部工作原理,例如变量名称。外部代码不需要知道数组,因为它应该只在类本身中使用。

另一个小细节是,该名称也在那里声明为私有,因为所有类变量通常都应该是私有的,以便进行封装。如果需要访问它们,则应通过 setter 和 getter 函数完成,如果需要,它们可以为值提供额外的验证。

代码流也略有不同。我们不是直接从外部代码中使用类变量,而是让类本身生成问候语并将其作为字符串返回。这样,外部代码就不需要知道问候语文本是如何生成的,也不需要知道名称的存储位置。它只需要知道 API,即函数、它们的参数和返回值。它们的实际工作方式应该留给类本身。

此外,由于这显然是用于问候类的功能,因此当功能被编码到类中时,它使代码更可重用。OOP 的部分意义在于创建良好的可重用代码,这些代码可以在需要的地方使用。当类功能位于类本身中时,不需要在其他地方对相同的代码进行编码。

记住封装和可重用性。这是 OOP 的两个要点。

2赞 Joseph Erickson 3/17/2011 #7

OOP 背后的一个基本思想是,你有对象对代码中的某些数据结构负责。你上面写的实际上只是一个数据结构,而不是一个完整的对象,它保存了问候语的数据,但实际上并没有处理它并为其他对象提供它的责任。

一个“更好”的对象看起来像这样:

class greeting {
   protected $greet = array('Hi','Hello', 'Howzit', 'Ola', 'Whats up');
   protected $name;
   function __construct($name) {
       $this->name = $name;
   }
   public function greet() {
       return $this->greet[rand(0,5)] . ' ' . $this->name;
   }
}

$hi = new greeting('INSERTNAMEHERE');/*NAME OF PERSON GOES HERE*/
echo $hi->greet();

现在,您的greet对象完全控制了问候语,并且您的应用程序代码不必关心该对象的设置方式,它只需要关心它可以用来获取问候语的几个函数。如果你想最终以某种方式国际化或改变问候语,你需要做的就是改变那个对象来做到这一点。

另外,请注意,使用对象不会使您的代码 OOP。它需要是面向对象的,这意味着使用对象来定义应用程序需要执行的离散任务,而不是让应用程序的其他部分完全访问其所有数据以执行它将要执行的内容。然后,你只是在构建数据结构。

3赞 Jerry Coffin 3/17/2011 #8

我必须不同意[编辑:一些]你得到的其他答案。设为私有并添加 a 对增加封装几乎没有什么作用。greetgetter

然而,无论如何,我认为这是非常糟糕的OO设计。一个对象应该代表一些东西(我敢说,一些实际的服从?IMO,这里的“对象”实际上是人(或其他什么),其中一个人在问候另一个人。问候语本身应该是从一个对象传递到另一个对象的消息。我们可能会编写一个“greeting”类来保存问候语的文本,但即使我们这样做了,问候语对象本身也应该独立于问候语的源或目标。

79赞 Gordon 3/17/2011 #9

为了保持简单,我会说没关系。不过,它不是太合适的 OOP,也不是特别容易理解代码。不过,有工作代码总比没有代码要好。

让我们看一下你的代码:

1 class Greeting {
2
3    public $greet = array('Hi','Hello', 'Howzit', 'Ola', 'Whats up');
4
5    function __construct($name) {
6        $this->name = $name;
7        shuffle($this->greet);
8    }
9 }

第 1 行:表示这个类代表问候语的概念。什么是问候语?我会说“你好约翰”或“嗨约翰”或“你好约翰”之类的话是一种问候语。事实上,你似乎同意,因为在......

第 3 行:......你确实有一个类似的问候语列表,只是没有名字。但是这个属性保证了为什么你的类被命名为 Greeting,而它实际上已经封装了多个问候语。那么这个类不应该被称为问候(注意复数)吗?

第 3 行:将属性命名为“greet”也不是一个好主意。它是一个属性,所以不要给它一个动词的名字。动词用于方法。

第 3 行:虽然有人会告诉你不同的情况,但公开房产很少是一个好主意。属性与内部状态有关,不应直接访问该状态,而只能通过方法访问。

第 5 行:然后你的构造函数告诉我 Greeting 必须有一个名字。如果我还没有查看源代码,我会错误地认为这是 Greeting 的名称。但你真的是指一个人的名字。该论点应该反映这一点,并被命名为更具指示性的名称,例如$greetedPersonsName。

第 6 行:动态分配属性是一个嘘声。如果我查看类定义,我想立即查看属性。在某些方法中发现它们会使代码难以理解。在生成 API 文档时,这也不会被拾取。避免它。

第 7 行:这是另一件意想不到的事情。这是一个不明显的副作用。如果我要实例化一个新的问候语,我希望问候语按它们列出的顺序显示。在 ctor 中洗牌是违反最小惊讶原则的。洗牌应该从公共方法中发生,该方法只执行洗牌,例如shuffle

public function shuffleGreetings()
{
    shuffle($this->greetings);
}

假设该类的想法实际上是一个单一的 Greeting,它只是使用默认的可能值之一初始化自身,我们也可以添加一个 Getter ,如下所示:

public function getGreeting()
{
    return $this->_greetings[0] . ' ' . $this->name;
}

这总比做起来好

echo $hi->greet[1] .' '. $hi->name;

因为它隐藏了实现细节。我不需要知道 Greeting 对象具有可能的问候语数组。我只想将问候语与设置名称相结合。不过,它仍然远非完美,因为您仍然会像

$hi = new Greeting('John'); // A Greeting named John? Why $hi then?
$hi->shuffleGreetings();    // Shuffling Greetings in a Greeting?
echo $hi->getGreeting();    // Why is it "Hello John" all of a sudden?

正如你所看到的,API 仍然充满了 WTF。开发人员仍然需要查看您的源代码才能了解发生了什么。

更多关于副作用的信息

虽然放入可能很诱人,但你不应该这样做。对于相同的输入,方法应该返回相同的内容。当我连续调用两次时,我可以预期它会返回相同的结果。你会期望 1+1 总是返回 2,所以请确保你的方法也这样做。shufflegetGreetinggetGreeting

同样,如果希望使用单个方法从 greetings 属性返回随机项,请不要对 greetings 数组进行随机排序。如果改用 shuffle 方法,则还将更改 greetings 属性。这会影响到从属性读取的任何函数,例如,当您这样做时

public function getRandomGreeting()
{
    $this->shuffleGreetings();
    return $this->getGreeting();
}

开发人员将体验到类似这样的事情:

$hi = new Greeting('John');
$hi->shuffleGreetings();
echo $hi->getGreeting();       // for example "Hello John"
echo $hi->getRandomGreeting(); // for example "Hi John"
echo $hi->getGreeting();       // for example "Howdy John" <-- WTF!!

使用不更改属性的实现,例如

public function getRandomGreeting()
{
    $randomKey = array_rand($this->greetings);
    return $this->greetings[$randomKey] . ' ' . $this->name;
}

这是没有副作用的:

$hi = new Greeting('John');
$hi->shuffleGreetings();
echo $hi->getGreeting();       // for example "Hello John"
echo $hi->getRandomGreeting(); // for example "Hi John"   
echo $hi->getGreeting();       // still "Hello John". Expected!

不过,API 还远非漂亮。如果我考虑问候语的属性,我只是不考虑“人名”。只是说“嗨”或“你好”仍然是一个有效的问候语。它不需要名称。怎么样

public function greetPerson($personName)
{
    return $this->getGreeting() . ' ' . $personName;
}

然后我们可以做

$hi = new Greeting;
$hi->shuffleGreetings();
echo $hi->greetPerson('John');

为了最终隐藏我们的 Greeting 包含一个需要洗牌的数组,让我们将 shuffleGreetings 方法移回 ctor 并将该类重命名为 RandomGreeting。

class RandomGreeting …

    public function __construct()
    {
        $this->shuffleGreetings();
    }

乍一看,这似乎有悖常理,因为我告诉过你不要在 ctor 中洗牌。但是,随着该类重命名为 RandomGreeting,更值得期待的是,幕后发生了一些事情。我们只是不需要知道到底是什么。为了反映这一点,我们现在还应该保护 shuffleGreetings 方法。我们只是将其完全隐藏在公共界面中。现在我们的代码是这样的:

$hi = new RandomGreeting;
echo $hi->greetPerson('John'); // ex "Howdy John"

这不会给你任何 WTF,因为你的代码清楚地传达了你会得到一个随机的问候语。类名清楚地传达了它的作用。

现在这个赌注更好了,我们可以到此结束,但人们仍然可以争辩说,问候语不应该能够单独问候,而是由一个人完成的事情。

改进它

我们的研究结果应该使我们得出结论,问候语应该是一个封装问候语消息的哑类型,而不是其他什么。它应该做的就是返回该消息。由于 Greeting 在存储消息字符串旁边没有任何实际行为,因此最简单的方法是创建一个 Value 对象,例如,一个对象通过属性值等于另一个对象:

class Greeting
{
    protected $value;

    public function __construct($value)
    {
        $this->value = $value;
    }

    public function getValue()
    {
        return $this->value;
    }
}

另一种方法是将各种可用的问候语转换为单独的类型。当你的对象没有行为时,这样做几乎没有什么好处。仅当您想利用多态性时才需要它。但是,具有具体的子类型确实会在以后考虑一些额外的事情,因此让我们假设我们需要它。

在 OOP 中执行此操作的正确方法是定义一个接口

interface Greeting
{
    public function getGreeting();
}

它定义了一个想要像 Greeting 一样运行的类,必须有一个 getGreeting 方法。由于接口不实现任何逻辑,我们还添加了一个抽象类型,其中包含 greeting 属性和返回它的逻辑:

abstract class GreetingType implements Greeting
{
    protected $greeting;
    public function getGreeting()
    {
        return $this->greeting;
    }
}

当存在抽象类时,也需要有从抽象类派生的具体类。因此,让我们使用 Inheritance 来定义具体的 Greeting 类型:

class HiGreeting extends GreetingType
{
    protected $greeting = 'Hi';
}

class HelloGreeting extends GreetingType
{
    protected $greeting = 'Hello';
}

class HowdyGreeting extends GreetingType
{
    protected $greeting = 'Howdy';
}

没有必要让 Interface 和 Abstract 实现接口。我们本可以使具体的问候语不从 GreetingType 扩展。但是,如果我们只是在所有不同的 Greeting 类上重新实现了 getGreeting 方法,我们将复制代码并且更容易引入错误,如果我们需要更改某些内容,我们将不得不触及所有这些类。有了 GreetingType,一切都是集中式的。

反之亦然。您不一定需要接口。我们本来可以只使用抽象类型。但是,我们将仅限于 GreetingType,而使用接口,我们可以更轻松地添加新类型。我承认我现在想不出任何一个,所以它可能是YAGNI。但是要添加的太少了,我们现在也可以保留它。

我们还将添加一个返回空字符串的 Null 对象。稍后会详细介绍。

class NullGreeting extends GreetingType
{
    protected $greeting = '';
}

对象创建

因为我不想在我的消费类中乱扔垃圾并引入耦合,所以我将使用简单的 Factory 来封装对象创建:new classname

class GreetingFactory
{
    public function createGreeting($typeName = NULL)
    {
        switch(strtolower($typeName)) {
            case 'hi':    return new HiGreeting;
            case 'howdy': return new HowdyGreeting;
            case 'hello': return new HelloGreeting;
            default:      return new NullGreeting;
        }
    }
}

Factory 是为数不多的代码之一,您可以在其中实际使用 swich/case,而无需检查是否可以将 Conditional 替换为 Polymorphism

责任

完成对象创建后,我们终于可以开始添加 Greetings 类了:

class Greetings
{
    protected $greetings;
    protected $nullGreeting;
    public function __construct(NullGreeting $nullGreeting)
    {
        $this->greetings = new ArrayObject;
        $this->nullGreeting = $nullGreeting;
    }
    public function addGreeting(Greeting $greetingToAdd)
    {
        $this->greetings->append($greetingToAdd);
    }
    public function getRandomGreeting()
    {
        if ($this->hasGreetings()) {
            return $this->_getRandomGreeting();
        } else {
            return $this->nullGreeting;
        }
    }
    public function hasGreetings()
    {
        return count($this->greetings);
    }
    protected function _getRandomGreeting()
    {
        return $this->greetings->offsetGet(
            rand(0, $this->greetings->count() - 1)
        );
    }
}

正如你所看到的,Greetings 实际上只是一个 ArrayObject 的包装器。它确保除了实现 Greeting 接口的对象之外,我们不能向集合添加任何其他内容。它还允许我们从集合中随机选择一个问候语。它还通过返回 NullGreeting 来确保您始终从对 getRandomGreeting 的调用中获得 Greeting。这太棒了,因为没有它,你就必须这样做

$greeting = $greetings->getRandomGreeting();
if(NULL !== $greeting) {
    echo $greeting->getMessage();
}

为了避免当 getRandomGreeting 方法未返回 Greeting 对象时(当 Greetings 类中还没有 Greeting 时)出现“致命错误:尝试在非对象上调用方法”。

除此之外,班级没有其他责任。如果您不确定您的类是否执行了太多操作,或者具有应该更好地放在其他对象上的方法,请查看该类中的方法。 它们是否使用该类的属性?如果没有,您可能应该移动该方法

完成任务

现在,为了最终使用所有这些代码,我们现在添加 Person 类。由于我们想确保我们可以在其上调用 getName 方法,因此我们在执行此操作之前创建一个接口

interface Named
{
    public function getName();
}

我们可以将该接口命名为 IPerson 或其他名称,但它只有一个方法 getName,最合适的名称是 Named,因为任何实现该接口的类都是一个命名的东西,包括但不限于我们的 Person 类:

class Person implements Named
{
    protected $name;
    protected $greeting;
    public function __construct($name, Greeting $greeting)
    {
        $this->name = $name;
        $this->greeting = $greeting;
    }
    public function getName()
    {
        return $this->name;
    }
    public function greet(Named $greetable)
    {
        return trim(sprintf(
            '%s %s',
            $this->greeting->getGreeting(),
            $greetable->getName()
        ));
    }
}

我们的人有一个必需的名字,我们要求它也有一个问候语。除了返回它的名字之外,它所能做的就是迎接另一个命名的东西,可能是另一个人。就是这样。

现在把这些放在一起:

$greetings->addGreeting($greetingsFactory->createGreeting('Hi'));
$greetings->addGreeting($greetingsFactory->createGreeting('Howdy'));
$greetings->addGreeting($greetingsFactory->createGreeting('Hello'));
$john = new Person('John Doe', $greetings->getRandomGreeting());
$jane = new Person('Jane Doe', $greetings->getRandomGreeting());

echo $john->greet($jane), 
     PHP_EOL, 
     $jane->greet($john);

Codepad 上的现场演示

当然,对于一件非常简单的事情来说,这是相当多的代码。有些人会说它过度设计。但是你要求正确的 OOP,虽然我确信还有改进的余地,但它在我的书中是相当合适和可靠的。现在很容易维护,因为责任更接近他们应该在的地方。

评论

7赞 dynamic 6/9/2011
我不敢相信你真的用这 5 行代码做了这个解释
6赞 wadkar 8/11/2011
我无法用言语来形容!!这是我读过的最好的单篇权威而清晰、广泛而简洁、深入而简单、实用而充满理论解释的 OOP 作品。我简直不敢相信你仅仅从 5 行简单的 OOP 就对所有(我认为如果还剩下什么)的功能进行了如此全面而美丽的解释!!@Gordon鞠躬。正如我们在大学里常说的“Tu god hai”(字面意思是:你是上帝)。我要求 SO 管理员/模组将其固定在某个地方,以便每个人都可以阅读它。
0赞 yentsun 7/1/2012
“那这门课不应该叫问候(注意复数)吗?”绝不应该。
1赞 John V. 8/15/2012
tl;博士(+1,因为我确实读过它,这是一个了不起的答案。干得好。
0赞 Andrew 10/3/2014
当我读到这句话:“只是说'嗨'或'你好'仍然是一个有效的问候语”时,我立即知道你来自哪个国家,而没有查看个人资料:)