“Updater”设计模式,而不是“Builder”

"Updater" design pattern, as opposed to "Builder"

提问人:Gulzar 提问时间:5/30/2021 最后编辑:Gulzar 更新时间:10/16/2021 访问量:627

问:

这实际上是与语言无关的,但我总是更喜欢 Python。


构建器设计模式用于在创建对象之前通过委托创建过程来验证配置是否有效。

一些代码需要澄清:

class A():
    def __init__(self, m1, m2):  # obviously more complex in life
        self._m1 = m1
        self._m2 = m2

class ABuilder():
    def __init__():
        self._m1 = None
        self._m2 = None

    def set_m1(self, m1):
        self._m1 = m1
        return self

    def set_m2(self, m1):
        self._m2 = m2
        return self

    def _validate(self):
        # complicated validations
        assert self._m1 < 1000
        assert self._m1 < self._m2

    def build(self):
        self._validate()
        return A(self._m1, self._m2)

我的问题与此类似,但有一个额外的限制,由于性能限制,我每次都无法重新创建对象。
相反,我只想更新现有对象。


我想出的糟糕的解决方案:

我可以按照这里的建议去做,就像这样使用setters

class A():
    ...

    set_m1(self, m1):
        self._m1 = m1

    # and so on

但这很糟糕,因为使用 setters

  1. 胜过封装的目的
  2. 击败了 buillder(现在的更新程序)的目的,它应该验证在创建或更新后是否保留了一些复杂的配置,在这种情况下是更新。

正如我前面提到的,我无法每次都重新创建对象,因为这很昂贵,而且我只想更新一些字段或子字段,并且仍然验证或子验证。


我可以向它们添加更新和验证方法并调用这些方法,但这违背了委派更新责任的目的,并且在字段数量上很棘手。A

class A():
   ...

   def update1(m1):
      pass # complex_logic1

   def update2(m2):
      pass # complex_logic2

   def update12(m1, m2):
      pass # complex_logic12

我可以强制更新具有可选参数的方法中的每个字段A

class A():
   ...

   def update("""list of all fields of A"""):
      pass

这又是不容易处理的,因为这种方法很快就会成为一种神法,因为有很多可能的组合。

强制方法始终接受 中的更改,并在 中进行验证也行不通,因为需要查看 的内部状态才能进行分解,从而导致循环依赖。AUpdaterUpdaterA


如何委托更新对象中的字段A

在某种程度上,

  1. 不会破坏A
  2. 实际上将更新的责任委托给另一个对象
  3. 随着变得越来越复杂,易于处理A

我觉得我错过了一些微不足道的东西,无法将构建扩展到更新。

python oop 语言不可知构建 对象分析

评论

0赞 kaya3 5/30/2021
如果我们非常严格,那么一个类将字段设置委托给另一个类的任何方式实际上都违反了封装。保证对象不会因更新而进入非法状态的方法是,如果使用错误的参数调用,则不要公开任何可能使其处于非法状态的方法。
0赞 Gulzar 5/30/2021
@kaya3 有没有办法定义只有更新程序才能使用的 setter?我不称其为“破坏封装”。不过,仅仅定义全局二传手确实会打破它。我们可以称它为朋友。updaterA
0赞 kaya3 5/30/2021
想想 OOP 的“消息传递”解释。如果一个方法只应该由特定的调用者调用,那么“消息”必须是只有该调用者才能发送的消息;但是 OOP 通过它接受什么样的消息以及它应该如何处理它们来定义对象的接口,而不是它应该从哪里接受这些消息。充其量,您可以使消息的“调用者”部分(即有一个参数来说明哪个对象正在调用),然后该对象检查调用者是否被允许,但没有什么可以阻止调用者发送不同的参数。
0赞 kaya3 5/30/2021
如果你希望你的对象是可更新的,那么更新方法(即 setter)属于对象本身,而不是不同的类。如果某些更新由于导致非法状态而不应被允许,则 setter 方法有责任确保它只执行合法更新。通常,如果尝试进行非法更新,您会抛出异常(例如 在 Java 或 Python 中)。IllegalArgumentExceptionValueError
1赞 kaya3 5/31/2021
有趣的问题;在这种情况下,我认为您会将该类及其内部类视为单个模块,如果两个类都不能用于将任何一个类的实例置于非法状态,那么将该模块视为正确封装是有意义的。那么问题来了,这种粗粒度的封装是否更好;模块是从外部世界封装的,但在模块中,类不是相互封装的。就个人而言,我宁愿通过在构造函数中验证来封装类级别。AABuilderA

答:

0赞 ahoffer 10/16/2021 #1

我不确定我是否理解您的所有担忧,但我想尝试回答您的帖子。根据你所写的内容,我假设:

  • 验证很复杂,必须检查对象的多个属性,以确定对对象的任何更改是否有效。
  • 对象必须始终处于有效状态。不允许对使对象无效的更改。
  • 复制对象、进行更改、验证对象,然后在验证失败时拒绝更改的成本太高。

使用方法将验证逻辑移出构建器并移动到单独的类(如 ModelValidator)中validateModel(model)

第一个选项是使用命令模式

  • 创建名为 Python 抽象类/接口的抽象类或接口(我不认为 Python 抽象类/接口,但这很好)。该接口实现了两个方法,以及 .UpdateUpdateexecute()undo()
  • 具体类的名称类似于 、 或 。UpdateAdressUpdatePortfolioUpdatePaymentInfo
  • 每个具体的 Update 对象还包含对模型对象的引用。
  • 具体类保存特定类型的更新所需的状态。这些方法存在于类中:UpdateAddress
UpdateAddress
    setStreetNumber(...)
    setCity(...)
    setPostcode(...)
    setCountry(...)

update 对象需要保存属性的当前值和新值。喜欢:

setStreetNumber(aString):
    self.oldStreetNumber = model.getStreetNumber
    self.newStreetNumber = aString

调用 execute 方法时,将更新模型:

execute:
    model.setStreetNumber(newStreetNumber)
    model.setCity(newCity)
    # Set postcode and country

    if not ModelValidator.isValid(model):
        self.undo()
        raise ValidationError

撤消方法如下所示:

undo:
    model.setStreetNumber(oldStreetNumber)
    model.setCity(oldCity)
    # Set postcode and country

这是很多打字,但它会起作用。通过不同类型的更新可以很好地封装模型对象。您可以通过在更新对象上调用这些方法来执行或撤消更改。您甚至可以存储更新对象列表,以便进行多级撤消和重试。

但是,对于程序员来说,打字需要很多。请考虑使用持久性数据结构。持久数据结构可用于非常快速地复制对象 -- 近似恒定的时间复杂度。这是一个持久数据结构的 python 库

假设您的数据位于 dict 的持久数据结构版本中。我引用的库将其称为 .PMap

更新类的实现可以更简单。从构造函数开始:

UpdateAddress(pmap)
    self.oldPmap = pmap
    self.newPmap = pmap

二传手更容易:

setStreetNumber(aString):
   self.newPmap = newPmap.set('streetNumber', aString) 

Execute 传回模型的新实例,其中包含所有更新。

execute:
    if ModelValidator.isValid(newModel):
         return newModel;
    else:
         raise ValidationError

原始对象根本没有改变,这要归功于持久数据结构的魔力。

最好的办法是不要做任何这些。请改用 ORM对象数据库。这就是“企业级”解决方案。这些库为您提供了复杂的工具,如事务和对象版本历史记录。