如何运用领域驱动设计 - 领域服务

概述

本文将介绍领域驱动设计(DDD)战术模式中另一个非常重要的概念 - 领域服务。在前面两篇博文中,我们已经学习到了什么是值对象和实体,并且能够比较清晰的定位它们自身的行为。但是在某些时候,你会发现某一些业务行为好像不容易落到单个实体或者值对象身上,并且会为放置这一部分业务逻辑而困惑。此时,你可能需要一个领域服务来完成操作。

那么,到底什么是领域服务呢?怎么发现领域中的领域服务呢?领域服务和传统的应用服务又有什么区别呢?本文将从不同的角度来带大家重新认识一下“领域服务”这个概念,并且给出相应的代码片段(本教程的代码片段都使用的是C#,后期的实战项目也是基于 DotNet Core 平台)。

什么是领域服务

在开始之前,还是说一点题外话吧:如果大家读过这个系列的前几篇文章,可能都会发现该系列的风格都是从原著的解析开始,然后结合了自身的一些案例和实际场景来为大家解读领域驱动中的一些概念。我也不知道这样的写作方式能不能让大家更清楚的理解,所以如果大家有什么建议的话可以在评论区留言,我一定会认真的听取大家的意见和建议。

在文章中,我会尽可能避免各类名称的简写(比如事件溯源,有些同学喜欢简写为ES),虽然简写有时候确实会很方便,但是会让人与人之间的沟通成本无形的增大,所以在我的博文中只要能不用简写的地方我都不会使用简写。

另外还有一点就是,可能前期属于概念性的东西比较多,所以就没有现成的github代码供大家参考,不多大家不用担心,在完成这几次的概念学习之后我们就开始我们的code time(●'◡'●)。

回到正题吧,什么是领域服务呢?看看原著原著《领域驱动设计:软件核心复杂性应对之道》中所提及到的领域服务的概念:

在某些情况下,最清楚、最实用的设计会包含一些特殊的操作,这些操作从概念上讲不属于任何对象。与其把它们强制地归于哪一类,不如顺其自然地在模型中引入一种新的元素,这就是Service(服务)。
当领域中的某个要的过程或转换操作不属于实体或值对象的自然职责时,应该在模型中添加一个作为独立接口的操作,并将其声明为Service.定义接口时要使用模型语言,并确保操作名称是UBIQUITOUS LANGUAGE中的术语。此外,应该将Service定义为无状态的。

额。。。。“李姐万岁”。这个概念不好理解的原因是因为:首先它假设我们寻找到了领域中一些“包含特殊的操作”,也就是说我们在此时已经具备了划分领域中各种对象以及其对应行为的能力,然后我们再来考虑提取出这个传说中的“service”(也就是我们本次的主题领域服务)。而往往现实则是,作为一个初学者,我们并不能合理的抽象出各个对象,并且也没有一个好的案例来进行体验性的思考。所以在读这个概念的时候就很迷惑,我们无法找到概念中的“这些操作”是什么东西,也就更不能理解这个Service是什么了。

复制代码

“在自己的私人飞机里面玩儿电子游戏是什么感觉呢?呃.....好像前提是我得有钱买一架飞机吧?”

从实际场景下手

我思考了很多种方法来表述“领域服务”,但是想了半天好像都不太容易能让人理解。所以该篇博文采用先从案例入手的思路,希望大家能从这个案例能够理解出领域服务的用处。

来回顾一下上一篇文章 《如何运用DDD - 实体》 中我们所提炼出来的一个实体对象:

复制代码

public class Itinerary
{public int ID { get; set; }public List<Person> Participants { get; set; }public List<Address> Places { get; set; }public ItineraryNote  Note { get; set; }public ItineraryTime TripTime { get; set; }public ItineraryStatus Status { get; set; }//ctorpublic void ChangeNote(string content){Note = new ItineraryNote(content);}
}

该实体对象表明了一次旅行的行程。目前作为示例,我们仅仅知道了在该领域中我们允许修改行程的备注信息,所以我们在上一篇文章中为它赋予了修改备注的一个行为。

根据项目的进展,我们现在捕获到了另一个需求:如果行程没有结束,用户访问到该行程,系统会根据用户目前所在的地点为用户推荐附近好吃的美食。

这是一个非常人性化以及好用的功能,也是该产品可以和其他同类型的产品系统竞争的优势。所以我们理应将它放置于领域来考虑。从该功能需求的描述来看,我们要做的是一个推荐美食的行为。但是让我们矛盾的是,推荐美食这一个动作,我们应该将它归属于谁呢?给旅程?让旅程实体来推荐美食?很显然,你并不会这么做。旅程仅仅关心的是本次旅行的基本信息,地点人物时间等,我们不会将推荐美食这一个动作给它,让它成为一个万能的机器。

来回顾一下上面所说的概念:“在某些情况下,最清楚、最实用的设计会包含一些特殊的操作,这些操作从概念上讲不属于任何对象。” 仔细读几遍,纳尼?这不是说的就是这个情况吗?在现在这个情况下,我们出现了一个推荐美食的操作,但是它却不属于任何对象。

当走到这一步时,可能我们已经有一点理解领域服务了。接下来,继续往下走。现在,我们已经明白了,可能我们需要一个Service来处理这一个操作。尝试着来建立一个 RecommendFoodsService 。

复制代码

public class RecommendFoodsService
{public List<RecommendFoodInfo> RecommendFoods(Itinerary currentItinerary){//todo}
}

在该领域服务中,有一个RecommedFoods的方法,它通过获取到当前的旅程,返回一个推荐美食的列表。它内部的实现方法可能是这样的:(在这里我们假设ItineraryPlaces中的最后一个地点就是我们的当前地点,而且我们已经有一个叫做餐厅 Restaurant 的实体,该实体提供了有关餐馆的一系列信息和行为。当然,你可以自己尝试建立餐厅这样一个实体,以便加深对实体章节的印象)

复制代码

public class RecommendFoodsService
{public List<FoodInfo> RecommendFoods(Itinerary currentItinerary){var recommendFoods = new List<FoodInfo>();//Get Last Addressint lastCountIndex = currentItinerary.Places.Count -1;var currentAddress = currentItinerary.Places[lastCountIndex];var nearbyRestaurants = Restaurants.Where(s=> s.Address.isNearby(currentAddress)).ToList();foreach(var restaurant in nearbyRestaurants){var food = restaurant.GetRankNoOneFood();recommendFoods.Add(new FoodInfo(food,restaurant.Address));}return recommendFoods;}
}

OK,到目前我们已经完成了一个演示版本的领域服务,在该服务中,我们通过获取到当前的旅程的位置,根据该位置,从系统中存在的餐馆集合中找到了距离该位置最近的餐厅,然后再将这些餐厅中排名评价最好的一道菜推荐给用户。

来看看上面的行为中出现了哪些东西,首先是我们的行程,然后是餐馆。通过合理的处理这两个实体之间的关系,我们完成了我们的一系列操作,并且返回了一个美食信息的集合(在这里美食信息我们定义为了一个值对象)。要注意,虽然我们里面包含了几个实体和几个值对象,以及使用了他们之间的不同行为,但是从推荐美食这一个行为来看,他们其实是一个整体,是密不可分的处理逻辑(敲重点!!!)。

更贴近现实

上面的版本我们将他作为一个演示版本来定义,是因为在实际的情况中,我们往往是通过存储库(Repository,有关该内容的介绍会在后期文章中介绍)来获取到实体集合的信息的,就如同上面代码中的Restaurants。有可能更贴近于我们现实中的代码是类似于下面这样,不过我们现在可以不用考虑这种写法,因为里面涉及到了存储库(仓储 Repository) 和 聚合根(AggregateRoot) 的概念,而现在我们只需要理解好领域服务就好了。

复制代码

 public List<FoodInfo> RecommendFoods(int currentItineraryID){var recommendFoods = new List<FoodInfo>();//Get Last Addressvar currentItinerary = itineraryRepository.Get(currentItineraryID);int lastCountIndex = currentItinerary.Places.Count -1;var currentAddress = currentItinerary.Places[lastCountIndex];var nearbyRestaurants = restaurantRepository.GetNearbyRestaurant(currentAddress);foreach(var restaurant in nearbyRestaurants){var food = restaurant.GetRankNoOneFood();recommendFoods.Add(new FoodInfo(food,restaurant.Address));}return recommendFoods;}

来吧,根据我们现在所理解和发现的内容,来看一下领域服务的一些特点:

  • 领域服务处理的是领域中的对象,比如实体、值对象等

  • 领域服务是负责对领域中一系列对象的编排处理

  • 当我们发现一个操作无法赋予一个实体或者值对象,且该操作又对业务流程很重要时,我们往往需要使用领域服务

  • 领域服务中的操作,从领域的角度来看,它是一个整体

如果你在进行下面的操作时,可能证明你需要一个领域服务:

  • 通过A和B,得到一个C。

  • A需要一个繁琐的内部策略才能得到一个结果B。

(ps: A,B,C指的是领域对象中的值对象或者实体)

领域服务VS应用服务

其实在使用领域驱动中,还有一个服务叫做应用服务,应用服务是划分在应用层的服务。而往往都是因为叫做服务,所以大家很难区分它与领域服务有什么区别,最终的结果就是要么造成应用服务很庞大(所有的逻辑编排都在该层处理了),要么就是应用服务很薄弱(就一句调用领域服务的代码)。无独有偶,当应用服务开始混乱时,领域服务也会变得混乱,因为原有领域服务的逻辑你可能给了应用服务,而应用服务的逻辑又给了领域服务。

在比较两者之前,来看一看传统领域驱动设计为大家提供的四层架构示意图:

从图中可以看到,应用层保持了对领域层的引用关系,也就是说在应用层中,可以访问到领域对象。所以让应用层也具备了编排领域对象的能力。这一点和我们的领域对象的特征相同了,所以在很多时候,大家对应用服务和领域服务的区分难度就加大了。

关于应用服务,因为在原著中我没有找到对应的关键语句,所以选取了网上的一些结论供大家参考:

应用服务是用来表达用例和用户故事(User Story)的主要手段。
应用层通过应用服务接口来暴露系统的全部功能。在应用服务的实现中,它负责编排和转发,它将要实现的功能委托给一个或多个领域对象来实现,它本身只负责处理业务用例的执行顺序以及结果的拼装.

从上面的结论中我们大概可以知道,应用服务是为了让应用能够运用并且支撑对外的用户能够访问领域对象和执行领域逻辑的一层。就好比在dotnetoore中,用户可以通过访问我们定义的controller来访问我们的业务对象,并且还可以通过controller暴露出来的接口来执行业务逻辑。

因此,我们可以将应用服务考虑为执行业务逻辑的一个中介(可能这样定义也不太好),它没有涉及到核心领域的任何逻辑过程,它只负责了一些的验证,构件的支持等(比如日志,性能监控等)。

扩展上面的需求

在上面识别领域服务中,我们已经捕获到了这样一个需求:“如果行程没有结束,用户访问到该行程,系统会根据用户目前所在的地点为用户推荐附近好吃的美食。” 后来需求又增加了一项:“我们可以用短信的方式将美食通知给客户。”

那么考虑这样一个需求,我们该把短信通知这一个功能实现放在哪儿呢?或者说将发短信这个行为操作放在哪儿呢?我们来考虑一下将他放置在领域服务中:

复制代码

public class RecommendFoodsService
{public List<FoodInfo> RecommendFoods(Itinerary currentItinerary){var recommendFoods = new List<FoodInfo>();//Get Last Addressint lastCountIndex = currentItinerary.Places.Count -1;var currentAddress = currentItinerary.Places[lastCountIndex];var nearbyRestaurants = Restaurants.Where(s=> s.Address.isNearby(currentAddress)).ToList();foreach(var restaurant in nearbyRestaurants){var food = restaurant.GetRankNoOneFood();recommendFoods.Add(new FoodInfo(food,restaurant.Address));}//在这里添加短信发送?SmsUtil.Send(currentItinerary.Participants,recommendFoods);return recommendFoods;}
}

我们在原有代码的基础上,添加了一行代码,为其实现短信通知功能,现在这样已经符合我们的需求了。但是!!!!将短信通知放置在这里好吗?为解开这个问题,我们需要考虑:“短信发送是我领域提炼出来的行为吗?”,“如果没有这个行为,对业务逻辑有什么影响?”

来想一想,发短信是领域提炼出来的吗?我们一直都在关心有关旅程的问题,很显然旅程中的各种才是我们主要关心的对象。那么发短信就不是我们所提炼出来的东西,它只是需要我们附带的支持功能罢了。

那么如果没有这个行为,对业务逻辑有什么影响呢?它会不会影响我完成美食推荐这个行为?很显然,不会!还记得我们在上文说的一个领域服务的特点吗:领域服务中的操作,从领域的角度来看,它是一个整体。如果整体中的一部分丧失它就不能完成业务了。那么在现在这个推荐美食的业务中,如果把餐厅的一部分拿掉会是什么样子呢?OMG,这个服务已经废了,它失去了已有的功能。那如果把短信发送拿掉呢?好像没有一点点影响。

那么这个短信发送,到底放在哪儿呢?应用服务!!!!!

复制代码

public class ItineraryApplicationService 
{public string RecommendFoods(int currentItineraryID){Logger.Log("执行推荐美食业务");var participants = itineraryRepository.Getparticipants(currentItineraryID);var foods = RecommendFoodsService.RecommendFoods(currentItineraryID);SmsUtil.Send(foods);return foods.toJson();}
}

我们在应用层定义了一个叫做ItineraryApplicationService的应用服务,它对外提供了一个RecommendFoods的接口,客户端(App,网页等)可以透过该API来完成推荐美食这一系列的操作。推荐美食的行为我们已经封装在了领域服务中,应用服务根本不需要知道内部的逻辑就可以完成操作,这也验证了我们上面说的一点:从领域的角度看,领域服务是一个整体

最常见的认证授权是领域服务吗

就一般的应用来说,认证授权是应用服务。为什么呢?因为它往往只是给你提供了维持系统允许的基础功能,而并非你领域执行的必须。也许,这还不好理解,那么我们就来尝试一下将它定义为领域服务来看一看。考虑改成那个发短信的例子,我们实现了一个错误版本的领域服务,那么现在我们把领域服务的发短信替换为身份验证代码,然后放置在方法块最前面。来吧?继续回答上面的问题,他们是一个整体吗?如果剥离了这个代码,对行为有什么影响?慢慢的你就会将它从领域服务中拿出来。
但是假如你正在实现一个组织权限软件,它可能会被定义在领域之中。因为你的领域就是认证的一系列操作,你需要认真的去思考它,一旦失去了认证的代码可能你的应用就无法提供正常的功能。

使用领域服务

你己经和领域专家谈论过涉及多个实体的领域概念了,但你不确定哪个实体“拥有”行为。看起来该行为并不属于任何一个实体,但当你尝试将该行为强制适配到实体中的任何一个时,处理起来就会有点棘手了。这一思维模式就是需要领域服务的强烈迹象。[嘘,这句话是我copy的。(*^__^*) ]

不要过多的使用领域服务

是不是只有领域服务才能调度值对象和实体等领域对象呢?当然不是,应用服务也可以。
这也是一个大家常见的问题:将所有实体、值对象、仓储都通过领域服务来编排完成业务逻辑。从而使得应用服务层非常的薄,往往只有一行调用领域服务的代码(日志,性能等代码通过一些现有框架自动完成)。

尝试将部分调度权限分配给应用服务,它不会影响你的领域代码可读性,反而会使得阅读更加清晰。当你发现你的逻辑编排只是调用实体或值对象之间的行为,而没有构成一个完整的领域业务行为的时候(比如有一个Api表示了获取一次旅行地点距离的功能,你可以不用将该功能考虑为领域服务,在应用服务中通过传入的ID,在仓储中获取本次旅行的行程地址,然后交给系统中的距离转换功能计算出距离,然后返回给客户端),请考虑将它设置为应用服务。

不要将过多的行为都给了领域服务

为什么会这样说呢?如果你发现在你建立的领域模型中,实体和值对象的行为只是零星一点,而实体和值对象实现行为操作的动作都是通过领域服务来完成的。那么,你也许用错了领域服务,去重新认识你所识别出的实体和值对象,为它们赋予他们自身的行为,删除这些错误的领域服务。

总结

本次我们介绍了领域驱动设计战术模式中的领域服务。同时也对比了领域服务和应用服务,该部分内容可能介绍的还不是太完整,希望大家能从例子中理解两者之间的差异,后期如果有时间的话会为大家写一篇博文专门来区别领域服务和应用服务。在讲解的过程中,我们还涉及到了一切战术模式中的其他概念,比如Repository和AggregateRoot,这两个概念将在后期的文章中为大家带来介绍。

  • 15楼的评论中 @todo 朋友给了一个很好的建议,是当时写作的时候忽略的问题。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/312435.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

指令打印与驱动打印随笔

本文对指令打印和驱动打印做了一个简要的介绍&#xff0c;分享了在开发客户端打印组件时的一些过程并提出了一个新轮子用于解决老的问题并引出更多的新问题。全文大概 3500 字无图&#xff0c;阅读大概需要 7 分钟。驱动打印是指&#xff1a;使用 PrintDocument 进行打印。通过…

er图转为数据流程图_「数据架构」实体关系模型介绍

实体-关系模型(或ER模型)描述特定知识领域中相关的事物。基本的ER模型由实体类型(对感兴趣的事物进行分类)和指定实体之间可能存在的关系(那些实体类型的实例)组成。在软件工程中&#xff0c;为了执行业务流程&#xff0c;ER模型通常用于表示业务需要记住的内容。因此&#xff…

WeihanLi.Npoi 1.7.0 更新介绍

WeihanLi.Npoi 1.7.0 更新介绍Intro昨天晚上发布了 WeihanLi.Npoi 1.7.0 版本&#xff0c;增加了 ColumnInputFormatter/ ColumnOutputFormatter&#xff0c;又进一步增强了导入导出的灵活性&#xff0c;来看下面的示例ColumnInputFormatter/ColumnOutputFormatter示例 Model:i…

【复杂系统迁移 .NET Core平台系列】之迁移项目工程

源宝导读&#xff1a;微软跨平台技术框架—.NET Core已经日趋成熟&#xff0c;已经具备了支撑大型系统稳定运行的条件。本文将介绍明源云ERP平台从.NET Framework向.NET Core迁移过程中的实践经验。一、背景随着ERP的产品线越来越多&#xff0c;业务关联也日益复杂&#xff0c;…

分析股票大数据_Python大数据分析量学祖师爷网站数据

本文通过Python抓取股海明灯涨停预报数据进行分析&#xff0c;股海明灯网站涨停预报数据跟踪功能是需要VIP权限&#xff0c;但这个权限分析数据的功能有限&#xff0c;我们抓取数据后丰富相关功能。一、点击涨停预报后打开以下页面。通过python程序将数据抓取到后台数据库&…

【复杂系统迁移 .NET Core平台系列】之界面层

源宝导读&#xff1a;微软跨平台技术框架—.NET Core已经日趋成熟&#xff0c;已经具备了支撑大型系统稳定运行的条件。本文将介绍明源云ERP平台从.NET Framework向.NET Core迁移过程中的实践经验。一、背景随着ERP的产品线越来越多&#xff0c;业务关联也日益复杂&#xff0c;…

.NET Core MVC扩展实践

源宝导读&#xff1a;明源云ERP的底层架构正在向.Net Core跨平台迁移&#xff0c;我们在过程中遇到了部分不兼容的问题。本文将介绍技术团队如何解决.Net Core与已有MVC框架不兼容问题的解决方案。一、背景云ERP的建模平台是基于.NET Framework构建的&#xff0c;在向.NET Core…

matlab title多个标题_MATLAB中的直方图处理及均衡化

直方图是多种空间域处理技术的基础。仿图操作能有效地用于图像增强&#xff0c;直方图固有的信息在其他图像处理应用中也是非常有用的&#xff0c;如图像压缩与分割。訪图在软件中易于计算&#xff0c;也适用于商用硬件设备&#xff0c;因此直方图成为实时图像处理的一个流行工…

成本计算引擎动态规则解析技术详解

源宝导读&#xff1a;随着企业数字系统应用的越来越深入&#xff0c;业务计算方式也变的越来越复杂&#xff0c;灵活度要求也越来越高。本文将介绍通过将配置动态转换成可执行代码的方式&#xff0c;解决业务计算高度灵活化配置的技术方案。一、背景ERP本质上是一种“业务密集型…

Kubernetes,多云和低代码数据科学:2020年最热门的数据管理趋势

新兴技术为我们的数据之旅铺平了道路。我们已经看到Kubernetes在应用程序自动化方面处于领先地位&#xff0c;越来越多的公司将赌注押在了云上&#xff0c;以及当今的企业对数据科学的依赖程度正不断提&#xff0c;再加上对大数据的人工智能高级分析&#xff0c;可以看到数据管…

matlab光盘映像文件可以删除吗_DVD-Cloner 2020 for mac(DVD光盘刻录工具) 7.00.715

DVD-Cloner Gold 2020版是功能强大的DVD光盘刻录工具&#xff0c;DVD-Cloner Gold 2020版可以轻松刻录光盘以及蓝光光盘&#xff0c;用户可以将任何的文件刻录到DVD光盘中&#xff0c;并可以制作为蓝光光盘&#xff0c;可以很方便地录入视频到光盘中进行保存&#xff0c;刻录好…

性能优化 = 改改代码?

大家好&#xff0c;我是Z哥。好久没写技术文章了&#xff0c;最近正好有进行一些思考&#xff0c;顺手写出来分享给大家。如果不是程序员的话&#xff0c;可以快速扫一眼正文的几个小标题&#xff0c;快速略过即可&#xff0c;毕竟思路和专业无关&#xff0c;很多是相通的。&am…

使用refs获取节点_闲庭信步聊前端 - 原来你是这样的Refs

一、refs 的由来什么是refsrefs是拿到真实的DOM节点和React元素实例的一种方法。在React官方文档中有提到Refs 提供了一种方式&#xff0c;允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。 React是单向的数据流&#xff0c;父子组件的交互是通过props。修改子组件…

从TimeSpan说起

小编在编写WPF程序时&#xff0c;需要做一个判断&#xff1a;定时使用Modbus协议使用Quartz.net 定时任务读取设备中的数据&#xff0c;同时也使用定时任务判断是否长时间获取不到数据的情况&#xff0c;如果程序中超过一分钟没有获取到数据&#xff08;数据没有更新&#xff0…

如何维持手机电池寿命_延长手机电池寿命终极技巧教学,iPhone和安卓手机皆适合...

每隔一阵子就开始有不少用户询问&#xff0c;iPhone电池健康度又剩下多少了&#xff0c;怎么朋友的还维持在100%&#xff0c;是不是我手机电池出问题&#xff0c;引起不少用户翻白眼。会有电池健康度&#xff0c;最主要是要让大家了解目前手机电池当前寿命&#xff0c;只要电池…

2020年了,再不会Https就老了

合格的web后端程序员&#xff0c;除搬砖技能&#xff0c;还必须会给各种web服务器启用Https&#xff0c;本文结合ASP.NET Core部署模型聊一聊启用Https的方式。温故知新目前常见的Http请求明文传输&#xff0c;请求可能被篡改&#xff0c;访问的站点可能被伪造。HTTPS是HTTP加上…

c语言枚举类型例题_[开源资讯]Zig 0.6.0 发布,想要挑战 C 语言

Zig 0.6.0 已发布&#xff0c;这是一门通用编程语言&#xff0c;专为稳定性、可维护性和性能而设计&#xff0c;追求替代 C 语言在系统编程上的最佳地位。Zig 具有以下值得关注的特性&#xff1a;手动管理内存与 C 语言竞争而非依赖它&#xff0c;Zig 标准库不依赖于 libc轻量而…

【实战 Ids4】║ 给授权服务器加个锁——HTTPS配置

在上篇文章《【实战 Ids4】║客户端、服务端、授权中心全线打通&#xff01;》中&#xff0c;我们正式的将三站打通&#xff0c;发布过后&#xff0c;有小伙伴反馈&#xff0c;可能Nginx对配置HTTPS安全协议有点儿问题&#xff0c;我也就半夜趁着没人打扰&#xff0c;疯狂的研究…

快速幂 a*b%c

2020.12.30开始学习AcWing算法《算法竞赛进阶指南》&#xff1b; 在CSDN上传博客方便复习。 //wecccccccc //2020.12.30 #include <iostream> using namespace std; typedef long long int ll;ll fast_power(ll a, ll b, ll c) {ll ans 1;a % c;//防止一开始输入的值过…

thinkphp 助手函数url不生成https_如何用ThinkPHP框架写一个快递查询接口

ThinkPHP是php程序员们经常使用的框架之一&#xff0c;运用框架来开发网站无疑减轻了我们代码量&#xff0c;加快了我们的开发速度&#xff0c;框架底层封装的方法和函数使用起来简直不能太爽。今天给大家总结一个小案例&#xff0c;如何实现一个快递查询的接口。所谓接口&…