介绍
这种架构原理是由[1] Alistair Cockburn于2005年提出的。这是DDD(域驱动设计架构)的多种形式之一。 目的是找到一种方法来解决或减轻由面向对象编程引入的一般警告。 这也称为端口和适配器体系结构。 六边形的概念与六面结构无关,也与几何形状无关。 六边形确实有六个边,但其目的是说明许多端口的概念。 此形状也更容易分为两部分,并可以用作应用程序业务逻辑的表示。 想法是将我们要开发的应用程序分为三个部分。 左,核心和右。 进入一个更广泛的概念,我们想区分内部和外部的概念。 内部是业务逻辑,应用程序本身和外部是我们用来连接应用程序并与之交互的内容。
核心
应用程序的核心可以定义为应用程序的业务逻辑发生的地方。 应用程序核心接收数据,对其执行操作,并可以选择与其他外部方(如数据库或持久性实体)进行通信。
港口
端口代表应用程序的边界。 通常,它们被实现为供外部各方使用的接口。 尽管它们共享同一域,但它们的实现位于应用程序外部。
主要端口
主端口也称为驱动端口。 这些是外部与应用程序核心之间的第一个通信点。 因此,它们仍然可以称为入站端口。 这些端口“驱动”应用程序。 这意味着这是请求到达应用程序的地方。 在这种情况下,上游包含数据,而下游包含对该请求的响应。 主端口位于六角形的左侧。
次要端口
相反,辅助端口称为从动端口。 它们也位于外部,并且与位于六角形右侧的主端口对称。 应用程序核心使用辅助端口将数据上游上传到外部实体。 例如,需要数据库中数据的操作将使用辅助端口。 应用程序“驱动”端口以获取数据。 因此,下游包含来自右侧外部实体的数据。 由于应用程序将数据发送到外部,因此辅助端口也称为出站端口。
转接器
适配器本质上是端口的实现。 它们不应在代码的任何地方直接调用
主适配器
主适配器是主端口的实现。 这些完全独立于应用程序核心。 这代表了该体系结构的更明显优势之一。 通过在外部实现端口,我们可以控制实现方式。 这意味着我们可以自由地实现将数据传递到应用程序的不同形式,而不会影响应用程序本身。 就像端口一样,主适配器也可以称为驱动适配器。 REST服务和GUI就是这样的例子。
辅助适配器
辅助适配器是辅助端口的实现。 与主适配器一样,它们也独立于应用程序核心,具有相同的明显优势。 在更多情况下,我们发现在次端口中存在有关技术选择的更困难的问题。 通常,总是存在一个关于我们实际上如何实现持久层的问题。 选择正确的数据库,文件系统或任何其他内容可能很困难。 通过使用适配器,我们可以根据需要轻松地互换适配器。 这意味着无论实现如何,我们的应用程序也不会更改。 它只会知道它需要调用的操作,而不会知道它们是如何实现的。 与主适配器相同,辅助适配器也称为从动适配器。
实作
该应用程序管理歌词的存储系统。 它存储相关的艺术家和歌词文本。 然后,我们可以访问将随机显示特定歌词和相关艺术家的端点。 我们还可以执行所有其他POST,PUT,DELETE和GET操作,以通过JPA(Java Persistence API)存储库执行CRUD(创建,读取,更新,删除)操作。 我故意通过通用操作使此应用程序简单。 理解核心,域和基础结构等概念非常重要,这也是我使用所有这些操作创建此应用程序的原因。
结构体
在前面的几点中,我提到了一些对于设置我们的应用程序很重要的关键字。 由于这是一个演示应用程序,因此一些注意事项很重要。 我希望应用程序简单,但也要代表大多数应用程序在其核心功能。 大多数应用程序都有一个持久性框架,一个业务模型和一个表示层。 在此示例中,我选择了Spring以使用MVC(模型视图控制器)模式。 如果您想了解有关MVC [2]模式的更多信息,请单击参考部分中的以下链接。 要运行该应用程序,我们将使用Spring Boot。 为了访问我们的数据库,我使用了JPA存储库,最后我选择了内存数据库中的H2。 您还将看到我正在使用JUnit Jupiter [3],Mockito [4]和AssertJ [5]。 这些不在本教程的讨论范围之内,但是,如果您有兴趣了解有关这些框架的更多信息,请遵循以下参考部分中的链接。
xmlns= "http://maven.apache.org/POM/4.0.0" xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation= "http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" >
4.0.0
pom
favourite-lyrics-domain
favourite-lyrics-core
favourite-lyrics-jpa
favourite-lyrics-starter
favourite-lyrics-test
favourite-lyrics-rest
org.springframework.boot
spring-boot-starter-parent
2.2.2.RELEASE
org.jesperancinha.lyrics
favourite-lyrics
0.0.1-SNAPSHOT
favourite-lyrics
Favourite Lyrics App
13
1.4.200
1.18.10
5.2.2.RELEASE
org.jesperancinha.lyrics
favourite-lyrics-domain
${project.version}
org.jesperancinha.lyrics
favourite-lyrics-core
${project.version}
org.jesperancinha.lyrics
favourite-lyrics-rest
${project.version}
org.jesperancinha.lyrics
favourite-lyrics-jpa
${project.version}
com.h2database
h2
${h2.version}
runtime
org.springframework
spring-tx
${spring-tx.version}
org.projectlombok
lombok
${lombok.version}
true
域
让我们来看看我们需要的域名。 在这种情况下,域就是我们可以在应用程序核心和端口之间共享的任何内容。
首先要做的是定义我们希望如何传输数据。 在我们的情况下,我们通过DTO(数据传输对象)执行此操作:
@AllArgsConstructor
@Builder
@Data
@NoArgsConstructor
public class LyricsDto {
private String lyrics ;
private String participatingArtist ;
}
通常,您可能需要一个可以在整个体系结构中传播的异常。 这是一种有效的方法,但也非常简单。 对此的进一步讨论将需要撰写新文章,并且超出了本文的范围:
public class LyricsNotFoundException extends RuntimeException {
public LyricsNotFoundException ( Long id ) {
super ( "Lyrics with id %s not found!" . formatted ( id ));
}
}
此外,在这里您可以创建出站端口。 就我们而言,就这一点而言,我们知道我们想要持久性,但是我们对持久性的实现方式不感兴趣。 这就是为什么我们现在仅创建一个接口的原因。
public interface LyricsPersistencePort {
void addLyrics ( LyricsDto lyricsDto );
void removeLyrics ( LyricsDto lyricsDto );
void updateLyrics ( LyricsDto lyricsDto );
List < LyricsDto > getAllLyrics ();
LyricsDto getLyricsById ( Long lyricsId );
}
注意,我们的接口声明了所有必要的CRUD方法。
核心
Core与Domain携手合作。 它们都可以合并到一个模块中。 但是,这种分离非常重要,因为它使核心仅实现业务逻辑。
核心是我们找到服务接口的地方:
public interface LyricsService {
void addLyrics ( LyricsDto lyricsDto );
void removeLyrics ( LyricsDto lyricsDto );
void updateLyrics ( LyricsDto lyricsDto );
List < LyricsDto > getAllLyrics ();
LyricsDto getLyricsById ( Long lyricsId );
}
及其实现:
@Service
public class LyricsServiceImpl implements LyricsService {
private final LyricsPersistence lyricsPersistencePort ;
public LyricsServiceImpl ( LyricsPersistencePort lyricsPersistencePort ) {
this . lyricsPersistencePort = lyricsPersistencePort ;
}
@Override
public void addLyrics ( LyricsDto lyricsDto ) {
lyricsPersistencePort . addLyrics ( lyricsDto );
}
@Override
@Transactional
public void removeLyrics ( LyricsDto lyricsDto ) {
lyricsPersistencePort . removeLyrics ( lyricsDto );
}
@Override
public void updateLyrics ( LyricsDto lyricsDto ) {
lyricsPersistencePort . updateLyrics ( lyricsDto );
}
@Override
public List < LyricsDto > getAllLyrics () {
return lyricsPersistencePort . getAllLyrics ();
}
@Override
public LyricsDto getLyricsById ( Long lyricsId ) {
return lyricsPersistencePort . getLyricsById ( lyricsId );
}
}
内核的更改也表示业务逻辑的更改,这就是为什么对于小型应用程序而言,实际上没有理由将端口和适配器进一步分为不同的模块。 但是,复杂性的增加可能导致将核心拆分为其他具有不同职责的不同核心。 请注意,外部模块应仅使用该接口,而不应使用该接口的实现。
JPA
让我们创建一个实体。 首先,我们应该创建一个反映我们要保存的数据的实体。 在这种情况下,我们需要考虑参与艺术家和歌词本身。 因为它是一个实体,所以还需要其ID。 请注意,艺术家是可以设置在另一个实体中的字段。 在此示例中,我没有这样做,因为这将增加进一步的复杂性和另一个ER(实体关系)数据库范例,这超出了范围:
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table ( name = "LYRICS" )
@Data
public class LyricsEntity {
@Id
@GeneratedValue ( strategy = IDENTITY )
@Column
private Long lyricsId ;
@Column
private String lyrics ;
@Column
private String participatingArtist ;
}
现在,我们可以实现我们的JPA存储库实现。 这也被称为我们的出海口。 这是我们的CRUD的住所:
public interface LyricsRepository extends JpaRepository < LyricsEntity , Long > {
void deleteAllByParticipatingArtist ( String name );
LyricsEntity findByParticipatingArtist ( String Name );
LyricsEntity findByLyrics ( String Lyrics );
}
最后,我们可以实现我们的端口。 这是核心和JPA存储库之间的一步。 这是我们的适配器,它是我们想要访问JPA存储库的方式的实现:
@Service
public class LyricsJpaAdapter implements LyricsPersistencePort {
private LyricsRepository lyricsRepository ;
public LyricsJpaAdapter ( LyricsRepository lyricsRepository ) {
this . lyricsRepository = lyricsRepository ;
}
@Override
public void addLyrics ( LyricsDto lyricsDto ) {
final LyricsEntity lyricsEntity = getLyricsEntity ( lyricsDto );
lyricsRepository . save ( lyricsEntity );
}
@Override
public void removeLyrics ( LyricsDto lyricsDto ) {
lyricsRepository . deleteAllByParticipatingArtist ( lyricsDto . getParticipatingArtist ());
}
@Override
public void updateLyrics ( LyricsDto lyricsDto ) {
final LyricsEntity byParticipatingArtist = lyricsRepository . findByParticipatingArtist ( lyricsDto . getParticipatingArtist ());
if ( Objects . nonNull ( byParticipatingArtist )) {
byParticipatingArtist . setLyrics ( lyricsDto . getLyrics ());
lyricsRepository . save ( byParticipatingArtist );
} else {
final LyricsEntity byLyrics = lyricsRepository . findByLyrics ( lyricsDto . getLyrics ());
if ( Objects . nonNull ( byLyrics )) {
byLyrics . setParticipatingArtist ( lyricsDto . getParticipatingArtist ());
lyricsRepository . save ( byLyrics );
}
}
}
@Override
public List < LyricsDto > getAllLyrics () {
return lyricsRepository . findAll ()
. stream ()
. map ( this :: getLyrics )
. collect ( Collectors . toList ());
}
@SneakyThrows
@Override
public LyricsDto getLyricsById ( Long lyricsId ) {
return getLyrics ( lyricsRepository . findById ( lyricsId )
. orElseThrow (( Supplier < Throwable >) () -> new LyricsNotFoundException ( lyricsId )));
}
private LyricsEntity getLyricsEntity ( LyricsDto lyricsDto ) {
return LyricsEntity . builder ()
. participatingArtist ( lyricsDto . getParticipatingArtist ())
. lyrics ( lyricsDto . getLyrics ())
. build ();
}
private LyricsDto getLyrics ( LyricsEntity lyricsEntity ) {
return LyricsDto . builder ()
. participatingArtist ( lyricsEntity . getParticipatingArtist ())
. lyrics ( lyricsEntity . getLyrics ())
. build ();
}
}
这样就完成了右侧的应用程序实现。 请注意,我已经非常简单地实现了更新操作。 如果即将到来的DTO已通过participationArtist进行了并行处理,则更新歌词。 如果即将到来的DTO已通过歌词进行了并行处理,请更新partitioningArtist。 还要注意getLyricsById方法。 如果具有指定ID的歌词不存在,它将抛出域定义的LyricsNotFoundException。 所有机制都可以访问数据库。 接下来,我们将看到REST服务的实现,该服务使用入站端口将上游数据传输到应用程序。
休息
我使用了使用Spring MVC框架实现rest服务的典型方法。 本质上,我们需要的只是首先是一个接口,用于定义我们在请求中所需要的东西,这就是我们的inbout端口:
public interface LyricsController {
@PostMapping ( "/lyrics" )
ResponseEntity < Void > addLyrics ( @RequestBody LyricsDto lyricsDto );
@DeleteMapping ( "/lyrics" )
ResponseEntity < String > removeLyrics ( @RequestBody LyricsDto lyricsDto );
@PutMapping ( "/lyrics" )
ResponseEntity < String > updateLyrics ( @RequestBody LyricsDto lyricsDto );
@GetMapping ( "/lyrics/{lyricsId}" )
ResponseEntity < LyricsDto > getLyricsById ( @PathVariable Long lyricsId );
@GetMapping ( "/lyrics" )
ResponseEntity < List < LyricsDto >> getLyrics ();
@GetMapping ( "/lyrics/random" )
ResponseEntity < LyricsDto > getRandomLyric ();
}
最后是它的实现:
@Slf4j
@RestController
public class LyricsControllerImpl implements LyricsController {
private final LyricsServicePort lyricsServicePort ;
private final Random random = new Random ();
public LyricsControllerImpl ( LyricsServicePort lyricsServicePort ) {
this . lyricsServicePort = lyricsServicePort ;
}
@Override
public ResponseEntity < Void > addLyrics ( LyricsDto lyricsDto ) {
lyricsServicePort . addLyrics ( lyricsDto );
return new ResponseEntity <>( HttpStatus . CREATED );
}
@Override
public ResponseEntity < String > removeLyrics ( LyricsDto lyricsDto ) {
lyricsServicePort . removeLyrics ( lyricsDto );
return new ResponseEntity <>( HttpStatus . OK );
}
@Override
public ResponseEntity < String > updateLyrics ( LyricsDto lyricsDto ) {
lyricsServicePort . updateLyrics ( lyricsDto );
return new ResponseEntity <>( HttpStatus . OK );
}
@Override
public ResponseEntity < LyricsDto > getLyricsById ( Long lyricsId ) {
try {
return new ResponseEntity <>( lyricsServicePort . getLyricsById ( lyricsId ), HttpStatus . OK );
} catch ( LyricsNotFoundException ex ) {
log . error ( "Error!" , ex );
return new ResponseEntity <>( HttpStatus . NOT_FOUND );
}
}
@Override
public ResponseEntity < List < LyricsDto >> getLyrics () {
return new ResponseEntity <>( lyricsServicePort . getAllLyrics (), HttpStatus . OK );
}
@Override
public ResponseEntity < LyricsDto > getRandomLyric () {
final List < LyricsDto > allLyrics = lyricsServicePort . getAllLyrics ();
final int size = allLyrics . size ();
return new ResponseEntity <>( allLyrics . get ( random . nextInt ( size )), HttpStatus . OK );
}
}
在这里,我们实现了完整的典型休息服务,我们可以在其中创建歌词,更新歌词,删除歌词和阅读歌词。 我们可以通过三种不同的方式来完成后者。 我们可以阅读所有内容,通过id获取一个,或者随机获取一个。 在应用方面明智,我们已准备就绪。 此时,我们仍然没有Spring环境和Spring Boot Launcher。 接下来让我们看一下。
Spring靴
我们的应用程序需要启动器才能启动。 这是通过Spring Boot完成的:
@SpringBootApplication
@EnableTransactionManagement
public class LyricsDemoApplicationLauncher {
public static void main ( String [] args ) {
SpringApplication . run ( LyricsDemoApplicationLauncher . class );
}
}
然后,我们需要配置我们的环境,并确保Spring Boot知道H2和JPA环境。 您可以在application.properties文件中执行此操作:
# h2
spring.h2.console.path=/spring-h2-favourite-lyrics-console
spring.h2.console.enabled=true
# datasource
spring.datasource.url=jdbc:h2:file:~/spring-datasource-favourite-lyrics-url
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=sa
# hibernate
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true
对我们来说幸运的是,spring会寻找名称为schema.sql的架构文件。 因此,让我们创建一个非常基本的架构:
drop table if exists LYRICS ;
create table LYRICS
(
LYRICS_ID bigint auto_increment primary key not null ,
PARTICIPATING_ARTIST VARCHAR ( 100 ) NOT NULL ,
LYRICS VARCHAR ( 100 ) NOT NULL
);
春天,还寻找data.sql。 因此,让我们输入一些数据:
insert into LYRICS ( PARTICIPATING_ARTIST , LYRICS ) values ( 'William Orbit' , 'Sky fits heaven so fly it' );
insert into LYRICS ( PARTICIPATING_ARTIST , LYRICS ) values ( 'Ava Max' , 'Baby I '' m torn' );
insert into LYRICS ( PARTICIPATING_ARTIST , LYRICS ) values ( 'Faun' , 'Wenn wir uns wiedersehen' );
insert into LYRICS ( PARTICIPATING_ARTIST , LYRICS ) values ( 'Abel' , 'Het is al lang verleden tijd' );
insert into LYRICS ( PARTICIPATING_ARTIST , LYRICS ) values ( 'Billie Eilish' , 'Chest always so puffed guy' );
现在我们准备好了。 现在,所有有关启动应用程序和进行测试的内容。 所有方法都应易于通过卷曲或邮递员进行测试。 例如,您可以使用curl获取随机歌词:
$ curl localhost:8080/lyrics/random
{“lyrics”:”Chest always so puffed guy”,”participatingArtist”:”Billie Eilish”}
结论
这是大多数人了解六角形建筑原理的一种练习。 使用Spring只是一种可能。 您可以使用其他语言来实现此体系结构。 您可以找到C#,Python,Ruby和许多其他语言的原始示例。 在Java环境中,您也可以使用任何EE框架(例如JavaEE,JakartaEE或任何其他企业框架)来执行此操作。 关键是要始终记住将内部与外部隔离开来,并确保通过端口进行的通讯是清晰的,并且通过适配器的实现仍独立于应用程序核心。
我已经用代表不同职责的不同模块实现了该应用程序。 但是,您也可以在单个模块中实现此功能。 您可以尝试一下,您会发现原理并没有真正改变。 唯一的区别是,单独的模块允许您独立进行更改,并允许您创建模块的不同版本。 在一个模块中,您将必须同时发布所有内容,以进行代码更改。 但是,这两种方式都遵循并遵循此体系结构,因为最终的目的是制作接口并将其用于数据流而不是其实现。 它们的实现将在水下使用,并且可以互换而不影响内部(也称为应用程序核心)。
我已经将此应用程序的所有源代码都放在了GitLab中 。
感谢您的阅读!
From: https://dev.to/jofisaes/hexagonal-architecture-ports-and-adapters-1h4m