这是在PHP中正确的面向对象编程吗?

Aar*_*ron 21 php oop

这可以归类为正确的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;
Run Code Online (Sandbox Code Playgroud)

Gor*_*don 79

为了保持简单,我会说没关系.虽然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 }
Run Code Online (Sandbox Code Playgroud)

第1行:说这个类代表了一个问候的概念.什么是问候语?我会说像"你好约翰"或"嗨约翰"或"你好约翰"这样的问候.事实上,你似乎同意,因为......

第3行:...你有一个类似的问候清单,没有名字.但是这个属性保证了为什么你的班级被命名为问候语的问题,当它真正封装了多个问候语时.这个班不应该被称为问候(记住复数)吗?

第3行:命名属性"问候"也不是一个好主意.这是一个属性,所以不要给它一个动词的名字.动词是用于方法的.

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

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

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

第7行:这shuffle是另一个意想不到的事情.这是一个非明显的副作用.如果我要实例化一个新的问候语,我希望问候语按照它们列出的顺序出现.在ctor中将它们洗牌是违反最小原则的原则.改组应该是一种公共方法,除了改组之外什么都不做,例如

public function shuffleGreetings()
{
    shuffle($this->greetings);
}
Run Code Online (Sandbox Code Playgroud)

假设这个类的想法真的是一个单独的问候语,只是用一个默认的可能值初始化自己,我们也可以像这样添加一个Getter:

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

这比做好

echo $hi->greet[1] .' '. $hi->name;
Run Code Online (Sandbox Code Playgroud)

因为它隐藏了实现细节.我不需要知道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?
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,API仍然充满了WTF.开发人员仍然需要查看源代码以了解正在发生的事情.

更多关于副作用

虽然它可能是很有诱惑力把shufflegetGreeting你不应该这样做.对于相同的输入,方法应该返回相同的内容.当我getGreeting连续两次调用时,我可以期望它返回相同的结果.你会期望1 + 1总是返回2,所以确保你的方法也是如此.

同样,如果您希望使用单个方法从greetings属性返回随机项,请不要随意更改greetings数组.如果您使用shuffle方法,您还可以更改问候语属性.这会掠过从房产中读取的任何功能,例如当你这样做时

public function getRandomGreeting()
{
    $this->shuffleGreetings();
    return $this->getGreeting();
}
Run Code Online (Sandbox Code Playgroud)

开发人员会遇到这样的事情:

$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!!
Run Code Online (Sandbox Code Playgroud)

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

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

这没有副作用:

$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!
Run Code Online (Sandbox Code Playgroud)

虽然API仍然很不错.如果我想一下问候语的属性,我就不会想到"人的名字".只说"嗨"或"你好"仍然是一个有效的问候语.它不需要名称.怎么样

public function greetPerson($personName)
{
    return $this->getGreeting() . ' ' . $personName;
}
Run Code Online (Sandbox Code Playgroud)

然后我们就可以做到

$hi = new Greeting;
$hi->shuffleGreetings();
echo $hi->greetPerson('John');
Run Code Online (Sandbox Code Playgroud)

最后隐藏我们的Greeting包含一个需要改组的数组,让我们将shuffleGreetings方法移回ctor并将类重命名为RandomGreeting.

class RandomGreeting …

    public function __construct()
    {
        $this->shuffleGreetings();
    }
Run Code Online (Sandbox Code Playgroud)

起初这似乎有点违反直觉,因为我告诉过你不要在ctor中洗牌.但是,如果将类重命名为RandomGreeting,那么可以预期幕后会发生一些事情.我们根本不需要知道究竟是什么.为了反映这一点,我们现在也应该保护shuffleGreetings方法.我们完全将它从公共界面隐藏起来.现在我们的代码如下所示:

$hi = new RandomGreeting;
echo $hi->greetPerson('John'); // ex "Howdy John"
Run Code Online (Sandbox Code Playgroud)

这不会给你任何WTF,因为你的代码清楚地告知你会得到一个随机问候.classname清楚地传达了它的功能.

现在这是一个更好的赌注,我们可以在这里结束,但人们仍然可以争辩说,问候语不应该能够自己问候,而是由人来代替.

改善它

我们的研究结果应该让我们得出结论,问候语应该是一个愚蠢的类型来封装问候消息而不是其他任何东西.它应该做的只是返回那条消息.由于Greeting在存储消息字符串旁边没有任何实际行为,因此最简单的方法是创建一个Value Object,例如,按属性值等于另一个对象的对象:

class Greeting
{
    protected $value;

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

    public function getValue()
    {
        return $this->value;
    }
}
Run Code Online (Sandbox Code Playgroud)

另一种方法是将各种可用的问候语分成不同的类型.当你的对象没有行为时,没有什么好处.如果你想利用多态性,你只需要它.但是,具体的子类型确实会在以后考虑一些额外的事情,所以让我们假设我们需要它.

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

interface Greeting
{
    public function getGreeting();
}
Run Code Online (Sandbox Code Playgroud)

定义一个想要表现得像一个问候的类必须有一个getGreeting方法.由于接口不实现任何逻辑,我们还添加一个包含greeting属性的抽象类型和返回它的逻辑:

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

当有抽象类时,还需要从抽象类派生的具体类.所以让我们使用继承来定义我们具体的Greeting类型:

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

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

class HowdyGreeting extends GreetingType
{
    protected $greeting = 'Howdy';
}
Run Code Online (Sandbox Code Playgroud)

实现接口的接口和抽象并不是绝对必要的.我们本可以使我们的具体问候语不从GreetingType扩展.但是如果我们刚刚在所有各种Greeting类上重新实现了getGreeting方法,那么我们就会复制代码并且更容易引入错误,如果我们需要更改某些内容,我们必须触及所有这些类.使用GreetingType,它全部集中.

反过来也是如此.你不一定需要一个界面.我们本可以只使用抽象类型.但是我们将限制在GreetingType中,而使用接口,我们可以更轻松地添加新类型.我承认我现在没想到,所以可能是YAGNI.但是补充一点,我们现在也可以保留它.

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

class NullGreeting extends GreetingType
{
    protected $greeting = '';
}
Run Code Online (Sandbox Code Playgroud)

对象创建

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

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

工厂是您可以使用swich/case而无需检查是否可以使用多态替换条件的少数代码之一.

责任

在创建对象的情况下,我们终于可以开始添加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)
        );
    }
}
Run Code Online (Sandbox Code Playgroud)

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

$greeting = $greetings->getRandomGreeting();
if(NULL !== $greeting) {
    echo $greeting->getMessage();
}
Run Code Online (Sandbox Code Playgroud)

当getRandomGreeting方法没有返回Greeting对象时(当Greetings类中没有Greeting时),以避免"致命错误:尝试在非对象上调用方法".

班级没有其他责任.如果您不确定您的类是否做得太多,或者方法应该更好地用于其他对象,请查看该类中的方法.他们是否与该班级的财产合作?如果没有,你应该移动那个方法.

完成它

现在最后要使用所有代码,我们现在添加Person类.由于我们要确保可以在其上调用getName方法,因此我们先创建一个接口

interface Named
{
    public function getName();
}
Run Code Online (Sandbox Code Playgroud)

我们可以将接口命名为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()
        ));
    }
}
Run Code Online (Sandbox Code Playgroud)

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

现在把它们放在一起:

$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);
Run Code Online (Sandbox Code Playgroud)

在键盘上现场演示

认为这是一个非常简单的事情的相当多的代码.有些人称之为过度工程.但是你要求正确的OOP,虽然我确信仍有改进的余地,但在我的书中这是非常正确和坚固的.现在很容易维护,因为责任更接近应有的位置.

  • 哇!非常好的解释! (22认同)
  • 我无法相信你实际上从这5行代码中做了这个解释 (7认同)
  • 我说不出来!这是最好的单一权威但清晰,广泛但简洁,深入而简单,实用,但充满了我读过的OOP的理论解释.我无法相信你创造了如此全面而美丽的解释所有(我不认为还有什么遗留的)OOP的功能只是从5个简单的线!@Gordon鞠躬.就像我们以前在大学里说的那样"_Tu god hai_"(字面意思是:你是上帝).我要求SO管理员/ mods将其固定在某处,以便每个人都可以阅读它. (6认同)

Pek*_*ica 7

嗯,两件事:

  • 改组那个数组并从外部访问它,并且让外部调用依赖于被洗牌的数组,感觉不对.有一个返回数组混乱结果的方法会更好,也可能就是这个名字.

  • 预先声明所有对象属性将是一种很好的方式,因此可以在以后记录.所以如果你使用$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;
 }
}
Run Code Online (Sandbox Code Playgroud)


Rob*_*itt 6

从初学者的角度来看,这将是肯定的,但由于这是一个单一的对象,它不能像你可以用几个对象那样定向.

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

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

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

这些被称为实体,不应该直接相互作用,这就是为什么分开.

因此,如果我们想要交互Sessions和Security以确保会话是安全的,我们会向会话对象添加一个方法来返回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;
    }
}
Run Code Online (Sandbox Code Playgroud)

以下将使用如下:

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

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

echo $Greeting->getGreeting();
Run Code Online (Sandbox Code Playgroud)

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

这允许代码可以传输到其他项目,而不必担心很多问题,也称为库.

如果这些对象是完全相关的,那么捆绑在一起就可以做到这样的事情:

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

echo $Greeting->getGreeting();
Run Code Online (Sandbox Code Playgroud)