避免在@AfterMapping中使用DRY



对于同一个Bird实体,我有两个MapStruct Mapping类,它们的方法(customCodeBirdMapping)几乎做同样的事情,但返回类型略有不同。

BirdViewMapper

@Mapper(componentModel = "spring")
public interface BirdViewMapper {

    @Mapping(source = "bird.listHealthCheck", target = "listHealthCheck")
    BirdViewDTO birdToBirdViewDTO(Bird bird);
    @AfterMapping
    default void customCodeBirdMapping(Bird source, @MappingTarget BirdViewDTO target) {
        if (!source.getListTransmitter().isEmpty()) {
            Transmitter trans = (source.getListTransmitter()
                    .stream()
                    .max(Comparator.comparing(Transmitter::getDateAttached)).get());
            target.setChannel(trans.getChannel());
            target.setChannelOffset(trans.getChannelOffset());
        }
        if (!source.getListPIT().isEmpty()) {
            PIT pit = (source.getListPIT()
                    .stream()
                    .max(Comparator.comparing(PIT::getDateInserted)).get());
            target.setPitCode(pit.getCode());
        }
        if (!source.getListHealthCheck().isEmpty()) {
            HealthCheck healthCheck = (source.getListHealthCheck()
                    .stream()
                    .max(Comparator.comparing(HealthCheck::getCatchDate)).get());
            target.setDateLastHealthCheck(healthCheck.getCatchDate());
        }
    }
}

BirdMapper

@Mapper(componentModel = "spring")
public interface BirdMapper {
    BirdDashboardDTO birdToBirdListDTO(Bird bird);
    @AfterMapping
    default void customCodeBirdMapping(Bird source, @MappingTarget BirdDashboardDTO target) {
        if (!source.getListTransmitter().isEmpty()) {
            Transmitter trans = (source.getListTransmitter()
                    .stream()
                    .max(Comparator.comparing(Transmitter::getDateAttached)).get());
            target.setChannel(trans.getChannel());
            target.setChannelOffset(trans.getChannelOffset());
        }
        if (!source.getListPIT().isEmpty()) {
            PIT pit = (source.getListPIT()
                    .stream()
                    .max(Comparator.comparing(PIT::getDateInserted)).get());
            target.setPitCode(pit.getCode());
        }
        if (!source.getListHealthCheck().isEmpty()) {
            HealthCheck healthCheck = (source.getListHealthCheck()
                    .stream()
                    .max(Comparator.comparing(HealthCheck::getCatchDate)).get());
            target.setDateLastHealthCheck(healthCheck.getCatchDate());
        }
    }
}

请注意,BirdViewMapper和BirdMapper都使用@AfterMapping来设置一些子实体的最近值,但返回/目标类型不同。

Bird dto用于不同的视图,一个用于鸟类列表(只有几个字段),一个用于单个鸟类的视图(多个字段)。

我在我的映射器类上使用customCodeBirdMapping方法违反了DRY规则-有没有更好的方法-一种避免重复的方法?

在您的两个示例中,您展示了您有一个用于某种类型的birdto的Mapper。有多种方法可以解决你的问题。

  1. 将两个映射器合并为一个。反正他们是有关系的。然后只需在映射器中编写常用的java私有方法。这些可以在@AfterMapping方法中调用。这样可以防止代码重复。最简单的方法显然是让dto继承值。

    @Data
    public class BirdDTOParent{
        private Channel channel;
        // fields shared by BirdViewDTO and BirdDashboardDTO
    }
    @Data
    public class BirdViewDTO exntends BirdDTOParent{
        // specific fields to BirdViewDTO 
    }
    @Data
    public class BirdDashboardDTO extends BirdDTOParent{
        // specific fields to BirdDashboardDTO  
    }
    

    和mapper中的方法:

    @AfterMapping
    default void customCodeBirdMapping(Bird source, @MappingTarget BirdDashboardDTO target) {
       mappingHelper(source,target);
    }
    private void mappingHelper(Bird source, BirdDTOParent target) {
      if (!source.getListTransmitter().isEmpty()) {
            Transmitter trans = (source.getListTransmitter()
                    .stream()
                    .max(Comparator.comparing(Transmitter::getDateAttached)).get());
            target.setChannel(trans.getChannel());
            target.setChannelOffset(trans.getChannelOffset());
        }
        ...
    }
    

    如果你不想这样做,那么你可以在运行时强制转换。然而,这要丑陋得多。如果dto有这么多共同点,你应该让它们继承

    的值。
    private void mappingHelper(Bird source, Object target) {
        if (!source.getListTransmitter().isEmpty()) {
            Transmitter trans = (source.getListTransmitter()
                    .stream()
                    .max(Comparator.comparing(Transmitter::getDateAttached)).get());
        if(target instanceof BirdDashboardDTO ) {
            ((BirdDashboardDTO) target).setChannel(trans.getChannel());
            ((BirdDashboardDTO) target).setChannelOffset(trans.getChannelOffset()); 
        } else if (target instanceof BirdViewDTO) {
            ((BirdViewDTO) target).setChannel(trans.getChannel());
            ((BirdViewDTO) target).setChannelOffset(trans.getChannelOffset());
        }
        ...
    }
    
  2. 您可以让映射器使用其他映射器的逻辑。

    @Mapper(componentModel = "spring", uses = {SomeOtherMapper.class})
    public interface BirdMapper {
    

    即使是你用@Named注解定义的函数,也可以在原始的和使用"mapper

  3. 使用静态助手类或spring bean。Mapstruct允许将spring bean注入到映射器中。为了实现这个Mapstruct,你必须将Mapper从一个接口更改为一个抽象类——我如何在生成的Mapper类中注入spring依赖项呢?

我认为最干净的方法是第一个。但我想这取决于你:)

编辑

第一种方法可以进一步细化,为父类创建一个@AfterMapping方法,并为每个子实现添加@AfterMapping方法

@Mapper(config = MappingConfig.class)
public interface BirdMapper {

    BirdViewDTO entityToViewDTO(BirdEntity birdEntity);
    BirdDashboardDTO entityToDashboardDTO(BirdEntity birdEntity);
    @AfterMapping
    default void mapParent(BirdEntity source, @MappingTarget BirdDTOParent target){
        System.out.println("aaaaa");
    }
    @AfterMapping
    default void mapViewDTO(BirdEntity source, @MappingTarget BirdViewDTO target){
        System.out.println("bbbbbb");
    }
    @AfterMapping
    default void mapDashboardDTO(BirdEntity source, @MappingTarget BirdDashboardDTO target){
        System.out.println("cccccc");
    }
}

生成的映射文件如下所示

@Generated(
value = "... mapstruct generated string ...",
date = "... mapstruct generated string ...",
comments = "... mapstruct generated string ...")
@Component
public class BirdMapperImpl implements BirdMapper {
    @Override
    public BirdViewDTO entityToViewDTO(BirdEntity birdEntity) {
        if ( birdEntity == null ) {
            return null;
        }
        BirdViewDTO birdViewDTO = new BirdViewDTO();
        birdViewDTO.setName( birdEntity.getName() );
        birdViewDTO.setViewProperty( birdEntity.getViewProperty() );
        mapParent( birdEntity, birdViewDTO );
        mapViewDTO( birdEntity, birdViewDTO );
        return birdViewDTO;
    }
    @Override
    public BirdDashboardDTO entityToDashboardDTO(BirdEntity birdEntity) {
        if ( birdEntity == null ) {
            return null;
        }
        BirdDashboardDTO birdDashboardDTO = new BirdDashboardDTO();
        birdDashboardDTO.setName( birdEntity.getName() );
        birdDashboardDTO.setDashboardProperty( birdEntity.getDashboardProperty() );
        mapParent( birdEntity, birdDashboardDTO );
        mapDashboardDTO( birdEntity, birdDashboardDTO );
        return birdDashboardDTO;
    }
}

透视

从角度来看,Dan Abramov关于WET代码库的演讲可能值得一看。

DRY或不DRY

考虑重构重复数据删除时(DRY)在一些代码中,人们应该问自己相关的代码段是否恰好相同,或者它们是否必须相同。在您的示例中,两个bird类是否根据定义共享相同的代码?如果它们不这样做,一个小的改变可能会导致两段代码分离,如果它们被合并/重复数据删除/干燥,那么DRY代码最终会成为一个整体,或者无论如何都必须分离出来。在您的示例中,根据定义,这两个类似乎是相同的,因此它们是重复数据删除的主要候选对象。然而,在大多数情况下,这两段相似的代码并不完全相同,因此答案并不那么明确。

如何干燥

在重复数据删除代码时,要小心,因为类(或一般的OOP)是一种工具,可以很容易地使所有东西看起来像钉子(这是对古老谚语的曲解)。在这种情况下,引入一个通用的超类可能很有吸引力,但静态实用函数可能更灵活、更合适。

最新更新