API和PHP示例的固有原则
#php #api #solid

有时,在尝试实施某种概念上的事情时,很难找到现实世界 /工作示例,更实用,并且远离“矩形 /正方形”的东西。作为开发人员,我们可能只想看到他们如何在我们的日常工作中工作,以学习并牢记他们的牢记。

因此,在阅读了许多概念 /定义文章之后,但是很少看到与API相关的示例之后,我决定使用现实世界中的API示例对其进行文章,部分是我过去使用过的。在下面,您会看到许多代码片而不是纯文本(例如,我比讲话更好)。

简而言之

让我们看到这些原理一对一,具有简单的定义,描述和代码示例。

1-单一责任原则(SRP)

课程(顺便说一句,他们的方法)应该只有一个责任。因此,如果我们将一堆无关的工作推到一个班级中,这将打破原则,使该班级不快乐。相反,我们可以为不同的作业创建不同的类以遵循该原则。

在这里,我们有一个用于创建令牌的API端点POST /token,使用tokenAction()方法来创建TokenController类,用于使用User entity进行用户名和密码凭据检查用户。

不良练习:

class User extends Entity {
  public int $id;
  public string $name, $email;
  public UserSettings $settings;

  // More props & setters/getters.

  // @tofix: Does not belong here.
  public function validatePassword(string $password): bool {
    // Run validation stuff.
  }

  // @tofix: Does not belong here.
  public function sendMail(string $subject, string $body): void {
    // Run mailing stuff.
  }
}
class TokenControler extends Controller {
  // @call POST /token
  public function tokenAction(): Payload {
    [$username, $password]
      = $this->request->post(['username', 'password']);

    // @var User (say it's okay, no 404)
    $user = $this->repository->getUserByUsername($username);

    if ($user->validatePassword($password)) {
      if ($user->settings->isTrue('mailOnAuthSuccess')) {
        $user->sendMail(
         'Success login!',
         'New successful login, IP: ' . $this->request->getIp()
        );
      }

      $token = new Token($user);
      $token->persist();

      return $this->jsonPayload(Status::OK, [
        'token'  => $token->getValue(),
        'expiry' => $token->getExpiry()
      ]);
    }

    if ($user->settings->isTrue('mailOnAuthFailure')) {
      $user->sendMail(
        'Failed login!',
        'Suspicious login attempt, IP: ' . $this->request->getIp()
      );
    }

    return $this->jsonPayload(Status::UNAUTHORIZED, [
      'error' => 'Invalid credentials.'
    ]);
  }
}

由于我们正在用两种无关方法(不属于那里)塞满User实体,因此可能有很多理由随着时间的推移更改此类。

因此,在这里,我们可以通过简单地将这些无关的方法移动到更具体的和的一项更改类中来使用更好的方法。

更好的练习:

class User extends Entity {
  public int $id;
  public string $name, $email;
  public UserSettings $settings;

  // More props & setters/getters.
}
// Each item is in its own file.
class UserHolder {
  public function __construct(
    protected readonly User $user
  ) {}
}

class UserPasswordValidator extends UserHolder {
  public function validate(string $password): bool {
    // Run validation business using $this->user->password.
  }
}
class UserAuthenticationMailer extends UserHolder {
  public function sendSuccessMail(string $ip): void {
    // Run mailing business using $this->user->email.
  }
  public function sendFailureMail(string $ip): void {
    // Run mailing business using $this->user->email.
  }
}
class TokenControler extends Controller {
  // @call POST /token
  public function tokenAction(): Payload {
    // ...

    $validator = new UserPasswordValidator($user);
    if ($validator->validate($password)) {
      if ($user->settings->isTrue('mailOnAuthSuccess')) {
        $mailer = new UserAuthenticationMailer($user);
        $mailer->sendSuccessMail($this->request->getIp());
      }

      // ...
    }

    if ($user->settings->isTrue('mailOnAuthFailure')) {
      $mailer ??= new UserAuthenticationMailer($user);
      $mailer->sendFailureMail($this->request->getIp());
    }

    // ...
  }
}

现在,我们的代码变得更加颗粒状,如此灵活和扩展。现在的测试更加容易,因为我们可以作为单独的企业主分别测试这些无关的方法。

但是,想看到这种方法的另一个好处吗?让我们在tokenAction()和一个名为UserIpValidator的类中再添加一张IP效度的检查。

class UserIpValidator extends UserHolder {
  public function validate(string $ip): bool {
    $ips = new IpList($this->user->settings->get('allowedIps'));
    return $ips->blank() || $ips->contains($ip);
  }
}
class TokenControler extends Controller {
  // @call POST /token
  public function tokenAction(): Payload {
    // ...

    $validator = new UserIpValidator($user);
    if (!$validator->validate($this->request->getIp())) {
      return $this->jsonPayload(Status::FORBIDDEN, [
        'error' => 'Non-allowed IP.'
      ]);
    }

    // ...
  }
}

2-开放原理(OCP)

类(顺便说一句,他们的方法)应开放以进行扩展,但要进行修改(行为更改)。,换句话说,我们应该能够扩展它们而不改变其行为。因此,我们可以使用抽象遵循该原理。

在这里,我们有一个用于接受付款的API端点POST /payment,使用paymentAction()方法的PaymentController类用于处理用户的订阅来处理用户的付款,而DiscountCalculator用于将这些付款的折扣应用于这些付款的折扣。

>

不良练习:

class DiscountCalculator {
  // @see Spaghetti Pattern.
  public function calculate(User $user, float $amount): float {
    $discount = match ($user->subscription->type) {
      'basic'  => $amount >= 100.0 ? 10.0 : 0,
      'silver' => $amount >= 75.0  ? 15.0 : 0,
      default  => throw new Error('Invalid subscription type!')
    };
    return $discount ? $amount / 100 * $discount : $amount;
  }
}
class PaymentController extends Controller {
  // @call POST /payment
  public function paymentAction(): Payload {
    [$grossTotal, $creditCard]
      = $this->request->post(['grossTotal', 'creditCard']);

    // @var User (say it's okay, no 404)
    $user = $this->repository->getUserByToken($token_ResolvedInSomeWay);

    $calculator = new DiscountCalculator();
    $discount   = $calculator->calculate($user, $grossTotal);
    $netTotal   = $grossTotal - $discount;

    try {
      $payment = new Payment(amount: $netTotal, card: $creditCard);
      $payment->charge();

      if ($payment->okay()) {
        $this->repository->saveUserPayment($user, $payment);
      }

      return $this->jsonPayload(Status::OK, [
        'netTotal'      => $netTotal,
        'transactionId' => $payment->transactionId
      ]);
    } catch (PaymentError $e) {
      $this->logger->logError($e);

      return $this->jsonPayload(Status::INTERNAL, [
        'error'  => 'Payment error.'
        'detail' => $e->getMessage()
      ]);
    } catch (RepositoryError $e) {
        $this->logger->logError($e);

        $payment->cancel();

        return $this->jsonPayload(Status::INTERNAL, [
          'error'  => 'Repository error.',
          'detail' => $e->getMessage()
        ]);
      }
  }
}

由于DiscountCalculator类并未对更改进行关闭,因此每当系统添加新的订阅类型时,我们将始终需要更改此类以支持新的订阅类型。

因此,在这里,我们可以通过简单地创建基于抽象的订阅类型的类并在DiscountCalculator类中使用它来使用更好的方法。

更好的练习:

// Each item is in its own file.
abstract class Discount {
  public abstract function calculate(float $amount): float;

  // In respect of DRY principle.
  protected final function calculateBy(
    float $amount, float $threshold, float $discount
  ): float {
    if ($amount >= $threshold) {
      return $amount / 100 * $discount;
    }
    return 0.0;
  }
}

// These classes can have such constants
// like THRESHOLD, DISCOUNT instead, BTW.
class BasicDiscount extends Discount {
  public function calculate(float $amount): float {
    return $this->calculateBy(
      $amount, threshold: 100.0, discount: 10.0
    );
  }
}
class SilverDiscount extends Discount {
  public function calculate(float $amount): float {
    return $this->calculateBy(
      $amount, threshold: 75.0, discount: 15.0
    );
  }
}

class DiscountFactory {
  public static function create(User $user): Discount {
    // Create a Discount instance by $user->subscription->type.
  }
}
class DiscountCalculator {
  // @see Delegation Pattern.
  public function calculate(Discount $discount, float $amount): float {
    return $discount->calculate($amount);
  }
}
class PaymentController extends Controller {
  // @call POST /payment
  public function paymentAction(): Payload {
    // ...

    $calculator = new DiscountCalculator();
    $discount   = $calculator->calculate(
      DiscountFactory::create($user),
      $grossTotal
    );
    $netTotal   = $grossTotal - $discount;

    // ...
  }
}

现在,DiscountCalculator类使用真实的计算器(实际成为其代表)并符合原理。因此,如果将来需要任何更改,我们将不再需要更改calculate()方法。我们可以简单地添加一个新的相关类(例如“金”订阅类型的GoldDiscount),然后根据需要更新出厂类。

3- liskov替代原理(LSP)

子类应该能够使用超类的所有功能,并且所有子类都应该是可用的,而不是它们的超类(通过具有父母领域和行为的相同领域和行为)。将不会使用继承类的所有功能,然后将有不必要的代码块,或者子类改变其超类方法的工作方式,那么此代码将更容易出错。

在这里,我们有一个用于使用文件的API端点POST /file,使用writeAction()方法编写文件的FileController类,以及相关工作的File / ReadOnlyFile类。< / p>

不良练习:

class File {
  public function read(string $name): string {
    // Read file contents & return all read contents.
  }
  public function write(string $name, string $contents): int {
    // Write file contents & return written size in bytes.
  }
}

class ReadOnlyFile extends File {
  // @override Changes parent behavior.
  public function write(string $name, string $contents): int {
    throw new Error('Cannot write read-only file!');
  }
}

class FileFactory {
  public static function create(string $name): File {
    // Create a File instance controlling the name &
    // deciding the instance type in some logic way.
  }
}
class FileController extends Controller {
  // @call POST /file
  public function writeAction(): Payload {
    // Auth / token check here.

    [$name, $contents]
      = $this->request->post(['name', 'contents']);

    // @var File
    $file = FileFactory::create($name);

    // We are blindly relying on write() method here,
    // & not doing any check or try/catch for errors.
    $writtenBytes = $file->write($name, $contents);

    return $this->jsonPayload(Status::OK, [
      'writtenBytes' => $writtenBytes
    ]);
  }
}

由于writeAction()(因此客户端代码)依赖File类及其write()方法,因此此操作无法正常工作,因为由于此依赖而没有检查任何错误。

因此,在这里,我们需要首先使用摘要来修复文件类,然后通过添加简单检查 Writanity

来修复客户端代码

更好的练习:

// Each item is in its own file.
interface IFile {
  public function isReadable(): bool;
  public function isWritable(): bool;
}
interface IReadableFile {
  public function read(string $name): string;
}
interface IWritableFile {
  public function write(string $name, string $contents): int;
}

// For the sake of DRY.
trait FileTrait {
  public function isReadable(): bool {
    return $this instanceof IReadableFile;
  }
  public function isWritable(): bool {
    return $this instanceof IWritableFile;
  }
}

class File implements IFile, IReadableFile, IWritableFile {
  use FileTrait;
  public function read(string $name): string {
    // Read file contents & return all read contents.
  }
  public function write(string $name, string $contents): int {
    // Write file contents & return written size in bytes.
  }
}

class ReadOnlyFile implements IFile, IReadableFile {
  use FileTrait;
  public function read(string $name): string {
    // Read file contents & return all read contents.
  }
}

class FileFactory {
  public static function create(string $name): IFile {
    // Create a File instance controlling the name &
    // deciding the instance type in some logic way.
  }
}
class FileController extends Controller {
  // @call POST /file
  public function writeAction(): Payload {
    // ...

    // @var IFile
    $file = FileFactory::create($name);

    // Now we have an option to check it,
    // whether file is writable or not.
    $writtenBytes = null;
    if ($file->isWritable()) {
      $writtenBytes = $file->write($name, $contents);
    }

    // ...
  }
}

LSP违规的信号;

  • 如果一个子类给无法实现的超类行为带来错误(例如:write()文件> readonlyfile:readonlyerror)。内部(覆盖)问题。
  • 如果子类无法实现超类行为,则无法实现(例如:write()file()方法> readonlyfile:“什么都不做...”)。内部(覆盖)问题。
  • 如果子类方法始终返回被覆盖方法的相同(固定或常数)值。这是一种非常微妙的违规行为,很难发现。内部(覆盖)问题。
  • 如果客户端知道子类型,则主要使用filedeleter的“ instanceof”关键字(例如:delete()方法:“如果file instance of readonlyfile,则返回”)。外部(客户)问题。

4-接口隔离原理(ISP)

界面不应被迫承担超过他们所需的责任,而且课程也不应被迫用他们不需要的功能实施接口。此原则类似于单一责任原则( SRP),SRP是关于类的,但ISP是关于接口的。因此,我们可以为不同的作业创建不同的接口以遵循该原则。

在这里,我们有一个用于通知用户的API端点POST /notifyNotifyController类带有notifyAction()方法用于向用户发送通知的方法,而Notifier类用于此工作实现INotifier,这是通过方法填充的 verbosely 。 >

不良练习:

// Each item is in its own file.
interface INotifier {
  public function sendSmsNotification(
    string $phone, string $subject, string $message
  ): void;
  public function sendPushNotification(
    string $devid, string $subject, string $message
  ): void;
  public function sendEmailNotification(
    string $email, string $subject, string $message
  ): void;
}

class Notifier implements INotifier {
  public function sendSmsNotification(
    string $phone, string $subject, string $message
  ): void {
    // Send a notification to given phone.
  }
  public function sendPushNotification(
    string $devid, string $subject, string $message
  ): void {
    // Send a notification to given device by id.
  }
  public function sendEmailNotification(
    string $email, string $subject, string $message
  ): void {
    // Send a notification to given email.
  }
}
class NotifyController extends Controller {
  // @call POST /notify
  public function notifyAction(): Payload {
    [$subject, $message]
      = $this->request->post(['subject', 'message']);

    // @var User (say it's okay, no 404)
    $user = $this->repository->getUserByToken($token_ResolvedInSomeWay);

    $notifier = new Notifier();
    if ($user->settings->isTrue('notifyViaSms')) {
      $notifier->sendSmsNotification($user->phone, $subject, $message);
    }
    if ($user->settings->isTrue('notifyViaPush')) {
      $notifier->sendPushNotification($user->devid, $subject, $message);
    }
    if ($user->settings->isTrue('notifyViaEmail')) {
      $notifier->sendEmailNotification($user->email, $subject, $message);
    }

    return $this->jsonPayload(Status::OK);
  }
}

由于我们将许多方法推入INotifier接口,因此我们远离这个座右铭:许多客户特定(或 niche )接口比一个通用(或简单地简单地)说 fat <​​/em>)接口。

因此,在这里,我们需要做的是将每个作业与有其自己的工作的单独接口分开。

更好的练习:

// Each item is in its own file.
interface ISmsNotifier {
  public function send(
    string $phone, string $subject, string $message
  ): void;
}
interface IPushNotifier {
  public function send(
    string $devid, string $subject, string $message
  ): void;
}
interface IEmailNotifier {
  public function send(
    string $email, string $subject, string $message
  ): void;
}

class SmsNotifier implements ISmsNotifier {
  public function send(
    string $phone, string $subject, string $message
  ): void {
    // Send a notification to given phone.
  }
}
class PushNotifier implements IPushNotifier {
  public function send(
    string $devid, string $subject, string $message
  ): void {
    // Send a notification to given device by id.
  }
}
class EmailNotifier implements IEmailNotifier {
  public function send(
    string $email, string $subject, string $message
  ): void {
    // Send a notification to given email.
  }
}
class NotifyController extends Controller {
  // @call POST /notify
  public function notifyAction(): Payload {
    // ...

    if ($user->settings->isTrue('notifyViaSms')) {
      $notifier = new SmsNotifier();
      $notifier->send($user->phone, $subject, $message);
    }
    if ($user->settings->isTrue('notifyViaPush')) {
      $notifier = new PushNotifier();
      $notifier->send($user->devid, $subject, $message);
    }
    if ($user->settings->isTrue('notifyViaEmail')) {
      $notifier = new EmailNotifier();
      $notifier->send($user->email, $subject, $message);
    }

    // ...
  }
}

让我们对NotifyController做得更好,使其更加程序化和更少的说服力,使用工厂通过用户设置获取通知器实例。

class NotifierFactory {
  public static function generate(User $user): iterable {
    if ($user->settings->isTrue('notifyViaSms')) {
      yield [$user->phone, new SmsNotifier()];
    }
    if ($user->settings->isTrue('notifyViaPush')) {
      yield [$user->devid, new PushNotifier()];
    }
    if ($user->settings->isTrue('notifyViaEmail')) {
      yield [$user->email, new EmailNotifier()];
    }
  }
}
class NotifyController extends Controller {
  // @call POST /notify
  public function notifyAction(): Payload {
    // ...

    // Iterate over available notifier instances & call send() for all.
    foreach (NotifierFactory::generate($user) as [$target, $notifier]) {
      $notifier->send($target, $subject, $message);
    }

    // ...
  }
}

5-依赖性反转原理(DIP)

子类的变化不应影响超类。在换句话说,高级类不应取决于低级类别,两者都应取决于抽象。同样,抽象不应取决于细节,细节(具体实现)应取决于抽象。因此,我们可以在高级和低级类之间使用抽象(主要是接口)来遵循该原理。

在这里,我们有一个用于记录某些应用程序活动的API端点POST /log,使用logAction() LogController类来记录这些活动的方法,而Logger类作为这些作品的服务。

不良练习:

// Each item is in its own file.
class FileLogger {
  public function log(string $data): void {
    // Put given log data into file.
  }
}

class Logger {
  public function __construct(
    private readonly FileLogger $logger
  ) {}
}
class LogController extends Controller {
  // @call POST /log
  public function logAction(): Payload {
    // Auth / token check here.

    $logger = new Logger();
    $logger->log($this->request->post('log'));

    return $this->jsonPayload(Status::OK);
  }
}

由于Logger类使用特定的记录器实现,因此这根本不是灵活的代码,如果随着时间的推移需要任何替换或其他日志服务,就会引起问题。每当我们要将日志发送到数据库或其他位置时,我们都需要更改Logger类。

因此,在这里,可以解决此问题的方法,从Logger构造函数中删除该混凝土类注入(详细实现)并使用抽象(接口)而不更改客户端代码。

更好的练习:

// Each item is in its own file.
interface ILogger {
  public function log(string $data): void;
}

class FileLogger implements ILogger {
  public function log(string $data): void {
    // Put given log data into file.
  }
}

// For future, maybe.
class DatabaseLogger implements ILogger {
  public function log(string $data): void {
    // Put given log data into database.
  }
}

class Logger {
  public function __construct(
    private readonly ILogger $logger
  ) {}
}