Mengurangi kompleksitas kelas

10

Saya telah melihat beberapa jawaban dan mencari di Google, tetapi saya tidak dapat menemukan sesuatu yang membantu (yaitu, itu tidak akan memiliki efek samping yang canggung).

Masalah saya, secara abstrak, adalah bahwa saya memiliki objek dan perlu melakukan urutan operasi yang panjang di atasnya; Saya menganggapnya sebagai semacam jalur perakitan, seperti membangun mobil.

Saya percaya benda-benda ini akan disebut Object Methods .

Jadi, dalam contoh ini di beberapa titik saya akan memiliki CarWithoutUpholstery di mana saya kemudian perlu menjalankan installBackSeat, installFrontSeat, installWoodenInserts (operasi tidak saling mengganggu, dan bahkan mungkin dilakukan secara paralel). Operasi ini dilakukan oleh CarWithoutUpholstery.worker () dan menghasilkan objek baru yang akan menjadi CarWithUpholstery, di mana saya akan menjalankan mungkin cleanInsides (), verifikasi No NolUpholsteryDefects (), dan sebagainya.

Operasi dalam satu fase sudah independen, yaitu, saya sudah bergulat dengan mereka yang dapat dieksekusi dalam urutan apa pun (kursi depan dan belakang dapat dipasang dalam urutan apa pun).

Logika saya saat ini menggunakan Refleksi untuk kesederhanaan implementasi.

Yaitu, begitu saya memiliki CarWithoutUpholstery, objek memeriksa dirinya sendiri untuk metode yang disebut performSomething (). Pada saat itu ia menjalankan semua metode ini:

myObject.perform001SomeOperation();
myObject.perform002SomeOtherOperation();
...

saat memeriksa kesalahan dan hal-hal lainnya. Sementara urutan operasi adalah penting, saya telah diberi urutan leksikografis dalam kasus saya pernah menemukan beberapa rangka penting setelah semua. Ini bertentangan dengan YAGNI , tetapi biayanya sangat kecil - semacam sederhana () - dan itu bisa menghemat penggantian nama metode besar-besaran (atau memperkenalkan beberapa metode lain untuk melakukan tes, misalnya berbagai metode) di telepon.

Contoh berbeda

Mari kita katakan bahwa alih-alih membangun mobil saya harus menyusun laporan Polisi Rahasia pada seseorang, dan menyerahkannya kepada Tuan Jahat saya . Objek terakhir saya adalah ReadyReport. Untuk membangunnya saya mulai dengan mengumpulkan informasi dasar (nama, nama keluarga, pasangan ...). Ini adalah Fase A. Saya, tergantung apakah ada pasangan atau tidak, saya mungkin harus melanjutkan ke fase B1 atau B2, dan mengumpulkan data seksualitas pada satu atau dua orang. Ini dibuat dari beberapa pertanyaan yang berbeda untuk Minion Jahat yang berbeda mengendalikan kehidupan malam, kamera jalanan, kwitansi penjualan toko seks dan apa yang tidak. Dan seterusnya dan seterusnya.

Jika korban tidak memiliki keluarga, saya bahkan tidak akan memasuki fase GetInformationAboutFamily, tetapi jika saya lakukan, maka tidak relevan apakah saya pertama-tama menargetkan ayah atau ibu atau saudara kandung (jika ada). Tapi saya tidak bisa melakukan itu jika saya belum melakukan FamilyStatusCheck, yang karena itu termasuk dalam fase sebelumnya.

Semuanya bekerja dengan luar biasa ...

  • jika saya perlu beberapa operasi tambahan saya hanya perlu menambahkan metode pribadi,
  • jika operasi ini umum untuk beberapa fase saya bisa mewarisi dari superclass,
  • operasi sederhana dan mandiri. Tidak ada nilai dari satu operasi yang pernah diperlukan oleh yang lain (operasi yang tidak dilakukan dalam fase yang berbeda),
  • objek di telepon tidak perlu melakukan banyak tes karena mereka bahkan tidak bisa eksis jika objek pencipta mereka tidak memverifikasi kondisi tersebut di tempat pertama. Yaitu, ketika menempatkan sisipan di dasbor, membersihkan dasbor dan memverifikasi dasbor, saya tidak perlu memverifikasi bahwa sebenarnya ada dasbor .
  • memungkinkan untuk pengujian mudah. Saya dapat dengan mudah mengejek objek parsial dan menjalankan metode apa pun di atasnya, dan semua operasi adalah kotak hitam deterministik.

...tapi...

Masalahnya muncul ketika saya menambahkan satu operasi terakhir di salah satu objek metode saya, yang menyebabkan modul keseluruhan melebihi indeks kompleksitas wajib ("kurang dari metode pribadi N").

Saya sudah membawa masalah ini ke atas dan menyarankan bahwa dalam kasus ini, kekayaan metode pribadi bukanlah tanda bencana. Kompleksitas adalah di sana, tapi itu ada karena operasi adalah kompleks, dan benar-benar itu tidak semua yang rumit - itu hanya panjang .

Dengan menggunakan contoh Evil Overlord, masalah saya adalah bahwa Evil Overlord (alias Dia Yang Tidak Akan Ditolak ) meminta semua informasi diet, Pelayan diet saya memberi tahu saya bahwa saya perlu menanyakan restoran, dapur, pedagang kaki lima, pedagang kaki lima tanpa izin, rumah kaca pemilik dll., dan Tuan Jahat (sub) - yang dikenal sebagai Dia yang Juga Tidak Akan Ditolak - mengeluh bahwa saya melakukan terlalu banyak permintaan dalam fase GetDietaryInformation.

Catatan : Saya sadar bahwa dari beberapa sudut pandang ini sama sekali bukan masalah (mengabaikan kemungkinan masalah kinerja, dll.). Semua yang terjadi adalah bahwa metrik tertentu tidak bahagia, dan ada pembenaran untuk itu.

Apa yang saya pikir bisa saya lakukan

Terlepas dari yang pertama, semua opsi ini bisa dilakukan dan, saya pikir, dapat dipertahankan.

  • Saya telah memverifikasi bahwa saya bisa licik dan menyatakan setengah metode saya protected. Tapi saya akan mengeksploitasi kelemahan dalam prosedur pengujian, dan selain membenarkan diri sendiri ketika tertangkap, saya tidak suka ini. Juga, ini adalah ukuran sementara. Bagaimana jika jumlah operasi yang diperlukan berlipat ganda? Tidak mungkin, tetapi bagaimana?
  • Saya dapat membagi fase ini secara sewenang-wenang menjadi AnnealedObjectAlpha, AnnealedObjectBravo dan AnnealedObjectCharlie, dan memiliki sepertiga operasi yang dilakukan pada setiap tahap. Saya mendapat kesan bahwa ini sebenarnya menambah kompleksitas (N-1 lebih banyak kelas), tanpa manfaat kecuali untuk lulus tes. Tentu saja saya dapat berpendapat bahwa CarWithFrontSeatsInstalled dan CarWithAllSeatsInstalled adalah tahapan yang secara logis berturut-turut . Risiko metode Bravo nantinya dibutuhkan oleh Alpha kecil, dan bahkan lebih kecil jika saya memainkannya dengan baik. Tetapi tetap saja.
  • Saya dapat mengelompokkan operasi yang berbeda, serupa dari jarak jauh, dalam satu operasi. performAllSeatsInstallation(). Ini hanya tindakan sementara dan itu meningkatkan kompleksitas operasi tunggal. Jika saya perlu melakukan operasi A dan B dalam urutan yang berbeda, dan saya telah mengemasnya di dalam E = (A + C) dan F (B + D), saya harus melepaskan ikatan E dan F dan mengocok kode sekitar .
  • Saya dapat menggunakan array fungsi lambda dan menghindari pemeriksaan secara rapi, tapi saya merasa kikuk. Namun ini adalah alternatif terbaik sejauh ini. Itu akan menghilangkan refleksi. Dua masalah yang saya miliki adalah bahwa saya mungkin akan diminta untuk menulis ulang semua objek metode, tidak hanya hipotesis CarWithEngineInstalled, dan sementara itu akan menjadi keamanan pekerjaan yang sangat baik, itu benar-benar tidak menarik banyak; dan bahwa pemeriksa cakupan kode memiliki masalah dengan lambdas (yang dapat dipecahkan, tetapi masih ).

Begitu...

  • Menurut Anda, mana pilihan terbaik saya?
  • Apakah ada cara yang lebih baik yang belum saya pertimbangkan? ( mungkin lebih baik aku berterus terang dan bertanya langsung apa itu? )
  • Apakah desain ini cacat tanpa harapan, dan saya lebih baik mengakui kekalahan dan parit - arsitektur ini sama sekali? Tidak baik untuk karir saya, tetapi apakah menulis kode yang dirancang dengan buruk akan menjadi lebih baik dalam jangka panjang?
  • Apakah pilihan saya saat ini sebenarnya adalah One True Way, dan saya harus berjuang untuk mendapatkan metrik kualitas yang lebih baik (dan / atau instrumentasi) terinstal? Untuk opsi terakhir ini, saya perlu referensi ... Saya tidak bisa hanya melambaikan tangan ke @PHB sambil bergumam. Ini bukan metrik yang Anda cari . Tidak peduli seberapa besar aku ingin bisa
LSerni
sumber
3
Atau, Anda dapat mengabaikan peringatan "melampaui kompleksitas maksimum".
Robert Harvey
Jika saya bisa saya akan. Entah bagaimana, metrik ini telah mendapatkan kualitas sakralnya sendiri. Saya terus mencoba dan meyakinkan PTB untuk menerimanya sebagai alat yang bermanfaat, tetapi hanya alat. Saya mungkin belum berhasil ...
LSerni
Apakah operasi sebenarnya membangun objek, atau Anda hanya menggunakan metafora jalur perakitan karena berbagi properti spesifik yang Anda sebutkan (banyak operasi dengan urutan ketergantungan parsial)? Signifikansi adalah bahwa yang pertama menunjukkan pola pembangun, sedangkan yang kedua mungkin menyarankan beberapa pola lain dengan rincian lebih lengkap terisi.
outis
2
Tirani metrik. Metrik adalah indikator yang berguna, tetapi menerapkannya sambil mengabaikan mengapa metrik digunakan tidak membantu.
Jaydee
2
Jelaskan kepada orang-orang di atas perbedaan antara kompleksitas esensial dan kompleksitas tidak disengaja. en.wikipedia.org/wiki/No_Silver_Bullet Jika Anda yakin bahwa kompleksitas yang melanggar aturan itu penting, maka jika Anda refactor per programmer.stackexchange.com/a/297414/51948 Anda mungkin berseluncur di sekitar aturan dan menyebar keluar kompleksitas. Jika merangkum hal-hal ke dalam fase tidak sembarangan (fase berhubungan dengan masalah) dan mendesain ulang mengurangi beban kognitif untuk pengembang yang mempertahankan kode, maka masuk akal untuk melakukan ini.
Fuhrmanator

Jawaban:

15

Urutan operasi yang panjang sepertinya adalah kelasnya sendiri, yang kemudian membutuhkan objek tambahan untuk melakukan tugasnya. Sepertinya Anda sudah dekat dengan solusi ini. Prosedur panjang ini dapat dipecah menjadi beberapa langkah atau fase. Setiap langkah atau fase dapat berupa kelasnya sendiri yang memperlihatkan metode publik. Anda ingin setiap fase menerapkan antarmuka yang sama. Kemudian jalur perakitan Anda melacak daftar fase.

Untuk membangun contoh perakitan mobil Anda, bayangkan Carkelas:

public class Car
{
    public IChassis Chassis { get; private set; }
    public Dashboard { get; private set; }
    public IList<Seat> Seats { get; private set; }

    public Car()
    {
        Seats = new List<Seat>();
    }

    public void AddChassis(IChassis chassis)
    {
        Chassis = chassis;
    }

    public void AddDashboard(Dashboard dashboard)
    {
        Dashboard = dashboard;
    }
}

Saya akan meninggalkan implementasi beberapa komponen, seperti IChassis, Seatdan Dashboarddemi singkatnya.

Yang Cartidak bertanggung jawab untuk membangun itu sendiri. Baris perakitan adalah, jadi mari kita merangkumnya di kelas:

public class CarAssembler
{
    protected List<IAssemblyPhase> Phases { get; private set; }

    public CarAssembler()
    {
        Phases = new List<IAssemblyPhase>()
        {
            new ChassisAssemblyPhase(),
            new DashboardAssemblyPhase(),
            new SeatAssemblyPhase()
        };
    }

    public void Assemble(Car car)
    {
        foreach (IAssemblyPhase phase in Phases)
        {
            phase.Assemble(car);
        }
    }
}

Perbedaan penting di sini adalah bahwa CarAssembler memiliki daftar objek fase perakitan yang diimplementasikan IAssemblyPhase. Anda sekarang berurusan secara eksplisit dengan metode publik, yang berarti setiap fase dapat diuji dengan sendirinya, dan alat kualitas kode Anda lebih bahagia karena Anda tidak memasukkan begitu banyak ke dalam satu kelas tunggal. Proses perakitan juga mati sederhana. Lewati saja fase-fase dan panggil orang yang Assemblelewat. Selesai The IAssemblyPhaseantarmuka juga sederhana mati hanya dengan metode umum tunggal yang mengambil benda Mobil:

public interface IAssemblyPhase
{
    void Assemble(Car car);
}

Sekarang kita perlu kelas konkret kita mengimplementasikan antarmuka ini untuk setiap fase tertentu:

public class ChassisAssemblyPhase : IAssemblyPhase
{
    public void Assemble(Car car)
    {
        car.AddChassis(new UnibodyChassis());
    }
}

public class DashboardAssemblyPhase : IAssemblyPhase
{
    public void Assemble(Car car)
    {
        Dashboard dashboard = new Dashboard();

        dashboard.AddComponent(new Speedometer());
        dashboard.AddComponent(new FuelGuage());
        dashboard.Trim = DashboardTrimType.Aluminum;

        car.AddDashboard(dashboard);
    }
}

public class SeatAssemblyPhase : IAssemblyPhase
{
    public void Assemble(Car car)
    {
        car.Seats.Add(new Seat());
        car.Seats.Add(new Seat());
        car.Seats.Add(new Seat());
        car.Seats.Add(new Seat());
    }
}

Setiap fase secara individual bisa sangat sederhana. Beberapa hanya satu garis. Beberapa mungkin hanya membutakan menambahkan empat kursi, dan yang lain harus mengkonfigurasi dashboard sebelum menambahkannya ke mobil. Kompleksitas dari proses berlarut-larut ini dibagi menjadi beberapa kelas yang dapat digunakan kembali yang mudah diuji.

Meskipun Anda mengatakan pesanan tidak masalah sekarang, mungkin di masa depan.

Untuk menyelesaikan ini, mari kita lihat proses merakit mobil:

Car car = new Car();
CarAssembler assemblyLine = new CarAssembler();

assemblyLine.Assemble(car);

Anda dapat sub-kelas CarAssembler untuk merakit mobil atau truk jenis tertentu. Setiap fase adalah satu unit dalam keseluruhan yang lebih besar, sehingga lebih mudah untuk menggunakan kembali kode.


Apakah desain ini cacat tanpa harapan, dan saya lebih baik mengakui kekalahan dan parit - arsitektur ini sama sekali? Tidak baik untuk karir saya, tetapi apakah menulis kode yang dirancang dengan buruk akan menjadi lebih baik dalam jangka panjang?

Jika memutuskan apa yang saya tulis perlu ditulis ulang adalah tanda hitam pada karier saya, maka saya akan bekerja sebagai penggali parit sekarang, dan Anda tidak boleh mengambil nasihat saya. :)

Rekayasa perangkat lunak adalah proses belajar, penerapan, dan kemudian belajar ulang yang abadi. Hanya karena Anda memutuskan untuk menulis ulang kode Anda tidak berarti Anda akan dipecat. Jika itu masalahnya, tidak akan ada insinyur perangkat lunak.

Apakah pilihan saya saat ini sebenarnya adalah One True Way, dan saya harus berjuang untuk mendapatkan metrik kualitas yang lebih baik (dan / atau instrumentasi) terinstal?

Apa yang telah Anda uraikan bukanlah One True Way, namun ingatlah bahwa metrik kode juga bukan One True Way. Metrik kode harus dilihat sebagai peringatan. Mereka dapat menunjukkan masalah pemeliharaan di masa depan. Mereka adalah pedoman, bukan hukum. Ketika alat metrik kode Anda menunjukkan sesuatu, saya pertama-tama akan menyelidiki kode untuk melihat apakah itu benar menerapkan prinsip-prinsip SOLID . Berkali-kali kode refactoring agar lebih SOLID akan memuaskan alat metrik kode. Terkadang tidak. Anda harus menggunakan metrik ini berdasarkan kasus per kasus.

Greg Burghardt
sumber
1
Ya, jauh lebih baik daripada memiliki satu kelas dewa.
BЈовић
Pendekatan ini berhasil, tetapi sebagian besar karena Anda bisa mendapatkan elemen untuk mobil tanpa parameter. Bagaimana jika Anda harus melewati mereka? Kemudian Anda dapat membuang semua ini dari jendela dan sepenuhnya memikirkan kembali prosedurnya, karena Anda tidak benar-benar memiliki pabrik mobil dengan 150 parameter, itu gila.
Andy
@ DavidPacker: Sekalipun Anda harus membuat parameter banyak hal, Anda dapat merangkumnya juga dalam beberapa jenis kelas Config yang Anda berikan ke konstruktor objek "assembler" Anda. Dalam contoh mobil ini, warna cat, warna interior dan bahan, paket trim, mesin, dan banyak lagi dapat disesuaikan, tetapi Anda tidak perlu memiliki 150 penyesuaian. Anda mungkin akan memiliki selusin, yang cocok untuk objek pilihan.
Greg Burghardt
Tetapi hanya menambahkan konfigurasi akan menentang tujuan dari indah untuk loop, yang merupakan rasa malu yang luar biasa, karena Anda harus mengurai konfigurasi dan menetapkannya ke metode yang tepat yang pada gilirannya akan memberi Anda objek yang tepat. Jadi Anda mungkin akan berakhir dengan sesuatu yang sangat mirip dengan desain aslinya.
Andy
Saya melihat. Alih-alih memiliki satu kelas dan lima puluh metode, saya sebenarnya akan memiliki lima puluh kelas lean assembler dan satu kelas orkestra. Pada akhirnya hasilnya tampak sama, juga kinerja bijaksana, tetapi tata letak kode lebih bersih. Saya suka itu. Akan sedikit lebih canggung untuk menambahkan operasi (saya harus mengatur mereka di kelas mereka, ditambah menginformasikan kelas mengatur; Saya melihat tidak ada jalan keluar yang mudah dari kebutuhan ini), tetapi saya masih sangat menyukainya. Izinkan saya beberapa hari untuk menjelajahi pendekatan ini.
LSerni
1

Saya tidak tahu apakah ini mungkin bagi Anda, tetapi saya akan berpikir tentang menggunakan pendekatan yang lebih berorientasi data. Idenya adalah bahwa Anda menangkap beberapa (deklaratif) ekspresi aturan & kendala, operasi, dependensi, negara, dll ... Kemudian kelas dan kode Anda lebih umum, berorientasi sekitar mengikuti aturan, menginisialisasi keadaan saat ini, melakukan operasi untuk ubah status, dengan menggunakan tangkapan deklaratif aturan dan ubah status daripada menggunakan kode secara langsung. Seseorang mungkin menggunakan deklarasi data dalam bahasa pemrograman, atau, beberapa alat DSL, atau aturan atau mesin logika.

Erik Eidt
sumber
Beberapa operasi sebenarnya diimplementasikan dengan cara ini (sebagai daftar aturan). Untuk tetap dengan contoh mobil, ketika memasang sekotak, lampu dan colokan saya tidak benar-benar menggunakan tiga metode yang berbeda, tetapi hanya satu, yang secara umum sesuai dengan pin listrik yang terpasang di dua tempat, dan mengambil daftar tiga daftar sekering , lampu, dan colokan. Sebagian besar metode lain sayangnya tidak mudah menerima pendekatan ini.
LSerni
1

Entah bagaimana masalahnya mengingatkan saya pada ketergantungan. Anda ingin mobil dalam konfigurasi tertentu. Seperti Greg Burghardt, saya akan pergi dengan objek / kelas untuk setiap langkah / item / ....

Setiap langkah menyatakan apa yang ditambahkan ke dalam campuran (apa itu provides) (bisa beberapa hal), tetapi juga apa yang dibutuhkannya.

Kemudian, Anda menentukan konfigurasi akhir yang diperlukan di suatu tempat, dan memiliki algoritma yang melihat apa yang Anda butuhkan, dan memutuskan langkah-langkah mana yang perlu dieksekusi / bagian mana yang perlu ditambahkan / dokumen mana yang perlu dikumpulkan.

Algoritma resolusi ketergantungan tidak terlalu sulit. Kasus paling sederhana adalah di mana setiap langkah menyediakan satu hal, dan tidak ada dua hal yang menyediakan hal yang sama, dan dependensinya sederhana (tidak ada 'atau' dalam dependensi). Untuk kasus yang lebih kompleks: lihat saja alat seperti debian apt atau sesuatu.

Akhirnya, membangun mobil Anda kemudian mengurangi ke langkah apa yang mungkin ada, dan membiarkan CarBuilder mengetahui langkah-langkah yang diperlukan.

Namun satu hal penting adalah Anda harus menemukan cara agar CarBuilder tahu tentang semua langkah. Coba lakukan ini menggunakan file konfigurasi. Menambahkan langkah ekstra hanya akan membutuhkan tambahan pada file konfigurasi. Jika Anda membutuhkan logika, coba tambahkan DSL di file konfigurasi. Jika semuanya gagal, Anda terjebak mendefinisikannya dalam kode.

Sjoerd Job Postmus
sumber
0

Beberapa hal yang Anda sebutkan menonjol sebagai 'buruk' bagi saya

"Logika saya saat ini menggunakan Refleksi untuk kesederhanaan implementasi"

Tidak ada alasan untuk ini. apakah Anda benar-benar menggunakan perbandingan string nama metode untuk menentukan apa yang harus dijalankan ?! Jika Anda mengubah urutan, apakah Anda harus mengubah nama metode ?!

"Operasi dalam satu fase sudah independen"

Jika mereka benar-benar independen satu sama lain, ini menunjukkan bahwa kelas Anda telah mengambil lebih dari satu tanggung jawab. Bagi saya, kedua contoh Anda tampaknya cocok untuk satu jenis tunggal

MyObject.AddComponent(IComponent component) 

pendekatan yang akan memungkinkan Anda untuk membagi logika untuk setiap operasi menjadi kelas atau kelas sendiri.

Bahkan saya membayangkan mereka tidak benar-benar independen, saya membayangkan bahwa masing-masing memeriksa keadaan objek dan memodifikasinya, atau setidaknya melakukan semacam validasi sebelum memulai.

Dalam hal ini Anda dapat:

  1. Membuang OOP keluar jendela, dan memiliki layanan yang beroperasi pada data yang terbuka di kelas Anda yaitu.

    Service.AddDoorToCar (Mobil Mobil)

  2. Memiliki kelas pembangun yang membangun objek Anda dengan merakit data yang diperlukan terlebih dahulu dan melewati kembali objek yang telah selesai yaitu.

    Car = CarBuilder.DenganDoor (). WithDoor (). WithWheels ();

(Logika withX dapat dibagi lagi menjadi subbuilder)

  1. Ambil pendekatan Fungsional dan lulus fungsi / delgates menjadi metode modifikasi

    Car.Modify ((c) => c.doors ++);

Ewan
sumber
Ewan berkata: "Buang OOP keluar jendela, dan memiliki layanan yang beroperasi pada data yang terbuka di kelas Anda" --- Bagaimana ini tidak berorientasi objek?
Greg Burghardt
?? ini bukan, ini bukan pilihan OO
Ewan
Anda menggunakan objek, yang berarti "berorientasi objek". Hanya karena Anda tidak dapat mengidentifikasi pola pemrograman untuk kode Anda tidak berarti itu tidak berorientasi objek. Prinsip-prinsip SOLID adalah aturan yang baik untuk diikuti. Jika Anda menerapkan beberapa atau semua prinsip SOLID, itu berorientasi objek. Sungguh, jika Anda menggunakan objek dan tidak hanya melewati struct sekitar untuk prosedur yang berbeda atau metode statis, itu berorientasi objek. Ada perbedaan antara "kode berorientasi objek" dan "kode yang baik". Anda dapat menulis kode berorientasi objek yang buruk.
Greg Burghardt
itu bukan OO 'karena Anda mengekspos data seolah-olah itu adalah struct dan beroperasi di kelas yang terpisah seperti dalam prosedur
Ewan
tbh Saya hanya menambahkan komentar utama untuk mencoba dan menghentikan yang tak terhindarkan "itu bukan OOP!" komentar
Ewan