Các mẫu để nhân giống thay đổi lên một mô hình đối tượng ..?


22

Đây là một kịch bản phổ biến luôn làm tôi khó chịu.

Tôi có một mô hình đối tượng với một đối tượng cha. Cha mẹ chứa một số đối tượng con. Một cái gì đó như thế này.

public class Zoo
{
    public List<Animal> Animals { get; set; }
    public bool IsDirty { get; set; }
}

Mỗi đối tượng con có dữ liệu và phương thức khác nhau

public class Animal
{
    public string Name { get; set; }
    public int Age { get; set; }

    public void MakeMess()
    {
        ...
    }
}

Khi đứa trẻ thay đổi, trong trường hợp này khi phương thức MakeMess được gọi, một số giá trị trong cha mẹ cần được cập nhật. Giả sử khi một ngưỡng nhất định của Động vật gây ra sự lộn xộn, thì cờ IsDenty của Sở thú cần được đặt.

Có một vài cách để xử lý tình huống này (mà tôi biết).

1) Mỗi Động vật có thể có một tài liệu tham khảo Sở thú mẹ để truyền đạt những thay đổi.

public class Animal
{
    public Zoo Parent { get; set; }
    ...

    public void MakeMess()
    {
        Parent.OnAnimalMadeMess();
    }
}

Cảm giác này giống như một lựa chọn tồi tệ nhất vì nó kết hợp Animal với đối tượng cha của nó. Nếu tôi muốn một con vật sống trong một ngôi nhà thì sao?

2) Một tùy chọn khác, nếu bạn đang sử dụng ngôn ngữ hỗ trợ các sự kiện (như C #) là đăng ký phụ huynh để thay đổi sự kiện.

public class Animal
{
    public event OnMakeMessDelegate OnMakeMess;

    public void MakeMess()
    {
        OnMakeMess();
    }
}

public class Zoo
{
    ...

    public void SubscribeToChanges()
    {
        foreach (var animal in Animals)
        {
            animal.OnMakeMess += new OnMakeMessDelegate(OnMakeMessHandler);
        }
    }

    public void OnMakeMessHandler(object sender, EventArgs e)
    {
        ...
    }
}

Điều này có vẻ để làm việc nhưng từ kinh nghiệm trở nên khó để duy trì. Nếu Động vật từng thay đổi Sở thú, bạn phải hủy đăng ký các sự kiện tại Sở thú cũ và đăng ký lại tại Sở thú mới. Điều này chỉ trở nên tồi tệ hơn khi cây thành phần sâu hơn.

3) Tùy chọn khác là di chuyển logic lên tới cha mẹ.

public class Zoo
{
    public void AnimalMakesMess(Animal animal)
    {
        ...
    }
}

Điều này có vẻ rất không tự nhiên và gây ra sự trùng lặp của logic. Ví dụ: nếu tôi có một đối tượng Nhà không chia sẻ bất kỳ cha mẹ thừa kế chung nào với Zoo ..

public class House
{
    // Now I have to duplicate this logic
    public void AnimalMakesMess(Animal animal)
    {
        ...
    }
}

Tôi chưa tìm thấy một chiến lược tốt để xử lý các tình huống này. Những gì khác có sẵn? Làm thế nào điều này có thể được thực hiện đơn giản hơn?


Bạn nói đúng về việc số 1 là xấu, và tôi cũng không thích số 2; nói chung bạn muốn tránh tác dụng phụ, và thay vào đó bạn đang tăng chúng. Về tùy chọn # 3, tại sao bạn không thể đưa AnimalMakeMess vào một phương thức tĩnh mà tất cả các lớp có thể gọi?
Doval

4
# 1 không hẳn là xấu nếu bạn giao tiếp qua một giao diện (IAnimalObserver) thay vì lớp Parent cụ thể đó.
coredump

Câu trả lời:


11

Tôi đã phải đối phó với điều này một vài lần. Lần đầu tiên tôi sử dụng tùy chọn 2 (sự kiện) và như bạn nói nó trở nên thực sự phức tạp. Nếu bạn đi theo con đường đó, tôi khuyên bạn nên kiểm tra đơn vị rất kỹ lưỡng để đảm bảo các sự kiện được thực hiện chính xác và bạn không để lại các tài liệu tham khảo lơ lửng, nếu không, đó là một nỗi đau rất lớn để gỡ lỗi.

Lần thứ hai, tôi chỉ thực hiện tài sản của cha mẹ như một chức năng của trẻ em, vì vậy hãy giữ một Dirtytài sản trên mỗi con vật và để Animal.IsDirtytrả lại this.Animals.Any(x => x.IsDirty). Đó là trong mô hình. Phía trên mô hình có Bộ điều khiển và công việc của bộ điều khiển là biết rằng sau khi tôi thay đổi mô hình (tất cả các hành động trên mô hình được chuyển qua bộ điều khiển để nó biết rằng có gì đó đã thay đổi), sau đó nó biết rằng nó phải gọi lại Các chức năng đánh giá, như kích hoạt ZooMaintenancebộ phận để kiểm tra xem có Zoobị bẩn nữa không. Ngoài ra, tôi chỉ có thể đẩy ZooMaintenanceséc ra cho đến một thời gian dự kiến ​​sau đó (cứ sau 100 ms, 1 giây, 2 phút, 24 giờ, bất cứ điều gì cần thiết).

Tôi thấy cái sau đã được bảo trì đơn giản hơn nhiều, và nỗi sợ về các vấn đề hiệu suất của tôi không bao giờ thành hiện thực.

Chỉnh sửa

Một cách khác để đối phó với điều này là một mẫu Message Bus . Thay vì sử dụng một Controllerví dụ như trong ví dụ của tôi, bạn tiêm cho mọi đối tượng một IMessageBusdịch vụ. Sau đó, Animallớp có thể xuất bản một tin nhắn, như "Mess Made" và Zoolớp của bạn có thể đăng ký tin nhắn "Mess Made". Dịch vụ xe buýt tin nhắn sẽ đảm nhiệm việc thông báo Zookhi bất kỳ động vật nào xuất bản một trong những tin nhắn đó và nó có thể đánh giá lại IsDirtytài sản của nó .

Điều này có lợi thế là Animalskhông còn cần một Zootài liệu tham khảo và Zookhông cần phải lo lắng về việc đăng ký và hủy đăng ký khỏi các sự kiện từ mỗi sự kiện Animal. Hình phạt là tất cả Zoocác lớp đăng ký tin nhắn đó sẽ phải đánh giá lại các thuộc tính của chúng, ngay cả khi đó không phải là một trong những động vật của nó. Đó có thể hoặc không thể là một vấn đề lớn. Nếu chỉ có một hoặc hai Zootrường hợp, nó có thể tốt.

Chỉnh sửa 2

Đừng giảm giá tính đơn giản của tùy chọn 1. Bất kỳ ai xem lại mã sẽ không gặp nhiều vấn đề khi hiểu về nó. Sẽ rõ ràng với ai đó khi nhìn vào Animallớp rằng khi MakeMessđược gọi là nó sẽ truyền thông điệp đến Zoovà nó sẽ rõ ràng đối với Zoolớp nơi nó nhận được tin nhắn của nó. Hãy nhớ rằng trong lập trình hướng đối tượng, một cuộc gọi phương thức được sử dụng để được gọi là "thông điệp". Trong thực tế, lần duy nhất có ý nghĩa để thoát khỏi tùy chọn 1 là nếu nhiều hơn chỉ Zoolà phải thông báo nếu Animallàm cho một mớ hỗn độn. Nếu có nhiều đối tượng cần được thông báo, thì có lẽ tôi sẽ chuyển sang một tin nhắn hoặc bộ điều khiển.


5

Tôi đã tạo một sơ đồ lớp đơn giản mô tả tên miền của bạn: nhập mô tả hình ảnh ở đây

Mỗi cái Animal có một cái Habitat nó rối tung lên.

Các Habitatkhông quan tâm những gì hoặc có bao nhiêu con vật nó có (trừ khi nó về cơ bản là một phần của thiết kế của bạn trong trường hợp này bạn mô tả nó không phải là).

Nhưng Animalkhông quan tâm, bởi vì nó sẽ hành xử khác nhau trong mỗi Habitat.

Sơ đồ này tương tự như Sơ đồ UML của mẫu thiết kế chiến lược , nhưng chúng tôi sẽ sử dụng nó khác nhau.

Dưới đây là một số ví dụ mã trong Java (Tôi không muốn mắc bất kỳ lỗi cụ thể nào về C #).

Tất nhiên bạn có thể tự điều chỉnh thiết kế, ngôn ngữ và yêu cầu này.

Đây là giao diện Chiến lược:

public interface Habitat {
    public void messUp(float magnitude);

    public float getCleanliness();
}

Một ví dụ về bê tông Habitat. tất nhiên mỗi Habitatlớp con có thể thực hiện các phương thức này khác nhau.

public class Zoo implements Habitat {
    public float cleanliness = 1;

    public float getCleanliness() {
        return cleanliness;
    }

    public void messUp(float magnitude) {
        cleanliness -= magnitude;
    }
}

Tất nhiên bạn có thể có nhiều lớp con động vật, trong đó mỗi lớp làm rối nó khác nhau:

public class Animel {
    private Habitat habitat;

    public void makeMess() {
        habitat.messUp(.05f);
    }

    public Animel addTo(Habitat habitat) {
        this.habitat = habitat;
        return this;
    }
}

Đây là lớp máy khách, điều này về cơ bản giải thích cách bạn có thể sử dụng thiết kế này.

public class ZooKeeper {
    public Habitat zoo = new Zoo();

    public ZooKeeper() {
        new Animal()
            .addTo( zoo )
            .makeMess();

        if (zoo.getCleanliness() < 0.5f) {
            System.out.println("The zoo is really messy");
        } else {
            System.out.println("The zoo looks clean");
        }
    }
}

Tất nhiên trong ứng dụng thực tế của bạn, bạn có thể cho Habitatbiết và quản lý Animalnếu bạn cần.


3

Tôi đã có một số lượng thành công khá lớn với các kiến ​​trúc như tùy chọn 2 của bạn trong quá khứ. Đây là tùy chọn chung nhất và sẽ cho phép sự linh hoạt cao nhất. Nhưng, nếu bạn có quyền kiểm soát người nghe và không quản lý nhiều loại đăng ký, bạn có thể đăng ký các sự kiện dễ dàng hơn bằng cách tạo giao diện.

interface MessablePlace
{
  void OnMess(object sender, MessEvent e);
}

class MessEvent
{
  String DetailsOrWhatever;
}

Tùy chọn giao diện có ưu điểm là gần như đơn giản như tùy chọn 1 của bạn, nhưng cũng cho phép bạn nuôi động vật khá dễ dàng trong một Househoặc FairlyLand.


3
  • Tùy chọn 1 thực sự khá đơn giản. Đó chỉ là một tài liệu tham khảo ngược. Nhưng khái quát nó với giao diện được gọi Dwellingvà cung cấp một MakeMessphương thức trên nó. Điều đó phá vỡ sự phụ thuộc tròn. Rồi khi con vật làm bừa, nó cũng gọi dwelling.MakeMess().

Theo tinh thần của lex Parsimoniae , tôi sẽ đi với cái này, mặc dù tôi có thể sử dụng giải pháp chuỗi bên dưới, biết tôi. (Đây chỉ là mô hình tương tự mà @Benjamin Albert gợi ý.)

Lưu ý rằng nếu bạn đang lập mô hình các bảng cơ sở dữ liệu quan hệ, mối quan hệ sẽ đi theo một cách khác: Động vật sẽ có một tham chiếu đến Zoo và bộ sưu tập Động vật cho Sở thú sẽ là kết quả truy vấn.

  • Đưa ý tưởng đó ra xa hơn, bạn có thể sử dụng một kiến ​​trúc bị xiềng xích. Đó là, tạo một giao diện Messablevà trong mỗi mục lộn xộn, bao gồm một tham chiếu đến next. Sau khi tạo ra một mớ hỗn độn, hãy gọi MakeMessđến mục tiếp theo.

Vì vậy, Zoo ở đây có liên quan đến việc tạo ra một mớ hỗn độn, bởi vì nó cũng trở nên lộn xộn. Có:

Zoo implements Messable
House implements Messable
Animal implements Messable
   Messable next

   MakeMess()
       messy = true
       next.MakeMess

Vì vậy, bây giờ bạn có một chuỗi những thứ nhận được thông báo rằng một mớ hỗn độn đã được tạo ra.

  • Tùy chọn 2, một mô hình xuất bản / đăng ký có thể hoạt động ở đây, nhưng cảm thấy thực sự nặng nề. Đối tượng và container có mối quan hệ đã biết, vì vậy có vẻ hơi nặng tay khi sử dụng một cái gì đó chung chung hơn thế.

  • Tùy chọn 3: Trong trường hợp cụ thể này, gọi Zoo.MakeMess(animal)hoặc House.MakeMess(animal)thực sự không phải là một lựa chọn tồi, bởi vì một ngôi nhà có thể có ngữ nghĩa khác nhau để gây lộn xộn hơn Sở thú.

Ngay cả khi bạn không đi theo con đường chuỗi, có vẻ như có hai vấn đề ở đây: 1) vấn đề là về việc truyền một sự thay đổi từ một đối tượng sang vật chứa của nó, 2) Có vẻ như bạn muốn loại bỏ một giao diện cho các thùng chứa để trừu tượng nơi động vật có thể sống.

...

Nếu bạn có các hàm hạng nhất, bạn có thể chuyển một hàm (hoặc ủy nhiệm) vào Animal để gọi sau khi nó làm cho lộn xộn. Điều đó hơi giống với ý tưởng chuỗi, ngoại trừ với chức năng thay vì giao diện.

public Animal
    Function afterMess

    public MakeMess()
        messy = true
        afterMess()

Khi con vật di chuyển, chỉ cần thiết lập một đại biểu mới.

  • Được thực hiện đến cùng cực, bạn có thể sử dụng Lập trình hướng đối tượng (AOP) với lời khuyên "sau" trên MakeMess.

2

Tôi sẽ đi với 1, nhưng tôi sẽ biến mối quan hệ cha-con cùng với logic thông báo thành một trình bao bọc riêng. Điều này loại bỏ sự phụ thuộc của Animal vào Zoo và cho phép tự động quản lý các mối quan hệ cha-con. Nhưng điều này đòi hỏi bạn phải làm lại các đối tượng trong hệ thống phân cấp thành các giao diện / lớp trừu tượng trước và viết một trình bao bọc cụ thể cho mỗi giao diện. Nhưng điều đó có thể được loại bỏ bằng cách tạo mã.

Cái gì đó như :

public interface IAnimal
{
    string Name { get; set; }
    int Age { get; set; }

    void MakeMess();
}

public class Animal : IAnimal
{
    public string Name { get; set; }
    public int Age { get; set; }

    public void MakeMess()
    {
        // makes mess
    }
}

public class ZooAnimals
{
    class AnimalInZoo : IAnimal
    {
        public IAnimal _animal;
        public ZooAnimals _zoo;

        public AnimalInZoo(IAnimal animal, ZooAnimals zoo)
        {
            _animal = animal;
            _zoo = zoo;
        }

        public string Name { get { return _animal.Name; } set { _animal.Name = value; } }
        public int Age { get { return _animal.Age; } set { _animal.Age = value; } }

        public void MakeMess()
        {
            _animal.MakeMess();
            _zoo.IsDirty = true;
        }
    }

    private Collection<AnimalInZoo> animals = new Collection<AnimalInZoo>();

    public IAnimal Add(IAnimal animal)
    {
        if (animal is AnimalInZoo)
        {
            var inZoo = (AnimalInZoo)animal;
            if (inZoo._zoo != this)
            {
                // animal is in a different zoo, what to do ?
                // either move animal to this zoo
                // or throw an exception so caller is forced to remove the animal from previous zoo first
            }
        }

        var anim = new AnimalInZoo(animal, this);
        animals.Add(anim);
        return anim;
    }

    public IAnimal Remove(IAnimal animal)
    {
        if (!(animal is AnimalInZoo))
        {
            // animal is not in zoo, throw an exception?
        }
        var inZoo = (AnimalInZoo)animal;
        if (inZoo._zoo != this)
        {
            // animal is in a different zoo, throw an exception?
        }

        animals.Remove(inZoo);
        return inZoo._animal;
    }

    public bool IsDirty { get; set; }
}

Đây thực sự là cách một số ORM thực hiện theo dõi thay đổi của họ trên các thực thể. Họ tạo ra các hàm bao quanh các thực thể và khiến bạn làm việc với chúng. Những trình bao bọc này thường được thực hiện bằng cách sử dụng sự phản chiếu và tạo mã động.


1

Hai tùy chọn tôi thường sử dụng. Bạn có thể sử dụng cách tiếp cận thứ hai và đặt logic để kết nối sự kiện trong chính bộ sưu tập trên cha mẹ.

Một cách tiếp cận khác (thực sự có thể được sử dụng với bất kỳ một trong ba tùy chọn) là sử dụng ngăn chặn. Tạo một AnimalContainer (hoặc thậm chí biến nó thành một bộ sưu tập) có thể sống trong nhà hoặc sở thú hoặc bất cứ thứ gì khác. Nó cung cấp chức năng theo dõi liên quan đến động vật, nhưng tránh các vấn đề về thừa kế vì nó có thể được đưa vào bất kỳ đối tượng nào cần nó.


0

Bạn bắt đầu với một thất bại cơ bản: các đối tượng trẻ em không nên biết về cha mẹ của chúng.

Các chuỗi có biết rằng chúng nằm trong một danh sách không? Không. Ngày tháng có biết chúng tồn tại trong lịch không? Không.

Tùy chọn tốt nhất là thay đổi thiết kế của bạn để loại kịch bản này không tồn tại.

Sau hơn, xem xét đảo ngược kiểm soát. Thay vì MakeMessbật Animalvới một tác dụng phụ hoặc sự kiện, hãy chuyển Zoovào phương thức. Lựa chọn 1 là tốt nếu bạn cần bảo vệ sự bất biến Animalluôn cần phải sống ở đâu đó. Đó không phải là cha mẹ, mà là một hiệp hội ngang hàng.

Thỉnh thoảng 2 và 3 sẽ ổn, nhưng nguyên tắc kiến ​​trúc quan trọng cần tuân thủ là trẻ em không biết về cha mẹ mình.


Tôi nghi ngờ đây giống như một nút gửi ở dạng hơn là một chuỗi trong danh sách.
Svidgen

1
@svidgen - Sau đó vượt qua trong một cuộc gọi lại. Dễ dàng hơn một sự kiện, dễ lý luận hơn và không có tài liệu tham khảo nghịch ngợm nào về những điều nó không cần biết.
Telastyn
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.