为什么我们需要正确地领域建模

简介: 为什么我们需要正确地领域建模

广而告知:我在GitChat的领域驱动战略设计实践达人课已经发布,目前正在进入预售期。点击文末“阅读原文”即可查看与订阅,也可以通过微信扫描下方的专属海报进行订阅。由于本文内容太长,你也可以移步至我的博客http://zhangyi.xyz获得更加阅读体验。记住我的博客,张逸的拼音全名,域名为xyz。


在领域驱动设计过程中,正确地进行领域建模是至为关键的环节。如果我们没有能够从业务需求中发现正确的领域概念,就可能导致职责的分配不合理,业务流程不清晰,出现没有任何领域行为的贫血对象,甚至做出错误的设计决策。

最初的实现


在一个结算系统中,业务需求要求导入一个结算账单模板的Excel文档,然后通过账单号查询该模板需要填充的变量值,生成并导出最终需要的结算账单。结算账单有多种,例如内部结算账单等。不同账单的模板并不相同,需要填充的变量值也不相同。

团队确实进行了领域建模,发现了如下的几个领域概念以及对应的服务和资源库对象:

  • InternalSettlementBill
  • InternalSettlementBillRepository
  • TemplateReplacement
  • BaseBillReviewExportTemplate
  • InternalSettlementBillService
  • BillReviewService

为了方便大家对这个设计有直观认识,我先贴出这些关键类型的实现代码:

package settlement.domain;
import lombok.Data;
@Data
public class InternalSettlementBill {
    private String billNumber;
    private String newAndOldBillNumber;
    private String flightIdentity;
    private String flightNumber;
    private String flightRoute;
    private String scheduledDate;
    private String passengerClass;
    private List<Passenger> passengers;
    private String serviceReason;
    private List<CostDetail> costDetails;
    private BigDecimal totalCost;
}
public interface InternalSettlementBillRepository {
    InternalSettlementBill queryByBillNumber(String billNumber);
}
package settlement.infrastructure.file;
import lombok.data;
import lombok.AllArgsConstructor;
@Data
@AllArgsConstructor
public class TemplateReplacement {
    private int rowIndex;
    private int cellNum;
    private String replaceValue;
}
pakcage settlement.domain;
import settlement.infrastructure.file.TemplateReplacement;
abstract class BaseBillReviewExportTemplate<T> {
    public final List<TemplateReplacement> queryAndComposeTemplateReplacementsBy(String billNumber) {
        T t = queryFilledDataBy(billNumber);
        return composeTemplateReplacements(t);
    }
    protected abstract T queryFilledDataBy(String billNumber);
    protected abstract List<TemplateReplacement> composeTemplateReplacements(T t);
}
pakcage settlement.domain;
import settlement.infrastructure.file.TemplateReplacement;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class InternalSettlementBillService extends BaseBillReviewExportTemplate<InternalSettlementBill> {
    @Resource
    private InternalSettlementBillRepository internalSettlementBillRepository;
    @Override
    protected InternalSettlementBill queryFilledDataBy(String billNumber) {
        return internalSettlementBillRepository.queryByBillNumber(billNumber);
    }
    @Override
    protected List<TemplateReplacement> composeTemplateReplacements(InternalSettlementBill t) {
        List<TemplateReplacement> templateReplacements = new ArrayList<>();
        templateReplacements.add(new TemplateReplacement(0, 0, t.getNewAndOldBillNumber()));
        templateReplacements.add(new TemplateReplacement(1, 0, t.getFlightIdentity()));
        templateReplacements.add(new TemplateReplacement(1, 2, t.getFlightRoute()));
        return templateReplacements;
    }
}
package settlement.domain;
import settlement.infrastructure.file.FileDownloader;
import settlement.infrastructure.file.PoiUtils;
import settlement.infrastructure.file.TemplateReplacement;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
@Service
public class BillReviewService {
    private static final String DEFAULT_REPLACE_PATTERN = "@replace";
    private static final int DEFAULT_SHEET_INDEX = 0;
    @Value("${file-path.bill-templates-dir}")
    private String billTemplatesDirPath;
    @Resource
    private PoiUtils poiUtils;
    @Resource
    private FileDownloader fileDownloader;
    @Resource
    private InternalSettlementBillService internalSettlementBillService;
    @Resource
    private ExportBillReviewConfiguration configuration;
    public void exportBillReviewByTemplate(HttpServletResponse response, String billNumber, String templateName) {
        try {
            String className = fetchClassNameFromConfigBy(templateName);
            List<TemplateReplacement> replacements = templateReplacementsBy(billNumber, className);
            HSSFWorkbook workbook = poiUtils.getHssfWorkbook(billTemplatesDirPath + templateName);
            poiUtils.fillCells(workbook, DEFAULT_SHEET_INDEX, DEFAULT_REPLACE_PATTERN, replacements);
            fileDownloader.downloadHSSFFile(response, workbook, templateName);
        } catch (Exception e) {
            logger.error("Export bill review by template failed, templateName: {}", templateName);
            e.printStackTrace();
        }
    }
    private List<TemplateReplacement> templateReplacementsBy(String billNumber, String className) {
        switch (className) {
            case "InternalSettlementBill":
                return internalSettlementBillService.queryAndComposeTemplateReplacementsBy(billNumber);
            default:
                return null;
        }
    }
    private String fetchClassNameFromConfigBy(String templateName) throws Exception {
        for (ExportBillReviewConfiguration.Item item : configuration.getItems()) {
            if (item.getTemplateName().equals(templateName)) {
                return item.getClassName();
            }
        }
        throw new Exception("can not found className by templateName in configuration file");
    }
}
package settlement.web.controllers;
import settlement.domain.*;
import settlement.web.model.ExportBillReviewRequest;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
@RestController
@RequestMapping("/bill-review")
public class BillReviewController {
    @Resource
    private BillReviewService billReviewService;
    @PostMapping("/export-template")
    public void exportBillReviewByTemplate(HttpServletResponse response, @RequestBody ExportBillReviewRequest request) {
        billReviewService.exportBillReviewByTemplate(response, request.getBillNumber(), request.getTemplateName());
    }
}

在这些类中,领域层的InternalSettlementBill表达的是“内部结算账单”领域概念,显然,如代码所示,这个对象是一个典型的贫血对象。BaseBillReviewExportTemplate类是一个抽象类,InternalSettlementBillService是继承它的子类。团队开发人员运用了模板方法模式,BaseBillReviewExportTemplate是获取各种结算账单的TemplateReplacement的一个抽象,因为开发人员发现这个过程是通用的:

  • 通过billNumber查询结算账单
  • 根据结算账单的值组装导出账单需要的模板替换对象

提炼领域知识


BaseBillReviewExportTemplate是一个领域服务,但它其实有一个很糟糕的命名,让人无法看懂它到底承担了什么职责?从命名看,它蕴含了多个概念:bill、review、export、template。究竟要做什么?是账单评阅的导出模板?还是导出账单评阅的模板?它代表了模板的名词概念,还是代表导出的领域行为?真是让人丈二和尚摸不着头脑。其实,阅读其代码实现,发现这个类要做的不过就是获得结算账单的所谓“模板替换(TemplateReplacement)”对象罢了?

TemplateReplacement表达的是什么概念呢?通过和团队成员沟通需求,结合代码我梳理出要实现的业务逻辑:

  • 用户首先导入一个结算账单模板的Excel工作薄;
  • Excel工作薄模板中对应的单元格中定义了一些变量值;系统需要从数据库中读取结算账单的信息,然后基于结算账单信息中的值去替换定义在模板中的这些变量;
  • 导出替换了变量值的Excel工作薄。

显然,替换模板中的变量值是我们期望完成的行为,其本质其实应该是一个模板变量:TemplateVariable。这个对象属于领域层的领域概念,不应该被定义在基础设施层。

如此,BaseBillReviewExportTemplate这个服务的命名就真可以说是名实不副了,不如更名为BaseBillTemplateVariablesComposer。但仔细看它的实现,我发现它不过就是通过一个Repository获得结算账单,再用结算账单中的对应值去组装模板变量。这个组装模板变量的行为放在这个服务中合适吗?遵循“信息专家模式”,InternalSettlementBill自身就具备了组装模板变量的信息,它才是承担组装职责的最佳专家啊!于是,我们可以转移职责:

package settlement.domain;
@Data
public class InternalSettlementBill {
    private String billNumber;
    private String newAndOldBillNumber;
    private String flightIdentity;
    private String flightNumber;
    private String flightRoute;
    private String scheduledDate;
    private String passengerClass;
    private List<Passenger> passengers;
    private String serviceReason;
    private List<CostDetail> costDetails;
    private BigDecimal totalCost;
    public List<TemplateVariable> composeVariables() {
        return Lists.newArrayList(
            new TemplateVariable(0, 0, this.newAndOldBillNumber()),
            new TemplateVariable(1, 0, this.flightIdentity()),
            new TemplateVariable(1, 2, this.flightRoute())
        );
    }
}

由于不同的结算模板都提供了不同的模板变量,我们就可以为其定义一个抽象的结算模板类型:

package settlement.domain;
public interface SettlementBill {
    List<TemplateVariable> composeVariables();
}
package settlement.domain;
public class InternalSettlementBill implements SettlementBill {}

在转移了组装模板变量的职责后,我已经看不出BaseBillTemplateVariablesComposer这个服务还有什么存在必要了!是的,它还承担了调用Repository去获得结算账单的职责,但在转移了组装模板变量的职责后,这个服务已经被弱化为只剩下查询职责了。这个查询结算账单的职责不是Repository提供的么?再对这个查询功能做一次封装有何意义?所以,在InternalSettlementBill摆脱“贫血对象”的身份后,看起来很酷的模板方法模式就变得没有任何价值了!


保持清晰的领域服务


再来看服务BillReviewService服务。从实现内容看,它才是真正负责导出结算账单的服务。这个服务的类名既含糊,实现代码又混乱,看起来它根本就不是一个纯粹的业务服务,因为它将业务逻辑与技术实现搅在了一起:既有Excel工作薄的获取,又有通过poi框架实现对单元格数据的填充,还有文件的下载,同时还通过结算账单获得了需要填充的模板变量值。

之所以会出现如此混乱的局面,除了没有有效地将技术实现与业务逻辑通过抽象去隔离之外,最关键的还是没有正确地建立领域模型。实际上,这里的结算账单模板不正是我们要操作的领域对象吗?实际上,我们要完成的业务功能是填充以及导出结算账单模板,而不是填充工作薄的单元格,自然也不是下载工作薄文件。所谓的“工作薄”概念,其实是实现层面的细节。

为保障设计的纯粹性,我们理当将结算账单模板定义为一个POJO类型的领域实体对象。即使需要将其导出为Excel工作薄,我们也可以令其持有数据,然后再将数据写入到工作薄。但是,由于结算账单模板的部分内容是通过模板文件直接导入的,除了需要替换的模板变量值之外,其余内容无需重新写入。如果硬要将其定义为纯粹的领域对象,就需要记录账单所有值在工作薄中的坐标,以便于在生成模板文件时正确地填充值;然而,这个模板的部分值在工作薄文件中已经存在了,再做一次无谓的填充就显得多余了。故而,我们需要做一个设计妥协,直接将poi框架的HSSFWorkbook作为结算账单模板对象内部持有的属性。让领域层依赖poi框架使得我们的领域模型不再纯粹,但为了技术实现的便利性,偶尔退让一步,也未为不可,只要我们能守住底线:保持系统架构的清晰层次。

一旦将工作薄对象赋予结算账单模板对象,则模板自身就不再有多种结算账单类别,因为它们的区别在于workbook。因此,我们没有必要为各种结算账单定义对应的模板对象,只需一个SettlementBillTemplate即可:

package settlement.domain;
import org.apache.poi.hsf.usermodel.*;
public class SettlementBillTemplate {
    private HSSFWorkbook workbook;
    private int sheetIndex;
    private String replacePattern;
    public SettlementBillTemplate(HSSFWorkbook workbook) {
        this(workbook, 0, "@replace");
    }
    public SettlementBillTemplate(HSSFWorkbook workbook, int sheetIndex, String replacePattern) {
        this.workbook = workbook;
        this.sheetIndex = sheetIndex;
        this.replacePattern = replacePattern;
    }
}

既然SettlementBillTemplate已经拥有了工作薄对象,为何不将填充模板变量值的功能赋予它呢?

public class SettlementBillTemplate {
    public void fillWith(SettlementBill bill) {
        HSSFSheet sheet = hssfWorkbook.getSheetAt(sheetIndex);
        bill.composeVariables().foreach( v -> {
            HSSFCell cell = sheet.getRow(v.getRowIndex()).getCell(v.getCellNum());
            String cellValue = cell.getStringCellValue();
            String replaceValue = v.getReplaceValue();
            if (replaceValue == null) {
                logger.warn("{} -> {} 替换值为空,未从数据库中查出相应字段值", cellValue, replaceValue);
                continue;
            }
            logger.info("{} -> {}", cellValue, replaceValue);
            if (cellValue.toLowerCase().contains(replacePattern)) {
                cell.setCellValue(cellValue.replace(replacePattern, replaceValue));
            } else {
                cell.setCellValue(replaceValue);
            }
        });
    }
}

现在,组装模板以及模板变量的工作已经完成,剩下的就是导出模板了。那么,谁该拥有导出模板的能力呢?虽然要导出的数据是SettlementBillTemplate拥有的,但它并不具备读取与下载工作薄文件的能力,既然如此,就只能将其放到领域服务。你看,我在分配表达领域逻辑的职责时,是将领域服务排在最后的顺序。

在此之前,我们还需要分离业务逻辑与技术实现。什么是业务逻辑?组装模板变量,组装模板以及导出模板都是业务逻辑,而读/写工作薄文件则是技术实现。既然如此,工作薄文件的读写职责就应该分配给基础设施层,然后在interfaces模块中定义它们的抽象接口。注:改进后的代码采用的代码结构皆以我的推荐为准。例如下面的接口定义是放在interfaces/file包中,实现放在gateways/file包中:

package settlement.interfaces.file;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
public interface WorkbookReader {
    HSSFWorkbook readFrom(String templateName);
}
public interface WorkbookWriter {
    void writeTo(HSSFWorkbook workbook, String targetPath);
}
package settlement.gateways.file;
import settlement.interfaces.file.WorkbookReader;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
public class ExcelWorkbookReader implements WorkbookReader {}
package settlement.gateways.file;
import settlement.interfaces.file.WorkbookWriter;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
public class ExcelWorkbookWriter implements WorkbookWriter {}


解决资源库多态的问题


还有一个问题没有解决,就是不同的结算账单是通过不同的Repository获得的。虽然模板已经没有类型的区别了,但用组装模板的模板变量值确实是不相同的。我们需要根据传入的templateName决定获得什么样的结算账单对象。但是,我们之前已经为InternalSettlementBill定义了对应的Repository,且它被定义为一个接口。是否可以将这个接口作为服务的属性,交给依赖注入去注入实现呢?例如:

public class SettlementBillTemplateExporter {
    @Resource
    private InternalSettlementBillRepository repository;
}

这是不对的。因为采用这样的定义,就意味着SettlementBillTemplateExporter服务只能查询InternalSettlementBill。要解决这个问题,似乎可以为资源库查询所有结算账单的行为定义一个统一接口,如SettlementBillFinder接口。然而,这一改进还是不能解决问题,因为决定实例化哪个Repository,是由调用者传递的templateName决定的。

在进行领域驱动设计时,为了隔离业务逻辑与技术实现,一般建议对技术实现尽可能做抽象,例如定义抽象的Repository接口,然后再利用依赖注入(Dependency Injection)完成对具体实现的注入。当我们使用框架来完成依赖注入时,就要求领域层的领域对象包括Repository、Service等对象都将由IoC框架来管理生命周期。这些IoC框架在带来依赖管理的便利时,也给我们的设计施加了一些约束。

一种解决办法是为资源库引入静态工厂:

package settlement.repositories;
public interface SettlementBillFinder {
    SettlementBill settlementBillBy(String billNumber);
}
package settlement.repositories;
public interface InternalSettlementBillRepository extends SettlementBillFinder {
    // other methods;
}
package settlement.domain;
import settlement.repositories.SettlementBillFinder;
import settlement.gateways.persistence.InternalSettlementBillMapper;
public class SettlementBillFinderFactory {
    public static SettlementBillFinder create(String templateName) {
        switch (templateName.toLowerCase()) {
            case "internal": 
                return new InternalSettlementBillMapper();
            // 其余分支略
        }
    }
}

然而,这样的设计是有问题的,因为它破坏了各层的职责。如上所示的SettlementBillFinderFactory是一个静态工厂,它需要创建具体的资源库对象,就意味着它依赖了基础设施层的类,即放在gateways/persistence中的InternalSettlementBillMapper类,而工厂自身却属于领域层。倘若采用这种做法的话,前面运用的依赖注入方法就变得没有意义了。在领域驱动设计的实现时,我们确实需要时时刻刻保持谨慎,防守住因为某种实现原因导致对整洁架构的破坏。

要做到这一点,可以考虑使用工厂方法模式,为工厂再定义一个抽象,转而将实现放到基础设施层。例如:

package settlement.domain;
public interface SettlementBillFinderFactory {
    SettlementBillFinder create();
}

可惜,这样一个多态的工厂让我们又走回了老路,因为需要调用者根据templateName决定使用哪一个具体工厂!这与通过templateName确定选择使用哪一个Repository又有何区别呢?反而引入了不必要的间接。

如果使用Spring来管理依赖注入,有一种做法是在服务中定义一个HashMap<String, String>,其中key值对应模板名,value值对应SettlementBillFinder实现子类的类名,然后在配置文件中配置这些映射信息。当服务传入一个templateName时,在这个hashmap中搜索获得的子类类型,然后利用反射来创建这些类。这一方法看起来保证了可扩展性,但实在太繁琐,太复杂,且反射的使用也在一定程度影响了性能。

有两种更简单的办法:

  • 让Repository的实现子类自行判断:如果我们将结算账单视为一个领域概念,就应该为其只抽象一个SettlementBillRepository。即无需为每种结算账单提供专有的资源库抽象。在定义Repository的查询方法时,将templateNamebillNumber都视为查询的条件,然后在实现类中根据templateName去查询不同的表,获得不同的结算账单领域对象。这个方法胜在简单,但较为死板不易扩展。
  • 采用惯例优于配置(CoC):依然将templateName作为服务方法的参数,也依旧提供一个SettlementBillRepository抽象,但在基础设施层为每个结算账单提供一个实现,且实现类遵循命名规则,即以{templateName}名字(单词首字母大写)为前缀,后缀统一为SettlementBillRepository,这样就可以基于规则组装类的类型名,再通过反射创建资源库对象。这一方法胜在能扩展,但依旧引入了反射。

这里我选择使用最简单的第一种方案,于是导出服务就变为:

package settlement.domain;
import settlement.repositories.SettlementBillRepository;
import settlement.interfaces.file.WorkbookReader;
import settlement.interfaces.file.WorkbookWriter;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import javax.servlet.http.HttpServletResponse;
public class SettlementBillTemplateExporter {
    @Service
    private WorkbookReader reader;    
    @Service
    private WorkbookWriter writer;
    @Repository
    private SettlementBillRepository repository;
    public void export(HttpServletResponse response, String templateName, String billNumber) {
        SettlementBill bill = repository.settlementBillBy(templateName, billNumber);
        SettlementBillTemplate template = new SettlementBillTemplate(reader.readFrom(templateName));
        template.fillWith(bill);
        writer.writeTo(response, template, templateName);
    }
}


尽可能保证领域层的整洁


事情还未结束,因为在领域服务的方法中出现了“恼人”的HttpServletReponse,它属于servlet包的核心对象。在干净的领域层中,怎么能容忍它的出现呢?(当然poi框架的依赖算是例外,前面已经分析过。)仔细分析,我发现在导出逻辑的实现中,其实仅仅用到了HttpServletResponse对象的getOutputStream()方法,返回的OutputStream对象则是JDK中java.io库中的一个类。既然如此,我们可以在领域层为这一需求提供一个抽象,例如定义接口OutputStreamProvider

package settlement.domain;
import java.io.OutputStream;
public interface OutputStreamProvider {
    OutputStream getOutputStream();
}

现在的领域服务就可以使用在领域层中自定义的OutputStreamProvider抽象。此外,还得加上一些异常处理:

package settlement.domain;
import settlement.domain.exceptions.*;
import settlement.repositories.SettlementBillRepository;
import settlement.interfaces.file.WorkbookReader;
import settlement.interfaces.file.WorkbookWriter;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
public class SettlementBillTemplateExporter {
    @Service
    private WorkbookReader reader;    
    @Service
    private WorkbookWriter writer;
    @Repository
    private SettlementBillRepository repository;
    public void export(OutputStreamProvider streamProvider, String templateName, String billNumber) {
        try {
            SettlementBill bill = repository.settlementBillBy(templateName, billNumber);
            SettlementBillTemplate template = new SettlementBillTemplate(reader.readFrom(templateName));
            template.fillWith(bill);
            writer.writeTo(streamProvider, template, templateName);
        } catch (DownloadTemplateFileException | OpenTemplateFileException ex) {
            throw new TemplateFileFailedException(ex.getMessage(), ex);
        }
    }
}

应用服务的定义就变得简单了:

package settlement.application;
import settlement.domain.SettlementBillTemplateExporter;
import settlement.domain.OutputStreamProvider;
import settlement.domain.exceptions.TemplateFileFailedException;
public class SettlementBillAppService {
    @Service
    private SettlementBillTemplateExporter exporter;
    public void exportByTemplate(OutputStreamProvider streamProvider, String templateName, String billNumber) {
        try {
            exporter.export(streamProvider, templateName, billNumber);
        } catch (TemplateFileFailedException ex) {
            throw new ApplicationException("Failed to export settlement bill file.", ex);
        }
    }
}

对应的,控制器的实现修改为:

package settlement.gateways.controllers;
import settlement.application.SettlementBillAppService;
import settlement.gateways.controllers.model.ExportBillReviewRequest;
import java.io.OutputStream;
import javax.servlet.http.HttpServletResponse;
@RestController
@RequestMapping("/bill-review")
public class BillTemplateController {
    @Resource
    private SettlementBillAppService settlementBillService;
    @PostMapping("/export-template")
    public void exportBillReviewByTemplate(HttpServletResponse response, @RequestBody ExportBillReviewRequest request) {
        exportService.exportByTemplate(response::getOutputStream, request.getTemplateName(), request.getBillNumber());
    }
}


代码的层次结构


如上代码的层次结构为:

settlement
    - application
        - SettlementBillAppService
    - domain
        - SettlementBill
        - TemplateVariable
        - InternalSettlementBill
        - SettlementBillTemplate
        - SettlementBillTemplateExporter
        - OutputStreamProvider
        - exceptions
            - TemplateFileFailedException 
            - DownloadTemplateFileException
            - OpenTemplateFileException
    - repositories(persistence技术实现的抽象)
        - SettlementBillRepository
    - interfaces(技术实现层面的抽象)
        - file
            - WorkbookReader
            - WorkbookWriter
    - gateways(包含技术实现层面)
        - persistence
            - SettlementBillMapper
        - file
            - ExcelWorkbookReader
            - ExcelWorkbookWriter
        - controllers
            - BillTemplateController 
            - model
                - ExportBillReviewRequest


总结


通过以上对代码的逐步演化,我们就此可以发现原来代码的诸多问题。这些问题往往是许多领域驱动设计新手容易犯的错误,包括:

  • 未能正确地表达领域知识
  • 贫血的领域模型
  • 层次不清,对DDD的分层架构理解混乱
  • 领域服务与应用服务概念混乱
  • 业务逻辑与技术实现纠缠在一起

回归这些问题的原点,其实还是在于团队没有正确地进行领域建模。如果还要继续深究,则在于团队没有为领域建立统一语言。我们看前面对模板导出业务的分析,每一个步骤都没有正确表达业务逻辑,因而获得的领域对象也是不正确的。又由于没有建立统一语言,导致对类和方法的命名都不能很好地体现领域概念,甚至导致某些表达领域概念的类被错误地放在了基础设施层。当我们在运用面向对象编程范式来实现领域驱动设计时,对OO思想的理解偏差与知识缺乏也反映到了代码的实现上,尤其是对“贫血模型”的理解,对职责分配的认知,都会直接反映到代码层面上。

最后,如果团队成员没有清晰地理解分层架构各层的含义,以及为何要引入分层架构,就无法守住分层架构各层的边界,最后就会导致业务复杂度与技术复杂度的混搭。若系统简单还好说,一旦系统的业务复杂度增加带来系统规模的扩大,不紧守架构层次的边界,就可能导致我们事先建立的分层架构名存实亡,代码变成大泥球,重新回归到太初的混沌世界。

相关文章
|
25天前
|
存储 测试技术 BI
软件体系结构 - 系统分析与设计(2.面向对象方法)
【4月更文挑战第6天】软件体系结构 - 系统分析与设计(2)
18 0
|
4月前
|
敏捷开发
软件设计中常用的开发模型
软件设计中常用的开发模型
|
9月前
|
设计模式 存储 测试技术
领域建模的体系化思维与6种方法论
本文希望能够通过总结过去自己对领域建模的一点粗浅经验给需要的同学能有些许启发,少走弯路。
54751 72
|
10月前
|
存储 人机交互 领域建模
领域模型随想
关于领域模型
86 0
|
12月前
|
存储 架构师 测试技术
「企业架构」企业架构和系统工程:组件或关系的准则
「企业架构」企业架构和系统工程:组件或关系的准则
|
12月前
|
存储 数据可视化 安全
「企业架构」企业架构框架图
「企业架构」企业架构框架图
|
存储 架构师 Java
架构决策的反模式(2)
架构决策的反模式
115 0
|
存储 缓存 架构师
架构决策的反模式(1)
架构决策的反模式
103 0
|
设计模式 容器
领域驱动设计总结——如何发掘深层次模型
本文为领域驱动设计系列总结的第四篇,主要对领域驱动设计概念做个介绍,本系列领域驱动设计总结主要是在Eric Evans 所编写的《领域驱动设计》 一书的基础上进行归纳和总结。本文主要介绍在领域驱动设计中如何发掘深层次模型。
233 0
|
编译器 领域建模 Scala
代数数据类型与领域建模
代数数据类型与领域建模