怎么实现接口解耦

tech2023-11-30  31

怎么实现接口解耦

If you’ve ever faced the same dilemma, surely you’ll grasp the sense of the following description: Cohesion and Decoupling, two seemingly naïve yet fundamental OOP cornerstones responsible for the incessant drumming that crushes the minds of most software developers, often crossing the boundary of what’s a tangible goal into utopia, away from the pragmatism and reality.

如果您曾经遇到过同样的难题,那么您肯定会掌握以下描述的含义: 凝聚力和解耦性 ,这两个看似幼稚却基本的OOP基石,不断的敲击打动了大多数软件开发人员的头脑,经常跨越乌托邦的切实目标的边界,脱离了实用主义和现实。

I don’t want to sound excessively ominous, but it’s fair to admit that designing cohesive, loosely-coupled systems is anything but a simple experience. In most cases, achieving such pristine features requires a lot of planning at the initial stages and going through the proper refactoring cycles later on as the system evolve and grow over time. Despite this, the OOP paradigm per se provides a set of well-trusted methodologies and principles right out of the box that together facilitate the implementation of cohesive, highly-decoupled components, even if they do have to be eventually refactored during their respective life cycles.

我不想听起来过于不祥,但可以公平地承认,设计具有凝聚力的,松耦合的系统绝非简单的体验。 在大多数情况下,要实现这种原始功能,就需要在初始阶段进行大量规划,并在以后随着系统的发展和发展而经历适当的重构周期。 尽管如此,OOP范本本身就提供了一组值得信赖的方法和原理,即使它们确实必须在各自的生命周期中最终进行重构,也可以共同促进内聚的,高度分离的组件的实现。 。

Consider, for instance, the use of a few segregated interfaces along with namespaced classes. When put to work in sync, they help wire up together components that are semantically related to each other, therefore increasing the overall level of cohesion exposed by a system. On the flip side, the definition of the interfaces themselves, either in the form of interface constructs or virtual/abstract classes, makes concrete implementations dependent upon abstractions, hence promoting the decoupling of components in such a way that they can be “rewired” differently at runtime, usually by means of Dependency Injection. When analyzed from a bird’s eye view, it becomes clear that the combined forces of good OOP practices and the proper mixture of design patterns are in most cases an effective solution that allows us to achieve a decent level of Cohesion and Decoupling.

例如,考虑使用几个分离的接口以及命名空间类。 当它们同步工作时,它们有助于将彼此在语义上相关的组件连接在一起,从而提高了系统所展现的整体凝聚力。 另一方面,接口本身的定义(以接口构造形式或虚拟/抽象类的形式)使具体实现依赖于抽象,因此促进了组件的解耦,从而可以以不同的方式“重新连接”组件在运行时,通常是通过依赖注入 。 从鸟瞰的角度来看,很明显,良好的面向对象操作规范和设计模式的适当结合在大多数情况下是一种有效的解决方案,可以使我们达到不错的凝聚力和去耦性。

Still, issues may arise when it’s necessary to “redefine” explicitly the way that one component, or even an entire layer, in a multi-tiered design must interplay with others. In a traditional system design, the dependencies between its decoupled components is defined through the respective interfaces, which are usually placed in the same component housing the concrete implementations. There might be a few particular situations where the dependencies must be reversed, in response to specific requirements. In such a case, the inversion in question can be easily accomplished by dropping the interface into one component and the corresponding implementations into a different one.

但是,当有必要明确“重新定义”多层设计中的一个组件甚至整个层必须与其他组件相互作用的方式时,可能会出现问题。 在传统的系统设计中,其解耦组件之间的依赖关系是通过相应的接口定义的,这些接口通常放置在容纳具体实现的同一组件中。 在某些特定情况下,必须根据特定要求反转依赖关系。 在这种情况下,可以通过将接口放到一个组件中,并将相应的实现放到另一个组件中来轻松实现所讨论的转换。

Considering that the concept may sound rather tangled and twisted at first, this shifting of protocols between components lives and breaths under the umbrella of a basic design pattern known as Separated Interface, which at least to some extent, goes hand in hand with the commandments of the Dependency Inversion Principle. Moreover, because of the pattern’s easy-going nature, it’s feasible to implement it in PHP without much fuss, so in this article I’ll show you how to exploit the benefits that separated interfaces bring to the table in a fairly approachable fashion.

考虑到概念一开始听起来可能很纠结和扭曲,所以在组件分离和生活之间的协议转移是在一种称为分离接口的基本设计模式的保护下进行的,该设计模式至少在某种程度上与依赖反转原理 。 此外,由于该模式易于使用,因此可以在PHP中实现而无需大惊小怪,因此,在本文中,我将向您展示如何利用分离的接口以相当容易的方式带来的好处。

闯入禁区–从域模型调用数据映射器逻辑 (Trespassing in Forbidden Terrain – Calling Data Mapper Logic from a Domain Model)

While the logic behind separated interfaces is pretty straightforward to digest, in all cases there must always be a compelling reason that justifies their implementation since the process of inverting the dependencies between components in an application is a far cry from being an arbitrary, whimsical decision.

尽管分离的接口背后的逻辑很容易理解,但是在所有情况下,总有一个令人信服的理由可以证明它们的实现是正确的,因为在应用程序中反转组件之间的依赖关系的过程与任意的,异想天开的决定相去甚远。

This raises an interesting question as well: why should one invert component dependencies if, after all, in their current state they work just fine? There may be some situations when the inversion can be used as a mechanism for increasing the level of decoupling between the involved components. In simple terms, by putting an interface in one module and the implementation somewhere else means to define explicitly the protocol that should be used in the outside world for accessing the module in question.

这也引起了一个有趣的问题:如果毕竟在当前状态下它们工作正常,为什么要倒置一个组件依赖关系呢? 在某些情况下,可以将反演用作增加所涉及组件之间去耦级别的机制。 简单来说,将接口放在一个模块中,将实现放在其他位置,则意味着明确定义在外部环境中访问所涉及模块所应使用的协议。

A good way to see the effects of separated interfaces in action is with some testable examples. So, let’s suppose that we have a simple Domain Model which needs to access some logic in the mapping layer. In a classic implementation this would pretty much be viewed as a cardinal sin since a Domain Model by definition must be unaware of any form of persistence down the line. When using a separated interface, though, we can achieve this and still keep the model completely agnostic.

通过一些可测试的示例,可以很好地了解分离的接口的作用。 因此,假设我们有一个简单的域模型,该模型需要访问映射层中的某些逻辑。 在经典的实现中,这几乎被视为主要罪恶,因为根据定义, 域模型必须不知道任何形式的持久性。 但是,当使用单独的接口时,我们可以实现这一目标,并且仍然使模型完全不可知。

To better understand the inner workings of this process, let’s begin implementing a basic model. Here’s how its first block looks:

为了更好地了解此过程的内部工作原理,让我们开始实现一个基本模型。 这是它的第一个块的外观:

<?php namespace Model; class AbstractEntity { protected $id; //set values for protected/private fields via the corresponding mutators public function __set($field, $value) { $this->checkField($field); $mutator = "set" . ucfirst(strtolower($field)); method_exists($this, $mutator) && is_callable(array($this, $mutator)) ? $this->$mutator($value) : $this->$field = $value; return $this; } // get values from protected/private fields via the corresponding accessors public function __get($field) { $this->checkField($field); $accessor = "get" . ucfirst(strtolower($field)); return method_exists($this, $accessor) && is_callable(array($this, $accessor)) ? $this->$accessor() : $this->$field; } protected function checkField($field) { if (!property_exists($this, $field)) { throw new InvalidArgumentException( "Setting or getting the field '$field'j is not valid for this entity."); } } // sanitize strings assigned to the fields of the entity protected function sanitizeString($value, $min = 2, $max = null) { if (!is_string($value) || strlen($value) < (integer) $min || ($max) ? strlen($value) > (integer) $max : false) { throw new InvalidArgumentException( "The value of the field accessed must be a valid string."); } return htmlspecialchars(trim($value), ENT_QUOTES); } // handle IDs public function setId($id) { if ($this->id !== null) { throw new BadMethodCallException( "The ID for this entity has been set already."); } if (!is_int($id) || $id < 1) { throw new InvalidArgumentException( "The ID of this entity is invalid."); } $this->id = $id; return $this; } public function getId() { return $this->id; } }

Considering that the goal here is to implement a rather prototypical model whose core functionality can be easily extended at will, the above AbstractEntity class behaves pretty much like a Layer Supertype which encapsulates logic common to all the domain subclasses that might be incidentally derived further down the road. This includes performing a few basic tasks, such as handling IDs, sanitizing strings, and mapping calls to mutators/accessors through __set() and __get().

考虑到这里的目标是实现一个相当典型的模型,该模型的核心功能可以轻松地随意扩展,因此上述AbstractEntity类的行为非常类似于Layer Supertype ,它封装了所有域子类共有的逻辑,这些逻辑可能是在子类的更深层次上衍生的。路。 这包括执行一些基本任务,例如处理ID, __set()字符串以及通过__set()和__get()将调用映射到更改器/访问器。

With the base class in place, now it’s fairly simple to create additional subclasses that define the data and behavior of concrete domain objects. How about setting a few charged with modeling the classic blog posts/comments relationship?

有了基类,现在就可以轻松地创建定义具体域对象的数据和行为的其他子类。 如何设置一些负责建模经典博客文章/评论关系的人?

<?php namespace Model; interface PostInterface { public function setId($id); public function getId(); public function setTitle($title); public function getTitle(); public function setContent($content); public function getContent(); public function getComments(); } <?php namespace Model; interface CommentFinderInterface { public function findById($id); public function findAll(array $conditions = array()); } <?php namespace Model; class Post extends AbstractEntity implements PostInterface { protected $title; protected $content; protected $comments; protected $commentFinder; public function __construct($title, $content, CommentFinderInterface $commentFinder) { $this->setTitle($title); $this->setContent($content); $this->commentFinder = $commentFinder; } public function setTitle($title) { $this->title = $this->sanitizeString($title); return $this; } public function getTitle() { return $this->title; } public function setContent($content) { $this->content = $this->sanitizeString($content); return $this; } public function getContent() { return $this->content; } public function getComments() { if ($this->comments === null) { $this->comments = $this->commentFinder->findAll( array("post_id" => $this->id)); } return $this->comments; } }

While admittedly the logic that drives the Post class is contrived, boiled down to just validating and assigning a few values to the fields of blog post objects, it exposes a detail worth stressing: it gets injected into its constructor an implementation of the previous CommentFinderInterface which is used for pulling in the associated comments for the persistence layer. This shows in a nutshell how to use a separated interface for reversing the dependencies between the model (at least a portion of it) and any other layer which must interact with it. In other words, the model is now a stand-up client module, responsible for defining the abstraction of its own accessing protocol.

尽管驱动Post类的逻辑是人为设计的,归结为只是验证并为博客文章对象的字段分配了一些值,但它暴露了一个值得强调的细节:将其注入到其构造函数中的是先前CommentFinderInterface的实现。用于为持久层提取关联的注释。 简而言之,它显示了如何使用分离的接口来反转模型(至少模型的一部分)与必须与模型交互的任何其他层之间的依赖关系。 换句话说,该模型现在是一个独立的客户端模块,负责定义其自身访问协议的抽象。

This simple change allows us to do all sorts of clever things without feeling a pinch of guilt, including ones that in any other context would be considered blasphemous. For example, we could make the model invoke mapping logic and retrieve the comments from the database while still keeping it unaware of the mapper itself. The model would always see the interface, not the implementer, be it a mapper or anything else.

这一简单的更改使我们可以做各种聪明的事情,而不会感到内,包括在任何其他情况下都被认为是亵渎神灵的事情。 例如,我们可以使模型调用映射逻辑并从数据库中检索注释,同时仍然不知道映射器本身。 该模型将始终看到接口,而不是实现者,无论是映射器还是其他任何东西。

Of course, before we see how to get the model calling logic in a data mapper, we should create the subclass charged with modeling the aforementioned comments. Here’s the additional derivative, along with the interface that it implements:

当然,在看到如何在数据映射器中获取模型调用逻辑之前,我们应该创建负责对上述注释进行建模的子类。 这是其他派生工具及其实现的接口:

<?php namespace Model; interface CommentInterface { public function setId($id); public function getId(); public function setContent($content); public function getContent(); public function setAuthor($author); public function getAuthor(); } <?php namespace Model; class Comment extends AbstractEntity implements CommentInterface { protected $content; protected $author; public function __construct($content, $author) { $this->setContent($content); $this->setAuthor($author); } public function setContent($content) { $this->content = $this->sanitizeString($content); return $this; } public function getContent() { return $this->content; } public function setAuthor($author) { $this->author = $this->sanitizeString($author); return $this; } public function getAuthor() { return $this->author; } }

There’s no need to peer too deep into the API of the earlier Comment class in order to grasp its underlying logic, as its responsibility is pretty narrow. It just creates comment objects in a valid state, which are hydrated via a couple of scalar fields such as the comment’s content and the corresponding author.

由于它的职责非常狭窄,因此无需深入了解早期Comment类的API即可掌握其底层逻辑。 它只是创建处于有效状态的注释对象,这些注释对象通过几个标量字段(例如注释的内容和相应的作者)混合在一起。

通过数据映射器从存储中提取注释 (Pulling in Comments from Storage through a Data Mapper)

Now it’s time to show off a little as we’ve managed to implement a functional Domain Model which clings off a separated interface and declares explicitly the contract that should be used for pulling in comments from the persistence layer. While this is all well and good, there’s still a missing piece in this puzzle; to get things finally rolling, we need to define at least one single implementer of this interface so that the comments can be injected into the internals of a few post objects.

现在该炫耀一下,因为我们设法实现了一个功能强大的域模型,该模型保留了一个单独的接口,并明确声明了应用于从持久层提取注释的协定。 尽管这一切都很好,但这个难题中仍然缺少一个环节。 为了使事情最终顺利进行,我们需要定义此接口的至少一个实现程序,以便可以将注释注入几个post对象的内部。

In this case in particular, the implementer will be a comment mapper as it reflects most of the use cases that might exist in production. Needless to say that it’s possible to switch over any other form of mediator to the persistence layer, such a web service or even an in-memory cache system. To keep the code samples a bit clearer and easier to follow, though, I’ll stick to a basic data mapper implementation.

特别是在这种情况下,实现者将是注释映射器,因为它反映了生产中可能存在的大多数用例。 不用说,可以将任何其他形式的中介程序切换到持久层,例如Web服务,甚至是内存中的缓存系统。 但是,为了使代码示例更加清晰和易于理解,我将坚持基本的数据映射器实现。

Sitting on a decent data mapper between the Domain Model and the database and being capable of at least of performing CRUD operations on a few domain objects is quite a burdening task, as the mapper not only must dissect the bones of objects themselves in order to persist them, but in turn it has to invert the process and reconstitute them on request. This duality of roles makes data mappers complex to set up and in most cases delegated, in the name of mental sanity, to the duties of a third-party library.

坐在域模型和数据库之间的一个体面的数据映射器上,并且至少能够对几个域对象执行CRUD操作是一项非常艰巨的任务,因为映射器不仅必须解剖对象本身的骨骼才能持久化它们,但是反过来,它必须颠倒这个过程,并根据要求重新构造它们。 角色的双重性使数据映射器的设置变得复杂,并且在大多数情况下,出于心理理智的考虑,将其委托给第三方库。

Even though, there’s no need to be have such a frantic attitude when it comes to implementing the data mappers since all that we need to do now is create a naïve one that implements the previous CommentFinderInterface so that the corresponding comments can be dragged out from the storage and injected into the internals of the associated post objects. The below one does exactly that:

即使,在实现数据映射器时也CommentFinderInterface如此CommentFinderInterface ,因为我们现在要做的就是创建一个简单的实现先前的CommentFinderInterface以便可以将相应的注释从列表中拖出。存储并注入关联的post对象的内部。 下面的代码正是这样做的:

<?php namespace Mapper; use LibraryDatabaseDatabaseAdapterInterface, ModelCommentFinderInterface, ModelNullComment, ModelComment; class CommentMapper implements CommentFinderInterface { protected $adapter; protected $entityTable = "comments"; public function __construct(DatabaseAdapterInterface $adapter) { $this->adapter = $adapter; } public function findById($id) { $this->adapter->select($this->entityTable, array("id" => $id)); if (!$row = $this->adapter->fetch()) { return null; } return $this->loadComment($row); } public function findAll(array $conditions = array()) { $this->adapter->select($this->entityTable, $conditions); $rows = $this->adapter->fetchAll(); return $this->loadComments($rows); } protected function loadComment(array $row) { return new Comment($row["content"], $row["author"]); } protected function loadComments(array $rows) { $comments = array(); foreach ($rows as $row) { $comments[] = $this->loadComment($row); } return $comments; } }

For the sake of brevity, I decided to keep the implementation of CommentMapper a simple wrapper around the findById() and findAll() methods. However, if you feel adventurous and want to add some extra methods that allow it to insert, update, and delete comments from the associated table, go ahead and have some fun leveraging your remarkable coding skills.

为了简洁起见,我决定将CommentMapper的实现保持为findById()和findAll()方法的简单包装。 但是,如果您喜欢冒险,并且想添加一些额外的方法来允许它从关联表中插入,更新和删除注释,请继续并利用您非凡的编码技巧来获得一些乐趣。

Naturally, the mapper’s most appealing facet is its expressive, not to say explicit, dependency toward the protocol declared by the model when it comes to retrieving comments from the storage. The actual beauty of this schema is because of the fact that although the dependency is prescriptive, it relies on the abstraction of an interface. This means that neither the model is aware of the mapper nor the mapper has an overall vision of the model. That’s pretty motivational considering that the level of decoupling between the two components have been nicely enhanced by using just a simple abstraction.

自然,当从存储中检索注释时,映射器最吸引人的方面是其表达性,而不是明确地依赖于模型声明的协议。 这种模式的真正优点是因为尽管依赖项是规定性的,但它依赖于接口的抽象。 这意味着模型既不了解映射器,也不了解映射器对模型的整体了解。 考虑到仅使用一个简单的抽象就已很好地增强了两个组件之间的去耦级别,这是非常有动机的。

Of course, all this talk might sound like just a lot of babbling if I didn’t back it up with a concrete example.

当然,如果我不举一个具体的例子,所有这些谈话可能听起来像是胡言乱语。

<?php use LibraryLoaderAutoloader, LibraryDatabasePdoAdapter, MapperCommentMapper, ModelPost, ModelComment; require_once __DIR__ . "/Library/Loader/Autoloader.php"; $autoloader = new Autoloader; $autoloader->register(); $adapter = new PdoAdapter("mysql:dbname=test", "fancyusername", "hardtoguesspassword"); $commentMapper = new CommentMapper($adapter); $post = new Post("A naive sample post", "This is the content of the sample post", $commentMapper); $post->id = 1; echo $post->title . " " . $post->content . "<br>"; foreach ($post->comments as $comment) { echo $comment->content . " " . $comment->author . "<br>"; }

Is it just me or does passing along the data mapper into a post object look like something that would make me cringe? There’s no reason to panic; both elements have been properly shielded from each other through the contract of a separated interface. Furthermore, since the mapper is merely an element taking advantage of Polymorphism, it’d be really easy to swap out other implementations of the CommentFinderInterface.

仅仅是我还是将数据映射器传递到发布对象中看起来像让我畏缩的东西? 没有理由惊慌。 通过分离接口的收缩,两个元素都已适当地彼此屏蔽。 此外,由于映射器仅仅是利用多态性的元素,因此交换掉CommentFinderInterface其他实现真的很容易。

In either case, the example should be illustrative enough for understanding what’s behind the rather bullying fences of separated interfaces, and how its implementation in a few specific use cases can be pretty useful when it comes to reversing the dependencies between one or more components while keeping a neat level of decoupling throughout the whole system.

无论哪种情况,该示例都应具有足够的说明性,以理解分离的接口颇为欺人的背后是什么,以及在反转一个或多个组件之间的依赖性的同时,在几个特定用例中的实现如何非常有用。整个系统中的去耦水平都很高。

结束语 (Closing Remarks)

If you’re like me, probably the first time you heard something about separated interfaces you thought about them pretty much like another twisted, hard-to-understand pattern, with little or no real application in the real world. Despite such a knee-jerk reaction, the logic that drives separated interfaces is a lot easier to digest than one might think. Plus, the slew of benefits that separated interfaces bring to the table are indeed plethoric as they allow you to set up in a straightforward fashion loosely-coupled client modules which define the contracts that should be used by other packages.

如果您像我一样,可能是您第一次听到有关分离接口的信息时,就非常像另一种难以理解的扭曲模式,在现实世界中几乎没有实际应用。 尽管做出了这样的举动,但驱动分离接口的逻辑比人们想象的要容易得多。 此外,分离的接口带给桌面的诸多好处的确是多方面的,因为它们使您能够以一种直接的方式来建立松散耦合的客户端模块,该模块定义了其他程序包应使用的合同。

As with the overwhelming list of design patterns that exist out there, having a blind worshiping attitude about separated interfaces just doesn’t make much sense, hence they should be used with due caution and only if the situation warrants reversing the dependencies between application components. Appeal to your common sense, don’t be whimsy, and use them when you actually think they’re going to have a positive impact in your design.

就像那里存在的绝大多数设计模式一样,对分离的接口采取盲目崇拜的态度并没有多大意义,因此应谨慎使用它们,并且仅当情况需要逆转应用程序组件之间的依赖关系时才应使用。 呼吁您的常识,不要一时兴起,当您实际认为它们将对您的设计产生积极影响时,请使用它们。

Image via kuppa / Shutterstock

图片来自kuppa / Shutterstock

翻译自: https://www.sitepoint.com/decoupling-interfaces-from-implementation/

怎么实现接口解耦

最新回复(0)