2010-01-27 22 views
13

Tôi đang tạo một trò chơi nhập vai chỉ để giải trí và tìm hiểu thêm về các nguyên tắc SOLID. Một trong những điều đầu tiên tôi tập trung là SRP. Tôi có một lớp "Character" đại diện cho một nhân vật trong game. Nó có những thứ như Name, Y tế, Mana, AbilityScores vvNguyên tắc trách nhiệm duy nhất (SRP) và cấu trúc lớp của rpg của tôi trông "lạ"

Bây giờ, bình thường tôi cũng sẽ đặt các phương thức trong lớp nhân vật của tôi vì vậy nó sẽ giống như thế này ...

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); 
    } 

Nhưng vì SRP Tôi đã quyết định chuyển tất cả các phương thức ra thành một lớp riêng biệt. Tôi tên là lớp "CharacterActions" và bây giờ là chữ ký phương pháp giống như thế này ...

public class CharacterActions 
{ 
    public static void GetBaseAttackBonus(Character character); 
    public static int RollDamage(Character character); 
    public static TakeDamage(Character character, int amount); 
} 

Chú ý rằng tôi bây giờ phải bao gồm các đối tượng nhân vật tôi đang sử dụng trong tất cả các phương pháp CharacterActions tôi. Đây có phải là cách phù hợp để tận dụng SRP không? Dường như nó hoàn toàn trái ngược với khái niệm đóng gói OOP.

Hoặc tôi đang làm điều gì đó hoàn toàn sai ở đây?

MỘT điều tôi thích về điều này là lớp nhân vật của tôi rất rõ ràng về những gì nó làm, nó chỉ đơn giản là đại diện cho một đối tượng nhân vật.

+1

SRP là nguyên tắc chịu trách nhiệm duy nhất. –

+0

ok cảm ơn, tiêu đề đã chỉnh sửa – mikedev

Trả lời

22

Cập nhật - Tôi đã làm lại câu trả lời vì sau nửa đêm ngủ, tôi thực sự không cảm thấy câu trả lời trước đó của tôi rất tốt.

Để xem một ví dụ về SRP trong hành động, chúng ta hãy xem xét một nhân vật rất đơn giản:

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; } 
} 

OK, vì vậy đây sẽ là một game nhập vai khá nhàm chán. Nhưng lớp này có ý nghĩa. Mọi thứ ở đây liên quan trực tiếp đến Character. Mọi phương pháp đều là một hành động được thực hiện bởi hoặc được thực hiện trên Character. Này, trò chơi rất dễ!

Hãy tập trung vào phần Attack và cố gắng làm này ít nhất nửa thú vị:

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; } 
} 

Bây giờ chúng ta đang nhận được ở đâu đó. Các nhân vật đôi khi bỏ lỡ, và thiệt hại/hit đi lên với mức độ (giả sử rằng STR tăng lên là tốt). Jolly tốt, nhưng điều này vẫn còn khá ngu si đần độn bởi vì nó không đưa vào tài khoản bất cứ điều gì về mục tiêu. Chúng ta hãy xem liệu chúng ta có thể khắc phục điều đó:

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); 
} 

Tại thời điểm này, câu hỏi nên đã được hình thành trong tâm trí của bạn: Tại sao Character chịu trách nhiệm về tính toán thiệt hại của riêng mình chống lại một mục tiêu? Tại sao nó lại có khả năng đó? Có điều gì đó vô hình lạ về những gì lớp học này đang làm, nhưng tại thời điểm này nó vẫn còn loại mơ hồ. Là nó thực sự giá trị tái cấu trúc chỉ để di chuyển một vài dòng mã ra khỏi lớp Character? Chắc là không.

Nhưng chúng ta hãy nhìn vào những gì sẽ xảy ra khi chúng tôi bắt đầu thêm nhiều tính năng hơn - nói từ một RPG năm 1990 thời điển hình:

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; 
} 

Đây là tất cả tiền phạt và dandy nhưng không phải là nó một chút lố bịch được làm tất cả số này-crunching trong lớp Character? Và đây là một phương pháp khá ngắn; trong một game nhập vai thực sự, phương pháp này có thể mở rộng thành hàng trăm dòng với các cú ném tiết kiệm và tất cả các cách thức khác của nerditude. Hãy tưởng tượng, bạn mang theo một lập trình viên mới, và họ nói: Tôi đã nhận được một yêu cầu cho một vũ khí hai hit đó là nghĩa vụ phải tăng gấp đôi bất cứ thiệt hại bình thường sẽ được; Tôi cần phải thay đổi ở đâu? Và bạn nói với anh ta, Kiểm tra lớp Character.Huh ??

Thậm chí tệ hơn, có lẽ trò chơi sẽ thêm một số nếp nhăn mới như, tôi không biết, tiền thưởng backstab hoặc một số loại tiền thưởng môi trường khác. Làm thế nào địa ngục là bạn phải hình dung ra trong lớp Character? Có thể bạn sẽ kết thúc việc gọi ra một số singleton, như:

protected int CalculateDamage(Character target) 
{ 
    // ... 
    int backstabBonus = Environment.Current.Battle.IsFlanking(this, target); 
    // ... 
} 

Yuck. Điều này thật khủng khiếp. Kiểm tra và gỡ lỗi này sẽ là một cơn ác mộng. Vậy ta phải làm sao? Lấy nó ra khỏi lớp Character. Lớp Character chỉ nên chỉ biết cách thực hiện những việc mà Character sẽ biết cách làm, và tính toán thiệt hại chính xác đối với mục tiêu thực sự không phải là một trong số chúng. Chúng tôi sẽ tạo một lớp học cho nó:

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; } 
} 

Tốt hơn nhiều. Lớp này thực hiện chính xác một điều. Nó không những gì nó nói trên tin. Chúng tôi đã loại bỏ sự phụ thuộc singleton, vì vậy lớp học này thực sự có thể kiểm tra ngay bây giờ, và nó cảm thấy rất nhiều quyền, phải không? Và bây giờ Character của chúng tôi có thể tập trung vào Character hành động:

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); 
     } 
    } 
} 

Ngay cả bây giờ đó là một chút nghi ngờ rằng một Character được trực tiếp gọi phương pháp TakeDamage khác 's Character, và trong thực tế bạn muốn có lẽ chỉ muốn các nhân vật để "submit "nó tấn công vào một loại động cơ chiến đấu nào đó, nhưng tôi nghĩ rằng phần tốt nhất còn lại là một bài tập cho người đọc.


Bây giờ, hy vọng, bạn hiểu tại sao điều này:

public class CharacterActions 
{ 
    public static void GetBaseAttackBonus(Character character); 
    public static int RollDamage(Character character); 
    public static TakeDamage(Character character, int amount); 
} 

... về cơ bản là vô dụng. Có gì sai với nó?

  • Nó không có mục đích rõ ràng; "hành động" chung không phải là đơn trách nhiệm;
  • Không thể thực hiện bất kỳ điều gì mà chính mình không thể tự thực hiện;
  • Nó phụ thuộc hoàn toàn vào Character và không có gì khác;
  • Nó có thể sẽ yêu cầu bạn để lộ các bộ phận của lớp Character mà bạn thực sự muốn riêng tư/được bảo vệ.

Lớp CharacterActions ngắt gói Character và không thêm chút gì cho riêng mình. Mặt khác, lớp DamageCalculator cung cấp một gói mới và giúp khôi phục sự gắn kết của lớp Character gốc bằng cách loại bỏ tất cả các phụ thuộc không cần thiết và chức năng không liên quan. Nếu chúng ta muốn thay đổi một cái gì đó về cách thiệt hại được tính toán, nó là rõ ràng nơi để xem xét.

Tôi hy vọng rằng điều này giải thích nguyên tắc tốt hơn ngay bây giờ.

+0

Bạn đã thấy tập hợp các dự báo này chưa? http://www.dimecasts.net/Casts/CastDetails/88 Đây là nơi tôi có ý tưởng từ đó. Về cơ bản, họ bắt đầu với một lớp Báo cáo có phương pháp in báo cáo và định dạng báo cáo. Đến cuối tập, họ tạo ra một lớp "ReportFormatter" và "ReportPrinter". Tôi không thực sự thấy sự khác biệt giữa những gì tôi đang làm và những gì họ đã làm. Nếu bạn đã theo dõi nó, bạn có thể vui lòng giúp tôi hiểu những gì tôi đang làm là khác nhau? – mikedev

+0

@mikedev: Ý tưởng đằng sau một cái gì đó như 'ReportFormatter' hay' ReportPrinter' là đôi khi một hành động riêng lẻ vô cùng phức tạp, bạn muốn đóng gói nó trong lớp riêng của nó để loại bỏ một số phức tạp đó khỏi thực thể chính. Để thích nghi điều này với ví dụ của bạn, nếu tính toán tiền thưởng tấn công là một hoạt động rất phức tạp, bạn có thể trừu tượng điều này thành một 'AttackBonusCalculator' hoặc một cái gì đó tương tự. Tuy nhiên, chỉ cần di chuyển ** tất cả ** các hành động từ lớp 'Character' vào một lớp" Hành động "chung không thêm bất cứ điều gì đặc biệt hữu ích. HTH. – Aaronaught

+0

@mikedev: Tôi sẽ thêm rằng những gì @Aaronaught đang nói là không xóa 'GetBaseAttackBonus' khỏi lớp Character, nhưng' AttackBonusCalculator' được gọi là form đó. – Zano

7

SRP không có nghĩa là một lớp không nên có phương pháp. Những gì bạn đã thực hiện được tạo ra là cấu trúc dữ liệu thay vì đối tượng đa hình. Có những lợi ích để làm như vậy, nhưng nó có thể không có ý định hoặc cần thiết trong trường hợp này.

Một cách bạn thường có thể biết một đối tượng có vi phạm SRP hay không là xem các biến cá thể mà các phương thức trong đối tượng sử dụng. Nếu có các nhóm phương thức sử dụng các biến mẫu cụ thể, nhưng không phải là các biến khác, thì đó thường là dấu hiệu cho thấy đối tượng của bạn có thể được chia nhỏ theo các nhóm biến mẫu.

Ngoài ra, có thể bạn không muốn phương pháp của mình tĩnh. Bạn có thể muốn tận dụng tính đa hình - khả năng làm điều gì đó khác biệt trong các phương pháp của bạn dựa trên loại cá thể mà phương thức được gọi.

Ví dụ: các phương pháp của bạn có cần thay đổi không nếu bạn có ElfCharacterWizardCharacter? Nếu các phương pháp của bạn hoàn toàn sẽ không bao giờ thay đổi và hoàn toàn tự chứa, thì có lẽ tĩnh là ổn ... nhưng thậm chí sau đó nó làm cho thử nghiệm khó khăn hơn nhiều.

0

Phương pháp mà tôi đi về khi thiết kế các lớp và đó là những gì OO dựa trên là một đối tượng mô hình hóa một đối tượng thế giới thực.

Hãy xem Nhân vật ... Thiết kế lớp nhân vật có thể khá thú vị. Nó có thể là một hợp đồng ICharacter Điều này có nghĩa rằng bất cứ điều gì muốn trở thành một nhân vật sẽ có thể thực hiện Walk(), Talk(), Attack(), Có một số thuộc tính như Health, Mana.

Sau đó, bạn có thể có một Wizard, một Wizard có đặc tính đặc biệt và cách tấn công khác với, giả sử Fighter.

Tôi có xu hướng không bị ép buộc bởi các nguyên tắc thiết kế, mà còn nghĩ về việc lập mô hình một đối tượng thế giới thực.

2

Tôi nghĩ điều đó phụ thuộc nếu hành vi của nhân vật của bạn có thể thay đổi. Ví dụ, nếu bạn muốn những hành động có sẵn để thay đổi (dựa trên cái gì khác xảy ra trong RPG), bạn có thể đi cho một cái gì đó như:

public interface ICharacter 
{ 
    //... 
    IEnumerable<IAction> Actions { get; } 
} 

public interface IAction 
{ 
    ICharacter Character { get; } 
    void Execute(); 
} 

public class BaseAttackBonus : IAction 
{ 
    public BaseAttackBonus(ICharacter character) 
    { 
     Character = character; 
    } 

    public ICharacter Character { get; private set; } 

    public void Execute() 
    { 
     // Get base attack bonus for character... 
    } 
} 

này cho phép nhân vật của bạn để có nhiều hành động như bạn muốn (có nghĩa là bạn có thể thêm/xóa các hành động mà không thay đổi lớp ký tự) và mỗi hành động chỉ chịu trách nhiệm về chính nó (và biết về ký tự) và các hành động có yêu cầu phức tạp hơn, kế thừa từ IAction để thêm thuộc tính, v.v. giá trị trả về khác nhau cho Execute và bạn có thể muốn một hàng đợi các hành động, nhưng bạn nhận được sự trôi dạt.

Lưu ý sử dụng ICharacter thay vì ký tự, vì các ký tự có thể có các đặc tính và hành vi khác nhau (Warlock, Wizard, v.v.), nhưng tất cả chúng đều có hành động. Bằng cách tách ra các hành động, nó cũng làm cho thử nghiệm dễ dàng hơn nhiều, vì bây giờ bạn có thể kiểm tra từng hành động mà không cần phải tạo ra một nhân vật đầy đủ, và với ICharacter bạn có thể tạo nhân vật của mình dễ dàng hơn.

+0

Ha, tôi vừa viết một số mã mẫu với IAction và ICharacter – Kane

+0

Chỉ cần rõ ràng, thuộc tính "Hành động" trong giao diện ICharacter có chứa tất cả các hành động có thể có mà ICharacter có thể thực hiện không? – mikedev

+0

@mikedev, vâng, tôi sẽ viết rằng nhân vật của bạn không còn phải chịu trách nhiệm quyết định những hành động nào có thể, hoặc khi chúng được thực thi. Đó có thể là do một cái gì đó bên ngoài với nhân vật, chẳng hạn như môi trường của họ. Vì vậy, bạn có thể xem xét một lớp (hoặc hai) có trách nhiệm quyết định hành động cũng như thực hiện chúng. – si618

2

Tôi không biết rằng tôi thực sự gọi đó là loại SRP ở nơi đầu tiên. "Mọi thứ liên quan đến foo" thường là dấu hiệu cho thấy bạn là không phải là sau SRP (điều này không sao, nó không phù hợp với tất cả các kiểu thiết kế).

Cách tốt nhất để xem ranh giới SRP là "có bất kỳ điều gì tôi có thể thay đổi về lớp học sẽ rời khỏi phần lớn lớp học không?" Nếu có, hãy tách chúng ra. Hoặc, để đặt nó theo một cách khác, nếu bạn chạm vào một phương pháp trong một lớp học, bạn có lẽ phải chạm vào tất cả chúng. Một trong những ưu điểm của SRP là nó giảm thiểu phạm vi của những gì bạn đang chạm khi bạn thực hiện thay đổi - nếu một tệp khác bị ảnh hưởng, bạn biết bạn không thêm lỗi vào nó!

Các lớp nhân vật nói riêng có mức độ nguy hiểm cao khi trở thành lớp học thần trong RPG. Một cách có thể tránh điều này sẽ là tiếp cận điều này theo một cách khác - bắt đầu với giao diện người dùng của bạn và tại mỗi bước, chỉ cần khẳng định rằng giao diện bạn muốn tồn tại từ góc nhìn của lớp bạn hiện đang viết đã tồn tại. Ngoài ra, hãy xem xét nguyên tắc Inversion of Control và cách thiết kế thay đổi khi sử dụng IoC (không nhất thiết phải là một thùng chứa IoC).

Các vấn đề liên quan