Yazılım Değişim Mühendisliği: Spagetti'den Temiz Tasarıma
Yayınlanan: 2022-03-11Sistemimize bir göz atabilir misiniz? Yazılımı yazan adam artık ortalıkta yok ve bir takım sorunlar yaşıyoruz. Ona bakacak ve bizim için temizleyecek birine ihtiyacımız var.
Makul bir süredir yazılım mühendisliğinde olan herkes, görünüşte masum olan bu talebin genellikle “her tarafında felaket yazılı” bir projenin başlangıcı olduğunu bilir. Bir başkasının kodunu devralmak, özellikle kod kötü tasarlanmışsa ve belgelere sahip değilse, bir kabus olabilir.
Bu nedenle, yakın zamanda müşterilerimizden birinden mevcut socket.io sohbet sunucusu uygulamasını (Node.js'de yazılmış) gözden geçirmesi ve iyileştirmesi yönünde bir talep aldığımda, son derece ihtiyatlı davrandım. Ama tepelere doğru koşmadan önce, en azından koda bir göz atmayı kabul etmeye karar verdim.
Ne yazık ki, koda bakmak yalnızca endişelerimi yeniden doğruladı. Bu sohbet sunucusu, tek, büyük bir JavaScript dosyası olarak uygulanmıştı. Bu tek yekpare dosyayı, temiz bir şekilde tasarlanmış ve bakımı kolay bir yazılım parçasına dönüştürmek gerçekten zor olurdu. Ama bir meydan okumadan hoşlanırım, bu yüzden kabul ettim.
Başlangıç Noktası - Yeniden Yapılanma İçin Hazırlanın
Mevcut yazılım, 1.200 satır belgesiz kod içeren tek bir dosyadan oluşuyordu. Evet. Ayrıca bazı hatalar içerdiği ve bazı performans sorunları olduğu biliniyordu.
Ek olarak, günlük dosyalarının incelenmesi (başka birinin kodunu devralırken her zaman iyi bir başlangıç noktasıdır) olası bellek sızıntısı sorunlarını ortaya çıkardı. Bir noktada, işlemin 1 GB'den fazla RAM kullandığı bildirildi.
Bu sorunlar göz önüne alındığında, iş mantığını hata ayıklamaya veya geliştirmeye çalışmadan önce kodun yeniden düzenlenmesi ve modülerleştirilmesi gerektiği hemen anlaşıldı. Bu amaçla, ele alınması gereken ilk sorunlardan bazıları şunları içeriyordu:
- Kod Yapısı. Kodun gerçek bir yapısı yoktu, bu da yapılandırmayı altyapıdan iş mantığından ayırmayı zorlaştırıyordu. Esasen endişelerin modülerleştirilmesi veya ayrılması yoktu.
- Fazlalık kod. Kodun bazı bölümleri (her olay işleyici için hata işleme kodu, web istekleri yapma kodu vb.) birden çok kez kopyalandı. Çoğaltılan kod hiçbir zaman iyi bir şey değildir, bu da kodun bakımını önemli ölçüde zorlaştırır ve hatalara daha açık hale getirir (gereksiz kod bir yerde sabitlendiğinde veya güncellendiğinde, diğerinde değilken).
- Sabit kodlanmış değerler. Kod, bir dizi sabit kodlanmış değer içeriyordu (nadiren iyi bir şey). Bu değerleri yapılandırma parametreleri aracılığıyla değiştirebilmek (kodda sabit kodlanmış değerlerde değişiklik gerektirmek yerine) esnekliği artıracak ve ayrıca test etme ve hata ayıklamayı kolaylaştırmaya yardımcı olabilir.
- Kerestecilik. Kayıt sistemi çok basitti. Analiz etmesi veya ayrıştırması zor ve beceriksiz olan tek bir dev günlük dosyası oluşturacaktı.
Temel Mimari Hedefler
Kodu yeniden yapılandırmaya başlama sürecinde, yukarıda tanımlanan belirli sorunları ele almanın yanı sıra, herhangi bir yazılım sisteminin tasarımında ortak olan (veya en azından olması gereken) bazı temel mimari hedefleri ele almaya başlamak istedim. . Bunlar şunları içerir:
- Sürdürülebilirlik. Asla bakımını yapması gereken tek kişi olmayı bekleyen bir yazılım yazmayın. Daima kodunuzun bir başkası için ne kadar anlaşılır olacağını ve onlar için değiştirmenin veya hata ayıklamanın ne kadar kolay olacağını düşünün.
- genişletilebilirlik Asla bugün uyguladığınız işlevselliğin ihtiyaç duyacağınız tek şey olduğunu varsaymayın. Yazılımınızı, genişletilmesi kolay olacak şekilde tasarlayın.
- Modülerlik. İşlevselliği, her biri kendi net amacı ve işlevi olan mantıksal ve farklı modüllere ayırın.
- Ölçeklenebilirlik. Günümüz kullanıcıları giderek daha sabırsız hale geliyor ve anında (veya en azından hemen yakın) yanıt süreleri bekliyor. Düşük performans ve yüksek gecikme, en kullanışlı uygulamanın bile piyasada başarısız olmasına neden olabilir. Eşzamanlı kullanıcı sayısı ve bant genişliği gereksinimleri arttıkça yazılımınız nasıl performans gösterecek? Paralelleştirme, veritabanı optimizasyonu ve eşzamansız işleme gibi teknikler, artan yük ve kaynak taleplerine rağmen sisteminizin yanıt verme yeteneğini geliştirmeye yardımcı olabilir.
Kodu Yeniden Yapılandırma
Amacımız, tek bir monolitik mongo kaynak kodu dosyasından modülerleştirilmiş bir dizi temiz mimariye sahip bileşene geçmek. Ortaya çıkan kodun bakımı, geliştirilmesi ve hata ayıklaması önemli ölçüde daha kolay olmalıdır.
Bu uygulama için kodu aşağıdaki farklı mimari bileşenler halinde düzenlemeye karar verdim:
- app.js - bu bizim giriş noktamız, kodumuz buradan çalışacak
- config - bu, yapılandırma ayarlarımızın bulunduğu yerdir
- ioW - tüm IO (ve iş) mantığını içerecek bir “IO sarmalayıcı”
- günlüğe kaydetme - günlüğe kaydetmeyle ilgili tüm kodlar (dizin yapısının tüm günlük dosyalarını içerecek yeni bir
logs
klasörünü de içereceğini unutmayın) - package.json - Node.js için paket bağımlılıklarının listesi
- node_modules - Node.js'nin gerektirdiği tüm modüller
Bu özel yaklaşımın sihirli bir yanı yoktur; kodu yeniden yapılandırmanın birçok farklı yolu olabilir. Ben şahsen bu organizasyonun aşırı karmaşık olmaksızın yeterince temiz ve iyi organize edilmiş olduğunu hissettim.
Ortaya çıkan dizin ve dosya organizasyonu aşağıda gösterilmiştir.
Kerestecilik
Günlüğe kaydetme paketleri, günümüzün geliştirme ortamları ve dillerinin çoğu için geliştirilmiştir, bu nedenle, günümüzde “kendi” günlük kaydı yeteneğinizi kullanmanıza nadiren ihtiyaç duyacaksınız.
Node.js ile çalıştığımız için, temelde Node.js ile kullanım için log4js kitaplığının bir versiyonu olan log4js-node'u seçtim. Bu kitaplık, çeşitli mesaj düzeylerini kaydetme yeteneği (UYARI, HATA, vb.) gibi bazı harika özelliklere sahiptir ve örneğin günlük olarak bölünebilen bir yuvarlanan dosyaya sahip olabiliriz, bu nedenle bunu yapmak zorunda değiliz. Açılması çok zaman alacak ve analiz edilmesi ve ayrıştırılması zor olan büyük dosyalarla uğraşın.
Amaçlarımız için, istenen bazı ek özellikleri eklemek için log4js-node çevresinde küçük bir sarmalayıcı oluşturdum. Daha sonra kodum boyunca kullanacağım log4js-node çevresinde bir sarmalayıcı oluşturmayı seçtiğimi unutmayın. Bu, bu genişletilmiş günlüğe kaydetme yeteneklerinin tek bir konumda uygulanmasını yerelleştirir, böylece günlüğe kaydetmeyi başlattığımda kodum boyunca fazlalık ve gereksiz karmaşıklıktan kaçınır.
G/Ç ile çalıştığımız ve birkaç bağlantı (soket) oluşturacak birkaç istemcimiz (kullanıcımız) olacağı için, günlük dosyalarında belirli bir kullanıcının etkinliğini izleyebilmek ve ayrıca bilmek istiyorum her günlük girişinin kaynağı. Bu nedenle, uygulamanın durumuyla ilgili bazı günlük girişlerine ve bazılarının kullanıcı etkinliğine özel olmasını bekliyorum.
Günlük sarmalayıcı kodumda, bir ERROR olayından önce ve sonra gerçekleştirilen eylemleri izlememe izin verecek olan kullanıcı kimliğini ve soketleri eşleyebiliyorum. Günlük sarmalayıcı, günlük girişinin kaynağını bilmem için olay işleyicilerine iletebileceğim farklı bağlamsal bilgilere sahip farklı günlükçüler oluşturmama da izin verecek.
Günlük sarmalayıcının kodu burada mevcuttur.
Yapılandırma
Genellikle bir sistem için farklı konfigürasyonları desteklemek gerekir. Bu farklılıklar, geliştirme ve üretim ortamları arasındaki farklılıklar olabileceği gibi, farklı müşteri ortamları ve kullanım senaryoları görüntüleme ihtiyacına da bağlı olabilir.
Bunu desteklemek için kodda değişiklikler gerektirmek yerine, yaygın uygulama, davranıştaki bu farklılıkları yapılandırma parametreleri yoluyla kontrol etmektir. Benim durumumda, farklı ayarlara sahip olabilecek farklı yürütme ortamlarına (hazırlama ve üretim) sahip olma yeteneğine ihtiyacım vardı. Ayrıca test edilen kodun hem evrelemede hem de üretimde iyi çalıştığından emin olmak istedim ve bu amaçla kodu değiştirmem gerekseydi, test sürecini geçersiz kılardı.
Bir Node.js ortam değişkeni kullanarak, belirli bir yürütme için hangi yapılandırma dosyasını kullanmak istediğimi belirleyebilirim. Bu nedenle, önceden kodlanmış tüm yapılandırma parametrelerini yapılandırma dosyalarına taşıdım ve istenen ayarlarla uygun yapılandırma dosyasını yükleyen basit bir yapılandırma modülü oluşturdum. Ayrıca, yapılandırma dosyasında bir dereceye kadar organizasyonu zorlamak ve gezinmeyi kolaylaştırmak için tüm ayarları kategorilere ayırdım.

İşte ortaya çıkan bir yapılandırma dosyası örneği:
{ "app": { "port": 8889, "invRepeatInterval":1000, "invTimeOut":300000, "chatLogInterval":60000, "updateUsersInterval":600000, "dbgCurrentStatusInterval":3600000, "roomDelimiter":"_", "roomPrefix":"/" }, "webSite":{ "host": "mysite.com", "port": 80, "friendListHandler":"/MyMethods.aspx/FriendsList", "userCanChatHandler":"/MyMethods.aspx/UserCanChat", "chatLogsHandler":"/MyMethods.aspx/SaveLogs" }, "logging": { "appenders": [ { "type": "dateFile", "filename": "logs/chat-server", "pattern": "-yyyy-MM-dd", "alwaysIncludePattern": false } ], "level": "DEBUG" } }
Kod Akışı
Şimdiye kadar farklı modülleri barındırmak için bir klasör yapısı oluşturduk, ortama özel bilgileri yüklemek için bir yol belirledik ve bir kayıt sistemi oluşturduk, bu yüzden işe özel kodu değiştirmeden tüm parçaları nasıl birbirine bağlayabileceğimizi görelim.
Kodun yeni modüler yapısı sayesinde app.js
giriş noktamız yeterince basittir ve yalnızca başlatma kodunu içerir:
var config = require('./config'); var logging = require('./logging'); var ioW = require('./ioW'); var obj = config.getCurrent(); logging.initialize(obj.logging); ioW.initialize(config);
Kod yapımızı ioW
klasörünün business ve socket.io ile ilgili kodları tutacağını söylemiştik. Özellikle aşağıdaki dosyaları içerecektir (ilgili kaynak kodunu görüntülemek için listelenen dosya adlarından herhangi birine tıklayabileceğinizi unutmayın):
-
index.js
– socket.io başlatma ve bağlantılarının yanı sıra olay aboneliğini ve ayrıca olaylar için merkezi bir hata işleyicisini işler -
eventManager.js
– işle ilgili tüm mantığı barındırır (olay işleyicileri) -
webHelper.js
– web istekleri yapmak için yardımcı yöntemler. -
linkedList.js
– bağlantılı bir liste yardımcı program sınıfı
Web isteği yapan kodu yeniden düzenledik ve ayrı bir dosyaya taşıdık ve iş mantığımızı aynı yerde ve değişmeden tutmayı başardık.
Önemli bir not: Bu aşamada eventManager.js
, gerçekten ayrı bir modüle çıkarılması gereken bazı yardımcı işlevler içerir. Ancak, bu ilk geçişteki amacımız, iş mantığı üzerindeki etkiyi en aza indirirken kodu yeniden düzenlemek olduğundan ve bu yardımcı işlevler iş mantığına çok karmaşık bir şekilde bağlı olduğundan, bunu, organizasyonun iyileştirilmesinde sonraki bir geçişe ertelemeyi seçtik. kod.
Node.js tanım gereği eşzamansız olduğundan, genellikle kodda gezinmeyi ve hata ayıklamayı özellikle zorlaştıran bir "geri arama cehennemi" yuvasıyla karşılaşırız. Bu tuzaktan kaçınmak için, yeni uygulamamda, söz verme modelini kullandım ve özellikle çok güzel ve hızlı bir vaat kitaplığı olan bluebird'den yararlandım. Sözler, kodu eşzamanlıymış gibi takip edebilmemizi sağlayacak ve ayrıca hata yönetimi ve çağrılar arasındaki yanıtları standart hale getirmek için temiz bir yol sağlayacaktır. Kodumuzda, merkezileştirilmiş hata işleme ve günlüğe kaydetmeyi yönetebilmemiz için her olay işleyicisinin bir söz vermesi gerektiğine dair üstü kapalı bir sözleşme vardır.
Tüm olay işleyicileri bir söz verecektir (eşzamansız arama yapsalar da yapmasalar da). Bununla, hata işlemeyi ve günlüğe kaydetmeyi merkezileştirebiliriz ve olay işleyicisinde işlenmemiş bir hatamız varsa, bu hatanın yakalanmasını sağlarız.
function execEventHandler(socket, eventName, eventHandler, data){ var sLogger = logging.createLogger(socket.id + ' - ' + eventName); sLogger.info(''); eventHandler(socket, data, sLogger).then(null, function(err){ sLogger.error(err.stack); }); };
Günlük kaydı tartışmamızda, her bağlantının içinde bağlamsal bilgiler bulunan kendi kaydedicisine sahip olacağından bahsetmiştik. Spesifik olarak, oluşturduğumuzda günlükçüye soket kimliğini ve olay adını bağlıyoruz, bu nedenle bu günlükçüyü olay işleyicisine ilettiğimizde, her günlük satırında şu bilgiler olacaktır:
var sLogger = logging.createLogger(socket.id + ' - ' + eventName);
Olay işleme ile ilgili bahsetmeye değer bir nokta daha: Orijinal dosyada, socket.io bağlantı olayının olay işleyicisinin içinde bulunan bir setInterval
işlev çağrımız vardı ve bu işlevi bir sorun olarak belirledik.
io.on('connection', function (socket) { ... Several event handlers .... setInterval(function() { try { var date = Date.now(); var tmp = []; while (0 < messageHub.count() && messageHub.head().date < date) { var item = messageHub.remove(); tmp.push(item); } ... Post Data to an external web service... } catch (e) { log('ERROR: ex: ' + e); } }, CHAT_LOGS_INTERVAL); });
Bu kod, aldığımız her bağlantı isteği için belirli bir aralıkta (bizim durumumuzda 1 dakikaydı) bir zamanlayıcı oluşturuyor. Dolayısıyla, örneğin, herhangi bir zamanda 300 çevrimiçi soketimiz varsa, o zaman her dakika çalışan 300 zamanlayıcımız olurdu. Bununla ilgili sorun, yukarıdaki kodda görebileceğiniz gibi, olay işleyicisi kapsamında soket veya tanımlanmış herhangi bir değişken kullanımının olmamasıdır. Kullanılan tek değişken, tüm bağlantılar için aynı olduğu anlamına gelen modül düzeyinde bildirilen bir messageHub
değişkenidir. Bu nedenle, bağlantı başına ayrı bir zamanlayıcıya kesinlikle gerek yoktur. Bu yüzden bunu bağlantı olay işleyicisinden kaldırdık ve bu durumda initialize
işlevi olan genel başlatma kodumuza dahil ettik.
Son olarak, yanıtları webHelper.js
, daha sonra hata ayıklama sürecine yardımcı olacak bilgileri günlüğe kaydedecek, tanınmayan herhangi bir yanıt için işleme ekledik:
if (!res || !res.d || !res.d.IsValid){ logger.debug(sendData); logger.debug(data); reject(new Error('Request failed. Path ' + params.path + ' . Invalid return data.')); return; }
Son adım, Node.js'nin standart hatası için bir günlük dosyası oluşturmaktır. Bu dosya, gözden kaçırmış olabileceğimiz işlenmemiş hataları içerecektir. Windows'ta düğüm işlemini (ideal değil ama biliyorsunuz…) bir hizmet olarak ayarlamak için, standart bir çıktı dosyası, standart hata dosyası ve çevresel değişkenler tanımlamanıza izin veren görsel bir kullanıcı arayüzüne sahip nssm adlı bir araç kullanıyoruz.
Node.js Performansı Hakkında
Node.js, tek iş parçacıklı bir programlama dilidir. Ölçeklenebilirliği geliştirmek için kullanabileceğimiz birkaç alternatif var. Düğüm kümesi modülü vardır veya yalnızca daha fazla düğüm işlemi ekleyerek iletme ve yük dengelemeyi yapmak için bunların üstüne bir nginx koyun.
Ancak bizim durumumuzda, her düğüm kümesi alt işlemi veya düğüm işleminin kendi bellek alanına sahip olacağı düşünüldüğünde, bu işlemler arasında kolayca bilgi paylaşamayacağız. Bu nedenle, bu özel durumda, çevrimiçi soketleri farklı işlemler için kullanılabilir durumda tutmak için harici bir veri deposu (redis gibi) kullanmamız gerekecek.
Çözüm
Tüm bunlar yerine getirildiğinde, bize verilen kodun önemli ölçüde temizlenmesini sağladık. Bu, kodu mükemmel hale getirmekle ilgili değil, daha ziyade desteklenmesi ve bakımı daha kolay olacak ve hata ayıklamayı kolaylaştıracak ve basitleştirecek temiz bir mimari temel oluşturmak için kodu yeniden yapılandırmakla ilgilidir.
Daha önce sıralanan temel yazılım tasarım ilkelerine (sürdürülebilirlik, genişletilebilirlik, modülerlik ve ölçeklenebilirlik) bağlı kalarak, farklı modül sorumluluklarını açık ve net bir şekilde tanımlayan modüller ve bir kod yapısı oluşturduk. Ayrıca, orijinal uygulamada, performansı düşüren yüksek bellek tüketimine yol açan bazı sorunlar belirledik.
Umarım makaleyi beğenmişsinizdir, başka yorumlarınız veya sorularınız varsa bana bildirin.