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ờ.
SRP là nguyên tắc chịu trách nhiệm duy nhất. –
ok cảm ơn, tiêu đề đã chỉnh sửa – mikedev