You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

面向对象编程:如何抽取通用方法?Trait是否适用?

问题描述

我要给不同页面(webpageA、webpageB)开发WebScraper,提取标题和预览图,由于两个页面的标题标签规则不同,需要两套解析逻辑。我的目标是通过这个实践理解继承原则。

目前已实现的逻辑:控制器接收带URL的请求,判断URL所属页面后动态创建对应WebpageService[A/B]实例,调用startScraping()执行爬取。两个服务均实现了包含getTitle()和getImage()签名的IPageService接口。

现有WebpageAService代码如下:

class WebpageAService implements IPageService
{
    private string $url;
    private array $data;
    private array $errorLog = [];
    private array $message = [];

    private $keyMap = [
      'title' => 'mappedTitleKey',
      // 其他映射键...
    ];
  
   public function __construct($url) {
     $this->url = $url;
   }

    public function startScraping() {
      $this->data = getTheDataFromApi();
      return [
        'title' => $this->getTitle(),
        'image' => $this->getImage()
      ];
    }

    public function getTitle(): string
    {
        $key = 'title';
        if (! $this->keyExists($key)) {
            return "Please enter a $key";
        }

        return $this->getValue($key);
    }

    public function getImage(): string { /* 与getTitle()逻辑类似的实现 */ }

    private function keyExists(string $key): bool
    {
        if (! isset($this->data[$this->keyMap[$key]])) {
            $this->errorLog[$key] = "Error: $key with the mapped key: {$this->keyMap[$key]} not found on {$this->url}";
            return false;
        }
        return true;
    }

    private function getValue(string $key): string
    {
        return strlen($this->data[$this->keyMap[$key]]) > 0 ? 
            $this->data[$this->keyMap[$key]] : $this->message['length'];
    }
}

我的问题:能不能把keyExists()和getValue()这两个私有方法抽出来,避免在每个webpage[A/B]类中重复编写?如果可以,用Trait是否合理?希望得到相关建议。


解决方案与建议

1. 完全可以抽取重复逻辑,Trait是合理选择之一

keyExists()和getValue()属于通用的数据校验与取值逻辑,仅依赖类中的$data、$keyMap、$url、$errorLog、$message属性,和具体页面的解析规则无关,用Trait复用完全可行。

2. Trait实现示例

trait PageDataProcessor
{
    protected function keyExists(string $key): bool
    {
        if (! isset($this->data[$this->keyMap[$key]])) {
            $this->errorLog[$key] = "Error: $key with the mapped key: {$this->keyMap[$key]} not found on {$this->url}";
            return false;
        }
        return true;
    }

    protected function getValue(string $key): string
    {
        return strlen($this->data[$this->keyMap[$key]]) > 0 ? 
            $this->data[$this->keyMap[$key]] : $this->message['length'];
    }
}

注意:将方法访问修饰符从private改为protected,确保Trait方法能被使用它的类访问,同时不暴露给外部。

3. 在WebpageService类中使用Trait

class WebpageAService implements IPageService
{
    use PageDataProcessor; // 引入Trait

    private string $url;
    private array $data;
    private array $errorLog = [];
    private array $message = [];

    private $keyMap = [
      'title' => 'mappedTitleKey',
      // ...
    ];
  
    // 原有代码保持不变...
}

class WebpageBService implements IPageService
{
    use PageDataProcessor; // 同样引入Trait

    private string $url;
    private array $data;
    private array $errorLog = [];
    private array $message = [];

    private $keyMap = [
      'title' => 'differentTitleKey', // 与A页面不同的映射规则
      // ...
    ];
  
    // 实现getTitle()、getImage()等方法,逻辑与A一致但依赖自身keyMap...
}

4. 替代方案:抽象基类(更贴合继承原则学习目标)

如果所有WebpageService除了keyMap和个别细节外,大部分逻辑(比如startScraping()流程)都相似,用抽象基类实现复用更符合继承原则的设计思路:

abstract class BasePageService implements IPageService
{
    protected string $url;
    protected array $data;
    protected array $errorLog = [];
    protected array $message = [];
    protected abstract array $keyMap; // 强制子类定义自身的映射规则

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

    public function startScraping() {
        $this->data = $this->fetchData();
        return [
            'title' => $this->getTitle(),
            'image' => $this->getImage()
        ];
    }

    public function getTitle(): string
    {
        $key = 'title';
        if (! $this->keyExists($key)) {
            return "Please enter a $key";
        }
        return $this->getValue($key);
    }

    abstract public function getImage(): string; // 强制子类实现图片提取逻辑

    protected function keyExists(string $key): bool
    {
        if (! isset($this->data[$this->keyMap[$key]])) {
            $this->errorLog[$key] = "Error: $key with the mapped key: {$this->keyMap[$key]} not found on {$this->url}";
            return false;
        }
        return true;
    }

    protected function getValue(string $key): string
    {
        return strlen($this->data[$this->keyMap[$key]]) > 0 ? 
            $this->data[$this->keyMap[$key]] : $this->message['length'];
    }

    protected abstract function fetchData(): array; // 强制子类实现数据获取逻辑
}

子类继承示例:

class WebpageAService extends BasePageService
{
    protected array $keyMap = [
        'title' => 'mappedTitleKey',
        'image' => 'mappedImageKey'
    ];

    public function getImage(): string
    {
        $key = 'image';
        if (! $this->keyExists($key)) {
            return "Please enter a $key";
        }
        return $this->getValue($key);
    }

    protected function fetchData(): array
    {
        return getTheDataFromApi(); // A页面专属的数据获取逻辑
    }
}

5. 两种方案选择建议

  • 若仅需复用keyExists()和getValue()这两个孤立方法,Trait更轻便,不强制类的继承结构,灵活性更高。
  • 若多个WebpageService存在大量通用逻辑(如startScraping()流程、属性声明),抽象基类更合适,能统一代码结构,也更贴合你学习继承原则的目标。

内容的提问来源于stack exchange,提问作者Max Pattern

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 15:25:09