深入探讨依赖注入
从测试角度探讨依赖注入
依赖反转原则是SOLID 中最难理解的原则,而依赖注入则是单元测试的基石,本文将从测试角度探讨依赖反转与依赖注入,并将Laravel 的service container、constructor injection 与method injection 应用在实务上。
Version
PHP 7.0.0
Laravel 5.2.29
实际案例
假设目前有3家货运公司,每家公司的计费方式不同,使用者可以动态选择不同的货运公司,将一步步的重构成依赖注入方式
传统写法
传统我们会使用 if else 与 new 来建立物件。
BlackCat.php
app/Services/BlackCat.php
namespace App \ Services ;
class BlackCat
{
/
- @param int $weight
- @return int
/
public function calculateFee ( $weight )
{
return 100 + $weight 10 ;
}
}
黑猫的计费方式。
Hsinchu.php
app/Services/Hsinchu.php
namespace App \ Services ;
class Hsinchu
{
/
- @param int $weight
- @return int
/
public function calculateFee ( $weight )
{
return 80 + $weight 15 ;
}
}
新竹货运的计费方式。
PostOffice.php
app/Services/PostOffice.php
namespace App \ Services ;
class PostOffice
{
/
- @param int $weight
- @return int
/
public function calculateFee ( $weight )
{
return 70 + $weight 20 ;
}
}
邮局的计费方式。
ShippingService.php
app/Services/ShippingService.php
namespace App \ Services ;
use Exception ;
class ShippingService
{
/
- @param string $companyName
- @param int $weight
- @return int
- @throws Exception
*/
public function calculateFee ( $companyName , $weight )
{
if ( $companyName == 'BlackCat' ) {
$blackCat = new BlackCat();
return $blackCat ->calculateFee( $weight );
}
elseif ( $companyName == 'Hsinchu' ) {
$hsinchu = new Hsinchu();
return $hsinchu ->calculateFee( $weight );
}
elseif ( $companyName == 'PostOffice' ) {
$postOffice = new PostOffice();
return $postOffice ->calculateFee( $weight );
}
else {
throw new Exception ( 'No company exception' );
}
}
}
calculateFee() 传入2个参数: $companyName 与 $weight。
使用者可自行由 $companyName 挑选货运公司,并传入 $weight 计算运费。
使用 if else 判断 $companyName 字串,并 new 出相对应物件,这是初学者学习物件导向时的写法。
ShippingService.php
app/Services/ShippingService.php
/
- @param string $companyName
- @param int $weight
- @return int
- @throws Exception
*/
public function calculateFee ( $companyName , $weight )
{
switch ( $companyName ) {
case 'BlackCat' :
$blackCat = new BlackCat();
return $blackCat ->calculateFee( $weight );
case 'Hsinchu' :
$hsinchu = new Hsinchu();
return $hsinchu ->calculateFee ( $weight );
case 'PostOffice' :
$postOffice = new PostOffice();
return $postOffice ->calculateFee( $weight );
default :
throw new Exception ( 'No company exception' );
}
}
将 if else 重构成 switch,可稍微改善程式码的可读性。
使用Interface
目前的写法,执行上没有什么问题,若以TDD开发,我们将得到第一个绿灯。
我们将继续重构成更好的程式。
目前我们是实际去 new Blackcat() 、new Hsinchu() 与 new PostOffice(),也就是说 ShippingService 将直接相依于 BlackCat、Hshinchu 与PostOffice 3个class。
物件导向就是希望达到高内聚,低耦合的设计。所谓的低耦合,就是希望能减少相依于外部的class的数量。
何谓相依 ?
简单的说,有2 种写法会产生相依 :
去new 其他class。
去extends 其他class。
由于PHP 不用编译,所以可能较无法体会相依的严重性,但若是需要编译的程式语言,若你相依的class 的property 或method 改变,可能导致你的程式无法编译成功,也就是你必须配合相依的class 做相对应的修改才能通过编译,因此我们希望降低对其他class 的相依程度与数量。
GoF四人帮在设计模式曾说: Program to an Interface, not an Implementation。也就是程式应该只相依于interface,而不是相依于实际class,目的就是要藉由interface,降低对于实际class的相依程度。
若我们能将 BlackCat、Hshinchu 与 PostOffice 3个class抽象化为1个 interface,则 ShippingService 将从相依3个class,降低成只相依于1个interface,
将大大降低 ShippingService与其他class的相依程度。
若以编译的角度,由于 ShippingService 只相依于 interface,因此 BlackCat、Hshinchu 与 PostOffice 做任何修改都不会影响我 ShippingService 的编译。
LogisticsInterface.php
app/Services/LogisticsInterface.php
namespace App \ Services ;
interface LogisticsInterface
{
/
- @param int $weight
- @return int
*/
public function calculateFee ( $weight ) ;
}
从 BlackCat 抽取出 LogisticsInterface,将 BlackCat、Hsinchu 与 PostOffice 抽象化成 LogisticsInterface。
BlackCat.php
app/Services/BlackCat.php
namespace App \ Services ;
class BlackCat implements LogisticsInterface
{
/
- @param int $weight
- @return int
/
public function calculateFee ( $weight )
{
return 100 $weight * 10 ;
}
}
BlackCat 实现 LogisticsInterface
Hsinchu.php
app/Services/Hsinchu.php
namespace App \ Services ;
class Hsinchu implements LogisticsInterface
{
/
- @param int $weight
- @return int
/
public function calculateFee ( $weight )
{
return 80 $weight * 15 ;
}
}
Hsinchu 实现 LogisticsInterface。
PostOffice.php
app/Services/PostOffice.php
namespace App \ Services ;
class PostOffice implements LogisticsInterface
{
/
- @param int $weight
- @return int
/
public function calculateFee ( $weight )
{
return 70 $weight * 20 ;
}
}
PostOffice 实现 LogisticsInterface。
ShippingService.php
app/Services/ShippingService.php
namespace App \ Services ;
use Exception ;
class ShippingService
{
/
- @param string $companyName
- @param int $weight
- @return int
- @throws Exception
*/
public function calculateFee ( $companyName , $weight )
{
switch ( $companyName ) {
case 'BlackCat' :
$logistics = new BlackCat();
return $logistics ->calculateFee( $weight );
case 'Hsinchu' :
$logistics = new Hsinchu();
return $logistics ->calculateFee( $weight );
case 'PostOffice' :
$logistics = new PostOffice();
return $logistics ->calculateFee( $weight );
default :
throw new Exception ( 'No company exception' );
}
}
}
$logistics 的型别都是 LogisticsInterface,目前PHP 7对于变数还没有支援type hint,所以程式码看起来差异不大,
但藉由PHPDoc,在PhpStorm打 $logistics->,已经可以得到语法提示: calculateFee( ),
表示PhpStorm已经知道 BlackCat、Hsinchu 与 PostOffice 都是 LogisticsInterface 型别的物件,
也就是对于 ShippingService 来说,BlackCat、Hsinchu 与 PostOffice 都已经抽象化成 LogisticsInterface
工厂模式
虽然已经将 BlackCat、Hsinchu 与 PostOffice 抽象化成 LogisticsInterface,但是在 ShoppingService 中,仍看到 new Blackcat()、new Hsinchu()
与new PostOffice(),对于ShoppingService而言,我们看到了3个问题:
违反单一职责原则 : calculateFee()原本应该只负责计算运费,现在却还要负责建立货运公司物件。
违反开放封闭原则 :将来若有新的货运公司供使用者选择,势必修改switch。
实质相依数为3 :虽然已经重构出interface,但实际上却还必须new 3个class。
比较好的方式是将 new 封装在 LogisticsFactory 中
LogisticsFactory.php
app/Services/LogisticsFactory.php
namespace App \ Services ;
use Exception ;
class LogisticsFactory
{
/
- @param string $companyName
- @return LogisticsInterface
- @throws Exception
*/
public static function create (string $companyName )
{
switch ( $companyName ) {
case 'BlackCat' :
return new BlackCat();
case 'Hsinchu ' :
return new Hsinchu();
case 'PostOffice' :
return new PostOffice();
default :
throw new Exception ( 'No company exception' );
}
}
}
Simple Factory模式使用了 static create(),专门负责建立货运公司物件:
- 专门负责建立货运公司的逻辑,符合单一职责原则。
ShippingService.php
app/Services/ShippingService.php
namespace App \ Services ;
use Exception ;
class ShippingService
{
/
- @param string $companyName
- @param int $weight
- @return int
- @throws Exception
*/
public function calculateFee ( $companyName , $weight )
{
$logistics = LogisticsFactory::create( $companyName );
return $logistics ->calculateFee( $weight );
}
}
将来有新的货运公司,也只要统一修改 LogisticsFactory 即可,将其变化封装在 LogisticsFactory,对于 ShoppingService 开放封闭。
ShoppingService 从相依于3个class降低成仅相依于 LogisticsInterface 与 LogisticsFactory,实质相依数降为2。
程式的可测试性
符合spec 的程式,并不代表是好的程式,一个好的程式还要符合5 个要求 :
容易维护。
容易新增功能。
容易重复使用。
容易上Git,不易与其他人冲突。
容易写测试。
使用interface + 工厂模式,已经达到以上前4点要求,算是很棒的程式。
根据单元测试的定义:
单元测试必须与外部环境、类别、资源、服务独立,而不能直接相依。这样才是单纯的测试目标物件本身的逻辑是否符合预期。
若要对 ShippingService 进行单元测试,势必将 BlackCat、Hsinchu 与 PostOffice 加以抽换隔离,但使用了工厂模式之后,
ShippingService 依然直接相依了 LogisticsFactory,而 LogisticsFactory 又直接相依 BlackCat、Hsinchu 与 PostOffice,
当我们对 ShippingService 做单元测试时,由于无法对 LogisticsFactory 做抽换隔离,因此无法对 ShippingService 做单元测试。
简单的说,interface + 工厂模式,仍然无法达到可测试性的要求,我们必须继续重构。
依赖反转
为了可测试性,单元测试必须可决定待测物件的相依物件,如此才可由单元测试将待测物件的相依物件加以抽换隔离。
换句话说,我们不能让待测物件直接相依其他class,而应该由单元测试订出interface,让待测物件仅能相依于interface,而实际相依的物件可由单元测试来决定,如此我们才能对相依物件加以抽换隔离。
这也就是所谓的依赖反转原则 :
高阶模组不该依赖低阶模组,两者都应该要依赖其抽象。
抽象不要依赖细节,细节要依赖抽象。
好像越讲越抽象XDD。
其中相依与依赖是相同的,只是翻译用字的问题。
何谓高阶模组? 何谓低阶模组?
高阶与低阶是相对的。
简单的说:
- 当A class去new B class,A就是高阶模组,B就是低阶模组。
若以本例而言 :
ShippingService 相对于 BlackCat,ShippingService 是高阶模组,BlackCat 是低阶模组,
单元测试相对于 ShippingService,单元测试是高阶模组,ShippingService 是低阶模组。
ShippingController 相对于 ShippingService,ShippingController 是高阶模组,ShippingService 是低阶模组。
何谓抽象? 何谓细节?
interface 为抽象, abstract class 为抽象。
class 为细节去implement interface,class 为细节去extends abstract class。
若以本例而言 :
在没有使用interface 前 :
ShippingService 直接 new BlackCat()。
ShippingService 直接相依于 BlackCat。
也就是高阶模组依赖低阶模组。
使用了interface 之后 :
ShippingService 没有相依于 BlackCat,也就是高阶模组没有依赖于低阶模组。
ShippingService 改成相依于 LogisticsInterface,也就是高阶模组依赖其抽象(因为new而相依)。
BlackCat 改成相依于 LogisticsInterface,也就是低阶模组也依赖其抽象(因为implements而相依)。
也就是目前高阶模组与低阶模组都改成依赖其抽象。
高阶模组 ShippingService 原本依赖的是低阶模组 BlackCat 的 calculateFee(),有了 interface 之后,变成反过来低阶模组 BlackCat 要依赖高阶模组所定义 LogisticsInterface的calculateFee(),所以称为依赖反转。
更简单的说,依赖反转就是要你使用interface 来写程式,而不要直接相依于class。
我们之前已经重构出 LogisticsInterface,事实上已经符合依赖反转。
依赖注入
有了依赖反转还不足以达成可测试性,依赖反转只确保了待测物件的相依物件相依于interface。
既然相依物件相依于interface,若单元测试可以产生该interface 的物件,并加以注入,就可以将相依物件加以抽换隔离,这就是依赖注入。
Constructor Injection
ShippingService.php
app/Services/ShippingService.php
namespace App\Services;
class ShippingService
{
/ @var LogisticsInterface */
private $logistics;
/ * ShippingService constructor. * @param LogisticsInterface $logistics */public function __construct(LogisticsInterface $logistics){ $this->logistics = $logistics;}/ * @param int $weight * @return int */public function calculateFee($weight){ return $this->logistics->calculateFee($weight);}
}
12行
/ @var LogisticsInterface */
private $logistics;
/
- ShippingService constructor.
- @param LogisticsInterface $logistics
*/
public function __construct(LogisticsInterface $logistics)
{
$this->logistics = $logistics;
}
原本相依的 LogisticsInterface 型别的物件,改由 constructor 注入,藉由PHP的 type hint,描述要注入的物件型别为 LogisticsInterface。
原本使用interface +工厂模式,实质相依数为2,改用constructor injection之后,连 LogisticsFactory 都不需要了,仅相依于 LogisticsInterface,实质相依数降为1。
17行
/
- @param int $weight
- @return int
*/
public function calculateFee ( $weight )
{
return $this ->logistics->calculateFee( $weight );
}
将原本的 logistics 物件改成field。
Service Container
我们目前已经有了依赖注入,对于可测试性只剩下最后一哩路,若我们能将mock 出的假物件,透过依赖注入取代掉原来的相依物件,就能将相依物件加以抽换隔离,达成隔离测试的要求,service container 就是要帮我们将相依物件抽换隔离。
Laravel 4称为IoC container,Laravel 5称为service container。
17以下句子来自于30天快速上手TDD Day 5:如何隔离相依性-基本的可测试性
事实上IoC (Inversion of Conttrol)与DI (Dependency Inversion)讲的是同一件事情,也就是由单元测试决定待测物件的相依物件。
单元测试
ShippingService.php
tests/Services/ShippingServiceTest.php
use App \ Services \ BlackCat ;
use App \ Services \ LogisticsInterface ;
use App \ Services \ ShippingService ;
class ShippingServiceTest extends TestCase
{
/ @test /
public function黑猫单元测试()
{
/ arrange /
$expected = 110 ;
$weight = 1 ;
$mock = Mockery::mock(BlackCat::class); $mock ->shouldReceive( 'calculateFee' ) ->once() ->withAnyArgs() ->andReturn( $expected ); App::instance(LogisticsInterface::class, $mock ); $target = App::make(ShippingService::class); / act */ $actual = $target ->calculateFee( $weight ); / assert */ $this ->assertEquals( $expected , $actual ); }
}
14行
$mock = Mockery::mock(BlackCat::class);
$mock ->shouldReceive( 'calculateFee' )
->once()
->withAnyArgs()
->andReturn( $expected );
因为单元测试,我们只想测试 ShippingService,因此想将其相依的 LogisticsInterface 物件抽换隔离,因此利用 Mockery 根据 BlackCat 建立假物件$mock,
并定义 calculateFee() 回传的期望值为 $expected。
once() 为预期 calculateFee() 会被执行一次,且只会被执行一次,若完全没被执行,或执行超过一次,PHPUnit会显示红灯。
withAngArgs() 为不特别在乎 calculateFee() 的参数型别与个数,一般来说,单元测试在乎的是被mock method是否被正确执行,以及其回传值是否如预期,至于参数则不太重要。
20行
App::instance(LogisticsInterface::class, $mock );
mock物件已经建立好,接着要告诉service container,当constructor injection的type hint遇到 LogisticsInterface 时,该使用我们刚建立的 $mock 物件抽换隔离,而不是原来的相依物件。
App::instance() 用到的地方不多,一般就是用在需要mock时。
22行
$target = App::make(ShippingService::class);
当mock与service container都准备好时,接着要建立待测物件准备测试,这里不能再使用new建立物件,而必须使用service container提供的 App::make() 来建立物件,因为我们就是希望靠service container帮我们将mock物件抽换隔离原来的相依物件,因此必须改用service container提供的 App::make()。
整合测试
ShippingService.php
/ @test /
public function黑猫整合测试()
{
/ arrange /
$expected = 110 ;
$weight = 1 ;
App::bind(LogisticsInterface::class, BlackCat::class);$target = App::make(ShippingService::class);/ act */ $actual = $target ->calculateFee( $weight );/ assert */ $this ->assertEquals( $expected , $actual );
}
当执行整合测试时,我们会希望实际执行相依物件的功能,而不再使用mock 将其相依物件抽换隔离。
第8行
App::bind(LogisticsInterface::class, BlackCat::class);
当constructor injection配合type hint时,若是class,Laravel的service container会自动帮我们注入其相依物件,但若type hint为interface时,因为可能有很多class implements该interface,所以必须先使用 App::bind( ) 告诉service container,当type hint遇到 LogisticsInterface 时,实际上要注入的是 BlackCat 物件。
10行
$target = App::make(ShippingService::class);
当 App::bind() 完成后,就可以使用 App::make() 建立待测物件,service container也会根据刚刚 App::bind() 的设定,自动依赖注入 BlackCat 物件。
Method Injection
Laravel 4 提出了constructor injection 实现了依赖注入,而Laravel 5 更进一步提出了method injection。
有constructor injection 不就已经可测试了吗? 为什么还需要method injection 呢?
由于Laravel 4 只有constructor injection,所以只要class 要实现依赖注入,唯一的管道就是constructor injection,若有些相依物件只有单一method 使用一次,也必须使用constructor injection,这将导致最后constructor 的参数爆炸而难以维护。
对于一些只有单一method 使用的相依物件,若能只在method 的参数加上type hint,就可自动依赖注入,而不需要动用constructor,那就太好了,这就是method injection。
public function store (StoreBlogPostRequest $request )
{
// The incoming request is valid...
}
如大家熟悉的form request,就是使用method injection,相依的StoreBlogPostRequest物件并不是透过constructor注入,而是在 store() 注入。
ShippingService.php
namespace App \ Services ;
class ShippingService
{
/
- @param LogisticsInterface $logistics
- @param int $weight
- @return int
*/
public function calculateFee (LogisticsInterface $logistics , $weight )
{
return $logistics ->calculateFee( $weight );
}
}
重构成method injection后,就不必再使用constructor与field,程式更加精简。
第1个参数为我们要注入的 LogisticsInterface 物件,第2个参数为我们原本要传的 $weight 参数
单元测试
ShippingService.php
use App \ Services \ BlackCat ;
use App \ Services \ LogisticsInterface ;
use App \ Services \ ShippingService ;
class ShippingServiceTest extends TestCase
{
/ @test /
public function 黑猫单元测试()
{
/ arrange /
$expected = 110 ;
$weight = 1 ;
$mock = Mockery::mock(BlackCat::class); $mock ->shouldReceive( 'calculateFee' ) ->once() ->withAnyArgs() ->andReturn( $expected ); App::instance(LogisticsInterface::class, $mock ); / act */ $actual = App::call(ShippingService::class . '@calculateFee' , [ 'weight' => $weight ]); / assert */ $this ->assertEquals( $expected , $actual ); }
}
20行
/ act */
$actual = App::call(ShippingService::class . '@calculateFee' , [
'weight' => $weight
]);
之前mock 的部分,与constructor injection 相同,就不再解释。
关键在于 App::call(),这是一个在Laravel官方文件没有介绍的method,但Laravel内部却到处在用。
之前我们使用constructor injection,就要搭配 App::make() 才能自动依赖注入。
现在我们使用method injection,就要搭配 App::call() 才能自动依赖注入。
第1个参数要传的字串,是class完整名称,加上@与method名称。
第2 个参数要传的是阵列,也就是我们自己要传的参数,其中参数名称为key,参数值为value。
整合测试
ShippingService.php
public function 黑猫整合测试()
{
/ arrange */
$expected = 110 ;
$weight = 1 ;
App::bind(LogisticsInterface::class, BlackCat::class);/ act */ $actual = App::call(ShippingService::class . '@calculateFee' , [ 'weight' => $weight ]);/ assert */ $this ->assertEquals( $expected , $actual );
}
10行
/ act */
$actual = App::call(ShippingService::class . '@calculateFee' , [
'weight' => $weight
]);
关键一样是使用 App::call()。
为什么只能在controller 使用method injection,而无法在自己的presenter、service 或repository 使用method injection?
当初学习method injection时,我也非常兴奋,总算可以解决Laravel 4的constructor参数爆炸的问题,但发现只能用在controller,但无法用在自己的presenter、service或repository,一直学习到App::call ()时,问题才迎刃而解。
因为Laravel内部使用 App::call() 呼叫controller的method,因此你可以在controller无痛使用method injection,但若你自己的presenter、service或repository要使用method injection,就必须在controller搭配 App::call( ),如此service containter才会帮你自动依赖注入相依物件。
再谈可测试性
本文从头到尾,都是以可测试性的角度去谈依赖注入,而我个人也的确是在写单元测试之后,才领悟依赖反转与依赖注入的重要性。
若是不写测试,是否就不需要依赖反转与依赖注入呢?
之前曾经提到 :
IoC (Inversion of Conttrol) 与DI (Dependency Inversion) 讲的是同一件事情,也就是由单元测试决定待测物件的相依物件。
根据之前的经验,我们可以发现待测物件的相依物件都是在测试的App::bind()所决定。
之前有提到所谓的高阶模组与低阶模组是相对的,单元测试相对于service,单元测试是高阶模组,而service 是低阶模组。
对照于实际状况,controller 相对于service,controller是高阶模组,而service 是低阶模组。
我们可以在单元测试以 App::bind() 决定service的相依物件,同样的,我们也可以在controller以 App::bind() 去决定service的相依物件。
既然我们可以由controller去决定,去注入service的相依物件,我们就不再被底层绑死,不再依赖底层service,而是由低阶模组去依赖高阶模组所制定的interface,再由controller的 App::bind() 来决定低阶模组的相依物件,这就是所谓的依赖反转。
也就是说,若高层模组可以决定低阶模组的相依物件,那整个设计的弹性与扩充性会非常好,因为需求都来自于人,而人所面对的是高阶模组,而高阶模组可以透过依赖注入去决定低阶模组的相依物件,而不是被低阶模组绑死,可弹性地依照需求而改变。
若程式符合可测试性的要求,表示其具有低耦合的特性,也就是物件导向强调的高内聚,低耦合,因此程式将更容易维护,更容易新增功能,更容易重复使用,更容易上Git,不易与其他人冲突,也就是说我们可以将程式的可测试性,当成是否为好程式的指标之一。
生活中的依赖反转
举个生活上实际的例子,事实上硬体产业就大量使用依赖反转。
比如电脑需要将画面送到显示器,系统厂对design house 发出需求,此时系统厂相当于高阶模组,而design house 相当于低阶模组。
Design house 当然可以设计出IC 符合系统厂需求,但由于系统厂没有规定任何传输介面规格,只提出显示需求,因此design house 可以使用自己设计的专属传输介面,系统厂的电路板只要符合design house 的专属传输介面规格,就可以将电脑画面传送到显示器。
这样虽然可以达成需求,但有几个问题:
传输介面由design house 规定,只要design house 传输介面更改,系统厂的电路板就得跟着修改。
Design house 的专属传输介面,需要搭该公司的控制IC,因此系统厂还被绑死要使用该design house 的控制IC。
由于使用专属传输介面,因此系统厂无法使用替代料,只能乖乖使用该design house 的IC,没有议价空间,且备料时间也被绑死。
这就是典型的高阶模组依赖低阶模组,也就是系统厂被design house 绑死了。
所以系统厂很聪明,会联络各大系统厂一起制定传输介面规格,如VGA、HDMI、Display Port…等,如此deisgn house 就得乖乖的依照系统厂制定的传输介面规格来设计IC,这样系统厂就不再被单一design house 绑死,可以自行选择控制IC,还可以找替代料,增加议价空间,备料时间也更加弹性,这就是典型的低阶模组反过来依赖高阶模组所制定的规格,也就是依赖反转
Conclusion
Interface + 工厂模式无法达成可测试性的要求,因此才有了依赖注入与service container。
若很多method 都使用相同相依物件,可使用constructor injection,若只有单一method 使用的相依物件,建议改用method injection。
Method Injection必须搭配 App::call(),除了自动依赖注入相依物件外,也可以自行传入其他参数。
可测试性与物件导向是相通的,我们可以藉由程式的可测试性,当成是否为好程式的指标之一。
Sample Code
完整的范例
本文翻译转自:http://oomusou.io/tdd/tdd-di/
关键字:laravel, tdd, 依赖注入, phpunit
版权声明
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处。如若内容有涉嫌抄袭侵权/违法违规/事实不符,请点击 举报 进行投诉反馈!