突变方法的单独界面


11

我一直在进行一些代码的重构,我想我可能已经迈出了第一步。我正在用Java编写示例,但我想它可能是不可知的。

我有一个Foo定义为的接口

public interface Foo {

    int getX();

    int getY();

    int getZ();
}

和一个实现

public final class DefaultFoo implements Foo {

    public DefaultFoo(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public int getZ() {
        return z;
    }

    private final int x;
    private final int y;
    private final int z;
}

我还有一个接口MutableFoo,可提供匹配的变异子

/**
 * This class extends Foo, because a 'write-only' instance should not
 * be possible and a bit counter-intuitive.
 */
public interface MutableFoo extends Foo {

    void setX(int newX);

    void setY(int newY);

    void setZ(int newZ);
}

MutableFoo可能存在两种实现方式(我尚未实现它们)。其中之一是

public final class DefaultMutableFoo implements MutableFoo {

    /**
     * A DefaultMutableFoo is not conceptually constructed 
     * without all values being set.
     */
    public DefaultMutableFoo(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public int getX() {
        return x;
    }

    public void setX(int newX) {
        this.x = newX;
    }

    public int getY() {
        return y;
    }

    public void setY(int newY) {
        this.y = newY;
    }

    public int getZ() {
        return z;
    }

    public void setZ(int newZ) {
        this.z = newZ;
    }

    private int x;
    private int y;
    private int z;
}

我之所以将它们分开,是因为每个人使用它的可能性相同。这意味着,使用这些类的某人也很可能想要一个不可变的实例,因为他们想要一个可变的实例。

我拥有的主要用例是一个名为接口的接口StatSet,它代表游戏的某些战斗细节(命中率,攻击,防御)。但是,“有效”统计数据或实际统计数据是基础统计数据(不能更改)和受训练统计数据(可以增加)的结果。这两个是由

/**
 * The EffectiveStats can never be modified independently of either the baseStats
 * or trained stats. As such, this StatSet must never provide mutators.
 */
public StatSet calculateEffectiveStats() {
    int effectiveHitpoints =
        baseStats.getHitpoints() + (trainedStats.getHitpoints() / 4);
    int effectiveAttack = 
        baseStats.getAttack() + (trainedStats.getAttack() / 4);
    int effectiveDefense = 
        baseStats.getDefense() + (trainedStats.getDefense() / 4);

    return StatSetFactory.createImmutableStatSet(effectiveHitpoints, effectiveAttack, effectiveDefense);
}

每次战斗后,训练有素的统计数据都会增加

public void grantExperience() {
    int hitpointsReward = 0;
    int attackReward = 0;
    int defenseReward = 0;

    final StatSet enemyStats = enemy.getEffectiveStats();
    final StatSet currentStats = player.getEffectiveStats();
    if (enemyStats.getHitpoints() >= currentStats.getHitpoints()) {
        hitpointsReward++;
    }
    if (enemyStats.getAttack() >= currentStats.getAttack()) {
        attackReward++;
    }
    if (enemyStats.getDefense() >= currentStats.getDefense()) {
        defenseReward++;
    }

    final MutableStatSet trainedStats = player.getTrainedStats();
    trainedStats.increaseHitpoints(hitpointsReward);
    trainedStats.increaseAttack(attackReward);
    trainedStats.increaseDefense(defenseReward);
}

但战斗结束后并没有增加。使用某些物品,使用某些战术,巧妙地使用战场都可以带来不同的体验。

现在我的问题是:

  1. 是否有用于按访问者和更改者将接口拆分为单独接口的名称?
  2. 如果以同样的方式使用它们,或者是否存在我应该使用的其他更可接受​​的模式(例如Foo foo = FooFactory.createImmutableFoo();,可能返回DefaultFooDefaultMutableFoo由于createImmutableFoo返回而被隐藏Foo)则以这种方式拆分它们是“正确”的方法吗?
  3. 除了使接口层次结构复杂化之外,使用此模式是否有立即可预见的缺点?

之所以开始设计这种方式,是因为我的想法是,接口的所有实现者都应遵循最简单的接口,并且仅提供其他功能。通过向接口添加设置器,现在可以独立于其部分修改有效统计信息

为新建一个类EffectiveStatSet没有多大意义,因为我们不会以任何方式扩展功能。我们可以更改实现,并将EffectiveStatSet两种不同的方法组合在一起StatSets,但是我认为这不是正确的解决方案。

public class EffectiveStatSet implements StatSet {

    public EffectiveStatSet(StatSet baseStats, StatSet trainedStats) {
        // ...
    }

    public int getHitpoints() {
        return baseStats.getHitpoints() + (trainedStats.getHitpoints() / 4);
    }
}


1
我认为问题是您的对象是可变的,即使您通过其“不可变”接口访问它也是如此。最好使用Player.TrainStats()拥有可变的对象
Ewan

@gnat您对我可以从中了解更多信息有任何提及吗?基于已编辑的问题,我不确定如何或在何处应用该问题。
Zymus

@gnat:您的链接(及其第二级和第三级链接)非常有用,但是上口的智慧短语却无济于事。它只会引起误解和鄙视。
rwong

Answers:


6

对我来说,看来您有解决问题的办法。

是否有用于按访问者和更改者将接口拆分为单独接口的名称?

这可能有点令人发指,但实际上我将其称为“设计过度”或“设计过于复杂”。通过提供同一类的可变且不变的变体,您可以为同一问题提供两种功能上等效的解决方案,它们仅在非功能方面(例如性能表现,API和对副作用的安全性)不同。我猜这是因为您担心做出一个更喜欢的决定,或者是因为您尝试在C#中实现C ++的“ const”功能。我猜在所有情况下的99%中,如果用户选择了可变或不可变的变体,并不能解决任何问题,那么他可以解决其中的问题。因此,“使用的类的可能性”

例外情况是,当您设计一种新的编程语言或多用途框架时,将由成千上万的程序员或更多的程序员使用。然后,当您提供通用数据类型的不变且可变的变体时,它确实可以更好地扩展。但是在这种情况下,您将有成千上万种不同的使用情况-我想这可能不是您面临的问题?

如果它们同样有可能被使用,或者存在不同的,更易于接受的模式,则以这种“正确”的方法进行拆分

“被接受的模式”被称为KISS-使其简单而愚蠢。为您的库中的特定类/接口决定是否支持可变性。例如,如果您的“ StatSet”具有十二个或更多属性,并且大多数都是单独更改的,那么我希望使用可变的变体,而不是在不应该修改它们的情况下修改基本统计信息。对于Foo具有属性X,Y,Z(三维向量)的类,我可能更喜欢不变的变体。

除了使接口层次结构复杂化之外,使用此模式是否有立即可预见的缺点?

过于复杂的设计使软件更难测试,更难维护,更难开发。


1
我认为您的意思是“
亲吻

2

是否有用于按访问者和更改者将接口拆分为单独接口的名称?

如果此分隔符有用并且可以提供我看不到好处,那么可能会有一个名称。如果分离主义没有带来好处,那么其他两个问题就毫无意义。

您能告诉我们两个分开的接口为我们带来收益的任何业务用例吗?还是这个问题是学术问题(YAGNI)?

我可以想到带有可变购物车(您可以放入更多商品)的商店,该商品可能成为无法再由客户更改商品的订单。订单状态仍然可变。

我见过的实现并没有分开接口

  • java中没有接口ReadOnlyList

ReadOnly版本使用“ WritableInterface”,如果使用了write方法,则抛出异常


我修改了OP来解释主要用例。
Zymus

2
“ Java中没有接口ReadOnlyList”-坦率地说,应该有。如果您有一段代码接受List <T>作为参数,则无法轻易判断它是否适用于不可修改的列表。返回列表的代码,无法知道是否可以安全地对其进行修改。唯一的选择是依赖于可能不完整或不准确的文档,或者制作防御性副本以防万一。只读集合类型将使此过程变得更加简单。
2015年

@Jules:的确,Java的方法似乎如上所述:确保文档完整且准确,并制作防御性副本以防万一。它肯定可以扩展到大型企业项目。
rwong 2015年

1

可变收集接口和只读合同收集接口的分离是接口隔离原理的一个示例。我不认为每种原理的应用都有特殊的名称。

请注意此处的几个单词:“只读合同”和“集合”。

只读合同意味着类A为类提供B了只读访问权限,但并不意味着基础集合对象实际上是不可变的。不变是指它永远不会改变,无论任何代理人。只读合同仅表示不允许收件人更改它;A允许其他人(尤其是class )进行更改。

为了使对象不可变,它必须是真正不可变的-它必须拒绝更改其数据的尝试,无论代理程序请求它是什么。

该模式最有可能在表示数据集合(列表,序列,文件(流)等)的对象上观察到。


“不变”一词已成为时尚,但这一概念并不新鲜。有许多使用不变性的方法,以及许多使用其他方法(即其竞争对手)来实现更好设计的方法。


这是我的方法(不基于不变性)。

  1. 定义DTO(数据传输对象),也称为值元组。
    • 这DTO将包含三个字段:hitpointsattackdefense
    • 只是领域:公开,任何人都可以写,没有保护。
    • 但是,DTO必须是一个可抛弃的对象:如果class A需要将DTO传递给class B,它将制作它的一个副本并改为传递该副本。因此,B可以按自己喜欢的方式使用DTO(向其中写入),而不会影响A保持的DTO 。
  2. grantExperience功能分为两部分:
    • calculateNewStats
    • increaseStats
  3. calculateNewStats 将从两个DTO中获取输入,一个代表玩家的统计数据,另一个代表敌方的统计数据,然后执行计算。
    • 对于输入,呼叫者应根据您的需要在基本统计信息,经过训练的统计数据或有效统计数据中进行选择。
    • 其结果将是一个新的DTO其中,每个字段(hitpointsattackdefense)存储量递增该能力。
    • 新的“增加金额” DTO与这些值的上限(施加的最大上限)无关。
  4. increaseStats 是一种在播放器(而非DTO)上采用“增加金额” DTO的方法,并将该增量应用于播放器拥有的并代表播放器可训练DTO的DTO。
    • 如果这些统计信息有适用的最大值,则在此处强制执行。

如果calculateNewStats发现不依赖于任何其他玩家或敌人的信息(除了两个输入DTO中的值之外),此方法可能位于项目的任何位置。

如果calculateNewStats发现完全依赖于玩家和敌对对象(即将来的玩家和敌对对象可能具有新属性,并且calculateNewStats必须进行更新以消耗尽可能多的新属性),则calculateNewStats必须接受这两个属性对象,而不仅仅是DTO。但是,其计算结果仍将是增量DTO,或任何携带用于执行增量/升级的信息的简单数据传输对象。


1

这里有一个大问题:仅仅因为数据结构是不可变的,并不意味着我们不需要它的修改版本。数据结构的真正不变版本确实提供setXsetYsetZ方法-它们只是返回一个结构,而不是修改您调用它们的对象。

// Mutates mutableFoo
mutableFoo.setX(...)
// Creates a new updated immutableFoo, existing immutableFoo is unchanged
newFoo = immutableFoo.setX(...)

那么,如何在限制其他部分的同时使系统的一部分能够更改它呢?使用包含对不可变结构实例的引用的可变对象。基本上,不是您的玩家类能够更改其stats对象,同时又给其他所有类一个其属性的不变视图,它的统计不可变的,并且玩家是可变的。代替:

// Stats are mutable, mutates self.stats
self.stats.setX(...)

您将拥有:

// Stats are immutable, mutates self, setX() returns new updated stats
self.stats = self.stats.setX(...)

看到不同?stats对象是完全不变的,但是玩家的当前 stats是对不变对象的可变引用。根本不需要两个接口-只需使数据结构完全不可变,并在您碰巧使用它存储状态的地方管理对它的可变引用。

这意味着您系统中的其他对象不能依赖对stats对象的引用-玩家更新其统计信息后,它们所拥有的stats对象将与玩家拥有的stats对象不同。

无论如何,这更有意义,因为从概念上说,并不是真正的统计数据在变化,而是玩家当前的统计数据。不是stats对象。该基准的状态对象选手对象了。因此,依赖于该引用的系统的其他部分应该全部显式引用player.currentStats()或类似地引用,而不是掌握玩家的基础stats对象,将其存储在某个位置并依靠其通过变异来更新。


1

哇...这真的带我回去了。我尝试了几次相同的想法。我太固执了,不能放弃它,因为我认为有一些好东西要学习。我也看到其他人也在代码中尝试了这一点。我见过的大多数实现都将只读接口(如您的FooViewer或)ReadOnlyFoo和仅写接口称为FooEditor或(WriteableFoo或),在Java中,我想我FooMutator曾经见过。我不确定以这种方式做事是否有官方甚至通用的词汇。

在我尝试过的地方,这对我没有任何帮助。我会完全避免。我会按照其他人的建议去做,然后退后一步,考虑您是否真的需要更大的构想中的这个概念。我不确定是否有正确的方法来执行此操作,因为在尝试此操作时,我从未真正保留过自己生成的任何代码。每次我经过大量的努力并简化工作后都退出。我的意思是说其他人关于YAGNI,KISS和DRY的看法。

可能的缺点:重复。您可能必须为许多类创建这些接口,甚至对于一个类,您都必须命名每个方法并至少描述两次签名。在使我烦恼的所有事情中,必须以这种方式更改多个文件使我陷入最大的麻烦。我最终忘了在一个地方或另一个地方进行更改。一旦有了一个名为Foo的类以及名为FooViewer和FooEditor的接口,如果决定将其命名为Bar,那就更好了,除非您有一个非常出色的智能IDE,否则您必须重构->重命名三次。当我有大量来自代码生成器的代码时,我什至发现了这种情况。

就我个人而言,我不喜欢为也只有一个实现的事物创建接口。这意味着当我在代码中四处走动时,我不能只是直接跳到唯一的实现,我必须跳到界面,然后跳到界面的实现,或者至少按下一些额外的键才能到达那里。这些东西加起来。

然后是您提到的并发症。我怀疑我是否会再以这种特殊方式处理我的代码。即使对于最适合我的总体代码计划(我构建的ORM)的地方,我也将其替换并替换为易于编写的代码。

我真的会考虑您提到的构图。我很好奇,为什么您会觉得这不是一个好主意。我本来期望有EffectiveStats撰写和不可改变Stats和像StatModifiers这将进一步构成有一套StatModifiers表示代表任何可能会修改统计(临时效果,物品,地点在一些增强的地方,疲劳),但你EffectiveStats不需要理解,因为StatModifiers会管理这些东西是什么,以及它们将对哪些统计数据产生何种影响以及将产生什么样的影响。的StatModifier将是不同事物的接口,并且可以知道诸如“我在该区域中”,“药物何时消失”之类的信息,但是……甚至不必让任何其他对象知道这些事物。只需说出当前的统计信息和修改方式即可。更好的是,StatModifier可以简单地公开一种Stats基于新的不可变性而产生新方法的方法,该方法Stats将有所不同,因为已对其进行了适当的更改。然后,您可以执行类似操作,currentStats = statModifier2.modify(statModifier1.modify(baseStats))并且所有Stats操作都是不可变的。我什至不会直接编写代码,我可能会遍历所有修饰符,并将每个修饰符应用于以开头的先前修饰符的结果baseStats

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.