jjzjj

c# - 单一职责原则(SRP)和我的 rpg 的类结构看起来 "weird"

coder 2024-05-24 原文

我正在制作一个角色扮演游戏只是为了好玩并了解更多关于 SOLID 原则的信息。我关注的第一件事就是SRP。我有一个代表游戏中角色的“角色”类。它有诸如名称、生命值、法力值、能力分数等内容。

现在,通常我也会在我的 Character 类中放置方法,所以它看起来像这样......

   public class Character
   {
      public string Name { get; set; }
      public int Health { get; set; }
      public int Mana { get; set; }
      public Dictionary<AbilityScoreEnum, int>AbilityScores { get; set; }

      // base attack bonus depends on character level, attribute bonuses, etc
      public static void GetBaseAttackBonus();  
      public static int RollDamage();
      public static TakeDamage(int amount);
   }

但是由于 SRP,我决定将所有方法移到一个单独的类中。我将该类命名为“CharacterActions”,现在方法签名看起来像这样......
public class CharacterActions
{
    public static void GetBaseAttackBonus(Character character);
    public static int RollDamage(Character character);
    public static TakeDamage(Character character, int amount);
}

请注意,我现在必须在所有 CharacterActions 方法中包含我正在使用的 Character 对象。这是利用 SRP 的正确方法吗?它似乎完全违背了 OOP 封装的概念。

或者我在这里做错了什么?

我喜欢的一件事是我的 Character 类非常清楚它的作用,它只是代表一个 Character 对象。

最佳答案

更新 - 我重做了我的答案,因为经过半夜的 sleep ,我真的觉得我之前的答案不是很好。

要查看 SRP 的示例,让我们考虑一个非常简单的字符:

public abstract class Character
{
    public virtual void Attack(Character target)
    {
        int damage = Random.Next(1, 20);
        target.TakeDamage(damage);
    }

    public virtual void TakeDamage(int damage)
    {
        HP -= damage;
        if (HP <= 0)
            Die();
    }

    protected virtual void Die()
    {
        // Doesn't matter what this method does right now
    }

    public int HP { get; private set; }
    public int MP { get; private set; }
    protected Random Random { get; private set; }
}

好的,所以这将是一个非常无聊的角色扮演游戏。但是这个类是有道理的。这里的一切都与 Character 直接相关。每个方法要么是由 Character 执行的操作,要么是在 Attack 上执行的操作。嘿,游戏很简单!

让我们专注于 Character 部分,并尝试让它变得有趣:
public abstract class Character
{
    public const int BaseHitChance = 30;

    public virtual void Attack(Character target)
    {
        int chanceToHit = Dexterity + BaseHitChance;
        int hitTest = Random.Next(100);
        if (hitTest < chanceToHit)
        {
            int damage = Strength * 2 + Weapon.DamageRating;
            target.TakeDamage(damage);
        }
    }

    public int Strength { get; private set; }
    public int Dexterity { get; private set; }
    public Weapon Weapon { get; set; }
}

现在我们正在到达某个地方。角色有时会错过,并且伤害/命中会随着等级而增加(假设 STR 也会增加)。 Jolly 不错,但这仍然很乏味,因为它没有考虑任何有关目标的内容。让我们看看我们是否可以解决这个问题:
public void Attack(Character target)
{
    int chanceToHit = CalculateHitChance(target);
    int hitTest = Random.Next(100);
    if (hitTest < chanceToHit)
    {
        int damage = CalculateDamage(target);
        target.TakeDamage(damage);
    }
}

protected int CalculateHitChance(Character target)
{
    return Dexterity + BaseHitChance - target.Evade;
}

protected int CalculateDamage(Character target)
{
    return Strength * 2 + Weapon.DamageRating - target.Armor.ArmorRating -
        (target.Toughness / 2);
}

此时,您的脑海中应该已经有了一个问题:为什么 Character 负责计算自己对目标的伤害?为什么它甚至有这种能力?这个类在做什么有一些无形的奇怪,但在这一点上它仍然有点模棱两可。仅仅从 Character 类中移出几行代码真的值得重构吗?可能不是。

但是让我们看看当我们开始添加更多功能时会发生什么——比如典型的 1990 年代 RPG:
protected int CalculateDamage(Character target)
{
    int baseDamage = Strength * 2 + Weapon.DamageRating;
    int armorReduction = target.Armor.ArmorRating;
    int physicalDamage = baseDamage - Math.Min(armorReduction, baseDamage);
    int pierceDamage = (int)(Weapon.PierceDamage / target.Armor.PierceResistance);
    int elementDamage = (int)(Weapon.ElementDamage /
        target.Armor.ElementResistance[Weapon.Element]);
    int netDamage = physicalDamage + pierceDamage + elementDamage;
    if (HP < (MaxHP * 0.1))
        netDamage *= DesperationMultiplier;
    if (Status.Berserk)
        netDamage *= BerserkMultiplier;
    if (Status.Weakened)
        netDamage *= WeakenedMultiplier;
    int randomDamage = Random.Next(netDamage / 2);
    return netDamage + randomDamage;
}

这一切都很好,但在 Character 类中进行所有这些数字运算是不是有点荒谬?这是一个相当短的方法;在真正的 RPG 中,这种方法可能会扩展到数百行,包括豁免和所有其他 Nerd 方式。想象一下,你引进了一个新的程序员,他们说:我收到了一个双击武器的请求,它应该能将正常情况下的伤害加倍;我需要在哪里进行更改?然后你告诉他,检查 Character 类。 嗯??

更糟糕的是,也许游戏增加了一些新的皱纹,比如,哦,我不知道,背刺奖励,或其他类型的环境奖励。那么你到底应该如何在 Character 类中弄清楚呢?你可能最终会调用一些单例,比如:
protected int CalculateDamage(Character target)
{
    // ...
    int backstabBonus = Environment.Current.Battle.IsFlanking(this, target);
    // ...
}

哎呀。这太可怕了。测试和调试这将是一场噩梦。那么我们该怎么办?将其从 Character 类中取出。 Character 类应该 只有 知道如何做 Character 在逻辑上知道如何做的事情,并且计算对目标的确切伤害确实不是其中之一。我们将为它创建一个类:
public class DamageCalculator
{
    public DamageCalculator()
    {
        this.Battle = new DefaultBattle();
        // Better: use an IoC container to figure this out.
    }

    public DamageCalculator(Battle battle)
    {
        this.Battle = battle;
    }

    public int GetDamage(Character source, Character target)
    {
        // ...
    }

    protected Battle Battle { get; private set; }
}

好多了。这个类只做一件事。它做它在锡上所说的。我们已经摆脱了单例依赖,所以这个类现在实际上可以测试了,感觉更对了,不是吗?现在我们的 Character 可以专注于 Character 操作:
public abstract class Character
{
    public virtual void Attack(Character target)
    {
        HitTest ht = new HitTest();
        if (ht.CanHit(this, target))
        {
            DamageCalculator dc = new DamageCalculator();
            int damage = dc.GetDamage(this, target);
            target.TakeDamage(damage);
        }
    }
}

即使现在,一个 Character 直接调用另一个 TakeDamageCharacter 方法也有点可疑,实际上你可能只是希望角色将其攻击“提交”给某种战斗引擎,但我认为那部分是最好的留给读者作为练习。

现在,希望你明白为什么会这样:
public class CharacterActions
{
    public static void GetBaseAttackBonus(Character character);
    public static int RollDamage(Character character);
    public static TakeDamage(Character character, int amount);
}

……基本没用。它出什么问题了?
  • 没有明确的目的;通用的“ Action ”不是 单一的 责任;
  • 它无法完成 Character 本身无法完成的任何事情;
  • 完全取决于 Character 而没有别的;
  • 它可能需要您公开您真正想要私有(private)/ protected CharacterActions 类的部分。
  • Character 类打破了 DamageCalculator 的封装,它自己几乎没有添加任何东西。另一方面,Character 类提供了新的封装,并通过消除所有不必要的依赖项和不相关的功能来帮助恢复原始 ojit_code 类的内聚力。如果我们想改变计算伤害的方式,从哪里看就很明显了。

    我希望这现在能更好地解释原理。

    关于c# - 单一职责原则(SRP)和我的 rpg 的类结构看起来 "weird",我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/2144885/

    有关c# - 单一职责原则(SRP)和我的 rpg 的类结构看起来 "weird"的更多相关文章

    1. ruby-on-rails - rails : "missing partial" when calling 'render' in RSpec test - 2

      我正在尝试测试是否存在表单。我是Rails新手。我的new.html.erb_spec.rb文件的内容是:require'spec_helper'describe"messages/new.html.erb"doit"shouldrendertheform"dorender'/messages/new.html.erb'reponse.shouldhave_form_putting_to(@message)with_submit_buttonendendView本身,new.html.erb,有代码:当我运行rspec时,它失败了:1)messages/new.html.erbshou

    2. ruby-on-rails - 由于 "wkhtmltopdf",PDFKIT 显然无法正常工作 - 2

      我在从html页面生成PDF时遇到问题。我正在使用PDFkit。在安装它的过程中,我注意到我需要wkhtmltopdf。所以我也安装了它。我做了PDFkit的文档所说的一切......现在我在尝试加载PDF时遇到了这个错误。这里是错误:commandfailed:"/usr/local/bin/wkhtmltopdf""--margin-right""0.75in""--page-size""Letter""--margin-top""0.75in""--margin-bottom""0.75in""--encoding""UTF-8""--margin-left""0.75in""-

    3. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

      我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

    4. ruby-on-rails - Rails 源代码 : initialize hash in a weird way? - 2

      在rails源中:https://github.com/rails/rails/blob/master/activesupport/lib/active_support/lazy_load_hooks.rb可以看到以下内容@load_hooks=Hash.new{|h,k|h[k]=[]}在IRB中,它只是初始化一个空哈希。和做有什么区别@load_hooks=Hash.new 最佳答案 查看rubydocumentationforHashnew→new_hashclicktotogglesourcenew(obj)→new_has

    5. ruby - 检查 "command"的输出应该包含 NilClass 的意外崩溃 - 2

      为了将Cucumber用于命令行脚本,我按照提供的说明安装了arubagem。它在我的Gemfile中,我可以验证是否安装了正确的版本并且我已经包含了require'aruba/cucumber'在'features/env.rb'中为了确保它能正常工作,我写了以下场景:@announceScenario:Testingcucumber/arubaGivenablankslateThentheoutputfrom"ls-la"shouldcontain"drw"假设事情应该失败。它确实失败了,但失败的原因是错误的:@announceScenario:Testingcucumber/ar

    6. ruby-on-rails - 迷你测试错误 : "NameError: uninitialized constant" - 2

      我遵循MichaelHartl的“RubyonRails教程:学习Web开发”,并创建了检查用户名和电子邮件长度有效性的测试(名称最多50个字符,电子邮件最多255个字符)。test/helpers/application_helper_test.rb的内容是:require'test_helper'classApplicationHelperTest在运行bundleexecraketest时,所有测试都通过了,但我看到以下消息在最后被标记为错误:ERROR["test_full_title_helper",ApplicationHelperTest,1.820016791]test

    7. ruby-on-rails - 相关表上的范围为 "WHERE ... LIKE" - 2

      我正在尝试从Postgresql表(table1)中获取数据,该表由另一个相关表(property)的字段(table2)过滤。在纯SQL中,我会这样编写查询:SELECT*FROMtable1JOINtable2USING(table2_id)WHEREtable2.propertyLIKE'query%'这工作正常:scope:my_scope,->(query){includes(:table2).where("table2.property":query)}但我真正需要的是使用LIKE运算符进行过滤,而不是严格相等。然而,这是行不通的:scope:my_scope,->(que

    8. 使用 ACL 调用 upload_file 时出现 Ruby S3 "Access Denied"错误 - 2

      我正在尝试编写一个将文件上传到AWS并公开该文件的Ruby脚本。我做了以下事情:s3=Aws::S3::Resource.new(credentials:Aws::Credentials.new(KEY,SECRET),region:'us-west-2')obj=s3.bucket('stg-db').object('key')obj.upload_file(filename)这似乎工作正常,除了该文件不是公开可用的,而且我无法获得它的公共(public)URL。但是当我登录到S3时,我可以正常查看我的文件。为了使其公开可用,我将最后一行更改为obj.upload_file(file

    9. ruby - 安装 Ruby 时遇到问题(无法下载资源 "readline--patch") - 2

      当我尝试安装Ruby时遇到此错误。我试过查看this和this但无济于事➜~brewinstallrubyWarning:YouareusingOSX10.12.Wedonotprovidesupportforthispre-releaseversion.Youmayencounterbuildfailuresorotherbreakages.Pleasecreatepull-requestsinsteadoffilingissues.==>Installingdependenciesforruby:readline,libyaml,makedepend==>Installingrub

    10. c# - 如何在 ruby​​ 中调用 C# dll? - 2

      如何在ruby​​中调用C#dll? 最佳答案 我能想到几种可能性:为您的DLL编写(或找人编写)一个COM包装器,如果它还没有,则使用Ruby的WIN32OLE库来调用它;看看RubyCLR,其中一位作者是JohnLam,他继续在Microsoft从事IronRuby方面的工作。(估计不会再维护了,可能不支持.Net2.0以上的版本);正如其他地方已经提到的,看看使用IronRuby,如果这是您的技术选择。有一个主题是here.请注意,最后一篇文章实际上来自JohnLam(看起来像是2009年3月),他似乎很自在地断言RubyCL

    随机推荐