Geri Alma Motoru için Tasarım Modeli


117

Bir inşaat mühendisliği uygulaması için yapısal bir modelleme aracı yazıyorum. Tüm binayı temsil eden, aynı zamanda özel sınıflar olan düğüm koleksiyonları, çizgi öğeleri, yükler vb. İçeren büyük bir model sınıfım var.

Modelde her değişiklikten sonra derin bir kopya kaydeden bir geri alma motorunu zaten kodladım. Şimdi farklı şekilde kodlayabilir miyim diye düşünmeye başladım. Derin kopyaları kaydetmek yerine, her bir değiştirici eyleminin bir listesini karşılık gelen bir ters değiştiriciyle kaydedebilirim. Böylece, geri almak için mevcut modele ters değiştiricileri veya yinelemek için değiştiricileri uygulayabilirim.

Nesne özelliklerini vb. Değiştiren basit komutları nasıl uygulayacağınızı hayal edebiliyorum. Peki ya karmaşık komutlar? Modele yeni düğüm nesneleri eklemek ve yeni düğümlere referansları tutan bazı çizgi nesneleri eklemek gibi.

Bunu uygulamak nasıl olur?


"Algorthim'i Geri Al" yorumunu eklersem, bu onu "Geri Al Algoritmasını" arayıp bulabilmem için yapar mı? Bu aradım ve kopya olarak kapalı bir şey buldum.
Peter Turner

hay, ayrıca geliştirmekte olduğumuz uygulamada geri alma / yineleme geliştirmek istiyorum. QT4 çerçevesi kullanıyoruz ve birçok karmaşık geri alma / yineleme işlemine ihtiyacımız var .. Merak ediyordum, Command-Pattern kullanarak başarılı oldunuz mu?
Ashika Umanga Umagiliya

2
@umanga: İşe yaradı ama kolay olmadı. En zor kısım, referansları takip etmekti. Örneğin, bir Frame nesnesi silindiğinde, alt nesneleri: Düğümler, üzerine etki eden yükler ve geri alındığında yeniden yerleştirilmek üzere tutulması gereken diğer birçok kullanıcı ataması gerekir. Ancak bu alt nesnelerin bazıları diğer nesnelerle paylaşıldı ve geri alma / yineleme mantığı oldukça karmaşık hale geldi. Model o kadar büyük olmasaydı, hatıra yaklaşımını sürdürürdüm; uygulaması çok daha kolay.
Özgür Özçitak

bu, üzerinde çalışmak için eğlenceli bir sorundur, kaynak kod depolarının bunu nasıl yaptığını düşünün, svn gibi (işlemeler arasındaki farkları korurlar).
Alex

Yanıtlar:


88

Gördüğüm çoğu örnek bunun için Komut-Modelinin bir varyantını kullanıyor . Geri alınamayan her kullanıcı eylemi, eylemi yürütmek ve geri almak için tüm bilgileri içeren kendi komut örneğini alır. Daha sonra yürütülen tüm komutların bir listesini tutabilir ve bunları birer birer geri alabilirsiniz.


4
Bu temelde Cocoa, NSUndoManager'daki geri alma motorunun çalışma şeklidir.
amrox

33

OP'nin ima ettiği büyüklük ve kapsamın bir modeliyle uğraşırken hem hatıranın hem de komutun pratik olmadığını düşünüyorum. Çalışacaklardı, ancak sürdürmek ve genişletmek çok fazla iş olacaktı.

Bu tür bir problem için, modelde yer alan her nesne için farklı kontrol noktalarını desteklemek için veri modelinize destek oluşturmanız gerektiğini düşünüyorum . Bunu bir kez yaptım ve çok düzgün çalıştı. Yapmanız gereken en büyük şey, modeldeki işaretçilerin veya referansların doğrudan kullanımından kaçınmaktır.

Başka bir nesneye yapılan her başvuru, bazı tanımlayıcıları kullanır (bir tam sayı gibi). Nesneye ne zaman ihtiyaç duyulursa, bir tablodan nesnenin mevcut tanımına bakarsınız. Tablo, her nesne için önceki sürümlerin tümünü içeren bağlantılı bir liste ile birlikte hangi kontrol noktası için aktif olduklarına ilişkin bilgileri içerir.

Geri al / yinelemenin uygulanması basittir: İşleminizi yapın ve yeni bir kontrol noktası oluşturun; tüm nesne sürümlerini önceki denetim noktasına geri döndürür.

Kodda biraz disiplin gerektirir, ancak birçok avantajı vardır: model durumunun farklı depolamasını yaptığınız için derin kopyalara ihtiyacınız yoktur; kullanmak istediğiniz bellek miktarını ( CAD modelleri gibi şeyler için çok önemlidir) ya yineleme sayısına ya da kullanılan belleğe göre düzenleyebilirsiniz; model üzerinde çalışan işlevler için çok ölçeklenebilir ve az bakım gerektirir çünkü geri alma / yineleme uygulamak için hiçbir şey yapmaları gerekmez.


1
Dosya formatınız olarak bir veritabanı (örn. Sqlite) kullanıyorsanız, bu neredeyse otomatik olabilir
Martin Beckett

4
Modelde yapılan değişikliklerle ortaya çıkan bağımlılıkları izleyerek bunu artırırsanız, potansiyel olarak bir geri alma ağaç sisteminiz olabilir (yani, bir kirişin genişliğini değiştirirsem, sonra ayrı bir bileşen üzerinde biraz iş yaparsam, geri dönebilir ve geri alabilirim. kiriş diğer şeyleri kaybetmeden değişir). Bunun için kullanıcı arayüzü biraz hantal olabilir, ancak geleneksel doğrusal geri almadan çok daha güçlü olacaktır.
Sumudu Fernando

Bu id ve işaretçiler fikrini daha fazla açıklayabilir misiniz? Şüphesiz bir işaretçi / bellek adresi id kadar iyi çalışır?
paulm

@paulm: esasen gerçek veriler (id, version) tarafından indekslenir. İşaretçiler bir nesnenin belirli bir sürümüne atıfta bulunur, ancak bir nesnenin mevcut durumuna, ne olursa olsun, başvurmak istiyorsunuz, bu nedenle onu (id, sürüm) ile değil, kimliğe göre ele almak istersiniz. Sen olabilir yeniden yapılandırılması size (sürüm => verileri) tabloya bir işaretçi depolamak ve sadece her zaman en son almak, ama bu veriyi, muddies kaygıları biraz ısrarcı yaparken zarar mevkiinde eğilimi, ve zorlaştırır, böylece bazı yaygın sorgular yapın, bu yüzden normalde yapılacağı şekilde değildir.
Chris Morgan

17

GoF'den bahsediyorsanız, Memento kalıbı özellikle geri almayı ele alır.


7
Pek değil, bu onun ilk yaklaşımına değiniyor. Alternatif bir yaklaşım istiyor. İlk adım, her adım için tam durumu depolarken, ikincisi yalnızca "diff'leri" depolar.
Andrei Rînea

15

Başkalarının da belirttiği gibi, komut kalıbı Geri Al / Yinele uygulamasının çok güçlü bir yöntemidir. Ancak komut kalıbına değinmek istediğim önemli bir avantaj var.

Komut modelini kullanarak geri al / yinele uygularken, veriler üzerinde gerçekleştirilen işlemleri (bir dereceye kadar) soyutlayarak ve geri alma / yineleme sisteminde bu işlemleri kullanarak büyük miktarlarda yinelenen koddan kaçınabilirsiniz. Örneğin, bir metin düzenleyicide kes ve yapıştır, tamamlayıcı komutlardır (panonun yönetimi dışında). Başka bir deyişle, bir kesim için geri alma işlemi yapıştırmadır ve bir macun geri alma işlemi kesilir. Bu, metin yazmak ve silmek gibi çok daha basit işlemler için geçerlidir.

Buradaki anahtar, geri alma / yineleme sisteminizi düzenleyiciniz için birincil komut sistemi olarak kullanabilmenizdir. "Geri alma nesnesi oluştur, belgeyi değiştir" gibi bir sistem yazmak yerine, "geri alma nesnesi oluşturabilir, belgeyi değiştirmek için geri alma nesnesinde yineleme işlemi gerçekleştirebilirsiniz".

Şimdi, kuşkusuz, birçok kişi kendi kendine düşünüyor "Peki, komut modelinin amacının bir parçası değil mi?" Evet, ancak biri acil işlemler ve diğeri geri alma / yineleme için iki komut setine sahip çok fazla komut sistemi gördüm. Anlık işlemlere ve geri alma / yineleme işlemlerine özgü komutlar olmayacağını söylemiyorum, ancak çoğaltmanın azaltılması kodu daha sürdürülebilir hale getirecektir.


1
Hiç ^ -1 pasteolarak düşünmedim cut.
Lenar Hoyt


7

Bu, CSLA'nın geçerli olduğu bir durum olabilir . Windows Forms uygulamalarındaki nesnelere karmaşık geri alma desteği sağlamak için tasarlanmıştır.


6

Memento modelini kullanarak karmaşık geri alma sistemlerini başarıyla uyguladım - çok kolay ve doğal olarak bir Yinele çerçevesi sağlama avantajına da sahip. Daha ince bir yararı, toplu işlemlerin tek bir Geri Al içinde de yer alabilmesidir.

Özetle, iki yığın hatıra nesneniz var. Biri Geri Al için, diğeri Yinele için. Her işlem, ideal olarak modelinizin, belgenizin (veya her neyse) durumunu değiştirmek için bazı çağrılar olacak yeni bir hatıra yaratır. Bu, geri alma yığınına eklenir. Bir geri alma işlemi yaptığınızda, modeli tekrar değiştirmek için Memento nesnesinde Geri Al eylemini yürütmenin yanı sıra, nesneyi Geri Al yığınından çıkarır ve Yeniden Yap yığınına doğru itersiniz.

Belgenizin durumunu değiştirme yönteminin nasıl uygulanacağı tamamen uygulamanıza bağlıdır. Basitçe bir API çağrısı yapabiliyorsanız (ör. ChangeColour (r, g, b)), ilgili durumu almak ve kaydetmek için önüne bir sorgu koyun. Ancak desen aynı zamanda derin kopyalar yapmayı, bellek anlık görüntülerini, geçici dosya oluşturmayı vb. Destekleyecektir - bu tamamen sanal bir yöntem uygulaması olduğu için size kalmış.

Toplu eylemler yapmak için (örn. Kullanıcı Shift - Silme, yeniden adlandırma, niteliği değiştirme gibi üzerinde işlem yapılacak nesnelerin bir yükünü seçer), kodunuz tek bir hatıra olarak yeni bir Geri Al yığını oluşturur ve bunu gerçek işleme aktarır. bireysel işlemleri ekleyin. Bu nedenle, eylem yöntemlerinizin (a) endişelenecek global bir yığına sahip olması gerekmez ve (b) ister tek başına ister bir toplu işlemin parçası olarak çalıştırılsınlar aynı şekilde kodlanabilir.

Çoğu geri alma sistemi yalnızca bellek içindedir, ancak isterseniz geri alma yığınını devam ettirebilirsiniz, sanırım.


5

Çevik geliştirme kitabımdaki komut modelini okuyordum - belki de bu bir potansiyele sahiptir?

Her komutun komut arayüzünü (bir Execute () yöntemine sahip olan) uygulamasını sağlayabilirsiniz. Geri almak istiyorsanız, bir Geri Al yöntemi ekleyebilirsiniz.

daha fazla bilgi burada


4

Ben birlikte olduğum Mendelt Siebenga Command Desen kullanması gerektiğini gerçeği. Kullandığınız desen, zamanla çok israf edebilecek ve çok geçecek olan Memento Deseni idi.

Yoğun bellek kullanan bir uygulama üzerinde çalıştığınız için, geri alma motorunun ne kadar bellek kullanmasına izin verileceğini, kaç seviye geri alma kaydedileceğini veya bunların kalıcı olacağı bir miktar depolama alanını belirtebilmelisiniz. Bunu yapmazsanız, kısa süre içinde makinenin belleğinin yetersiz kalmasından kaynaklanan hatalarla karşılaşacaksınız.

Seçtiğiniz programlama dilinde / çerçevede geri alma için bir model oluşturmuş bir çerçeve olup olmadığını kontrol etmenizi tavsiye ederim. Yeni şeyler icat etmek güzel, ancak gerçek senaryolarda önceden yazılmış, hata ayıklanmış ve test edilmiş bir şeyi almak daha iyidir. İnsanların bildikleri çerçeveleri önerebilmeleri için, bunu yazdığınızı eklemenizin yardımı olacaktır.


3

Codeplex projesi :

Klasik Command tasarım modeline dayalı olarak, uygulamalarınıza Geri Al / Yinele işlevselliği eklemek için basit bir çerçevedir. Birleştirme eylemlerini, iç içe geçmiş işlemleri, gecikmiş yürütmeyi (en üst düzey işlem gerçekleştirme işleminde yürütme) ve olası doğrusal olmayan geri alma geçmişini (yinelemek için birden çok eylem seçeneğine sahip olabileceğiniz) destekler.


2

Okuduğum çoğu örnek, komut veya memento kalıbını kullanarak bunu yapıyor. Ancak bunu basit bir dekor yapısıyla tasarım desenleri olmadan da yapabilirsiniz .


Arka plana ne koyardın?

Benim durumumda, geri alma / yineleme işlevini istediğim işlemlerin mevcut durumunu koydum. İki dekora sahip olarak (geri al / yinele) geri alma kuyruğunu geri alıyorum (ilk öğeyi aç) ve yeniden sıraya sokma işlemine yerleştiriyorum. Kuyruklardaki öğe sayısı tercih edilen boyutu aşarsa, kuyruktan bir öğe çıkar.
Patrik Svensson

2
Ne aslında tarif IS bir tasarım deseni :). Bu yaklaşımla ilgili sorun, eyaletinizin çok fazla bellek almasıdır - birkaç düzinelerce durum sürümünü saklamak, bu durumda kullanışsız veya hatta imkansız hale gelir.
Igor Brejc

Veya normal ve geri alma işlemini temsil eden bir çift kapatma saklayabilirsiniz.
Xwtek

2

Yazılımınızı çok kullanıcılı işbirliği için de uygun hale getirecek olan geri alma işleminin akıllıca bir yolu , veri yapısının operasyonel bir dönüşümünü uygulamaktır .

Bu kavram çok popüler değil ama iyi tanımlanmış ve kullanışlıdır. Tanım size çok soyut görünüyorsa, bu proje JSON nesneleri için bir operasyonel dönüşümün Javascript'te nasıl tanımlanıp uygulandığının başarılı bir örneğidir.



1

Bir nesnenin tüm durumunu kaydetmek ve geri yüklemek için uygun bir form için "nesneler" için dosya yükleme ve kaydetme serileştirme kodunu yeniden kullandık. Bu serileştirilmiş nesneleri, hangi işlemin gerçekleştirildiği hakkında bazı bilgilerle birlikte geri alma yığınına itiyoruz ve serileştirilmiş verilerden yeterince bilgi toplanmadıysa bu işlemi geri alma konusunda ipuçları veriyoruz. Geri Al ve Yinele genellikle bir nesneyi diğeriyle değiştirmektir (teoride).

Bazı garip geri alma yineleme dizilerini gerçekleştirirken (bu yerler daha güvenli geri alma bilinçli “tanımlayıcıları” geri almak için güncellenmemiş) nesnelere yönelik işaretçilerden (C ++) kaynaklanan birçok hata olmuştur. Bu bölgedeki hatalar genellikle ... ummm ... ilginç.

Bazı işlemler, hız / kaynak kullanımı için özel durumlar olabilir - nesneleri boyutlandırma, nesneleri hareket ettirme gibi.

Çoklu seçim de bazı ilginç komplikasyonlar sağlar. Şans eseri, kodda zaten bir gruplama konseptimiz vardı. Kristopher Johnson'ın alt öğeler hakkındaki yorumu yaptığımız şeye oldukça yakın.


Modelinizin boyutu büyüdükçe, bu giderek daha fazla çalışmaz hale geliyor.
Warren P

Ne şekilde? Her nesneye yeni "şeyler" eklendiğinden, bu yaklaşım değişiklik olmadan çalışmaya devam eder. Nesnelerin serileştirilmiş biçimi boyut olarak büyüdükçe performans bir sorun olabilir - ancak bu büyük bir sorun olmamıştır. Sistem 20 yılı aşkın süredir sürekli geliştirilmekte ve 1000'lerce kullanıcı tarafından kullanılmaktadır.
Aardvark'ın

1

Bunu bir peg-jump bulmaca oyunu için bir çözücü yazarken yapmak zorundaydım. Her hareketi, yapılabileceği veya geri alınabileceği kadar yeterli bilgiyi içeren bir Command nesnesi yaptım. Benim durumumda bu, başlangıç ​​pozisyonunu ve her hareketin yönünü kaydetmek kadar basitti. Daha sonra tüm bu nesneleri bir yığın halinde sakladım, böylece program geri izleme sırasında ihtiyaç duyduğu kadar çok hareketi kolayca geri alabilir.


1

PostSharp'ta Geri Al / Yinele deseninin hazır uygulamasını deneyebilirsiniz. https://www.postsharp.net/model/undo-redo

Modeli kendiniz uygulamadan uygulamanıza geri alma / yineleme işlevi eklemenize olanak tanır. Modelinizdeki değişiklikleri izlemek için Kaydedilebilir desen kullanır ve ayrıca PostSharp'ta uygulanan INotifyPropertyChanged deseniyle çalışır.

Kullanıcı arabirimi kontrolleri sağlanır ve her işlemin adı ve ayrıntı düzeyinin ne olacağına karar verebilirsiniz.


0

Bir keresinde, uygulamanın modelinde bir komutla yapılan tüm değişikliklerin (örn. CDocument ... MFC kullanıyorduk) komutun sonunda model içinde tutulan dahili bir veritabanındaki alanları güncelleyerek devam ettirildiği bir uygulama üzerinde çalıştım. Bu nedenle, her eylem için ayrı bir geri alma / yineleme kodu yazmak zorunda kalmadık. Geri alma yığını, her kayıt değiştirildiğinde (her komutun sonunda) birincil anahtarları, alan adlarını ve eski değerleri basitçe hatırladı.


0

Tasarım Modellerinin (GoF, 1994) ilk bölümünde, geri alma / yineleme işleminin bir tasarım modeli olarak uygulanması için bir kullanım durumu vardır.


0

İlk fikir performansınızı gerçekleştirebilirsiniz.

Kalıcı veri yapılarını kullanın ve etrafta eski duruma referansların bir listesini tutmaya devam edin . (Ancak bu, yalnızca durum sınıfınızdaki tüm veriler değişmez ise ve üzerindeki tüm işlemler yeni bir sürüm döndürürse gerçekten işe yarar - ancak yeni sürümün derin bir kopya olması gerekmez, yalnızca değiştirilen parçaların kopyasını değiştirin -on-yazma'.)


0

Komut kalıbını burada çok yararlı buldum. Birkaç ters komut uygulamak yerine, API'min ikinci bir örneğinde gecikmeli yürütme ile geri alma kullanıyorum.

Düşük uygulama çabası ve kolay bakım istiyorsanız (ve 2. örnek için fazladan belleği karşılayabiliyorsanız) bu yaklaşım makul görünüyor.

Örnek için buraya bakın: https://github.com/thilo20/Undo/


-1

Bunun sizin için herhangi bir faydası olup olmayacağını bilmiyorum, ancak projelerimden birinde benzer bir şey yapmam gerektiğinde, http://www.undomadeeasy.com adresinden UndoEngine'i indirdim - harika bir motor ve gerçekten kaputun altında ne olduğu umrumda değildi - sadece işe yaradı.


Lütfen yorumlarınızı yalnızca çözüm sağlayacağınızdan eminseniz yanıt olarak gönderin! Aksi takdirde, sorunun altına yorum olarak göndermeyi tercih edin! (eğer şimdi buna izin vermiyorsa! lütfen itibar
kazanana

-1

Bence, UNDO / REDO genel olarak 2 şekilde uygulanabilir. 1. Komut Düzeyi (komut düzeyi Geri Al / Yinele olarak adlandırılır) 2. Belge düzeyi (genel Geri Al / Yinele olarak adlandırılır)

Komut seviyesi: Birçok cevabın işaret ettiği gibi, bu Memento kalıbı kullanılarak verimli bir şekilde başarılır. Komut ayrıca eylemi günlüğe kaydetmeyi destekliyorsa, yineleme kolayca desteklenir.

Sınırlama: Komutun kapsamı bir kez çıkarıldığında, geri alma / yineleme imkansızdır, bu da belge düzeyinde (genel) geri alma / yineleme

Çok fazla bellek alanı içeren bir model için uygun olduğu için durumunuzun genel geri alma / yineleme sürecine uyacağını tahmin ediyorum. Ayrıca bu, seçici olarak geri almak / yinelemek için de uygundur. İki ilkel tür vardır

  1. Tüm bellek geri al / yinele
  2. Nesne düzeyinde Geri Al Yinele

"Tüm bellek Geri Al / Yinele" de, belleğin tamamı bağlı bir veri (bir ağaç veya liste veya grafik gibi) olarak kabul edilir ve bellek, işletim sistemi yerine uygulama tarafından yönetilir. Bu yüzden yeni ve silme operatörleri, C ++ 'da ise,. Herhangi bir düğüm değiştirilirse, b. verileri tutma ve temizleme vb. İşleyiş şekli temelde tüm belleği kopyalamak (bellek tahsisinin gelişmiş algoritmalar kullanılarak uygulama tarafından zaten optimize edildiği ve yönetildiği varsayılarak) ve bir yığın halinde saklamaktır. Hafızanın kopyası istenirse sığ veya derin kopyası olması ihtiyacına göre ağaç yapısı kopyalanır. Yalnızca değiştirilen değişken için derin bir kopya yapılır. Her değişken özel ayırma kullanılarak tahsis edildiğinden, Uygulama, gerektiğinde onu ne zaman sileceğine dair son söze sahiptir. Geri Al / Yinele'yi bölümlere ayırmak zorunda kalırsak, programatik olarak seçici bir şekilde bir işlem kümesini Geri Al / Yinele'ye ihtiyaç duyarsak işler çok ilginç hale gelir. Bu durumda, yalnızca bu yeni değişkenlere veya silinmiş değişkenlere veya değiştirilmiş değişkenlere bir bayrak verilir, böylece Geri Al / Yinele yalnızca bu belleği geri alır / yeniden yapar Bir nesnenin içinde kısmi bir Geri Al / Yinele yapmamız gerekirse, işler daha da ilginç hale gelir. Böyle bir durumda, daha yeni bir "Ziyaretçi kalıbı" fikri kullanılır. Buna "Nesne Düzeyinde Geri Al / Yinele" denir veya silinmiş değişkenlere veya değiştirilmiş değişkenlere bir bayrak verilir, böylece Geri Al / Yinele yalnızca bu belleği geri alır / yeniden yapar Bir nesnenin içinde kısmi bir Geri Al / Yinele yapmamız gerekirse, işler daha da ilginç hale gelir. Böyle bir durumda, daha yeni bir "Ziyaretçi kalıbı" fikri kullanılır. Buna "Nesne Düzeyinde Geri Al / Yinele" denir veya silinmiş değişkenlere veya değiştirilmiş değişkenlere bir bayrak verilir, böylece Geri Al / Yinele yalnızca bu belleği geri alır / yeniden yapar Bir nesnenin içinde kısmi bir Geri Al / Yinele yapmamız gerekirse, işler daha da ilginç hale gelir. Böyle bir durumda, daha yeni bir "Ziyaretçi kalıbı" fikri kullanılır. Buna "Nesne Düzeyinde Geri Al / Yinele" denir

  1. Nesne seviyesi Geri Al / Yinele: Geri alma / yineleme bildirimi çağrıldığında, her nesne bir akış işlemi gerçekleştirir, burada akış sağlayıcı nesneden programlanan eski verileri / yeni verileri alır. Rahatsız edilmeyen veriler bozulmadan bırakılır. Her nesne, argüman olarak bir yayınlayıcı alır ve UNDo / Redo çağrısının içinde, nesnenin verilerini akışa alır / akışını kaldırır.

Hem 1 hem de 2, 1. BeforeUndo () 2. AfterUndo () 3. BeforeRedo () 4. AfterRedo () gibi yöntemlere sahip olabilir. Bu yöntemlerin temel Geri Al / Yinele Komutunda (bağlamsal komutta değil) yayınlanması gerekir, böylece tüm nesneler belirli eylemleri elde etmek için bu yöntemleri uygular.

İyi bir strateji, 1 ve 2'nin bir melezini oluşturmaktır. Güzel olan, bu yöntemlerin (1 ve 2) kendilerinin komut kalıplarını kullanmasıdır.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.