Делать объекты двух классов друг из друга плохой практикой? - PullRequest
0 голосов
/ 16 марта 2020

Вот минимальный псевдокод того, что я использую:

class A{
    //other variables
    B b;

    void delayedPartnerInit(B b){
        this.b=b;
    }
}

class B{
    //some other variables

    A a;

    void delayedPartnerInit(A a){
        this.a=a;
    }
}

Я мог бы превратить его в один класс, но некоторые члены (не показаны здесь) A должны существовать до того, как появятся данные о B Другими словами, объекты A и B создаются в разное время, но нуждаются в ссылке на переменные друг друга, когда оба набора переменных доступны.

Вопрос в том, есть ли лучший способ сделать это? Мне не хватает какой-то базовой c концепции программирования? Хотя в настоящее время я работаю над C#, у меня уже была эта мысль много раз при работе с другими языками.

Обновление : я использую это в Игровой движок Unity, где B - скрипт Unity C#. Так как Unity не позволяет нам создавать сценарии, не добавляя их к чему-либо, мне нужно 2 класса. Я получаю некоторые данные (данные А), которые необходимо обработать.

Я не упоминал об этом раньше, потому что задал их как общий c вопрос.

Примечание перед закрытием как дубликат : я проверил похожие вопросы, но нашел только определенные c вопросы, которые вызывали проблемы у авторов, которые пытались делать то, что я делаю. У меня вопрос, является ли это плохой практикой.

1 Ответ

0 голосов
/ 16 марта 2020

Сильно связанные классы, как правило, являются плохой практикой:

  • Изменения в одном классе приводят к изменениям в другом.
  • Вы не можете протестировать один из классов, не создав (или не насмехаясь) другой. , Что в вашем случае создает циклическую зависимость.
  • Оба класса зависят от реализации друг друга, а не от абстракций.
  • Труднее другим людям (или вам самим через полгода) понять и рассуждать о первом классе logi c без проверки второго класса.

Поскольку Unity не позволяет нам создавать скрипты, не добавляя их в то, что мне нужно, 2 класса. Я получаю определенные данные (данные А), которые требуют обработки

MVC шаблон для Unity предоставляет полезный трюк для разъединения монобихев:

interface ISomeObjectView {
    event Action OnUpdate;
    event Action Destroyed;
    event Action TriggerEntered;
    void SetTransform(Vector3 position, Quaternion rotation);
    void AddForce(Vector3 force);
    // Other methods or events you need to expose:
    // MouseOver, OnFixedUpdate, Move() or SetScale(), ...
}

Сам MonoBehaviour не содержит каких-либо логи c, он просто вызывает события и использует входящие значения:

public void SetTransform(Vector3 position, Quaternion rotation)
{
    // Params validation
    transform.rotation = rotation;
    transform.position = position;
}

private void Update()
    => OnUpdate?.Invoke();

MonoBehaviour logi c необходимо переместить в ваш класс данных или новый класс контроллера. Теперь ваш класс данных просто связывает себя с предоставленными интерфейсными событиями и методами без циклических зависимостей. Monobehaviour не требует никаких ссылок на другие классы, он просто предоставляет методы для манипулирования собой и событиями для захвата ввода.

Этот прием помогает несколькими способами:

  • MonoBehaviour не зависит на что-либо и не требует никаких ссылок на другие классы.
  • Ваши классы data / logi c не требуют специальных знаний о monobehaviours, только предоставленный интерфейс.
  • Вы можете иметь несколько реализаций интерфейса, переключение различных представлений в зависимости от ситуации.
  • Легко тестировать, легко насмехаться.
  • Вы можете перемещать все «вещи Unity» внутри MonoBehaviour и писать все связанные классы на чистом C#. Если вы хотите.

Обратите внимание, что использование event Action является не обычным способом для обработки событий! Я думаю, что это очень удобно, но я бы предложил использовать обычный EventHandler (UnityEvents - еще один вариант, который может удовлетворить ваши потребности).


Обновление : пример простого MVC.

Рассмотрим следующий Player контроллер:

[Serializable]
public class PlayerInfo {

    // Values configurable via inspector
    [SerializeField] private float speed = 1.5f;
    [SerializeField] private float jumpHeight = 5;
    [SerializeField] private float damage = 15;
    [SerializeField] private Player playerPrefab;

    public float Speed => speed;
    public float JumpHeight => jumpHeight ;
    public float Damage => damage;

    private Player playerInstance;

    public void InitializePlayer() {
        playerInstance = Instantiate(playerPrefab);
        playerInstance.Info = this;
    }

    public void TeleportTo(Vector3 newPosition) {
        playerInstance.transform.position = newPosition;
    }
}

public class Player : MonoBehaviour {
    public PlayerInfo Info { get; set; }
    private Rigidbody rb;

    private void Awake() {
        rb = GetComponent<Rigidbody>();
    }

    private void Update() {
        if (Input.GetButtonDown("Jump")
            rb.AddForce(Vector3.up * info.JumpHeight);
        Vector3 movement = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
        rb.AddForce(movement * info.Speed);
    }

    private void OnTriggerEnter(Collider other) {
        var enemy = other.GetComponent<Enemy>();
        if (enemy != null)
            enemy.TakeHit(info.Damage);
    }
}

Вот вам go. PlayerInfo создается до Player. PlayerInfo ссылается на Player и Player ссылается на PlayerInfo. Игрок используется для перемещения объекта игры и нападения на врагов, PlayerInfo содержит необходимую информацию. Что мы можем сделать здесь?

Во-первых, переписать MonoBehaviour без каких-либо логи c:

public class PlayerView : MonoBehaviour {

    private Rigidbody rb;

    // Events for future subscription.
    public event Action OnUpdate;
    public event TriggerEntered<Collider>;

    // Simple initialization of required components.
    private void Awake() {
        rb = GetComponent<Rigidbody>();
    }

    // Unity methods doing nothing but invoking events.
    private void Update() {
        OnUpdate?.Invoke();
    }

    private void OnTriggerEnter(Collider other) {
        TriggerEntered?.Invoke(other);
    }

    // We still need a method to move our player, right?
    public void Move(Vector3 direction) {
        rb.AddForce(direction);
    }

    public void SetPosition(Vector3 position) {
        transform.position = position;
    }
}

Теперь вам нужен класс, содержащий данные об игроке:

[Serializable]
public class PlayerModel {

    [SerializeField] private float speed = 1.5f;
    [SerializeField] private float jumpHeight = 5;
    [SerializeField] private float damage = 15;

    public float Speed => speed;
    public float JumpHeight => jumpHeight ;
    public float Damage => damage;
}

Теперь нам нужен способ объединить эти два элемента вместе:

public class PlayerController {

    private readonly PlayerModel model;
    private readonly PlayerView view;

    public PlayerController(PlayerModel model, PlayerView view) {
        // Validate values here.

        this.model = model;
        this.view = view;

        // Linking logic to events.
        view.OnUpdate += Move;
        view.TriggerEntered += Attack;
    }

    // Actual logic moved here.
    private void Move() {
        Vector3 movement = Vector3.zero;
        if (Input.GetButtonDown("Jump")
            movement += Vector3.up * model.JumpHeight;
        movement += new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")) * model.Speed;
        view.Move(movement);
    }

    private void Attack(Collider other) {
        var enemy = other.GetComponent<Enemy>();
        if (enemy != null)
            enemy.TakeHit(model.Damage);
    }

    // Method from PlayerInfo to set player position without actual movements.
    public void MoveTo(Vector3 position) {
        view.SetPosition(position);
    }
}

Теперь у вас есть 3 класса вместо 2, но классы моделей и представлений очень просты и не требуют каких-либо зависимостей. Вся работа выполняется классом Controller, который получает две другие части и связывает их вместе.

Становится еще лучше, когда интерфейсы вводятся в дополнение к классам: IPlayerModel, IPlayerView, IPlayerController.

Вы можете подумать, что было бы проще создать один класс вместо трех связанных классов, но в конечном итоге вы будете благодарны за использование этого шаблона: представление и модель чрезвычайно просты, легко читаемы, легко проверить на наличие ошибок, легко расширить с помощью новой функциональности. С другой стороны, отдельный класс быстро вырастет до сотен строк и станет кошмаром для тестирования и расширения.

Я настоятельно рекомендую эту статью с более сложными примерами MVC.

...