Laravel - 为 WEB 艺术家创造的 PHP 框架。

PHP THAT DOESN'T HURT. CODE HAPPY & ENJOY THE FRESH AIR.

Laravel logo

如何为类分配命名空间

PHP命名空间简介一文中,我对什么是命名空间和PHP怎么处理它们作了一个简短的介绍。

最近有很多小伙伴问我关于命名空间一些更高级的内容,我决定将部分这些内容整理出来。

我曾经看过一些很不错的分配命名空间的方法,本文将就这些方法的优缺点进行探讨。

##开始看本文之前要注意:##

  • 这不是一个指导手册,不可能为每种情况都提供最优解决方案。这仅仅是一些选择上的讨论,主要针对在中小型app中如何分配命名空间,而不是企业级应用里的情况,不能解决那些错综复杂的高级问题。
  • 这里说的“小”、“中”、“大”单纯指类和实体的数量,与代码量、用户数或者别的因素无关。
  • 我们将使用”一个发送收据(Receipt)给用户(User)的命令(Command)为例
  • 为了便于描述,例子中使用App作为全局命名空间,你也可以使用Vendor\Package代替。

##使用命名空间的目的##

在我们开始讨论如何分配命名空间之前,我们先说说为什么要科学的配置命名空间。我一直很感激Shawn McCool帮助我将一些混乱的想法组织为准确的计算机科学上的概念。

正如Shawn告诉我的那样,建立命名空间的意义在于增强代码内聚力:描述代码与代码之间是如何关联的。他指出在其他编程语言,命名空间大多被称为“包”( "Packages")或者模块("Modules")——一旦你意识到这一点,你就能理解为什么我们要将子命名空间看做一个尽可能少依赖地其他部分的独立模块(可以参照封装的概念encapsulation)。我们把模块化看作是建立命名空间的主要目标之一,也可以用它来评判一个命名空间配置方案是否合理。

当然,“将模块化看作科学分配命名空间的主要目标”这一说法仍有很大争议,但是我对这种说法是持认同态度的。

##分配命名空间的方法论##

接下来开始讨论:

  • 使用全局命名空间
  • 基于模式的分组方式
  • 基于上下文的分组方式
  • 兼顾上下文和模式的分组方式

##使用全局命名空间##

    <?php namespace App;

    class SendReceipt {}
    src
      Receipt
      ReceiptRepository
      SendReceipt
      SendReceiptHandler
      User
      UserRepository
      CreateUser
      CreateUserHandler

###优点###

我认为这是否过于简单了,却根本没有考虑到子命名空间?在一个非常小的应用里,这可能是适用的——如果你只有5个类,你根本不需要使用子命名空间。如果这只是一个简单的包用于实现一些简单功能,或者是个只有一个模块的小应用,那么使用全局命名空间是最合适不过的

###缺点###

当你要编写一个较为复杂的应用,在全局命名空间的情况下将很难在那些混杂的代码中找到需要的类。当你要找出身份的类(如Users类)和区分目的的类(如Receipts类),全局命名空间就像把他们混杂在一个大箱子里,根本没有实现模块化。

##基于模式的分组方式##

    <?php namespace App\Commands;

    class SendReceipt {}
    src
      Commands
        SendReceipt
        CreateUser
      Entities
        Receipt
        User
      Handlers
        SendReceiptHandler
        CreateUserHandler
      Repositories
        ReceiptRepository
        UserRepository

###优点###

当你想寻找一个命令时,你需要找到它存放在哪。如果你的大脑说:“我需要编辑一个命令”,“哪一条?”,“发送收据那一条”。这是一个一级组织的命名空间,但如果你用在中型网站将会觉得很郁闷

之外,因为相似的类(例如命令类)都放在一起,很容易就能对比相关的类——例如SendReceiptSendReminder的异同和联系。

这种方法让你很容易理清类与类之间的逻辑关系。例如:每条命令执行总线都知道处理器在App\Commands\{commandName},而命令在App\Handlers\{commandName}

###缺点###

这种方案将让各个类中相同的上下文却分散在各个命名空间。例如,你的应用中可能有App\Commands\SendReceiptApp\ReceiptApp\Entities\ReceiptApp\Providers\ReceiptServiceProviderApp\Handlers\Commands\SendReceiptHandlerApp\Repositories\ReceiptRepository等等的类,处理收据(Receipt)的相关逻辑将分散在各个命名空间中

如果你需要封装化和模块化,这种分组方式可能很太好。例如我们的“支付账单”的逻辑将分散在各个地方,穿过了所有的命名空间,为类分配命名空间不再为了将“支付账单”的逻辑模块化。类只是单纯的和其他结构相似的类放在一起,而不是因为他们真的相关的联系。

##基于上下文的分组方式##

    <?php namespace App\Billing;

    class SendReceipt {}
    src
        Billing
            Receipt
            ReceiptRepository
            SendReceipt
            SendReceiptHandler
        User
            User
            UserRepository
            CreateUser
            CreateUserHandler

###优点###

如果你现在只想单纯的处理好“支付账单”这个逻辑,这种方法让你将所有与支付账单相关的类都放在一起。收据类,实体类命令类,处理器类,数据仓库类,等等——这是一个漂亮的,整齐的,易于寻址的分组模式。

这样我们实现了封装和模块化。不用去管设计模式,将所有与计费相关的类都放在同一个地方——这有助于理解代码。甚至可以把他们看作一个单元,能够作为整个应用的扩展模块。

###缺点###

你的命令分散在各个类中。库、实体和命令处理器都是这样。

##兼顾上下文和模式的分组方式##

    <?php namespace App\Billing\Commands;

    class SendReceipt {}
    src
        Billing
            Entities
                Receipt
            Repositories
                ReceiptRepository
            Commands
                SendReceipt
            Handlers
                SendReceiptHandler
        User
              Entities
                  User
              Repositories
                  UserRepository
              Commands
                  CreateUser
              Handlers
                  CreateUserHandler

###优点###

可能有点专业,但用这种方式将最大程度的分离你的命名空间。而且你的代码量越大,类越多,越能体现这种划分方式的作用。在以上模型基础上再想象一下,再添加上升级用户命令(UpdateUser command),删除用户命令(DeleteUser command),订阅实体(Subscription entity),订阅数据仓库(Subscription repository)和订阅相关的处理器等等,使用这种方式将让代码逻辑十分清晰。

就像根据模式分组的分组方式一样,你可以对对应的类编程

你的类以模式分组,也使用根据上下文进行分类,所以你仍可以将你所有的“收据”的代码放在同一个地方,也仍可以享受到基于上下文的分组方式和模块化的好处。分组

###缺点###

类里面命名空间的名字越长,也就越难理解命名空间的作用,也就会有越可能出现错别字等一些小问题。而对于小中型的应用来说,这是很多余的。

如果你用模式来作为最底层的命名空间分组,那么你将不能完全享受到使用上下文分组的好处,反之亦然。

##例子##

上述就是关于组织命名空间的理论。那么怎么样才是正确的?我用 SaveMyProposals项目中的一些类做一个例子,看看我们应该怎么管理“发言”(Talks),“会议”(Conferences)这些类,和我们怎么实现“在会议上发言”

Global namespacing

    app
        Conference
        ConferenceRepository
        CreateConference
        CreateConferenceHandler
        CreateTalk
        CreateTalkHandler
        DeleteConference
        DeleteConferenceHandler
        DeleteTalk
        DeleteTalkHandler
        ProposeTalkToConference
        ProposeTalkToConferenceHandler
        RetractTalkProposal
        RetractTalkProposalHandler
        Talk
        TalkRepository
        UpdateConference
        UpdateConferenceHandler
        UpdateTalk
        UpdateTalkHandler

Group by pattern

    app
        Commands
            CreateConference
            CreateTalk
            DeleteConference
            DeleteProposal
            DeleteTalk
            ProposeTalkToConference
            RetractTalkProposal
            UpdateConference
            UpdateTalk
        Entities
            Conference
            Proposal
            Talk
        Handlers
            CreateConferenceHandler
            CreateTalkHandler
            CreateProposalHandler
            DeleteConferenceHandler
            DeleteProposalHandler
            DeleteTalkHandler
            ProposeTalkToConferenceHandler
            RetractTalkProposalHandler
            UpdateConferenceHandler
            UpdateTalkHandler
        Repositories
            ConferenceRepository
            TalkRepository

Group by context

    app
        Conferences
            Conference
            ConferenceRepository
            CreateConference
            CreateConferenceHandler
            DeleteConference
            DeleteConferenceHandler
            UpdateConference
            UpdateConferenceHandler
        Talks
            CreateTalk
            CreateTalkHandler
            DeleteTalk
            DeleteTalkHandler
            ProposeTalkToConference
            ProposeTalkToConferenceHandler
            Talk
            TalkRepository
            RetractTalkProposal
            RetractTalkProposalHandler
            UpdateTalk
            UpdateTalkHandler

Group by context and pattern

    app
        Conferences
            Commands
                CreateConference
                DeleteConference
                UpdateConference
            Entities
                Conference
            Handlers
                CreateConferenceHandler
                DeleteConferenceHandler
                UpdateConferenceHandler
            Repositories
                ConferenceRepository
        Talks
            Commands
                CreateTalk
                DeleteTalk
                ProposeTalkToConference
                RetractTalkProposal
                UpdateTalk
            Entities
                Talk
            Handlers
                CreateTalkHandler
                DeleteTalkHandler
                ProposeTalkToConferenceHandler
                RetractTalkProposalHandler
                UpdateTalkHandler
            Repositories
                TalkRepository

##结语##

所以,我们的答案是?

具体情况具体分析!

很大程度上说对于有较少类和实体的应用而言,简单的命名空间组织分组方式可能更好;而对于更复杂一些的系统来说,较大型的命名空间组织(兼顾上下文和模式的分组方式)则更优。但这也不是一个100%成立的定理。

我认为模块化和封装的思维能够让你的大脑有更多时间思考。想想你的每个子系统是否都划分清晰、正确。

最后,你需要想清楚你想要什么,想清楚你的bug是什么,想清楚你从各个方案能获得好处,那么你就能选择出最优的解决方案了。


原文地址——https://mattstauffer.co/blog/how-to-organize-class-namespaces

关于作者 雨师
广州
由于学艺不精,内容可能有误。如有错误欢迎联系邮箱312841925@qq.com指出。谢谢!