2009-12-16 31 views
12

Tôi cần triển khai khung làm việc Hoàn tác/Làm lại cho ứng dụng cửa sổ của tôi (trình soạn thảo như powerpoint), thực hành tốt nhất là gì, cách xử lý tất cả thay đổi thuộc tính của đối tượng và phản ánh trên giao diện người dùng.Thực hành tốt nhất để hoàn tác làm lại

Trả lời

27

Có hai kiểu cổ điển để sử dụng. Đầu tiên là memento pattern được sử dụng để lưu trữ ảnh chụp nhanh của trạng thái đối tượng hoàn chỉnh của bạn. Điều này có lẽ là hệ thống nhiều hơn so với mô hình lệnh, nhưng nó cho phép rollback rất đơn giản với một ảnh chụp cũ hơn. Bạn có thể lưu trữ các ảnh chụp nhanh trên đĩa a la PaintShop/PhotoShop hoặc giữ chúng trong bộ nhớ cho các đối tượng nhỏ hơn mà không cần phải kiên trì. Những gì bạn đang làm là chính xác những gì mô hình này được thiết kế cho, vì vậy nó phải phù hợp với hóa đơn tốt hơn một chút so với Command Pattern được đề xuất bởi những người khác. Ngoài ra, một lưu ý bổ sung là bởi vì nó không yêu cầu bạn phải có các lệnh đối ứng để hoàn tác một cái gì đó đã được thực hiện trước đó, nó có nghĩa là bất kỳ chức năng một cách nào có thể [như băm hoặc mã hóa] undone trivially bằng cách sử dụng lệnh đối ứng vẫn có thể được hoàn tác rất đơn giản chỉ bằng cách quay trở lại một ảnh chụp cũ hơn.

Cũng như chỉ ra, command pattern đó là có khả năng ít tài nguyên chuyên sâu, vì vậy tôi sẽ thừa nhận rằng trong những trường hợp cụ thể mà:

  • Có một trạng thái đối tượng lớn để được tiếp tục tồn và/hoặc
  • Có không có phương pháp phá hoại và
  • đâu lệnh đối ứng có thể được sử dụng rất trivially để đảo ngược bất kỳ hành động được thực

mô hình lệnh có thể phù hợp hơn [nhưng không nhất thiết, nó sẽ phụ thuộc rất nhiều vào tình huống]. Trong các trường hợp khác, tôi sẽ sử dụng mẫu ký tự.

Tôi có lẽ sẽ không sử dụng kết hợp hai vì tôi có xu hướng quan tâm đến nhà phát triển sẽ theo sau tôi và duy trì mã của tôi cũng như trách nhiệm đạo đức của tôi đối với chủ lao động của tôi đơn giản và rẻ tiền nhất có thể. Tôi thấy một mashup của hai mô hình dễ dàng trở thành một lỗ chuột không thể duy trì của sự khó chịu đó sẽ là tốn kém để duy trì.

+1

+1 Nếu trạng thái của đối tượng của bạn không quá lớn, điều này sẽ đơn giản hơn nhiều so với mẫu lệnh hoàn tác/làm lại có thể phức tạp rất dễ dàng . Và thường thì bạn sẽ có các lệnh phá hoại không thể hoàn tác được nếu không có bản sao trạng thái đầy đủ. Nhưng đối với các bang rất lớn, điều này có thể không thực tế. – Josh

+0

Hãy xem xét các cấu trúc dữ liệu hoàn toàn chức năng; họ có thể cung cấp cho bạn chia sẻ đáng kể giữa dữ liệu trực tiếp hiện tại và các vật lưu niệm. –

+0

Hãy xem xét sự khác biệt của nhà nước được mô tả trong câu trả lời của tôi. Có thể là giải pháp thay thế tốt hơn tùy thuộc vào trường hợp sử dụng của bạn. – vincent

4

Thực tiễn cổ điển là theo dõi Command Pattern.

Bạn có thể đóng gói bất kỳ đối tượng nào thực hiện hành động bằng lệnh và yêu cầu thực hiện hành động ngược lại bằng phương thức Hoàn tác(). Bạn lưu trữ tất cả các hành động trong một ngăn xếp cho một cách dễ dàng để tua qua chúng.

+1

này không cho phép cho các chức năng một cách như băm hoặc các chức năng toán học mà không thể được 'hoàn tác' sử dụng một phương pháp đối ứng. Do đó, mô hình này nên được sử dụng cẩn thận cho phương pháp này. – BenAlabaster

+1

Điều đó khá đúng. Đối với công cụ giao diện người dùng chung, nó đi một chặng đường dài. – womp

+0

Câu hỏi ngẫu nhiên từ người không bao giờ triển khai một trong các mẫu đó: Không thể mẫu lệnh dễ dàng lưu trữ ảnh chụp nhanh của trạng thái trước đó mà nó đã thay đổi để tạo điều kiện hoàn tác? Kinda giống như một liên minh không lành mạnh giữa Command và Memento? Đặc biệt là nếu nhiều lệnh có thể được lưu trữ một cách trivially snapshots cho mỗi hành động có thể là một chút tốn kém. – Joey

2

Hãy xem qua số Command Pattern. Bạn phải đóng gói mọi thay đổi đối với mô hình của mình thành các đối tượng lệnh riêng biệt.

+0

Xem nhận xét của tôi về câu trả lời của Womp để sử dụng mẫu lệnh cho các cơ chế 'hoàn tác' – BenAlabaster

0

Tôi đã viết một hệ thống thực sự linh hoạt để theo dõi các thay đổi.Tôi có một chương trình vẽ mà thực hiện 2 loại thay đổi:

  • thêm/xoá hình dạng
  • thay đổi sở hữu của một hình dạng

lớp Base:

public abstract class Actie 
{ 
    public Actie(Vorm[] Vormen) 
    { 
     vormen = Vormen; 
    } 

    private Vorm[] vormen = new Vorm[] { }; 
    public Vorm[] Vormen 
    { 
     get { return vormen; } 
    } 

    public abstract void Undo(); 
    public abstract void Redo(); 
} 

nguồn gốc lớp cho thêm hình dạng:

public class VormenToegevoegdActie : Actie 
{ 
    public VormenToegevoegdActie(Vorm[] Vormen, Tekening tek) 
     : base(Vormen) 
    { 
     this.tek = tek; 
    } 

    private Tekening tek; 
    public override void Redo() 
    { 
     tek.Vormen.CanRaiseEvents = false; 
     tek.Vormen.AddRange(Vormen); 
     tek.Vormen.CanRaiseEvents = true; 
    } 

    public override void Undo() 
    { 
     tek.Vormen.CanRaiseEvents = false; 
     foreach(Vorm v in Vormen) 
      tek.Vormen.Remove(v); 
     tek.Vormen.CanRaiseEvents = true; 
    } 
} 

nguồn gốc lớp cho hình dạng loại bỏ:

public class VormenVerwijderdActie : Actie 
{ 
    public VormenVerwijderdActie(Vorm[] Vormen, Tekening tek) 
     : base(Vormen) 
    { 
     this.tek = tek; 
    } 

    private Tekening tek; 
    public override void Redo() 
    { 
     tek.Vormen.CanRaiseEvents = false; 
     foreach(Vorm v in Vormen) 
      tek.Vormen.Remove(v); 
     tek.Vormen.CanRaiseEvents = true; 
    } 

    public override void Undo() 
    { 
     tek.Vormen.CanRaiseEvents = false; 
     foreach(Vorm v in Vormen) 
      tek.Vormen.Add(v); 
     tek.Vormen.CanRaiseEvents = true; 
    } 
} 

nguồn gốc lớp cho những thay đổi sở hữu:

public class PropertyChangedActie : Actie 
{ 
    public PropertyChangedActie(Vorm[] Vormen, string PropertyName, object OldValue, object NewValue) 
     : base(Vormen) 
    { 
     propertyName = PropertyName; 
     oldValue = OldValue; 
     newValue = NewValue; 
    } 

    private object oldValue; 
    public object OldValue 
    { 
     get { return oldValue; } 
    } 

    private object newValue; 
    public object NewValue 
    { 
     get { return newValue; } 
    } 

    private string propertyName; 
    public string PropertyName 
    { 
     get { return propertyName; } 
    } 

    public override void Undo() 
    { 
     //Type t = base.Vorm.GetType(); 
     PropertyInfo info = Vormen.First().GetType().GetProperty(propertyName); 
     foreach(Vorm v in Vormen) 
     { 
      v.CanRaiseVeranderdEvent = false; 
      info.SetValue(v, oldValue, null); 
      v.CanRaiseVeranderdEvent = true; 
     } 
    } 
    public override void Redo() 
    { 
     //Type t = base.Vorm.GetType(); 
     PropertyInfo info = Vormen.First().GetType().GetProperty(propertyName); 
     foreach(Vorm v in Vormen) 
     { 
      v.CanRaiseVeranderdEvent = false; 
      info.SetValue(v, newValue, null); 
      v.CanRaiseVeranderdEvent = true; 
     } 
    } 
} 

Với mỗi lần Vormen = mảng các mặt hàng được nộp cho sự thay đổi. Và nó nên được sử dụng như thế này:

Tuyên bố của ngăn xếp:

Stack<Actie> UndoStack = new Stack<Actie>(); 
Stack<Actie> RedoStack = new Stack<Actie>(); 

Thêm một hình dạng mới (ví dụ Point.)

VormenToegevoegdActie vta = new VormenToegevoegdActie(new Vorm[] { NieuweVorm }, this); 
UndoStack.Push(vta); 
RedoStack.Clear(); 

Loại bỏ một hình dạng được lựa chọn

VormenVerwijderdActie vva = new VormenVerwijderdActie(to_remove, this); 
UndoStack.Push(vva); 
RedoStack.Clear(); 

Đăng ký thay đổi thuộc tính

PropertyChangedActie ppa = new PropertyChangedActie(new Vorm[] { (Vorm)e.Object }, e.PropName, e.OldValue, e.NewValue); 
UndoStack.Push(ppa); 
RedoStack.Clear(); 

Cuối cùng hành động Undo/Redo

public void Undo() 
{ 
    Actie a = UndoStack.Pop(); 
    RedoStack.Push(a); 
    a.Undo(); 
} 

public void Redo() 
{ 
    Actie a = RedoStack.Pop(); 
    UndoStack.Push(a); 
    a.Redo(); 
} 

Tôi nghĩ rằng đây là cách hiệu quả nhất để thực hiện một thuật toán undo-redo. Ví dụ, hãy xem trang này trên trang web của tôi: DrawIt.

Tôi đã thực hiện hoàn tác làm lại nội dung ở khoảng dòng 479 của tệp Tekening.cs. Bạn có thể tải xuống mã nguồn. Nó có thể được thực hiện bởi bất kỳ loại ứng dụng nào.

+2

Xin lưu ý nếu bạn muốn quảng bá sản phẩm/blog của riêng mình, bạn ** phải tiết lộ liên kết của mình trong câu trả lời **, nếu không, câu trả lời của bạn có thể bị gắn cờ là spam. Nếu bạn không liên kết với trang web, tôi khuyên bạn nên nói như vậy để ngăn chặn điều này. Vui lòng đọc [Cách không phải là người gửi spam] (https://stackoverflow.com/help/promotion) – Mithrandir

0

Có ba cách tiếp cận ở đây khả thi. Memento Pattern (Snapshots), Command Pattern và State Diffing. Tất cả đều có lợi thế và bất lợi.

Tôi sẽ đi với nhà nước Diffing nếu bạn có thể nhận được đi với nó vì nó kết hợp giảm bộ nhớ với dễ thực hiện và bảo trì.

Lưu ý rằng VoxelShop được đề cập trong bài viết là nguồn mở.Vì vậy, bạn có thể có một cái nhìn tại sự phức tạp của mô hình lệnh ở đây: https://github.com/simlu/voxelshop/tree/develop/src/main/java/com/vitco/app/core/data/history

Dưới đây là đoạn trích từ bài viết:

Memento Pattern

enter image description here

Ưu

  • Triển khai độc lập với hành động được áp dụng. Sau khi triển khai, chúng tôi có thể thêm hành động mà không phải lo lắng về việc phá vỡ lịch sử.
  • Chuyển nhanh đến vị trí được xác định trước trong lịch sử. Điều này là thú vị khi các hành động được áp dụng giữa vị trí lịch sử hiện tại và mong muốn được tính toán tốn kém.

Nhược điểm

  • Yêu cầu bộ nhớ có thể cao hơn đáng kể so với các phương pháp khác.
  • Thời gian tải có thể chậm nếu ảnh chụp nhanh.

lệnh Pattern

enter image description here

Ưu

  • Memory dấu chân nhỏ. Chúng tôi chỉ cần lưu trữ các thay đổi đối với mô hình và nếu chúng nhỏ, thì ngăn xếp lịch sử cũng nhỏ.

Nhược điểm

  • Chúng ta không thể đi đến một vị trí tùy ý trực tiếp, nhưng thay vì cần phải bỏ áp dụng stack lịch sử cho đến khi chúng ta đạt được điều đó. Điều này có thể tốn thời gian.
  • Mọi hành động và ngược lại cần phải được đóng gói trong một đối tượng. Nếu hành động của bạn là không tầm thường, điều này có thể khó khăn. Sai lầm trong hành động (đảo ngược) thực sự khó gỡ lỗi và có thể dễ dàng dẫn đến các sự cố nghiêm trọng. Ngay cả những hành động tìm kiếm đơn giản thường liên quan đến một số lượng phức tạp. Ví dụ. trong trường hợp Trình chỉnh sửa 3D, đối tượng để thêm vào mô hình cần phải lưu trữ nội dung đã được thêm, màu nào hiện được chọn, nội dung nào bị ghi đè, nếu chế độ gương hoạt động, v.v.
  • Có thể gây khó khăn khi triển khai và bộ nhớ khi hành động không có đảo ngược đơn giản, ví dụ như khi làm mờ hình ảnh.

Nhà nước diffing

enter image description here

Ưu

  • thực hiện độc lập với hành động áp dụng. Khi chức năng lịch sử được thêm vào, chúng tôi có thể thêm hành động mà không phải lo lắng về việc phá vỡ lịch sử.
  • Yêu cầu bộ nhớ thường thấp hơn nhiều so với phương pháp chụp nhanh và trong nhiều trường hợp có thể so sánh với phương pháp mẫu lệnh. Tuy nhiên, điều này phụ thuộc rất nhiều vào loại hành động được áp dụng. Ví dụ. đảo ngược màu sắc của một hình ảnh bằng cách sử dụng mẫu lệnh nên rất rẻ, trong khi State Diffing sẽ lưu toàn bộ hình ảnh. Ngược lại khi vẽ một đường dài dạng tự do, cách tiếp cận Mẫu Lệnh có thể sử dụng nhiều bộ nhớ hơn nếu nó liên kết các mục nhập lịch sử cho mỗi pixel.

Nhược điểm/Hạn chế

  • Chúng ta không thể đi đến một vị trí tùy ý trực tiếp, nhưng thay vì cần phải bỏ áp dụng stack lịch sử cho đến khi chúng ta đạt được điều đó.
  • Chúng tôi cần tính toán sự khác biệt giữa các trạng thái. Điều này có thể tốn kém.
  • Việc triển khai sự khác biệt xor giữa các trạng thái mô hình có thể khó triển khai tùy thuộc vào mô hình dữ liệu của bạn.

tham khảo:

https://www.linkedin.com/pulse/solving-history-hard-problem-lukas-siemon

+0

Những phiếu bầu không có giải thích này thực sự không khuyến khích: / – vincent

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