Jenis data tanggung jawab tunggal dan khusus

10

Dalam beberapa bulan terakhir saya telah meminta orang-orang di sini di SE dan di situs lain menawarkan saya kritik konstruktif mengenai kode saya. Ada satu hal yang terus bermunculan hampir setiap waktu dan saya masih tidak setuju dengan rekomendasi itu; : P Saya ingin membahasnya di sini dan mungkin segalanya akan menjadi lebih jelas bagi saya.

Ini terkait dengan prinsip tanggung jawab tunggal (SRP). Pada dasarnya, saya memiliki kelas data Font,, yang tidak hanya menampung fungsi untuk memanipulasi data, tetapi juga untuk memuatnya. Saya diberitahu keduanya harus terpisah, bahwa fungsi pemuatan harus ditempatkan di dalam kelas pabrik; Saya pikir ini adalah interpretasi yang salah dari SRP ...

Sebuah Fragmen dari Kelas Font Saya

class Font
{
  public:
    bool isLoaded() const;
    void loadFromFile(const std::string& file);
    void loadFromMemory(const void* buffer, std::size_t size);
    void free();

    void some();
    void another();
};

Desain yang disarankan

class Font
{
  public:
    void some();
    void another();
};


class FontFactory
{
  public:
    virtual std::unique_ptr<Font> createFromFile(...) = 0;
    virtual std::unique_ptr<Font> createFromMemory(...) = 0;
};

Desain yang disarankan seharusnya mengikuti SRP, tapi saya tidak setuju - saya pikir itu terlalu jauh. The Fontkelas tidak lagi mandiri (tidak ada gunanya tanpa pabrik), dan FontFactorykebutuhan untuk mengetahui rincian tentang pelaksanaan sumber daya, yang mungkin dilakukan melalui persahabatan atau getter publik, yang selanjutnya mengekspos pelaksanaan Font. Saya pikir ini lebih merupakan kasus tanggung jawab yang terfragmentasi .

Inilah mengapa saya pikir pendekatan saya lebih baik:

  • Fontmandiri - Menjadi mandiri, lebih mudah dipahami dan dipelihara. Anda juga dapat menggunakan kelas tanpa harus memasukkan yang lain. Namun, jika Anda merasa perlu pengelolaan sumber daya yang lebih kompleks (pabrik), Anda juga dapat melakukannya dengan mudah (nanti saya akan berbicara tentang pabrik saya sendiri ResourceManager<Font>).

  • Mengikuti perpustakaan standar - Saya percaya tipe yang ditentukan pengguna harus berusaha semaksimal mungkin untuk menyalin perilaku tipe standar dalam bahasa masing-masing. The std::fstreammandiri dan menyediakan fungsi seperti opendan close. Mengikuti perpustakaan standar berarti tidak perlu menghabiskan upaya belajar cara lain dalam melakukan sesuatu. Selain itu, secara umum, komite standar C ++ mungkin lebih tahu tentang desain daripada siapa pun di sini, jadi jika pernah ragu, salin apa yang mereka lakukan.

  • Testabilitas - Ada yang salah, di mana masalahnya? - Apakah cara Fontmenangani data atau cara FontFactorymemuat data? Kamu tidak benar-benar tahu. Memiliki kelas mandiri dapat mengurangi masalah ini: Anda dapat menguji Fontsecara terpisah. Jika kemudian Anda harus menguji pabrik dan Anda tahu Fontberfungsi dengan baik, Anda juga akan tahu bahwa setiap kali terjadi masalah pasti ada di dalam pabrik.

  • Ini adalah konteks agnostik - (Ini sedikit bersinggungan dengan poin pertama saya.) Melakukan Fonthal tersebut dan tidak membuat asumsi tentang bagaimana Anda akan menggunakannya: Anda dapat menggunakannya sesuka Anda. Memaksa pengguna untuk menggunakan pabrik meningkatkan sambungan antar kelas.

Saya juga Punya Pabrik

(Karena desainnya Fontmemungkinkan saya untuk.)

Atau lebih tepatnya seorang manajer, bukan sekadar pabrik ... Fontmandiri sehingga manajer tidak perlu tahu bagaimana membangunnya; alih-alih manajer memastikan file atau buffer yang sama tidak dimuat ke memori lebih dari sekali. Anda bisa mengatakan sebuah pabrik dapat melakukan hal yang sama, tetapi bukankah itu akan merusak SRP? Pabrik kemudian tidak hanya harus membangun objek, tetapi juga mengelolanya.

template<class T>
class ResourceManager
{
  public:
    ResourcePtr<T> acquire(const std::string& file);
    ResourcePtr<T> acquire(const void* buffer, std::size_t size);
};

Berikut ini demonstrasi bagaimana manajer dapat digunakan. Perhatikan bahwa itu pada dasarnya digunakan persis seperti pabrik.

void test(ResourceManager<Font>* rm)
{
    // The same file isn't loaded twice into memory.
    // I can still have as many Fonts using that file as I want, though.
    ResourcePtr<Font> font1 = rm->acquire("fonts/arial.ttf");
    ResourcePtr<Font> font2 = rm->acquire("fonts/arial.ttf");

    // Print something with the two fonts...
}

Intinya ...

(Ini ingin menempatkan tl; dr di sini, tapi saya tidak bisa memikirkannya.: \)
Nah, begitulah, saya sudah membuat kasus saya sebaik mungkin. Silakan memposting argumen balasan yang Anda miliki dan juga keuntungan apa pun yang menurut Anda memiliki desain yang disarankan di atas desain saya sendiri. Pada dasarnya, coba tunjukkan bahwa saya salah. :)

Paul
sumber
2
Mengingatkan saya pada ActiveRecord vs DataMapper karya Martin Fowler .
Pengguna
Memberikan kenyamanan (desain Anda saat ini) di antarmuka terluar yang menghadap pengguna. Gunakan SRP secara internal sehingga akan memudahkan perubahan implementasi Anda di masa depan. Saya bisa memikirkan hiasan pemuat Font yang melompati huruf miring dan tebal; yang hanya memuat Unicode BMP, dll.
rwong
@rwong Saya tahu presentasi itu, saya punya bookmark untuk itu ( video ). :) Tapi saya tidak mengerti apa yang Anda katakan di komentar Anda yang lain ...
Paul
1
@ rwong Bukankah ini sudah satu liner? Anda hanya perlu satu baris, apakah Anda memuat Font secara langsung atau melalui ResourceManager. Dan apa yang membuat saya berhenti menerapkan RM jika pengguna mengeluh?
Paul

Jawaban:

7

Tidak ada yang salah dengan kode itu menurut saya, ia melakukan apa yang Anda butuhkan secara masuk akal dan cukup mudah untuk mempertahankannya.

Namun , masalah yang Anda miliki dengan kode ini adalah bahwa jika Anda ingin melakukan hal lain, Anda harus mengubah semuanya .

Inti dari SRP adalah bahwa jika Anda memiliki satu komponen 'CompA' yang melakukan algoritma A () dan Anda perlu mengubah algoritma A () Anda tidak harus mengubah 'CompB' juga.

Keterampilan C ++ saya terlalu berkarat untuk menyarankan skenario yang layak di mana Anda perlu mengubah solusi manajemen font Anda, tetapi kasus yang biasa saya buat adalah ide meluncur di lapisan caching. Idealnya, Anda tidak ingin hal yang memuat banyak hal tahu dari mana asalnya, atau hal yang dimuat tidak peduli dari mana asalnya, karena kemudian membuat perubahan lebih sederhana. Ini semua tentang rawatan.

Salah satu contoh mungkin Anda memuat font Anda dari sumber ketiga (misalnya gambar karakter sprite). Untuk mencapai ini, Anda perlu mengubah loader Anda (untuk memanggil metode ketiga jika dua yang pertama gagal) dan kelas Font itu sendiri untuk mengimplementasikan panggilan ketiga ini. Idealnya, Anda hanya akan membuat pabrik lain (SpriteFontFactory, atau apa pun), menerapkan metode loadFont (...) yang sama dan menempelkannya di daftar pabrik di suatu tempat yang dapat digunakan untuk memuat font.

Ed James
sumber
1
Ah, saya mengerti: jika saya menambahkan satu lagi cara untuk memuat font, saya harus menambahkan satu lagi fungsi perolehan ke manajer dan satu lagi fungsi memuat ke sumber daya. Memang, itu satu kelemahan. Bergantung pada apa sumber baru ini, bagaimanapun, Anda mungkin harus menangani data secara berbeda (TTF adalah satu hal, font sprite adalah hal lain), jadi Anda tidak dapat benar-benar memprediksi seberapa fleksibel desain tertentu akan menjadi. Tapi saya mengerti maksud Anda.
Paul
Ya, seperti yang saya katakan, keterampilan C ++ saya cukup berkarat jadi saya berjuang untuk menghasilkan demonstrasi masalah yang layak, saya setuju tentang hal fleksibilitas. Ini benar-benar tergantung apa yang Anda lakukan dengan kode Anda, seperti saya katakan, saya pikir kode asli Anda adalah solusi yang cukup masuk akal untuk masalah ini.
Ed James
Pertanyaan yang bagus dan jawaban yang bagus, dan yang terbaik adalah bahwa banyak pengembang dapat belajar darinya. Inilah sebabnya saya suka nongkrong di sini :). Oh, jadi komentar saya tidak sepenuhnya mubazir, SRP bisa sedikit rumit karena Anda harus bertanya pada diri sendiri 'bagaimana jika', yang tampaknya bertentangan dengan: 'optimasi prematur adalah akar dari semua kejahatan' atau ' Filosofi YAGNI. Tidak pernah ada jawaban hitam dan putih!
Martijn Verburg
0

Satu hal yang mengganggu saya tentang kelas Anda adalah bahwa Anda memiliki loadFromMemorydan loadFromFilemetode. Idealnya, Anda hanya memiliki loadFromMemorymetode; sebuah font seharusnya tidak peduli bagaimana data dalam memori muncul. Hal lain adalah Anda harus menggunakan konstruktor / destruktor alih-alih memuat dan freemetode. Dengan demikian, loadFromMemoryakan menjadi Font(const void *buf, int len)dan free()akan menjadi ~Font().

zvrba
sumber
Fungsi beban dapat diakses dari dua konstruktor, dan gratis disebut dalam destruktor - Saya hanya tidak menunjukkannya di sini. Saya merasa nyaman untuk dapat memuat font langsung dari file, daripada membuka file terlebih dahulu, menulis data ke buffer dan kemudian meneruskannya ke Font. Kadang-kadang saya juga perlu memuat dari buffer, itulah sebabnya saya memiliki kedua metode ini.
Paul